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

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 defines the Backend type alias using conditional compilation:

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 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 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 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 and 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 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 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:

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 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 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.

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 →