# How OpenLogi Handles Short and Long Press Bindings for Device Buttons

> Discover how OpenLogi manages short and long press button bindings with its 500ms threshold runtime system and PressBehavior state machine. Learn more today.

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

---

**OpenLogi distinguishes short and long press bindings through a threshold-based runtime system that fires a short action if the button is released within 500 milliseconds, or a long action if the button remains held past that deadline, as implemented in the `PressBehavior` state machine.**

The AprilNEA/OpenLogi repository implements a sophisticated input handling layer that allows device buttons to trigger different actions based on press duration. This architecture cleanly separates the binding definition from the runtime execution logic, enabling precise control over gesture recognition.

## Binding Type Definitions in [`value.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/value.rs)

OpenLogi defines three possible binding shapes in [`crates/openlogi-core/src/binding/value.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/value.rs) to accommodate different interaction patterns. The system uses a **threshold-based** approach for distinguishing press durations, encapsulated in the `Binding::LongPress` variant.

The `LongPressBinding` struct explicitly separates the two possible actions:

```rust
pub struct LongPressBinding {
    short: Action,
    long: Action,
}

```

This structure pairs with a fixed timing constant defined in the same file:

```rust
pub const LONG_PRESS_THRESHOLD: Duration = Duration::from_millis(500);

```

The complete `Binding` enum provides three distinct interaction models:

- **`Binding::Single(Action)`** – Fires a single action on any click, regardless of duration.
- **`Binding::Gesture(BTreeMap<GestureDirection, Action>)`** – Supports directional gestures (hold + swipe), where the `Click` entry serves as the short-press action.
- **`Binding::LongPress(LongPressBinding)`** – Implements the threshold-based logic that distinguishes short releases (< 500 ms) from long presses (≥ 500 ms).

## Runtime State Machine in [`button.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/button.rs)

When a physical button press occurs, the runtime in [`crates/openlogi-agent-core/src/runtime/button.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent-core/src/runtime/button.rs) creates a `PressBehavior` value that tracks the press state and determines which action to emit. The state machine initializes based on the stored binding configuration:

```rust
match binding {
    None => PressBehavior::LifecycleOnly,
    Some(Binding::Single(action)) => PressBehavior::Immediate(action.clone()),
    Some(binding @ Binding::Gesture(_)) => PressBehavior::Immediate(binding.click_action()),
    Some(Binding::LongPress(binding)) => PressBehavior::LongPressPending {
        short: binding.short().clone(),
        long:  binding.long().clone(),
        deadline: pressed_at + LONG_PRESS_THRESHOLD,
    },
}

```

The `LongPressPending` variant captures the **deadline calculation** (`pressed_at + LONG_PRESS_THRESHOLD`), creating a timer boundary that the runtime evaluates on every update cycle.

## Threshold-Based Action Resolution

The system resolves the final action through distinct code paths depending on when the button release occurs relative to the 500 ms threshold.

### Short Press Detection

If the button releases before the deadline expires, the runtime queries `release_action()` to retrieve the short action:

```rust
fn release_action(&self) -> Option<&Action> {
    match self {
        Self::LongPressPending { short, .. } => Some(short),
        _ => None,
    }
}

```

The press ends with `EndReason::Released`, emitting only the short action to the action dispatcher.

### Long Press Detection

If the button remains held when the deadline passes, the runtime calls `fire_long()` to transition the state and emit the long action:

1. The behavior transitions from `LongPressPending` to `LongPressFired`
2. The system immediately emits the `long` action
3. Subsequent release events **suppress** the short action, preventing double-triggering

This ensures that a long press fires exactly once at the threshold boundary, and the final release is treated as a cleanup event rather than a separate trigger.

## Configuring Short and Long Press Bindings

Developers configure these bindings through the core API by constructing `LongPressBinding` instances with paired actions:

```rust
use openlogi_core::binding::{Binding, LongPressBinding, Action};

// Configure a button to have a short-press "Copy" and a long-press "Paste"
let my_binding = Binding::LongPress(
    LongPressBinding::new(Action::Copy, Action::Paste)
);
config.set_binding("device-key", ButtonId::Back, my_binding);

```

At runtime, the agent creates the appropriate behavior state:

```rust
let pressed_at = Instant::now();
let behavior = PressBehavior::new(
    config.effective_bindings("device-key").get(&ButtonId::Back),
    pressed_at,
);
// `behavior` will be `LongPressPending` with a 500 ms deadline

```

## GUI Projection and Action Mapping

The configuration interface uses a projection function that strips long-press semantics to display only the primary (short) action. In [`crates/openlogi-core/src/bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/bindings.rs), the `bindings_for` function maps stored bindings to a simplified view:

```rust
pub fn bindings_for(...) -> BTreeMap<ButtonId, Action> {
    button_bindings_for(...)
        .into_iter()
        .map(|(button, binding)| (button, binding.click_action()))
        .collect()
}

```

This ensures that UI elements display the short behavior while the runtime continues to respect the full long-press semantics during actual device operation.

## Summary

- **OpenLogi** implements short and long press support through a dedicated `LongPressBinding` struct in [`openlogi-core/src/binding/value.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-core/src/binding/value.rs).
- The **500 ms threshold** is hardcoded as `LONG_PRESS_THRESHOLD` and applied consistently across all long-press bindings.
- The **runtime state machine** in [`openlogi-agent-core/src/runtime/button.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-agent-core/src/runtime/button.rs) uses `PressBehavior::LongPressPending` to track deadlines and prevent action collisions.
- **Short presses** emit their action only on release before the deadline, while **long presses** fire at the threshold boundary and suppress the short action on release.
- The **GUI projection** displays only short actions through the `click_action()` method, keeping the interface simple while preserving advanced runtime capabilities.

## Frequently Asked Questions

### What is the default long press threshold in OpenLogi?

The default threshold is **500 milliseconds**, defined as a constant in [`crates/openlogi-core/src/binding/value.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/value.rs). This value applies globally to all `LongPressBinding` instances and is not currently configurable per-device or per-binding in the analyzed source code.

### How does OpenLogi prevent both short and long actions from firing on a single long press?

The runtime transitions the internal state from `LongPressPending` to `LongPressFired` when the threshold expires. Once in the `LongPressFired` state, the `release_action()` method returns `None`, ensuring that the subsequent button release does not emit the short action. This state transition guarantees mutual exclusion between the two actions.

### Can the long press duration be customized per device?

Based on the source analysis, the `LONG_PRESS_THRESHOLD` is a **compilation-time constant** set to 500 ms. There is no evidence in [`value.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/value.rs) or [`button.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/button.rs) of per-device or per-binding threshold overrides. All long-press bindings share this fixed duration unless the source code is modified and recompiled.

### How are button bindings projected for the GUI configuration interface?

The system uses the `bindings_for` function in [`crates/openlogi-core/src/bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/bindings.rs) to project the internal binding storage into a simplified map. This projection calls `click_action()` on each binding, which returns the short-press action for `LongPressBinding` variants. The GUI therefore displays only the short action, while the full binding (including the long action) remains stored in the configuration for runtime use.