How OpenLogi Differentiates Short and Long Press Outcomes with 500ms Long-Press Bindings

OpenLogi distinguishes short and long press outcomes using a threshold-based LongPressBinding that stores two independent actions and measures press duration against a fixed 500ms constant to determine which behavior to trigger.

OpenLogi enables hardware buttons to perform context-sensitive actions based on press duration through its core binding system. The implementation relies on a dual-action configuration structure combined with precise timing logic that compares the elapsed press duration against a 500-millisecond threshold defined in the core library.

The 500ms Threshold Constant

OpenLogi defines the long-press detection threshold as a global constant in crates/openlogi-core/src/binding/value.rs. This duration determines exactly how long a physical button must remain depressed before the system recognizes the interaction as a long press rather than a short tap.

/// How long a physical button must remain down before its independent long
/// action fires.
pub const LONG_PRESS_THRESHOLD: Duration = Duration::from_millis(500);

This constant provides a consistent user experience across all button bindings in the system. When the runtime detects that a button has been held longer than this 500ms duration, it suppresses the short-press action and fires the long-press alternative.

Dual-Action Configuration Structure

The system models long-press capability through the Binding::LongPress variant, which encapsulates a LongPressBinding struct containing two independent action fields:

  • short: The action fired when the button releases before the 500ms threshold expires.
  • long: The action fired once when the press duration exceeds the threshold, permanently suppressing the short action for that press cycle.

This structure allows a single physical button to host two distinct behaviors without requiring separate hardware inputs. The configuration stores both possibilities, leaving the runtime to determine which to execute based on actual user interaction timing.

Runtime Duration Detection and Dispatch

The button-handling logic in crates/openlogi-agent-core/src/runtime/button.rs implements the time-based decision engine. The runtime tracks each press event through a precise lifecycle:

  1. Press Recording: On physical button press, the system captures pressed_at = Instant::now().
  2. Deadline Calculation: It computes a deadline as pressed_at + LONG_PRESS_THRESHOLD.
  3. Short Press Path: If the user releases the button before the deadline expires, the system fires the short action from the LongPressBinding.
  4. Long Press Path: If the deadline passes while the button remains depressed, the system fires the long action exactly once and ignores the short action for that press cycle.

This approach ensures that long-press actions trigger deterministically at the 500ms boundary, while short actions only execute upon early release.

Context-Aware Binding Projections

OpenLogi provides two distinct projection modes for accessing button bindings, optimized for different consumption contexts:

bindings_for (Action Projection): Used by UI components that only need immediate actions, this function extracts the short side (click_action()) of any binding. When displaying button hints in the settings panel, the UI receives only the short-press behavior.

button_bindings_for (Lifecycle Projection): Used by the runtime agent, this function preserves the complete LongPressBinding structure, allowing the agent to implement the full timing logic and decide at runtime which action to fire.

pub fn bindings_for(
    config: &Config,
    config_key: Option<&str>,
    app_bundle: Option<&str>,
) -> BTreeMap<ButtonId, Action> {
    button_bindings_for(config, config_key, app_bundle)
        .into_iter()
        .map(|(button, binding)| (button, binding.click_action()))
        .collect()
}

The test suite in crates/openlogi-core/src/bindings.rs validates this dichotomy, confirming that lifecycle projections preserve the full dual-action binding while action projections correctly default to the short-press behavior:

#[test]
fn lifecycle_projection_preserves_long_press_while_action_projection_uses_short() {
    let mut cfg = Config::default();
    let binding = Binding::LongPress(LongPressBinding::new(Action::Copy, Action::Paste));
    cfg.set_binding("2b042", ButtonId::Back, binding.clone());

    let lifecycle = button_bindings_for(&cfg, Some("2b042"), None);
    assert_eq!(lifecycle.get(&ButtonId::Back), Some(&binding));
    let actions = bindings_for(&cfg, Some("2b042"), None);
    assert_eq!(actions.get(&ButtonId::Back), Some(&Action::Copy));
}

Practical Implementation Example

Configuring a dual-action button requires defining both behaviors in the configuration file, then allowing the runtime to measure the physical press duration:

// Configuration definition (TOML representation)
[bindings."2b042".Back]
short = "Copy"
long  = "Paste"      // Fires after 500ms

// Runtime handling logic (simplified)
let pressed_at = Instant::now();
let deadline   = pressed_at + LONG_PRESS_THRESHOLD;

// On button release:
if Instant::now() < deadline {
    // Short press detected
    fire(binding.short());
} else {
    // Long press already fired once at deadline
}

Summary

  • Threshold Definition: The LONG_PRESS_THRESHOLD constant in binding/value.rs fixes the differentiation point at 500ms.
  • Dual Storage: LongPressBinding maintains separate short and long action fields within the Binding enum.
  • Timing Logic: The runtime in button.rs records press start time, calculates a deadline, and compares release timing against the threshold.
  • Selective Projection: bindings_for() surfaces only short actions for UI contexts, while button_bindings_for() preserves full binding data for runtime timing decisions.

Frequently Asked Questions

What happens if I release the button exactly at 500ms?

The comparison uses a strict less-than check against the deadline (Instant::now() < deadline). Releasing exactly at the 500ms boundary results in a short press action, as the threshold represents the minimum duration required to trigger the long-press behavior.

Can I customize the 500ms threshold for specific buttons?

No. According to the OpenLogi source code, LONG_PRESS_THRESHOLD is defined as a public constant in binding/value.rs, making it a global system setting. The current implementation does not support per-button or per-binding threshold customization.

How does the settings UI know which action to display?

The settings panel calls bindings_for(), which internally invokes binding.click_action(). This method always extracts the short action from a LongPressBinding, ensuring the UI displays the standard click behavior while hiding the secondary long-press capability from simple action lists.

Does the long-press action repeat if I hold the button for several seconds?

No. The long action fires exactly once when the deadline first elapses. The runtime dispatches the long-press event at the 500ms mark and does not repeat it, keeping the short action suppressed until the button releases and a new press cycle begins.

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 →