# How OpenLogi Captures Input Events on macOS, Linux, and Windows

> Discover how OpenLogi captures input events across macOS, Linux, and Windows. Learn about its cross-platform hook implementation using CGEventTap, evdev, and WH_MOUSE_LL.

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

---

**OpenLogi implements a low-level, cross-platform input-capture layer in the `openlogi-hook` crate, utilizing `CGEventTap` on macOS, `evdev` with `uinput` on Linux, and `WH_MOUSE_LL` hooks on Windows to capture device-specific mouse and keyboard events.**

OpenLogi captures input events using native OS primitives that provide reliable, low-latency streams of mouse and keyboard data. The project achieves this through the **`openlogi-hook`** crate, which abstracts platform-specific APIs into a unified Rust interface that the `openlogi-agent` consumes. Each operating system implementation runs on a dedicated thread with its own event loop, translating raw system events into a common `HookEvent` enum.

## Cross-Platform Architecture with HookBackend

The **`HookBackend`** type defined in [`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs) serves as the abstraction layer that compiles to platform-specific implementations at build time. When the `openlogi-agent` initializes, it creates a `HookBackend` instance that spawns the appropriate OS-specific thread and forwards captured events to the gesture watcher and DPI controller.

All three backends emit events through the **`HookEvent`** enum, which normalizes platform differences into uniform `MouseEvent`, `KeyEvent`, and `ScrollDelta` variants. This allows the rest of the system to consume input uniformly regardless of the underlying operating system.

## macOS Input Capture Using CGEventTap

On macOS, OpenLogi captures events using **`CGEventTap`** from the CoreGraphics framework. The implementation in [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs) creates a dedicated thread that runs a `CFRunLoop` owning the event tap.

The tap is created with `CGEventTap::new(CGEventTapLocation::HID, …)` and runs in **`HeadInsertEventTap`** mode, ensuring OpenLogi receives every mouse event before the system processes it. To differentiate input devices—such as distinguishing a high-resolution mouse wheel from a trackpad—the code calls `CGEventCopyIOHIDEvent` followed by `IOHIDEventGetSenderID` to map the CG event back to its originating HID device.

Additionally, a foreground-application observer monitors `NSWorkspaceDidActivateApplicationNotification` to track the active application, enabling per-application action scoping.

## Linux Input Capture Using evdev and uinput

The Linux implementation in [`crates/openlogi-hook/src/linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/linux.rs) captures events through the **`evdev`** interface. For each mouse device, a thread opens the device using `evdev::enumerate()` and calls **`grab()`** to take exclusive ownership of the event stream, preventing other applications from receiving the raw input.

Events pass through a **`translate()`** function that converts raw evdev data into the unified `MouseEvent` enum, handling buttons, motion, and scroll data. After processing, OpenLogi re-injects events through a virtual **`uinput::VirtualDevice`** so that other applications continue to receive the same input. The implementation specifically distinguishes between wheel devices and trackpad devices to handle high-resolution scroll data correctly.

## Windows Input Capture Using Low-Level Hooks

On Windows, OpenLogi installs low-level hooks via **`SetWindowsHookExW`** with the **`WH_MOUSE_LL`** and **`WH_KEYBOARD_LL`** flags, as implemented in [`crates/openlogi-hook/src/windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/windows.rs). A dedicated hook thread runs a message pump using `GetMessageW` and `DispatchMessageW` to keep the hooks alive and receive system callbacks.

The hook callbacks—`mouse_proc` and `keyboard_proc`—receive raw **`MSLLHOOKSTRUCT`** and **`KBDLLHOOKSTRUCT`** data, which the backend converts into OpenLogi’s internal event types. Unlike macOS and Linux, Windows does not expose a device ID through these hooks, so events are treated as coming from the default mouse. The implementation requires no additional permissions but monitors hook enable state and gracefully tears down the hooks on shutdown.

## Safety, Device Identification, and Watchdog Logic

Each backend implements device identification appropriate to its platform. macOS extracts the HID sender ID via `CGEventCopyIOHIDEvent`; Linux reads the `input_id` from the evdev node; Windows lacks device granularity and aggregates all input.

Lifecycle management is handled by a watchdog defined in **[`crates/openlogi-hook/src/watchdog.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/watchdog.rs)**. The watchdog monitors the hook’s health, re-enabling the tap if macOS disables it, or restarting the thread if the Linux or Windows hook crashes. This ensures continuous input capture even when the OS interferes with the monitoring layer.

## Implementation Example

The following Rust code demonstrates how the `openlogi-agent` initializes the input hook and subscribes to events:

```rust
use openlogi_hook::HookBackend;

/// Initialise the OS-level input hook. This spawns the platform-specific
/// thread and returns a handle that can be used to stop the hook.
fn start_input_hook() -> Result<HookBackend, openlogi_hook::HookError> {
    // `HookBackend::new()` chooses the implementation at compile time
    // (macOS → CGEventTap, Linux → evdev, Windows → WH_MOUSE_LL).
    let backend = HookBackend::new()?;
    Ok(backend)
}

// In the agent (simplified):
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let hook = start_input_hook()?;

    // Subscribe to events.
    hook.set_event_callback(|event| {
        // `event` is a `HookEvent` (MouseEvent, KeyEvent, …)
        println!("Captured: {:?}", event);
    });

    // Run until the agent shuts down…
    hook.wait_until_stopped();
    Ok(())
}

```

## Summary

- **OpenLogi** captures input events through the `openlogi-hook` crate, which provides a unified interface over OS-specific APIs.
- **macOS** uses `CGEventTap` in `HeadInsertEventTap` mode with `CFRunLoop`, extracting device IDs via `IOHIDEventGetSenderID` in [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs).
- **Linux** uses `evdev` with exclusive `grab()` and re-injects events via `uinput`, handling device-specific translation in [`crates/openlogi-hook/src/linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/linux.rs).
- **Windows** uses `SetWindowsHookExW` with `WH_MOUSE_LL` and `WH_KEYBOARD_LL` hooks, running a message pump in [`crates/openlogi-hook/src/windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/windows.rs).
- A **watchdog** in [`crates/openlogi-hook/src/watchdog.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/watchdog.rs) monitors hook health and handles re-arming or crash recovery across all platforms.
- The **`HookBackend`** API in [`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs) normalizes all events into the `HookEvent` enum for consumption by `openlogi-agent`.

## Frequently Asked Questions

### How does OpenLogi differentiate between a mouse and a trackpad?

On **macOS**, OpenLogi calls `CGEventCopyIOHIDEvent` and `IOHIDEventGetSenderID` to extract the originating HID device ID from the `CGEventTap` stream, allowing it to distinguish high-resolution mouse wheels from trackpads. On **Linux**, the implementation reads the `input_id` from the evdev node to identify the specific device. **Windows** does not expose device IDs through the `WH_MOUSE_LL` hook, so all events are treated as generic mouse input.

### Why does OpenLogi need to re-inject events on Linux?

On Linux, OpenLogi calls **`grab()`** on the evdev device to take exclusive ownership of the input stream, which prevents other applications from receiving events. To ensure the system remains usable, the backend creates a virtual **`uinput::VirtualDevice`** and re-injects processed events back into the kernel, allowing the original input to reach other applications while OpenLogi monitors or modifies the stream.

### What happens if the OS disables the input hook?

OpenLogi implements a **watchdog** mechanism in [`crates/openlogi-hook/src/watchdog.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/watchdog.rs) that monitors the health of the input capture layer. If macOS disables the `CGEventTap` due to permission changes or if the hook thread crashes on Linux or Windows, the watchdog detects the failure and re-enables or restarts the appropriate capture mechanism automatically.

### Does OpenLogi require elevated permissions to capture input?

**macOS** requires Accessibility permissions for `CGEventTap` to function. **Linux** requires read/write access to `/dev/input/event*` devices, typically granted via the `input` group or running as root. **Windows** does not require additional permissions for `WH_MOUSE_LL` or `WH_KEYBOARD_LL` hooks, though the application must run in the user's session to install the hooks successfully.