How OpenLogi Implements Long-Press and Short-Press Action Bindings
OpenLogi distinguishes between short-press and long-press actions using a deadline-based state machine that executes distinct callbacks depending on whether a button is released before or after a 500ms threshold.
The AprilNEA/OpenLogi repository provides a flexible input handling system for Logitech devices, featuring a sophisticated action binding system that supports differentiated short-press and long-press behaviors on the same physical button. This architecture allows users to bind two independent actions—such as Copy on click and Paste on hold—to maximize input efficiency without requiring additional hardware buttons.
Core Data Structures in openlogi-core
The Binding Enum
Located in crates/openlogi-core/src/binding.rs, the Binding enum defines all possible button behaviors. The variant relevant to press duration is LongPress(LongPressBinding), which encapsulates dual-action logic for handling time-sensitive inputs.
LongPressBinding Structure
The actual storage for paired actions resides in crates/openlogi-core/src/binding/value.rs. The LongPressBinding struct maintains two independent Action fields:
pub struct LongPressBinding {
short: Action,
long: Action,
}
Construction occurs via LongPressBinding::new(short, long), pairing the release action with the hold action. The system defines a LONG_PRESS_THRESHOLD constant of 500 milliseconds to differentiate between the two behaviors.
The click_action() helper method returns the short action for UI display purposes, allowing interfaces to show the default click behavior even when a distinct long-press action exists.
Runtime State Machine in openlogi-agent-core
State Tracking
The file crates/openlogi-agent-core/src/runtime/button.rs implements a state machine that tracks press duration across distinct states:
- LongPressPending — Button is currently pressed, storing the short action and a deadline timestamp calculated as
now + LONG_PRESS_THRESHOLD. - LongPressFired — The deadline elapsed; the long action executed and the short action is permanently suppressed for this press cycle.
- LifecycleOnly or Immediate — States for other binding types, such as single clicks or gestures, that do not use the deadline-based logic.
Action Dispatch Flow
When the agent receives a button event, the runtime executes the following deterministic sequence:
- Press event — State transitions to
LongPressPendingwith a 500ms deadline. - Release before deadline — The pending state resolves to
binding.short(), executing the short action immediately. - Deadline expiration — The state transitions to
LongPressFired, dispatchesbinding.long(), and ignores subsequent release events for this specific press.
This guarantees mutual exclusivity: once the 500ms threshold passes, the long action fires and the short action cannot trigger for that press.
User Configuration and TOML Parsing
Configuration Syntax
Users define long-press bindings in the TOML configuration file using a table structure that the parser recognizes by the presence of short and long keys:
[bindings.<button_id>]
short = "Copy"
long = "Paste"
Parser Implementation
The parser, located in crates/openlogi-core/src/config.rs around line 260, identifies the { short = "...", long = "..." } table format. It instantiates Binding::LongPress(LongPressBinding::new(...)) by mapping the string values to their corresponding Action variants, integrating the configuration into the runtime binding system.
Interaction with Gesture Bindings
Long-press bindings are mutually exclusive with gesture bindings within the OpenLogi system. The upgrade_to_gesture method, defined in crates/openlogi-core/src/binding/value.rs at line 124, converts a button to gesture recognition by discarding any existing long-press "long" action.
Single bindings, which fire immediately on press, occupy a distinct variant in the Binding enum and do not interact with the deadline-based state machine, ensuring clean separation of concerns between immediate, timed, and directional input types.
Code Examples
The following Rust code demonstrates programmatic creation of a long-press binding:
use openlogi_core::binding::{Binding, LongPressBinding, Action};
/// Create a long-press binding: click → Copy, hold → Paste
let lp = Binding::LongPress(LongPressBinding::new(Action::Copy, Action::Paste));
assert_eq!(lp.click_action(), Action::Copy); // Returns short action for UI display
// Runtime will dispatch Action::Paste if held ≥ 500ms
A corresponding TOML configuration example:
[[bindings]]
button = "ThumbwheelScrollUp"
short = "VolumeUp"
long = "Mute"
Summary
- OpenLogi uses the
LongPressBindingstruct incrates/openlogi-core/src/binding/value.rsto store paired short and long Action values. - The LONG_PRESS_THRESHOLD constant sets a 500ms deadline to differentiate press types.
- The runtime state machine in
crates/openlogi-agent-core/src/runtime/button.rstransitions fromLongPressPendingto either short-action execution orLongPressFired(long-action execution). - Long actions suppress short actions for the same press cycle, ensuring deterministic behavior.
- Configuration occurs via TOML tables parsed in
crates/openlogi-core/src/config.rs. - Gesture bindings via
upgrade_to_gestureare mutually exclusive with long-press bindings.
Frequently Asked Questions
What is the long-press threshold in OpenLogi?
OpenLogi defines the long-press threshold as 500 milliseconds via the LONG_PRESS_THRESHOLD constant in crates/openlogi-core/src/binding/value.rs. If a button remains pressed for this duration or longer, the system executes the long action instead of the short action.
How does OpenLogi prevent both actions from firing simultaneously?
The runtime state machine in crates/openlogi-agent-core/src/runtime/button.rs enforces mutual exclusivity through the LongPressFired state. Once the 500ms deadline passes and the long action dispatches, the state transitions to LongPressFired, causing the subsequent button release to ignore the short action entirely for that press cycle.
Can a button have both a long-press binding and a gesture binding?
No, these binding types are mutually exclusive according to the source code. Converting a button to use gesture recognition via the upgrade_to_gesture method (found at line 124 in crates/openlogi-core/src/binding/value.rs) discards the existing long-press configuration, as the system cannot simultaneously interpret directional swipes and press-duration timing on the same input.
How do I configure a long-press action in OpenLogi?
Add a TOML table under the [bindings] section with short and long keys specifying the desired Action names. The parser in crates/openlogi-core/src/config.rs automatically instantiates a Binding::LongPress when it encounters this structure, creating a LongPressBinding via LongPressBinding::new(short, long).
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 →