# How OpenLogi's Button Binding System Handles Short/Long Press Actions and Hold Shortcut Chords

> Discover how OpenLogi's button binding system manages short and long press actions, plus hold shortcut chords. Learn about its three-layer architecture for flexible input handling.

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

---

**OpenLogi uses a three-layer architecture—`Binding` enum for configuration, projection helpers for context-aware access, and effect conversion for platform-neutral injection—to support short/long press discrimination with a 500 ms threshold and lifecycle-aware hold-shortcut chords.**

OpenLogi's button binding system provides sophisticated input handling for gaming mice and custom controllers. This Rust-based framework, developed in the [AprilNEA/OpenLogi](https://github.com/AprilNEA/OpenLogi) repository, enables users to configure complex button behaviors including timed press differentiation and chord holding. This article examines the core mechanisms that make these features possible.

## Core Architecture: Three Key Abstractions

OpenLogi's binding system rests on three interconnected concepts:

| Component | Purpose | Location |
|-----------|---------|----------|
| **`Binding` enum** | Represents the shape of a button assignment | [`binding/value.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding/value.rs) |
| **Projection helpers** | Convert device configs to runtime-appropriate structures | [`bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/bindings.rs) |
| **Effect conversion** | Maps high-level actions to platform-neutral injection commands | [`effect.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/effect.rs) |

The **Binding enum** in [`binding/value.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding/value.rs) defines three variants:

- `Binding::Single` — one action per button press
- `Binding::Gesture` — directional actions (up/down/left/right)
- **`Binding::LongPress`** — distinct short and long press actions

## Short Press vs. Long Press Discrimination

### Configuration and Threshold

A button bound to `Binding::LongPress` stores two separate `Action` values. The `LongPressBinding` struct in [`binding/value.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding/value.rs) (lines 12–25) defines this structure:

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

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

```

The **500 ms threshold** determines which action fires. Release before 500 ms triggers `short`; hold beyond 500 ms triggers `long`.

### Context-Aware Projection

OpenLogi solves a critical UX problem: the GUI should display the short-press action, while the runtime agent needs full access to both arms. Two projection functions in [`bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/bindings.rs) handle this:

**`bindings_for`** — short-press projection for UI consumers:

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

```

This discards long-press data via `click_action()`, returning only the short action.

**`button_bindings_for`** — lifecycle-aware projection for the runtime agent:

```rust
// bindings.rs, lines 35-45
pub fn button_bindings_for(...) -> BTreeMap<ButtonId, Binding> { … }

```

This preserves the complete `Binding::LongPress` structure, enabling duration-based discrimination.

### Runtime Measurement

The agent's button watcher measures press duration against `LONG_PRESS_THRESHOLD`. The test `lifecycle_projection_preserves_long_press_while_action_projection_uses_short` in [`bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/bindings.rs) verifies this dual-projection behavior.

### Code Example: Configuring Short/Long Press

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

let mut cfg = Config::default();

// Back button: short press = Copy, long press = Paste
let lp = LongPressBinding::new(Action::Copy, Action::Paste);
cfg.set_binding("2b042", ButtonId::Back, Binding::LongPress(lp));

```

The GUI displays **Copy** (via `bindings_for`). The agent fires **Paste** when held > 500 ms (via `button_bindings_for`).

## Hold Shortcut Chords

### Action Representation

**`Action::HoldShortcut(KeyCombo)`** represents a chord that must remain pressed for the button's entire duration. Unlike instantaneous shortcuts, these follow the button's lifecycle—pressed on button down, released on button up.

### Effect Conversion

In [`effect.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/effect.rs) (lines 76–78), `HoldShortcut` converts to `Effect::HeldKey`:

```rust
Action::HoldShortcut(combo) => Effect::HeldKey(combo),

```

This platform-neutral effect instructs inject backends to:

1. Synthesize **down** events for all keys in the chord when the button presses
2. Automatically emit **up** events when the button releases

### KeyCombo Structure

The `KeyCombo` struct in [`binding/key_combo.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding/key_combo.rs) (lines 92–100) stores:

- Modifier bitmap
- Virtual key code
- Canonical `rendered_label` for UI display

```rust
/// A platform-neutral keyboard chord.
pub struct KeyCombo { … }

```

### Backend Implementation

Each OS-specific injector in `openlogi-inject/src/inject/*.rs` implements `HeldKey` handling. The same `Shortcut`-to-`KeyCombo` mapping tables service both instantaneous and held chords—the `Effect` variant determines lifecycle behavior.

### Code Example: Hold Shortcut Configuration

```rust
use openlogi_core::binding::{Action, KeyCombo, ButtonId};

let combo = KeyCombo::new(/* modifiers=⌘, key=C */);
cfg.set_binding(
    "2b042",
    ButtonId::SideButton1,
    Action::HoldShortcut(combo).into(),
);

```

When pressed, the injector receives `Effect::HeldKey(&combo)`, presses ⌘+C, and keeps it held until release.

### Runtime Handling Pattern

```rust
fn handle_button_press(button: ButtonId, press_time: Instant) {
    let binding = button_bindings_for(&cfg, Some("2b042"), None)
        .get(&button)
        .unwrap();

    match binding {
        Binding::LongPress(lp) => {
            if press_time.elapsed() > LONG_PRESS_THRESHOLD {
                inject_effect(lp.long().clone());
            } else {
                inject_effect(lp.short().clone());
            }
        }
        Binding::Single(act) => inject_effect(act.clone()),
        Binding::Gesture(map) => { /* … */ }
    }
}

```

## Key Implementation Files

| File | Role |
|------|------|
| [`binding/value.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding/value.rs) | `Binding` enum, `LongPressBinding`, `LONG_PRESS_THRESHOLD` |
| [`bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/bindings.rs) | Projection functions `bindings_for` and `button_bindings_for` |
| [`effect.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/effect.rs) | `Action` to `Effect` mapping, including `HoldShortcut` conversion |
| [`binding/key_combo.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding/key_combo.rs) | `KeyCombo` chord representation |
| `openlogi-inject/src/inject/*.rs` | OS-specific `HeldKey` implementation |

## Summary

- **Dual projection** enables UI simplicity (`bindings_for`) while preserving runtime flexibility (`button_bindings_for`)
- **500 ms threshold** in `LONG_PRESS_THRESHOLD` provides clear short/long press discrimination
- **Lifecycle-aware effects** through `Effect::HeldKey` ensure chords match button press duration
- **Platform neutrality** via `KeyCombo` allows consistent configuration across Windows, macOS, and Linux

## Frequently Asked Questions

### How does OpenLogi prevent long-press actions from appearing in the GUI?

The `bindings_for` function in [`bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/bindings.rs) projects all bindings through `click_action()`, which extracts only the short-press action from `LongPressBinding` variants. This design intentionally simplifies the UI while the runtime agent retains full access via `button_bindings_for`.

### What happens if a button is released exactly at 500 ms?

The threshold comparison uses `>` (greater than), not `>=`. Releasing at exactly 500 ms triggers the short-press action, as the elapsed duration does not strictly exceed `LONG_PRESS_THRESHOLD`. This edge case behavior favors the more common short-press intention.

### Can hold shortcuts combine with long-press bindings?

No—`Binding::LongPress` contains `Action` values, not nested `Binding` values. A single button cannot simultaneously discriminate press duration and emit a held chord. However, either the `short` or `long` action can itself be `Action::HoldShortcut`, achieving combined behavior through action selection rather than binding composition.

### How do OS-specific backends handle `HeldKey` differently from instantaneous keys?

Instantaneous keys use `Effect::Key`, which emits down-then-up events immediately. `Effect::HeldKey` defers the up events until the backend receives a release notification. The `KeyCombo` structure remains identical; only the timing of up-event synthesis differs.