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

> Learn how OpenLogi differentiates short and long press outcomes with 500ms long-press bindings. Discover its threshold-based approach for distinct actions.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-08-30

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.

```rust
/// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
#[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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.