How OpenLogi's HoldShortcut Action Enables Push-to-Talk: Implementation Deep Dive

The HoldShortcut action in OpenLogi keeps a key combination pressed while a physical button is held down, automatically releasing it on button-up—core behavior for push-to-talk functionality.

OpenLogi is an open-source Logitech device manager written in Rust that maps physical mouse and keyboard buttons to programmable actions. The HoldShortcut action is specifically designed for push-to-talk workflows, where holding a button temporarily activates a key chord (like Alt+Space for Discord or Ctrl+Shift+M for Zoom) and releases it the moment the button is let go.

How HoldShortcut Works: The Three-Layer Architecture

The implementation spans configuration parsing, core-to-effect translation, and platform-specific injection.

Configuration Layer: Defining the Action

In crates/openlogi-core/src/binding/action.rs (lines 81–85), the Action enum defines HoldShortcut as a variant wrapping a KeyCombo:

pub enum Action {
    // ... other variants ...
    HoldShortcut(KeyCombo),  // lines 81-85
    // ...
}

The KeyCombo is parsed from TOML strings like "Alt+Space" or "Ctrl+Shift+T". This parsing happens during profile loading, validating that the chord uses recognized key names before the agent starts.

Core-to-Effect Translation

When the agent processes a button event, it calls Action::effect() to convert the high-level action into a low-level Effect. In crates/openlogi-core/src/binding/effect.rs, HoldShortcut maps directly to Effect::HeldKey(KeyCombo):

impl Action {
    pub fn effect(&self) -> Effect {
        match self {
            // ...
            Action::HoldShortcut(combo) => Effect::HeldKey(combo.clone()),
            // ...
        }
    }

    // Accessor for the held combo (lines 99-104)
    pub fn held_combo(&self) -> Option<&KeyCombo> {
        match self {
            Action::HoldShortcut(c) => Some(c),
            _ => None,
        }
    }
}

This separation allows the core crate to remain platform-agnostic while the inject crate handles OS-specific key simulation.

Injector: The Actual Key Holding Logic

The critical implementation resides in crates/openlogi-inject/src/inject/windows.rs. Two functions cooperate to achieve true push-to-talk behavior:

hold_keys() (lines 301–313) — Entry point that receives the held key array and a KeyPhase:

pub fn hold_keys(keys: &[HeldKey], phase: KeyPhase) {
    // Maps HeldKey to virtual key codes, then delegates to platform layer
    let vks: Vec<VIRTUAL_KEY> = keys.iter()
        .filter_map(|hk| vk_from_held_key(hk))
        .collect();
    
    post_custom_shortcut(&vks, phase);
}

post_custom_shortcut() (lines 71–82) — Sends Windows SendInput events:

fn post_custom_shortcut(vks: &[VIRTUAL_KEY], phase: KeyPhase) {
    let inputs: Vec<INPUT> = vks.iter().map(|vk| {
        let mut input = INPUT::default();
        input.r#type = INPUT_KEYBOARD;
        // KEYDOWN or KEYUP based on phase
        // ...
        input
    }).collect();
    
    unsafe { SendInput(&inputs, std::mem::size_of::<INPUT>() as i32) };
}

The KeyPhase enum distinguishes Down from Up events. Critically, on release, the injector reverses the key order—releasing modifiers after the main key to ensure clean chord termination.

The Push-to-Talk Lifecycle

The agent's button watcher in crates/openlogi-agent-core/src/watchers/button.rs orchestrates the full cycle:

  1. Press detected → inject::hold_keys(..., KeyPhase::Down) sends key-down events
  2. Hold period → No traffic; OS maintains the pressed state
  3. Release detected → Same function with KeyPhase::Up sends reversed key-up events
  4. Error recovery → If any key in the chord lacks a platform mapping, it's skipped with a logged warning; the remaining chord proceeds normally

Graceful Degradation

HoldShortcut includes a safety mechanism: if the runtime cannot provide a release context (e.g., a CLI tool that only processes discrete actions), the action automatically downgrades to a normal CustomShortcut tap. This prevents keys from becoming permanently stuck in the pressed state.

Practical Configuration Examples

TOML Profile Setup


# Push-to-talk on mouse button 5 (typical side thumb button)

[[button]]
id = 5
action = { HoldShortcut = "Alt+Space" }  # Discord default push-to-talk

[[button]]
id = 6
action = { HoldShortcut = "Ctrl+Shift+M" }  # Zoom mute/unmute

Programmatic Usage

use openlogi_core::binding::{Action, KeyCombo};

// Parse and construct
let combo: KeyCombo = "Alt+Space".parse().expect("valid key combination");
let ptt_action = Action::HoldShortcut(combo);

// Verify properties
assert_eq!(ptt_action.label(), "Hold Alt+Space");
assert_eq!(ptt_action.held_combo(), Some(&combo));

Event Handler Integration

fn handle_mouse_button(button: ButtonState) {
    match button {
        ButtonState::Pressed(id) if id == 5 => {
            openlogi_inject::execute(
                &Action::HoldShortcut("Alt+Space".parse().unwrap())
            );
        }
        ButtonState::Released(id) if id == 5 => {
            // Same call; injector detects release from context
            openlogi_inject::execute(
                &Action::HoldShortcut("Alt+Space".parse().unwrap())
            );
        }
        _ => {}
    }
}

Platform Mapping Details

The injector automatically selects correct virtual key codes:

Logical Key Windows (VK_) macOS (kVK_)
Alt VK_MENU (0x12) kVK_Option (0x3A)
Space VK_SPACE (0x20) kVK_Space (0x31)
Ctrl VK_CONTROL (0x11) kVK_Control (0x3B)

Unrecognized keys log a warning and are omitted rather than failing the entire chord.

Summary

  • HoldShortcut is defined in action.rs (lines 81–85) and maps to Effect::HeldKey in effect.rs
  • The Windows injector in windows.rs implements hold_keys() and post_custom_shortcut() using SendInput
  • Lifecycle awareness ensures clean press/release cycles with automatic key-order reversal on release
  • Graceful degradation converts to a normal tap when release context is unavailable
  • Configuration uses simple TOML syntax: { HoldShortcut = "Modifier+Key" }

Frequently Asked Questions

What happens if I release the button mid-application shutdown?

The injector guarantees key-up delivery through RAII guards in the Windows implementation. If the agent process terminates abnormally, Windows automatically releases all synthetic keys, preventing stuck modifiers.

Can HoldShortcut handle mouse buttons in the chord, not just keyboard keys?

Per the current source in action.rs, KeyCombo only represents keyboard keys. Mouse button simulation requires the separate MouseButton action variant. Chords mixing keyboard and mouse would need custom effect chaining.

Why does the release reverse key order instead of using the same order as press?

Modifier keys (Alt, Ctrl, Shift) must be released after the main key to prevent the OS from interpreting the release as a new shortcut. Reversing ensures Space releases while Alt is still held, then Alt releases—mirroring how humans type chords.

Does HoldShortcut work on macOS and Linux?

The openlogi-core crate is platform-agnostic, but openlogi-inject currently implements hold_keys() only for Windows in windows.rs. macOS and Linux equivalents would require parallel implementations using CGEventPost (macOS) or XTestFakeKeyEvent (Linux X11).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →