# How OpenLogi Handles Input Capture on macOS, Linux, and Windows

> Discover how OpenLogi unifies input capture across macOS, Linux, and Windows using a single Rust API. Learn about platform specific backends for low level event handling.

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

---

**OpenLogi uses a single public API `openlogi_hook::Hook` that abstracts platform-specific backends—CGEventTap on macOS, evdev/uinput on Linux, and WH_MOUSE_LL/WH_KEYBOARD_LL hooks on Windows—to capture low-level mouse, scroll, and keyboard events through a unified Rust interface.**

The OpenLogi input capture system provides a cross-platform Rust API for monitoring low-level HID events across macOS, Linux, and Windows. According to the AprilNEA/OpenLogi source code, the implementation isolates OS-specific mechanisms behind the `HookBackend` trait in the `crates/openlogi-hook` crate, allowing applications to subscribe to mouse movements, button presses, scroll events, and keyboard input using a single consistent interface regardless of the host operating system.

## The Unified Hook API

The public interface for OpenLogi input capture resides in [`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs). Applications interact with the `Hook` struct, which provides a platform-agnostic API for installing and managing input hooks.

To begin capturing events, call `Hook::start()` with a closure that receives `HookEvent` enums and returns an `EventDisposition`:

- **HookEvent**: An enum variant containing either `MouseEvent` or `KeyEvent` data structures with normalized fields like position, button states, and scroll delta.
- **EventDisposition**: The callback must return either `PassThrough` to allow the event to reach the system, or `Suppress` to block it from other applications.

The `Hook` struct uses `cfg_select!` at compile time to instantiate the appropriate `HookBackend` implementation for the target platform, ensuring zero-cost abstraction with no runtime overhead for the dispatch logic.

## Platform-Specific Backend Implementations

While the API remains consistent, each operating system requires distinct low-level mechanisms for intercepting input events.

### macOS: CGEventTap and Accessibility Requirements

On macOS, OpenLogi input capture relies on a **CGEventTap** inserted at the HID (Human Interface Device) system location, implemented in [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs). The backend spawns a dedicated thread running a CFRunLoop to service the tap callback.

Key implementation details include:

- **Accessibility Permissions**: The hook requires the Accessibility privacy grant. The `Hook::has_accessibility()` method verifies permissions by calling `AXIsProcessTrusted` and probing a filtering tap via `can_filter_events` to ensure the process can actually intercept events.
- **Event Translation**: Native `CGEvent` objects are translated into `MouseEvent` or `KeyEvent` structs through the `translate` and `translate_key` functions, normalizing macOS-specific data into the cross-platform format.
- **Watchdog Threads**: To ensure reliability, `spawn_callback_watchdog` monitors callback execution time to prevent system stalls, while `spawn_lifecycle_watchdog` monitors the tap's health.
- **Foreground Application Tracking**: The implementation observes `NSWorkspaceDidActivateApplicationNotification` via `watch_frontmost_application_activations` to track which application currently has focus, enabling context-aware input processing.

### Linux: evdev Device Grabbing and uinput Re-injection

The Linux backend in [`crates/openlogi-hook/src/linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/linux.rs) implements OpenLogi input capture through direct **evdev** interface access, bypassing the display server for low-level hardware events.

The architecture functions as follows:

- **Device Enumeration**: The backend discovers mouse devices under `/dev/input/` and spawns a worker thread for each physical device, enabling simultaneous multi-mouse support.
- **Event Grabbing**: Each thread performs an exclusive grab on its evdev device, preventing the events from reaching the standard input subsystem while the hook is active.
- **Re-injection via uinput**: When the user callback returns `PassThrough`, events are re-injected into the system through a **uinput** virtual device, ensuring other applications receive the input. If `Suppress` is returned, the event is simply dropped.
- **Permissions Model**: Unlike macOS, no special privacy permission is required; the process only needs read/write access to device nodes, typically achieved by adding the user to the `input` group.
- **Scroll Handling**: Scroll-wheel events carry both line-based and pixel-based delta data via `ScrollDelta`, mirroring the macOS semantics for consistent cross-platform behavior.

### Windows: Low-Level Mouse and Keyboard Hooks

On Windows, OpenLogi uses **WH_MOUSE_LL** and **WH_KEYBOARD_LL** low-level hooks installed via `SetWindowsHookExW`, implemented across [`crates/openlogi-hook/src/windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/windows.rs) and [`crates/openlogi-hook/src/windows_worker.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/windows_worker.rs).

The Windows implementation distinguishes itself through:

- **User-Mode Operation**: No additional permissions are required beyond standard user privileges, as low-level hooks are standard Windows APIs available to all processes.
- **State Machine Architecture**: The [`windows_worker.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/windows_worker.rs) module defines a robust worker thread managed by a state machine using `WorkerPhase` and `WorkerEvent` enums, handling thread startup, graceful shutdown, and error recovery.
- **Synthetic Event Filtering**: To prevent recursive event loops, the backend checks for `openlogi_inject::SYNTHETIC_EVENT_USER_DATA` in the event's user-data field, filtering out any events generated by OpenLogi itself while processing physical hardware input.
- **Message Loop Integration**: The hook runs on a background thread that pumps a standard Windows message loop, required for low-level hook callbacks to function correctly.

## Cross-Platform Event Flow

Regardless of the underlying platform, OpenLogi input capture follows a consistent four-phase lifecycle:

1. **Installation**: Calling `Hook::start(cb)` instantiates the platform-specific backend and begins the capture thread. The backend initializes native resources (CGEventTap, evdev files, or Windows hooks).
2. **Event Loop**: The backend thread captures native OS events, translates them into `HookEvent` structures, and invokes the user-supplied callback on the hook's dedicated thread.
3. **Disposition**: The callback examines the event and returns `EventDisposition::PassThrough` to allow the event to continue (or be re-injected on Linux) or `EventDisposition::Suppress` to block it from the rest of the system.
4. **Shutdown**: Calling `Hook::stop()` or dropping the `Hook` object signals the backend to terminate the worker thread, release OS resources (closing taps, ungrabbing devices, or unhooking Windows procedures), and perform clean exit.

## Implementation Example

The following Rust code demonstrates initializing the hook with proper macOS permission checks and handling both mouse and keyboard events:

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

fn main() -> Result<(), openlogi_hook::HookError> {
    // Ensure the process has the required permission on macOS.
    if !Hook::has_accessibility() {
        Hook::prompt_accessibility();
        eprintln!("Please grant Accessibility permission and restart.");
        return Ok(());
    }

    // Install the hook. The closure runs on the hook's background thread.
    let hook = Hook::start(|event| {
        match event {
            HookEvent::Mouse(me) => {
                println!("Mouse event: {:?}", me);
                EventDisposition::PassThrough
            }
            HookEvent::Key(ke) => {
                println!("Key event: {:?}", ke);
                EventDisposition::PassThrough
            }
        }
    })?;

    // The hook runs until the program exits or `hook.stop()` is called.
    // … do other work here …
    // When finished:
    hook.stop();
    Ok(())
}

```

You can also retrieve the current foreground application or cursor position using auxiliary functions:

```rust
// Get the frontmost application (macOS & Windows)
if let Some(app) = openlogi_hook::frontmost_application() {
    println!("Frontmost app: {} ({})", app.display_name, app.id);
}

// Get cursor coordinates (macOS & Windows; None on Wayland)
if let Some(pos) = openlogi_hook::cursor_position() {
    println!("Cursor at ({}, {})", pos.x, pos.y);
}

```

## Summary

- OpenLogi exposes a unified `Hook` API in [`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs) that compiles to different backends per platform using `cfg_select!`.
- macOS requires Accessibility permissions and uses `CGEventTap` with dedicated watchdog threads to monitor callback execution and tap lifecycle.
- Linux utilizes `evdev` for reading from `/dev/input` and `uinput` for re-injection, requiring membership in the `input` group rather than special privacy grants.
- Windows employs `WH_MOUSE_LL` and `WH_KEYBOARD_LL` low-level hooks managed by a state machine worker thread that filters synthetic events via `SYNTHETIC_EVENT_USER_DATA`.

## Frequently Asked Questions

### What permissions does OpenLogi require for input capture on macOS?

On macOS, OpenLogi requires the Accessibility privacy grant. The `Hook::has_accessibility()` method checks `AXIsProcessTrusted` and probes a filtering tap via `can_filter_events` to verify the process can intercept events. You can prompt users with `Hook::prompt_accessibility()` if access is not granted, and the application should guide users to System Preferences to enable the permission.

### How does OpenLogi handle multiple input devices on Linux?

OpenLogi supports simultaneous input devices by spawning a dedicated worker thread for each device discovered under `/dev/input/`. The implementation exclusively grabs each evdev device to prevent events from reaching other applications, then uses a separate **uinput** virtual device to re-inject events when the callback returns `PassThrough`. This architecture ensures that multiple mice can operate concurrently without event loss.

### Does OpenLogi filter its own synthetic events on Windows?

Yes. On Windows, OpenLogi prevents feedback loops by checking for the `openlogi_inject::SYNTHETIC_EVENT_USER_DATA` marker in the event's user-data field. This identifier distinguishes between physical hardware events and events generated programmatically by OpenLogi itself, allowing the hook to suppress its own synthetic output while processing genuine user input.

### Which input types does OpenLogi support on each platform?

OpenLogi captures mouse-button presses, scroll events, pointer movements, and keyboard events on macOS and Windows. On Linux, the current implementation focuses on mouse devices via `evdev`, capturing button states, pointer coordinates, and scroll-wheel data with both line and pixel precision through the `ScrollDelta` struct. Keyboard support on Linux varies based on specific evdev configurations and device permissions.