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

> Explore the HoldShortcut action in OpenLogi for push-to-talk. Learn how it keeps key combinations pressed while a button is held for seamless voice communication.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: deep-dive
- Published: 2026-08-31

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action.rs) (lines 81–85), the `Action` enum defines `HoldShortcut` as a variant wrapping a `KeyCombo`:

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/effect.rs), `HoldShortcut` maps directly to `Effect::HeldKey(KeyCombo)`:

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`:

```rust
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:

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/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

```toml

# 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

```rust
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

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/action.rs) (lines 81–85) and maps to `Effect::HeldKey` in [`effect.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/effect.rs)
- The **Windows injector** in [`windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/windows.rs). macOS and Linux equivalents would require parallel implementations using `CGEventPost` (macOS) or `XTestFakeKeyEvent` (Linux X11).