How the OpenLogi Action Catalog Maps Buttons to Short, Long, and Hold Actions

The OpenLogi Action Catalog centralizes every possible device command in the Action enum and exposes them through the Binding struct, which stores separate Option<Action> fields for short presses, long presses, and hold actions.

The AprilNEA/OpenLogi repository implements a robust input handling system for Logitech devices. At its core, the Action Catalog provides a unified registry of all available commands, allowing users to map distinct behaviors to different button press durations. This architecture ensures that any new capability added to the Action enum automatically becomes available across the short, long, and hold binding slots.

The Action Enum and Catalog Structure

The canonical definition of the Action Catalog resides in crates/openlogi-core/src/binding/action.rs. This file defines the Action enum, where each variant represents a distinct user-visible command such as CaptureRegion, SwitchApp, or ToggleSmartShift.

Every action variant exposes three critical metadata methods that drive the UI and runtime logic:

  • label() – Returns the human-readable name displayed in configuration interfaces.
  • category() – Provides logical grouping (e.g., Navigation, Media, System) used to organize actions into UI sections.
  • catalog() – Returns the complete ordered list of all actions, which the application iterates when presenting binding options.

Action Metadata Implementation

According to the source code in action.rs, the catalog() method constructs a static ordered slice of every variant. This guarantees that the UI presents a consistent, exhaustive list of capabilities regardless of which binding slot (short, long, or hold) is being configured.

The Binding Structure for Input Handling

Button mappings are stored in the Binding struct defined in crates/openlogi-core/src/binding/binding.rs. This struct explicitly separates input behaviors into three distinct fields:

pub struct Binding {
    pub short: Option<Action>,   // Triggered by a quick press and release
    pub long:  Option<Action>,   // Triggered by press-and-hold exceeding the timeout threshold
    pub hold:  Option<Action>,   // Continuously fired while the button remains depressed
}

Each field is an Option<Action>, allowing any combination of mapped or unmapped behaviors. The short field handles immediate clicks, long captures deliberate press-and-hold gestures, and hold enables repeating actions like scrolling or volume adjustment.

Creating a Button Binding

The following example demonstrates how to instantiate a Binding with distinct actions for each press type:

use openlogi_core::binding::{Action, Binding};

let binding = Binding {
    short: Some(Action::CaptureRegion),
    long:  Some(Action::SwitchApp),
    hold:  Some(Action::ScrollUp),
};

Runtime Dispatch Flow

During device operation, the agent receives raw HID++ events from the hardware. The event decoder classifies each physical button interaction into a PressType variant (Short, Long, or Hold). The runtime then queries the active Binding to resolve which Action to execute.

The dispatch logic matches the press type to the corresponding struct field:

fn resolve_press(binding: &Binding, press_type: PressType) -> Option<Action> {
    match press_type {
        PressType::Short => binding.short,
        PressType::Long  => binding.long,
        PressType::Hold  => binding.hold,
    }
}

Once resolved, the selected Action is dispatched to the appropriate subsystem—such as the inject crate for synthetic keystrokes or the device crate for DPI adjustments—based on the action's variant type.

UI Integration with the Catalog

The desktop interface leverages Action::catalog() to populate three separate dropdown menus—one for each binding slot—ensuring that every available command can be assigned to any press type. The UI code in crates/openlogi-desktop/src/features/action_ring/editor.rs iterates over the catalog to build these selectors.

Localization is handled by the i18n service in crates/openlogi-desktop/src/services/i18n.rs, which consumes the string returned by Action::label() and translates it according to the user's locale before display.

Enumerating the Full Catalog

This pattern demonstrates how the UI retrieves the complete action list for rendering configuration options:

use openlogi_core::binding::Action;

// Retrieve the ordered list of all actions.
let all_actions = Action::catalog();

for action in &all_actions {
    println!("{} – {}", action.label(), action.category());
}

Summary

  • The Action enum in crates/openlogi-core/src/binding/action.rs serves as the central catalog, exposing label(), category(), and catalog() methods for UI consumption.
  • The Binding struct in crates/openlogi-core/src/binding/binding.rs maps physical button behaviors to three distinct fields: short, long, and hold.
  • Runtime dispatch matches decoded HID++ events against the appropriate Binding field to determine which command to execute.
  • The UI automatically reflects new capabilities because it derives its option lists directly from Action::catalog(), ensuring synchronization between the core library and the desktop interface.

Frequently Asked Questions

What determines the threshold between a short and long press?

The timing threshold is handled by the HID++ event decoder in the device communication layer, which classifies raw input into PressType::Short or PressType::Long before querying the Binding struct. The Binding itself stores only the resulting action mapping, not the timing logic.

Can the same action be assigned to multiple press types on one button?

Yes. Since short, long, and hold are independent Option<Action> fields, you can assign Some(Action::CaptureRegion) to all three. However, note that the hold action executes repeatedly while the button remains pressed, whereas short and long fire once per gesture.

Where are new actions defined in the codebase?

New actions are added as variants to the Action enum in crates/openlogi-core/src/binding/action.rs. After implementing the required metadata methods (label() and category()), the new variant automatically appears in Action::catalog() and becomes selectable in the UI for all three binding types.

How does the UI handle localization for action labels?

The desktop application references Action::label() to obtain the base identifier string, then passes this through the i18n service located in crates/openlogi-desktop/src/services/i18n.rs. This ensures that catalog entries display in the user's configured language without requiring changes to the core enum definition.

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 →