# How the Input Hook Mechanism Operates Across macOS, Linux, and Windows in OpenLogi

> Explore the input hook mechanism in OpenLogi across macOS Linux and Windows Understand how CGEventTap evdev and WH_MOUSE LL hooks capture events for cross platform functionality

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

---

**OpenLogi captures mouse and keyboard events through a unified `Hook` abstraction that uses `CGEventTap` on macOS, `evdev` grabs with `uinput` replay on Linux, and low-level `WH_MOUSE_LL`/`WH_KEYBOARD_LL` hooks on Windows.**

OpenLogi implements this cross-platform input hook mechanism in the `openlogi-hook` crate, which compiles to distinct OS-specific backends while exposing a single consistent API. The architecture selects the appropriate platform implementation at compile time using a `cfg_select!` macro, allowing the rest of the application to call `Hook::start` and `Hook::stop` without managing platform differences.

## Platform-Backend Architecture

The crate root in [`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs) defines the `Backend` type alias using conditional compilation:

```rust
cfg_select! {
    target_os = "macos" => { type Backend = macos::Backend; }
    target_os = "linux" => { type Backend = linux::Backend; }
    target_os = "windows" => { type Backend = windows::Backend; }
    _ => { type Backend = Unsupported; }
}

```

Each backend implements the `HookBackend` trait, exposing identical methods such as `Hook::start`, `Hook::stop`, `Hook::has_accessibility`, and `Hook::list_event_taps`. This design allows dependent crates to import `openlogi_hook::Hook` and operate identically across all supported platforms.

## macOS: CGEventTap and Watchdog Threads

**Core Mechanism**  
The macOS backend in [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs) installs a **CGEventTap** at the HID (Human Interface Device) location to filter raw mouse and keyboard events before they reach the Window Server. The tap is created via `CGEventTapCreate` and configured as a filtered tap that can modify or suppress events.

**Threading and Run-Loop**  
A dedicated thread spawns the tap and attaches it to a `CFRunLoop`. The loop runs in 500 ms slices using `CFRunLoopRunInMode`, allowing periodic checks for shutdown signals without blocking indefinitely.

**Watchdog Protection**  
The [`crates/openlogi-hook/src/macos/watchdog.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos/watchdog.rs) module implements two safeguards:
- **Callback watchdog**: Monitors the user-supplied callback and aborts the process if execution exceeds a configurable time budget, preventing a "freeze hazard" that would block system-wide input.
- **Lifecycle watchdog**: Tracks the tap’s enabled state and the accessibility permission grant. If the OS revokes accessibility rights or the tap repeatedly disables itself, the watchdog terminates the process to restore normal input operation.

**Device Identification**  
When a `CGEvent` arrives, the backend calls `IOHIDEventGetSenderID` to obtain the device’s IOKit registry ID. It then walks the IOKit registry—caching results per sender—to extract `VendorID`, `ProductID`, and the product name string. This `EventDevice` metadata enables Logitech-specific remapping profiles.

**Accessibility Checks**  
`Hook::has_accessibility` verifies both `AXIsProcessTrusted` and the ability to create a filtering tap (`can_filter_events`). `Hook::prompt_accessibility` invokes the system permission prompt via `AXIsProcessTrustedWithOptions` when needed.

## Linux: evdev Grabs and Virtual uinput Devices

**Core Mechanism**  
The Linux backend in [`crates/openlogi-hook/src/linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/linux.rs) operates by **exclusively grabbing** physical mouse devices under `/dev/input/` using `libevdev`. Intercepted events are passed to the user callback; allowed events are then written to a matching **virtual uinput device** that re-injects them into the kernel input subsystem.

**Per-Device Threading**  
The backend spawns one worker thread per detected mouse. Each thread polls its assigned device via `Device::fetch_events`, translates raw evdev events into `MouseEvent` structures, invokes the callback, and writes permitted events to the virtual device. A pipe (`stop_pipes`) wakes the poll when `Hook::stop` is called, enabling graceful shutdown.

**Recursion Prevention**  
To avoid capturing its own synthetic events, the backend filters out any device whose name starts with the `OPENLOGI_DEVICE_PREFIX` constant (`"OpenLogi "`). This ensures only physical hardware triggers the callback chain.

**Scroll Handling**  
Scroll wheel events are translated to `REL_WHEEL_HI_RES` with a value of 120 units per tick, matching the Linux kernel’s high-resolution scroll semantics.

**Front-Most Application Detection**  
The backend resolves the active application using X11 via `x11rb` (querying `WmClass`) or, on Wayland compositors, via the `wlr-foreign-toplevel` protocol or GNOME Shell D-Bus interfaces implemented in [`wlr_foreign_toplevel.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/wlr_foreign_toplevel.rs) and [`gnome_shell.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/gnome_shell.rs). Global cursor position is obtained from X11’s `query_pointer`; Wayland deliberately provides no global coordinate API, so that functionality is limited to X11 sessions.

## Windows: Low-Level Hook Message Pump

**Hook Installation**  
The Windows backend in [`crates/openlogi-hook/src/windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/windows.rs) installs **low-level hooks** using `SetWindowsHookExW` with `WH_MOUSE_LL` and `WH_KEYBOARD_LL`. These hooks operate on a dedicated thread separate from the main application to prevent UI blocking.

**Message Pump Thread**  
The `hook_thread` procedure runs a classic Win32 message pump: it calls `GetMessageW` and `DispatchMessageW` in a loop, storing the user callback in a global `Mutex<Option<HookCallback>>` accessible to the hook procedures.

**Event Translation**  
The `mouse_hook_proc` and `keyboard_hook_proc` functions translate Win32 messages into internal events:
- **Buttons**: `WM_LBUTTONDOWN`, `WM_RBUTTONDOWN`, and `WM_XBUTTONDOWN` map to `MouseEvent::Button`.
- **Scroll**: `WM_MOUSEWHEEL` and `WM_MOUSEHWHEEL` produce `MouseEvent::Scroll` using the constant `WHEEL_DELTA = 120.0`.
- **Motion**: `WM_MOUSEMOVE` generates `MouseEvent::Moved`, using a thread-local `LAST_POINT` variable to calculate cursor deltas.
- **Keyboard**: `WM_KEYDOWN` and `WM_KEYUP` map to `KeyEvent` with modifier flags derived from `VK_SHIFT`, `VK_CONTROL`, `VK_MENU`, and `VK_LWIN`.

**Foreground Application**  
The helper in [`crates/openlogi-hook/src/windows/foreground.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/windows/foreground.rs) calls `GetForegroundWindow`, obtains the process ID via `GetWindowThreadProcessId`, opens the process handle, and retrieves the full executable path with `QueryFullProcessImageNameW`. The lower-cased path serves as the profile identifier, while the file stem provides the display name. Cursor position is queried directly via `GetCursorPos`.

## Unified Hook API and Usage Example

Regardless of the underlying platform, applications interact with the hook through the common `Hook` interface:

```rust
use openlogi_hook::{Hook, HookEvent, EventDisposition};

fn main() -> Result<(), openlogi_hook::HookError> {
    // macOS only: verify accessibility permissions
    if !Hook::has_accessibility() {
        Hook::prompt_accessibility();
        eprintln!("Grant Accessibility access and retry");
        return Ok(());
    }

    // Install the hook with a user callback
    let hook = Hook::start(|event| {
        println!("Captured: {event:?}");
        
        // Example: suppress all right-clicks
        match event {
            HookEvent::Mouse(openlogi_hook::MouseEvent::Button { 
                id: openlogi_hook::ButtonId::RightClick, 
                pressed: true, 
                .. 
            }) => EventDisposition::Suppress,
            _ => EventDisposition::PassThrough,
        }
    })?;

    // ... application logic ...

    // Graceful shutdown
    hook.stop();
    Ok(())
}

```

The callback receives `HookEvent` variants (mouse move, button, scroll, or keyboard) and must return quickly to avoid triggering the macOS watchdog or stalling the Windows message pump.

## Summary

- **Compile-time selection**: The `cfg_select!` block in [`lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/lib.rs) aliases `Backend` to the appropriate platform module, ensuring zero-cost abstraction with no runtime dispatch.
- **macOS**: Uses `CGEventTap` at the HID layer with dual watchdog threads (callback and lifecycle) and IOKit registry walks for device identification.
- **Linux**: Employs `evdev` exclusive grabs per physical device, re-injecting allowed events via `uinput`, with separate threads per mouse and recursion prevention via device name filtering.
- **Windows**: Relies on `WH_MOUSE_LL` and `WH_KEYBOARD_LL` low-level hooks on a dedicated message-pump thread, tracking cursor deltas via thread-local storage.
- **Common API**: The `Hook` struct exposes `start`, `stop`, `has_accessibility`, and `list_event_taps` uniformly, abstracting OS-specific permission models and threading details.

## Frequently Asked Questions

### How does OpenLogi prevent system input freeze if the callback hangs?

On macOS, the **callback watchdog** in [`watchdog.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/watchdog.rs) monitors execution time and aborts the process if the user callback exceeds its budget, ensuring the system tap does not stall. On Windows, the hook procedure runs on a dedicated thread; if the callback blocks indefinitely, it stalls that thread’s message pump, which the OS may terminate to restore input. Linux evdev reads are inherently non-blocking per device thread, so a stalled callback only affects its specific device thread.

### Why does the Linux backend require both evdev and uinput?

The backend **grabs the physical device exclusively** via evdev to intercept events before they reach the display server, then uses a **virtual uinput device** to re-inject events that the callback permits to pass through. This architecture allows OpenLogi to suppress or modify events while maintaining compatibility with applications that expect standard input device behavior. The `OPENLOGI_DEVICE_PREFIX` filter prevents the backend from recursively capturing its own uinput events.

### What permissions are required on each platform?

- **macOS**: Requires Accessibility permission (TCC) verified via `AXIsProcessTrusted` and the ability to create a filtering tap (`can_filter_events`).
- **Linux**: Requires read access to `/dev/input/event*` (typically via the `input` group) and write access to `/dev/uinput` (typically via the `uinput` group).
- **Windows**: Requires no special permissions beyond standard user privileges, as low-level hooks (`WH_MOUSE_LL`/`WH_KEYBOARD_LL`) are per-process and do not require administrator elevation.

### How does device identification work for Logitech-specific remapping?

On **macOS**, the backend uses `IOHIDEventGetSenderID` to obtain the IOKit registry ID, then queries `VendorID` and `ProductID` via `IORegistryEntrySearchCFProperty`. On **Linux**, it reads evdev device properties and node names from `/sys/class/input/`. **Windows** low-level hooks do not provide native device identifiers in the `MSLLHOOKSTRUCT` or `KBDLLHOOKSTRUCT`, so events are treated as generic with modifiers derived from virtual-key state flags.