How OpenLogi's Button Binding System Handles Short/Long Press Actions and Hold Shortcut Chords
OpenLogi uses a three-layer architecture—Binding enum for configuration, projection helpers for context-aware access, and effect conversion for platform-neutral injection—to support short/long press discrimination with a 500 ms threshold and lifecycle-aware hold-shortcut chords.
OpenLogi's button binding system provides sophisticated input handling for gaming mice and custom controllers. This Rust-based framework, developed in the AprilNEA/OpenLogi repository, enables users to configure complex button behaviors including timed press differentiation and chord holding. This article examines the core mechanisms that make these features possible.
Core Architecture: Three Key Abstractions
OpenLogi's binding system rests on three interconnected concepts:
| Component | Purpose | Location |
|---|---|---|
Binding enum |
Represents the shape of a button assignment | binding/value.rs |
| Projection helpers | Convert device configs to runtime-appropriate structures | bindings.rs |
| Effect conversion | Maps high-level actions to platform-neutral injection commands | effect.rs |
The Binding enum in binding/value.rs defines three variants:
Binding::Single— one action per button pressBinding::Gesture— directional actions (up/down/left/right)Binding::LongPress— distinct short and long press actions
Short Press vs. Long Press Discrimination
Configuration and Threshold
A button bound to Binding::LongPress stores two separate Action values. The LongPressBinding struct in binding/value.rs (lines 12–25) defines this structure:
pub const LONG_PRESS_THRESHOLD: Duration = Duration::from_millis(500);
pub struct LongPressBinding {
short: Action,
long: Action,
}
The 500 ms threshold determines which action fires. Release before 500 ms triggers short; hold beyond 500 ms triggers long.
Context-Aware Projection
OpenLogi solves a critical UX problem: the GUI should display the short-press action, while the runtime agent needs full access to both arms. Two projection functions in bindings.rs handle this:
bindings_for — short-press projection for UI consumers:
// bindings.rs, lines 24-33
pub fn bindings_for(...) -> BTreeMap<ButtonId, Action> {
button_bindings_for(...)
.into_iter()
.map(|(button, binding)| (button, binding.click_action()))
.collect()
}
This discards long-press data via click_action(), returning only the short action.
button_bindings_for — lifecycle-aware projection for the runtime agent:
// bindings.rs, lines 35-45
pub fn button_bindings_for(...) -> BTreeMap<ButtonId, Binding> { … }
This preserves the complete Binding::LongPress structure, enabling duration-based discrimination.
Runtime Measurement
The agent's button watcher measures press duration against LONG_PRESS_THRESHOLD. The test lifecycle_projection_preserves_long_press_while_action_projection_uses_short in bindings.rs verifies this dual-projection behavior.
Code Example: Configuring Short/Long Press
use openlogi_core::binding::{Action, Binding, LongPressBinding, ButtonId};
use openlogi_core::config::Config;
let mut cfg = Config::default();
// Back button: short press = Copy, long press = Paste
let lp = LongPressBinding::new(Action::Copy, Action::Paste);
cfg.set_binding("2b042", ButtonId::Back, Binding::LongPress(lp));
The GUI displays Copy (via bindings_for). The agent fires Paste when held > 500 ms (via button_bindings_for).
Hold Shortcut Chords
Action Representation
Action::HoldShortcut(KeyCombo) represents a chord that must remain pressed for the button's entire duration. Unlike instantaneous shortcuts, these follow the button's lifecycle—pressed on button down, released on button up.
Effect Conversion
In effect.rs (lines 76–78), HoldShortcut converts to Effect::HeldKey:
Action::HoldShortcut(combo) => Effect::HeldKey(combo),
This platform-neutral effect instructs inject backends to:
- Synthesize down events for all keys in the chord when the button presses
- Automatically emit up events when the button releases
KeyCombo Structure
The KeyCombo struct in binding/key_combo.rs (lines 92–100) stores:
- Modifier bitmap
- Virtual key code
- Canonical
rendered_labelfor UI display
/// A platform-neutral keyboard chord.
pub struct KeyCombo { … }
Backend Implementation
Each OS-specific injector in openlogi-inject/src/inject/*.rs implements HeldKey handling. The same Shortcut-to-KeyCombo mapping tables service both instantaneous and held chords—the Effect variant determines lifecycle behavior.
Code Example: Hold Shortcut Configuration
use openlogi_core::binding::{Action, KeyCombo, ButtonId};
let combo = KeyCombo::new(/* modifiers=⌘, key=C */);
cfg.set_binding(
"2b042",
ButtonId::SideButton1,
Action::HoldShortcut(combo).into(),
);
When pressed, the injector receives Effect::HeldKey(&combo), presses ⌘+C, and keeps it held until release.
Runtime Handling Pattern
fn handle_button_press(button: ButtonId, press_time: Instant) {
let binding = button_bindings_for(&cfg, Some("2b042"), None)
.get(&button)
.unwrap();
match binding {
Binding::LongPress(lp) => {
if press_time.elapsed() > LONG_PRESS_THRESHOLD {
inject_effect(lp.long().clone());
} else {
inject_effect(lp.short().clone());
}
}
Binding::Single(act) => inject_effect(act.clone()),
Binding::Gesture(map) => { /* … */ }
}
}
Key Implementation Files
| File | Role |
|---|---|
binding/value.rs |
Binding enum, LongPressBinding, LONG_PRESS_THRESHOLD |
bindings.rs |
Projection functions bindings_for and button_bindings_for |
effect.rs |
Action to Effect mapping, including HoldShortcut conversion |
binding/key_combo.rs |
KeyCombo chord representation |
openlogi-inject/src/inject/*.rs |
OS-specific HeldKey implementation |
Summary
- Dual projection enables UI simplicity (
bindings_for) while preserving runtime flexibility (button_bindings_for) - 500 ms threshold in
LONG_PRESS_THRESHOLDprovides clear short/long press discrimination - Lifecycle-aware effects through
Effect::HeldKeyensure chords match button press duration - Platform neutrality via
KeyComboallows consistent configuration across Windows, macOS, and Linux
Frequently Asked Questions
How does OpenLogi prevent long-press actions from appearing in the GUI?
The bindings_for function in bindings.rs projects all bindings through click_action(), which extracts only the short-press action from LongPressBinding variants. This design intentionally simplifies the UI while the runtime agent retains full access via button_bindings_for.
What happens if a button is released exactly at 500 ms?
The threshold comparison uses > (greater than), not >=. Releasing at exactly 500 ms triggers the short-press action, as the elapsed duration does not strictly exceed LONG_PRESS_THRESHOLD. This edge case behavior favors the more common short-press intention.
Can hold shortcuts combine with long-press bindings?
No—Binding::LongPress contains Action values, not nested Binding values. A single button cannot simultaneously discriminate press duration and emit a held chord. However, either the short or long action can itself be Action::HoldShortcut, achieving combined behavior through action selection rather than binding composition.
How do OS-specific backends handle HeldKey differently from instantaneous keys?
Instantaneous keys use Effect::Key, which emits down-then-up events immediately. Effect::HeldKey defers the up events until the backend receives a release notification. The KeyCombo structure remains identical; only the timing of up-event synthesis differs.
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 →