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

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

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:

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

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 →