OpenLogi Button Remapping Architecture: A Deep Dive into the HID++ Pipeline

OpenLogi implements button remapping through a six-stage pipeline that intercepts raw Logitech HID++ events, queries device capabilities via the ReprogControlsV4 feature, and injects synthetic OS events based on user-defined TOML configurations.

The OpenLogi project delivers an open-source, cross-platform alternative to Logitech Options for reprogramming mice and keyboards at the firmware level. Its button remapping architecture transforms native hardware signals into configurable actions by leveraging the HID++ 2.0 protocol. This deep dive examines the crate-based pipeline—from device discovery in openlogi-device-registry to event injection in openlogi-inject—that enables hardware-level control diversion without proprietary vendor software.

The Six-Stage Architecture

OpenLogi organizes its button remapping system into discrete crates, each handling a specific layer of the transformation pipeline. This separation allows the architecture to remain portable across macOS, Linux, and Windows while maintaining a single source of truth for user configurations.

Stage 1: Device Discovery and Capability Probing

The openlogi-device-registry crate initiates the pipeline by enumerating all connected HID++ receivers. When the agent starts, the system creates Device objects for each Logitech peripheral and queries the feature table to identify reprogrammable controls. The detection logic resides in src/receiver.rs, which establishes the communication channel required for subsequent HID++ transactions.

Stage 2: HID++ Protocol Handling

Raw protocol communication is isolated within the openlogi-hidpp crate, specifically in src/feature/reprog_controls.rs. This layer provides a pure-Rust wrapper around the Logitech HID++ 2.0 specification, exposing the ReprogControlsV4 feature (FEATURE_ID = 0x1b04) that enables querying and modifying button behavior at the firmware level.

Stage 3: Control-Level Abstraction

The openlogi-device crate converts low-level protocol bytes into a safe API. In src/reprog_controls.rs, the system exposes ReprogControlsV4 methods that allow the rest of the codebase to check diversion capabilities and toggle raw-XY reporting without handling raw HID++ packets directly. This abstraction isolates protocol complexity from business logic.

Stage 4: OS Input Hook

Platform-specific event capture occurs in the openlogi-hook crate. The hook monitors low-level mouse and keyboard events through OS-specific implementations (src/macos.rs, src/linux.rs, src/windows.rs) and filters traffic through the source_is_remappable function defined in src/lib.rs. This gatekeeper rejects events from non-Logitech hardware, system trackpads, and already-synthesized inputs to prevent interference with native OS gestures.

Stage 5: Remap Engine and Action Injection

The openlogi-agent-core orchestrates the remapping logic, while openlogi-inject handles execution. When a remappable event arrives, the core consults the TOML configuration parsed by openlogi-core/src/config.rs to map ButtonId values to Action definitions. For gestures or suppressed inputs, the core calls ReprogControlsV4::set_cid_reporting to enable temporary diversion, then dispatches the openlogi-inject crate to synthesize new events via platform-native APIs (CoreGraphics on macOS, uinput on Linux, SendInput on Windows).

Stage 6: IPC and UI Presentation

User interaction flows through the openlogi-ipc crate, which implements a TarRPC-based communication channel between the background agent and frontend components. The openlogi-desktop GUI and openlogi-overlay ring interface query and modify the binding map through src/ipc.rs, while openlogi-ui/src/action_icons.rs renders visual indicators of remappable controls.

How a Button Press Is Remapped

The end-to-end flow from physical press to synthetic action follows six discrete steps, each implemented across the crate boundary:

  1. Device probing – Upon initialization, openlogi-device-registry queries each discovered device using ReprogControlsV4::get_ctrl_id_info to enumerate which Control IDs (CIDs) support diversion and raw-XY reporting.

  2. OS hook capture – The platform-specific hook receives the low-level event and executes source_is_remappable to verify the hardware signature, rejecting non-Logitech sources immediately.

  3. Configuration lookup – Valid events reach openlogi-agent-core, which performs a lookup in the user configuration to resolve the physical ButtonId (e.g., ButtonId::Back) to its assigned Action.

  4. Temporary diversion – If the action requires intercepting raw motion data or suppressing the native event, the core invokes ReprogControlsV4::set_cid_reporting(cid, diverted=true, raw_xy=true) in openlogi-device/src/reprog_controls.rs. This HID++ command instructs the firmware to stop emitting the standard button event and instead stream raw-XY data or diverted-button events.

  5. Synthetic injection – The openlogi-inject crate constructs platform-appropriate synthetic events from the Action definition. Implementation details vary by OS but converge in src/inject.rs, where the system translates high-level actions (key presses, DPI toggles, shell commands) into native event structures.

  6. IPC feedback – The TarRPC layer broadcasts state changes to openlogi-desktop and openlogi-overlay/src/ring.rs, updating the UI to reflect active diversions and current bindings.

Core Concepts and Data Structures

Understanding the OpenLogi button remapping architecture requires familiarity with several domain-specific abstractions implemented throughout the codebase:

  • Control IDs (CIDs) – Fixed hexadecimal identifiers for every physical button or touch panel on Logitech devices (e.g., GESTURE_BUTTON_CID = 0x00c3). The mapping from hardware CIDs to logical ButtonId enums lives in openlogi-device/src/reprog_controls.rs.

  • Temporary Diversion – A firmware-level flag set via set_cid_reporting that stops the device from transmitting its default button event. Diversion enables the system to implement "remap-to-nothing" behaviors and complex gesture recognition that requires holding a button while tracking motion.

  • Raw-XY Reporting – A HID++ mode enabled alongside diversion that streams high-resolution motion data while a button is depressed. The agent uses this data to calculate swipe gestures in user space rather than relying on the device's internal gesture interpretation.

  • Source Validation – The source_is_remappable policy enforced in openlogi-hook/src/lib.rs ensures the agent never interferes with system-level trackpad gestures or third-party hardware, maintaining system stability.

Implementation Examples

Querying Control Capabilities

To inspect whether a specific button supports diversion or raw-XY reporting, use the ReprogControlsV4 abstraction:

use openlogi_device::ReprogControlsV4;

let rc = ReprogControlsV4::new(chan.clone(), dev_idx, feat_idx);
let info = rc.get_ctrl_id_info(0).await?;  // Query first control

println!(
    "CID {:#04x} – divertable: {}, raw-xy: {}",
    info.cid,
    info.is_divertable(),
    info.supports_raw_xy()
);

This corresponds to the CtrlIdInfo structure defined in crates/openlogi-device/src/reprog_controls.rs at lines 88-119.

Enabling Temporary Diversion

To capture a button press for custom handling without emitting the native event:

// Divert the "Back" button (CID 0x0053) for gesture recognition
rc.set_cid_reporting(0x0053, true, true).await?;

The implementation in reprog_controls.rs (lines 47-55) wraps the HID++ setCidReporting command, setting both the diversion and raw-XY flags.

Loading and Executing Bindings

Configuration persists in a TOML file parsed by openlogi-core:


# ~/.config/openlogi/config.toml

[bindings]
Back = { command = "open -a Chrome" }
Forward = { key = "Alt+F4" }

The agent core consumes this configuration during event processing:

use openlogi_core::Config;
use openlogi_inject as inject;

let cfg = Config::load()?;  // Parses openlogi-core/src/config.rs
let action = cfg.bindings.get(&ButtonId::Back).unwrap();
inject::run(action).await?;  // Executes platform-specific injection

Modifying Bindings via IPC

Frontend components communicate with the agent through the TarRPC interface defined in openlogi-ipc/src/ipc.rs:

use openlogi_ipc::Client;
use openlogi_core::{ButtonId, Binding};

let client = Client::connect().await?;
client.set_binding(ButtonId::Back, Binding::DpiToggle).await?;

Summary

The OpenLogi button remapping architecture demonstrates a clean separation of concerns across six specialized crates:

  • Device Registry handles USB discovery and capability enumeration via src/receiver.rs
  • HID++ Layer manages protocol state in openlogi-hidpp and openlogi-device/src/reprog_controls.rs
  • OS Hook filters events through source_is_remappable to protect non-Logitech hardware
  • Agent Core coordinates diversion logic and configuration lookups
  • Inject Layer synthesizes cross-platform events via src/inject.rs
  • IPC System enables real-time UI updates through TarRPC channels

By combining firmware-level HID++ commands with OS-specific event injection, OpenLogi achieves deep hardware integration without kernel extensions or proprietary drivers.

Frequently Asked Questions

How does OpenLogi prevent interference with non-Logitech devices?

The architecture implements a strict filtering layer in openlogi-hook/src/lib.rs through the source_is_remappable function. This gatekeeper inspects the hardware vendor ID and event synthesis flags, rejecting inputs from trackpads, keyboards, and mice that do not match Logitech's USB vendor identifier. By enforcing this policy at the hook stage—before any remapping logic executes—the system ensures that native OS gestures and third-party hardware remain unaffected.

What is temporary diversion in the context of OpenLogi?

Temporary diversion is a firmware-level state managed via the ReprogControlsV4::set_cid_reporting API in openlogi-device/src/reprog_controls.rs. When enabled, the Logitech device stops transmitting its default button event and instead emits either raw-XY motion data or a diverted-button signature. This allows OpenLogi to suppress native clicks for gesture recognition (such as holding a button while swiping) or to implement "remap-to-nothing" behaviors where the physical button performs no default action.

How does OpenLogi handle cross-platform event injection?

The openlogi-inject crate abstracts platform differences behind a common Action interface. On macOS, it utilizes CoreGraphics event taps (CGEventTap) in src/inject/macos.rs; on Linux, it writes to virtual uinput devices; and on Windows, it invokes the SendInput Win32 API. All platform implementations converge in src/inject.rs, which constructs synthetic events from the TOML action definitions parsed by openlogi-core/src/config.rs.

Where is the button mapping configuration stored and how is it updated?

User configurations reside in a TOML file typically located at ~/.config/openlogi/config.toml, parsed by openlogi-core/src/config.rs. The openlogi-agent-core maintains this as the single source of truth during runtime. Changes propagate through the TarRPC-based IPC layer defined in openlogi-ipc/src/ipc.rs, allowing the openlogi-desktop GUI and openlogi-overlay to modify bindings in real-time. When updated, the new configuration is persisted to disk and immediately applied to the device via the ReprogControlsV4 abstraction.

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 →