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:
- Press detected →
inject::hold_keys(..., KeyPhase::Down)sends key-down events - Hold period → No traffic; OS maintains the pressed state
- Release detected → Same function with
KeyPhase::Upsends reversed key-up events - 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 toEffect::HeldKeyineffect.rs - The Windows injector in
windows.rsimplementshold_keys()andpost_custom_shortcut()usingSendInput - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →