How OpenLogi Handles Short and Long Press Bindings for Device Buttons

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

OpenLogi defines three possible binding shapes in 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:

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

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

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

When a physical button press occurs, the runtime in 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:

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:

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:

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:

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, the bindings_for function maps stored bindings to a simplified view:

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.
  • 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 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. 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 or 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 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.

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 →