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

Summary

  • Independent actions: LongPressBinding stores short and long actions as separate fields without interdependence
  • 500ms threshold: LONG_PRESS_THRESHOLD creates 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →