How OpenLogi Independent Short/Long Press Actions Work With the 500ms Threshold
The OpenLogi input system uses a 500ms LONG_PRESS_THRESHOLD to determine whether a button press fires a short action (release before deadline) or a long action (deadline reached while held), with both actions remaining mutually independent.
In the OpenLogi universal mouse and keyboard customization system, buttons can be configured with dual-action behavior. When a user presses and holds a button, the runtime must decide between two possible outcomes. This article explains how the 500ms threshold governs that decision without coupling the two actions.
Binding Definition: LongPressBinding Structure
Independent short and long actions are stored together in the LongPressBinding struct defined in crates/openlogi-core/src/binding/value.rs:
pub struct LongPressBinding {
short: Action,
long: Action,
}
This structure appears at lines 12-25 of [value.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/binding/value.rs#L12-L25). The short and long fields are completely independent—neither depends on the other, and either can be triggered in isolation based on timing.
The threshold itself is defined as a global constant:
pub const LONG_PRESS_THRESHOLD: Duration = Duration::from_millis(500);
Press Lifecycle: Creating the Pending State
When a button press occurs, the runtime in crates/openlogi-agent-core/src/runtime/button.rs creates a PressBehavior::LongPressPending variant. At lines 90-100 of [button.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-agent-core/src/runtime/button.rs#L90-L100), the PressBehavior::new method calculates the deadline:
PressBehavior::LongPressPending {
short: binding.short().clone(),
long: binding.long().clone(),
deadline: pressed_at + LONG_PRESS_THRESHOLD,
}
The deadline equals press time plus 500ms. Until this moment, both actions remain possible. After it passes, only the long action remains valid.
Threshold Evaluation: Two Trigger Paths
OpenLogi evaluates the 500ms threshold through two distinct mechanisms:
Deadline-Driven Long Action Firing
The button runtime worker periodically checks for expired deadlines. At lines 31-33 of [button.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-agent-core/src/runtime/button.rs#L31-L33), emit_due_long_presses handles this:
let due_presses = state.due_long_presses(now);
emit_selected_long_presses(state, &due_presses, now, emit);
When now >= deadline, the long action fires immediately and state transitions to LongPressFired. The short action becomes permanently suppressed for this press.
Release-Driven Short Action Firing
If the button releases before the deadline, the short action fires instead. The runtime compares released_at against pressed_at + LONG_PRESS_THRESHOLD to make this determination.
Unit Test Verification: Concrete Threshold Behavior
The test suite in crates/openlogi-agent-core/src/runtime/button/tests.rs validates the exact threshold semantics.
Short-Only Case: Release at 499ms
At lines 94-115, release_before_long_press_threshold_fires_only_the_short_action demonstrates:
released_at: pressed_at + LONG_PRESS_THRESHOLD.saturating_sub(Duration::from_millis(1)),
With release at 499ms, only the short action emits. The test asserts no long action fires.
Long-Only Case: Threshold at 500ms
At lines 33-78, threshold_fires_long_once_and_suppresses_short_on_release shows:
emit_due_long_presses(..., pressed_at + LONG_PRESS_THRESHOLD);
// later release after the threshold
Advancing the worker to exactly 500ms triggers the long action. Subsequent release ends the press without emitting the short action.
Cancellation Safety: Preventing Ghost Actions
Press cancellation—such as session invalidation during hold—must not leave orphaned actions. At lines 81-100, cancellation_never_fires_a_pending_short_or_long_action verifies that cleanup routines clear both pending actions regardless of how close to the deadline.
Configuration Example: TOML Profile Definition
Users define independent short/long press actions in device configuration files:
[profile.my_mouse]
button_back = { short = "Copy", long = "Paste" }
The TOML parser maps the { short = "...", long = "..." } table to Binding::LongPress wrapping a LongPressBinding. See the Binding enum at lines 71-80 of [value.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/binding/value.rs#L71-L80).
Runtime Behavior Summary
| Timing Scenario | Emitted Action | State After Event |
|---|---|---|
| Release before 500ms | short only | Press ended |
| 500ms reached while held | long only | LongPressFired |
| Release after long fired | none (already emitted) | Press ended |
| Cancel at any point | none | Pending cleared |
Key Source Files
crates/openlogi-core/src/binding/value.rs—LONG_PRESS_THRESHOLDconstant,LongPressBindingstruct,Bindingenumcrates/openlogi-agent-core/src/runtime/button.rs—PressBehaviorcreation, deadline evaluation,emit_due_long_pressescrates/openlogi-agent-core/src/runtime/button/tests.rs— Verification of threshold boundary conditionscrates/openlogi-core/src/binding.rs— Re-export ofLONG_PRESS_THRESHOLDfor cross-crate usage
Summary
- Independent actions:
LongPressBindingstores short and long actions as separate fields without interdependence - 500ms threshold:
LONG_PRESS_THRESHOLDcreates a precise boundary between short and long press detection - Deadline-based triggering: Long actions fire automatically when the threshold elapses, not on release
- Short action suppression: Once the deadline passes, the short action becomes unavailable for that press cycle
- Cancellation safety: Pending actions clear on any cancellation, preventing unintended emission
Frequently Asked Questions
Can the 500ms threshold be customized per device or button?
No. LONG_PRESS_THRESHOLD is a compile-time constant defined in crates/openlogi-core/src/binding/value.rs. The value is hardcoded to 500ms across all bindings in the OpenLogi system. Per-button or per-device threshold customization would require modifying the source and rebuilding.
What happens if both short and long actions are configured identically?
The actions fire independently based on timing. A quick tap emits one instance; a 500ms hold emits another. The runtime does not deduplicate identical actions. This can produce double-trigger behavior if the user accidentally holds slightly past the threshold.
Does the long action fire repeatedly if the button stays held?
No. The LongPressPending state transitions to LongPressFired after the first deadline evaluation. Subsequent worker ticks find no pending presses for that button. The long action fires exactly once per press cycle, regardless of hold duration beyond 500ms.
How does OpenLogi handle rapid successive presses near the threshold?
Each press creates an independent PressBehavior with its own deadline. The runtime tracks presses by button identity, so overlapping or closely-timed presses on the same button replace the previous pending state. This prevents action stacking but means a very quick release-and-repress sequence resets the 500ms window entirely.
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 →