# How OpenLogi Button Remapping Utilizes the macOS Input Hook System

> Discover how OpenLogi button remapping uses the macOS input hook system. Learn how it intercepts mouse events, translates them, and synthesizes new ones for custom control.

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

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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 logical `ButtonId` values (left, right, middle, back, forward)
- **ScrollWheel**: Builds a `MouseEvent::Scroll` with precise or line-based deltas (lines 363-371)
- **Mouse movement**: Creates a `MouseEvent::Moved` with 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

```toml
[profile.my_profile]

# Remap the "Back" button (ButtonId::Back) to "Forward"

button_remap = { Back = "Forward" }

```

### Wiring the Hook Callback

```rust
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

```rust
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 **`CGEventTap`** in [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs) to intercept hardware events at the OS level before they reach applications.
- The **`translate()`** function converts `CGEvent` objects into `MouseEvent` structures while filtering synthetic events using the **`SYNTHETIC_EVENT_USER_DATA`** constant.
- Button remaps execute by returning **`EventDisposition::Suppress`** for original events and injecting transformed events via the **`openlogi-inject`** crate.
- **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.