How OpenLogi Button Remapping Utilizes the macOS Input Hook System
OpenLogi implements button remapping by installing a low-level CGEventTap that intercepts macOS mouse events before they reach applications, translates raw CGEvent objects into internal MouseEvent structures, and synthesizes remapped events via the injection subsystem while filtering out synthetic events to prevent infinite loops.
OpenLogi provides system-level button remapping for Logitech devices by tapping into the macOS input stream at the Core Graphics layer. The application utilizes the OS input hook system to intercept hardware events before they propagate to the window server or user applications. This deep integration, implemented primarily in crates/openlogi-hook/src/macos.rs, allows OpenLogi to suppress original button presses and inject transformed events with minimal latency.
Installing the Low-Level Event Tap
OpenLogi creates the hook in crates/openlogi-hook/src/macos.rs within the Backend::start method on a dedicated background thread. Before installing the event tap, the implementation validates that the process has Accessibility permission via Backend::has_accessibility and verifies that a filtering tap can be established through can_filter_events.
The tap is configured to listen for all mouse-related event types—including button down/up, scroll, drag, and move events—via the hooked_event_types() function (lines 779-796). This configuration ensures the hook receives every hardware input event before it reaches the system event queue.
Translating Raw OS Events to OpenLogi Structures
When the tap callback fires, the translate() function converts macOS CGEvent objects into OpenLogi's internal MouseEvent enum. The translation layer handles distinct event types as follows:
- LeftMouseDown / LeftMouseUp: Maps to
MouseEvent::Button { id: ButtonId::LeftClick, ... }(lines 999-1006) - RightMouseDown / RightMouseUp: Maps to
ButtonId::RightClick(lines 1009-1016) - OtherMouseDown / OtherMouseUp: Uses
button_number_to_id()(lines 24-36) to convert hardware button numbers 0-4 into logicalButtonIdvalues (left, right, middle, back, forward) - ScrollWheel: Builds a
MouseEvent::Scrollwith precise or line-based deltas (lines 363-371) - Mouse movement: Creates a
MouseEvent::Movedwith delta X/Y coordinates (lines 68-84)
For each button event, OpenLogi identifies the source hardware via the HID event_sender_id and caches device metadata through sender_device_info() (lines 58-66). This device-specific tracking ensures that remaps apply only to Logitech devices while ignoring trackpads or other manufacturers.
Preventing Feedback Loops with Synthetic Event Tagging
OpenLogi’s inject subsystem tags every synthetic event with the constant openlogi_inject::SYNTHETIC_EVENT_USER_DATA. During translation, the hook checks this user-data field early in the pipeline (lines 83-97) and returns None for tagged events. This filtering mechanism ensures that injected remapped events do not re-enter the hook, preventing infinite feedback loops that would otherwise flood the system with recursive input.
Executing Button Remaps and Event Injection
The run_tap_callback constructs a HookEvent::Mouse and passes it to the core remapping engine. When the active profile defines a button remap—such as mapping ButtonId::Back to ButtonId::Forward—the engine returns EventDisposition::Suppress for the original event. The OpenLogi agent then invokes the openlogi-inject crate to post a new synthetic CGEvent with the target button ID, effectively replacing the physical input with the remapped action.
Safety Mechanisms and Cleanup
The hook implementation includes multiple safeguards to prevent system instability. The event loop runs in short 500ms slices and continuously validates that Accessibility permission remains granted. If permissions are revoked or the OS disables the tap, the system disables the hook immediately to prevent input lockup (lines 481-489).
A dedicated watchdog thread monitors the hook callback and aborts the process if the callback stalls (lines 30-38). This guarantee ensures that a hung HID tap cannot freeze the entire macOS input system, even if the remapping logic encounters an error.
Code Examples
Defining a Button Remap in Configuration
[profile.my_profile]
# Remap the "Back" button (ButtonId::Back) to "Forward"
button_remap = { Back = "Forward" }
Wiring the Hook Callback
let hook = openlogi_hook::Hook::new(|hook_event| {
match hook_event {
HookEvent::Mouse(mouse) => {
// Core logic decides whether to suppress or pass through
openlogi_core::process_mouse(mouse)
}
HookEvent::Key(key) => openlogi_core::process_key(key),
}
});
hook.start().expect("Failed to start OS hook");
Injecting a Synthetic Button Event
use openlogi_inject::inject::macos::{Inject, SYNTHETIC_EVENT_USER_DATA};
fn inject_forward_click() {
// Build a synthetic CGEvent for a forward-button click
let mut event = CGEvent::new_mouse_event(
CGEventType::OtherMouseDown,
CGPoint::new(0.0, 0.0),
4, // button number for Forward
);
event.set_integer_value_field(
EventField::EVENT_SOURCE_USER_DATA,
SYNTHETIC_EVENT_USER_DATA,
);
event.post(CGEventTapLocation::HID);
}
Summary
- OpenLogi installs a
CGEventTapincrates/openlogi-hook/src/macos.rsto intercept hardware events at the OS level before they reach applications. - The
translate()function convertsCGEventobjects intoMouseEventstructures while filtering synthetic events using theSYNTHETIC_EVENT_USER_DATAconstant. - Button remaps execute by returning
EventDisposition::Suppressfor original events and injecting transformed events via theopenlogi-injectcrate. - Safety features include continuous Accessibility permission monitoring, short run-loop slices, and a watchdog thread to prevent system hangs.
Frequently Asked Questions
What macOS permissions does OpenLogi require for button remapping?
OpenLogi requires Accessibility permission to create the CGEventTap. The Backend::has_accessibility check verifies this before installing the hook, and the system continuously monitors permissions during operation to avoid input system conflicts.
How does OpenLogi prevent remapped buttons from triggering infinite loops?
The injection subsystem tags synthetic events with the SYNTHETIC_EVENT_USER_DATA constant. The hook's translate() function checks this field and returns None for tagged events, ensuring injected events are ignored and only physical hardware triggers remapping logic.
Can OpenLogi distinguish between different mice and apply selective remapping?
Yes. The system captures the HID event_sender_id for each event and caches device info via sender_device_info(). This allows the remapping engine to apply profile-specific button maps only to Logitech devices while ignoring trackpads or other manufacturers.
What prevents OpenLogi from freezing system input if it crashes?
A dedicated watchdog thread monitors the hook callback and aborts the process if the callback stalls. Additionally, the hook uses short 500ms run-loop slices and validates Accessibility permissions continuously, ensuring clean shutdown if access is revoked.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →