# How to Implement Long-Press and Short-Press Action Pairs in OpenLogi

> Implement distinct long-press and short-press actions in OpenLogi using HID++ control IDs FB and CF. Configure button bindings via TOML or Rust API for custom device behavior.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: how-to-guide
- Published: 2026-09-13

---

**OpenLogi treats short-press and long-press as independent actions configured via TOML or the Rust API, using HID++ control IDs `0xFB` and `0xCF` to persist distinct behaviors to Logitech input devices.**

The OpenLogi project provides an open-source framework for remapping Logitech mice and keyboards at the firmware level. Understanding how to implement long-press and short-press action pairs in OpenLogi button bindings allows you to double the functionality of every physical button through the HID++ reprogrammable-control protocol.

## Core Architecture Overview

OpenLogi separates button binding logic into three distinct layers: the **core API** defining actions and buttons, the **configuration layer** parsing user-defined TOML, and the **device layer** translating these mappings into HID++ payloads.

### Action and Button Definitions

All possible behaviors are enumerated in the `Action` type, while physical buttons are identified via `ButtonId`. These primitives reside in:

- **[`crates/openlogi-core/src/binding/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action.rs)** — Defines the `Action` enum (e.g., `Undo`, `Copy`, `CustomShortcut`).
- **[`crates/openlogi-core/src/binding/mod.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/mod.rs)** — Defines the `ButtonId` enum (e.g., `Back`, `Forward`) and helper traits.

### Configuration Parsing

The `Config::bindings` structure stores a map from `ButtonId` to an action pair. According to the source in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs), the parser extracts two optional keys per button: `short` and `long`. During runtime, `Config::effective_bindings` builds a `BTreeMap<ButtonId, (Action, Option<Action>)>` where the tuple contains the required short-press action and an optional long-press action.

### HID++ Device Communication

The device layer uses feature ID-specific control identifiers to program the hardware:

- **`0xFB`** — Short-press control ID
- **`0xCF`** — Long-press control ID

These constants are defined in [`crates/openlogi-hidpp/src/feature/reprog_controls/control_ids.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/feature/reprog_controls/control_ids.rs). The `ReprogControls::write` method in [`crates/openlogi-hidpp/src/feature/reprog_controls.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/feature/reprog_controls.rs) transmits payloads via `write_long_register` to commit the configuration to the device firmware.

## Configuring Button Bindings via TOML

You define action pairs directly in your [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) (or per-application profile). Each button entry accepts a `short` key for tap actions and an optional `long` key for hold actions.

```toml

# Global bindings example

[bindings.back]
short = "Undo"          # Single tap triggers Undo

long  = "Copy"          # Hold triggers Copy

[bindings.forward]
short = "Redo"

# Omitting `long` causes long-press to fall back to the short-press action

```

The TOML parser validates these strings against the `Action` enum variants. When the configuration loads, OpenLogi builds the internal `(Action, Option<Action>)` tuple for each `ButtonId`.

## Implementing Bindings Programmatically

For developers extending OpenLogi or building custom configuration tools, the `Config` struct provides explicit setters for both press types.

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

fn configure_back_button(config: &mut Config) {
    // Assign short-press action
    config.set_binding(ButtonId::Back, Action::Undo);
    
    // Assign long-press action (optional)
    config.set_long_binding(ButtonId::Back, Action::Copy);
}

```

The `effective_bindings` method merges global and per-app profiles into the final `BTreeMap<ButtonId, (Action, Option<Action>)>` used by the agent at runtime.

## HID++ Protocol Implementation Details

Under the hood, OpenLogi communicates with Logitech devices using the HID++ reprogrammable-controls feature. The implementation writes distinct control IDs for each press duration.

```rust
// From crates/openlogi-hidpp/src/feature/reprog_controls.rs
let short_payload = ControlId::LaserButtonShortPress.to_payload(&action_short);
let long_payload  = ControlId::LaserButtonLongPress.to_payload(&action_long);

endpoint.call_long(0, short_payload).await?;
endpoint.call_long(0, long_payload).await?;

```

These calls utilize `write_long_register` (and its counterpart `read_long_register`) to modify the device's programmable-control table. The agent distinguishes press length via HID++ `press-duration` events and looks up the correct action from the `effective_bindings` map.

## Summary

- **OpenLogi Architecture** separates action definitions ([`action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/action.rs)), configuration parsing ([`config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/config.rs)), and HID++ device communication ([`reprog_controls.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/reprog_controls.rs)).
- **TOML Configuration** uses `short` and `long` keys within button sections to define dual-action bindings.
- **HID++ Control IDs** `0xFB` (short) and `0xCF` (long) are written to the device firmware via `write_long_register` calls.
- **Runtime Resolution** produces a `BTreeMap<ButtonId, (Action, Option<Action>)>` where the UI displays a single icon while the agent handles press-duration discrimination.

## Frequently Asked Questions

### What HID++ feature IDs does OpenLogi use for press types?

OpenLogi uses control ID `0xFB` for short-press actions and `0xCF` for long-press actions. These values are hardcoded in [`crates/openlogi-hidpp/src/feature/reprog_controls/control_ids.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/feature/reprog_controls/control_ids.rs) and are written to the device using the `ReprogControls` feature implementation.

### Can I assign only a long-press action without a short-press?

No, the current implementation requires a short-press action as the primary binding. The long-press action is optional and specified as `Option<Action>` in the `effective_bindings` map. If you omit the `long` key in TOML or skip `set_long_binding` in code, long-presses fall back to executing the short-press action.

### Where does OpenLogi store the binding configuration?

Bindings are stored in TOML configuration files parsed by [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs). The `Config::bindings` field holds the deserialized data, which is then converted into the runtime `BTreeMap` structure via `effective_bindings`. These configurations can be global or scoped to specific applications.

### How does the UI handle buttons with dual actions?

The UI (Actions Ring) displays a single icon representing the button, regardless of whether dual actions are configured. The press-duration discrimination happens at the agent level by monitoring HID++ `press-duration` events. The agent consults the `(Action, Option<Action>)` tuple from `effective_bindings` to determine which action to execute based on how long the user held the button.