How OpenLogi Hook Crate Captures and Synthesizes Input Events: A Cross-Platform Deep Dive

The openlogi-hook crate provides a unified Rust API for capturing and injecting mouse and keyboard events across macOS, Linux, and Windows, abstracting platform-specific implementations behind a safe, ergonomic façade.

The openlogi-hook crate serves as the core input interception layer for the AprilNEA/OpenLogi project, enabling both passive monitoring and active synthesis of hardware input events. According to the source code, this crate unifies three distinct platform backends behind a single consistent interface defined in src/lib.rs. It handles the complete lifecycle of input capture—from low-level OS hooks to high-level event filtering—while preventing feedback loops when injecting synthetic events.

Architecture of the Hook Crate

At the center of the crate is the Hook struct, which acts as a public façade for starting, stopping, and querying OS-level input hooks. Located in src/lib.rs, this struct owns a platform-specific backend that implements the HookBackend trait, ensuring safe teardown via Rust's Drop implementation.

The Backend Abstraction Layer

The HookBackend trait defines the contract that every platform implementation must satisfy, isolating OS-specific code while maintaining API stability. Each backend—macos::Backend, linux::Backend, and windows::Backend—implements methods including start, stop, has_accessibility, prompt_accessibility, list_event_taps, frontmost_app, and cursor_position.

Event Types and Device Identity

The crate defines HookEvent as a union of MouseEvent and KeyEvent. MouseEvent variants cover button presses, scroll wheel actions, movement deltas, and capture interruption signals. KeyEvent structures contain keycodes, press states, and modifier flags. Additionally, EventDevice holds optional vendor and product IDs along with product names, populated by each backend from the native event source to identify specific hardware devices.

Platform-Specific Input Capture

The crate implements three distinct capture mechanisms, each tailored to the host operating system's native input APIs.

macOS CGEventTap Implementation

On macOS, the backend in src/macos.rs creates a CGEventTap callback routed through a dedicated private run-loop thread. The translate function converts native CGEvent objects into the crate's cross-platform MouseEvent and KeyEvent types. This backend also manages two independent watchdog threads that protect against stalled callbacks or taps that get repeatedly disabled, exiting the process with a special exit code to recover system input if the hook becomes unresponsive.

Linux evdev and uinput Integration

The Linux backend in src/linux.rs accesses physical input devices through /dev/input using the evdev crate. It grabs raw device events and re-injects synthesized events through a uinput virtual device. This approach allows the hook to intercept hardware events at the kernel level while maintaining the ability to inject artificial input that appears identical to genuine hardware events.

Windows Low-Level Mouse Hooks

On Windows, the implementation in src/windows.rs installs a low-level mouse hook using the WH_MOUSE_LL window hook type. This captures global mouse events before they reach the destination application window, providing the same event filtering capabilities available on other platforms through the unified Hook API.

Input Synthesis and Synthetic Event Handling

A critical challenge in input hooking is preventing infinite feedback loops when the system re-captures events that it just injected. The openlogi-hook crate solves this through tight integration with the openlogi-inject crate.

Preventing Re-Capture Loops

Events generated by OpenLogi carry a special marker in their user_data field: openlogi_inject::SYNTHETIC_EVENT_USER_DATA. In the macOS backend's translate function (lines 99-103 of src/macos.rs), the code explicitly checks this field using event.get_integer_value_field(EventField::EVENT_SOURCE_USER_DATA). If the marker matches, the backend immediately drops the event, ensuring synthetic inputs never re-enter the callback pipeline.

Integration with openlogi-inject

The Inject helper from the companion crate handles the actual platform-specific event generation. It creates CGEvent objects on macOS or uinput events on Linux, automatically applying the synthetic marker. This separation allows the hook crate to remain focused on capture logic while the inject crate manages platform-specific synthesis details.

Accessibility and Safety Features

Operating systems restrict input monitoring to protect user privacy and system security. The crate implements several safeguards to handle these restrictions gracefully.

macOS Accessibility Permissions

Before starting the hook on macOS, the code checks for Accessibility permissions using AXIsProcessTrusted and a live probe via can_filter_events. If permissions are missing, Hook::start returns HookError::AccessibilityDenied. The API also exposes Hook::has_accessibility and Hook::prompt_accessibility to allow applications to check status and prompt users to grant permissions through System Preferences.

Watchdog Protection Mechanisms

The macOS implementation runs two independent watchdog threads alongside the main event tap. These monitors detect when the callback stalls or when the tap gets disabled by the system. Upon detecting either condition, the watchdogs terminate the process with a specific exit code, preventing scenarios where a malfunctioning hook could freeze or block system-wide input.

Practical Code Examples

The following examples demonstrate common patterns for working with the openlogi-hook crate, derived from the implementation in src/lib.rs and example files.

Starting the Hook and Handling Events

This minimal example from examples/print_events.rs shows how to initialize the hook and process every captured event:

use openlogi_hook::{Hook, EventDisposition};

fn main() -> Result<(), openlogi_hook::HookError> {
    // Ensure the process has the required macOS permission (no-op on Linux/Windows)
    if !Hook::has_accessibility() {
        eprintln!("Grant Accessibility permission first");
        return Err(openlogi_hook::HookError::AccessibilityDenied);
    }

    // Install the hook. The closure receives every captured event.
    let hook = Hook::start(|event| {
        println!("{:?}", event);
        // Let all events pass through unchanged.
        EventDisposition::PassThrough
    })?;

    // ... run your application ...

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

Filtering Specific Button Events

To suppress or modify specific inputs, match on the event type and return EventDisposition::Suppress:

use openlogi_hook::{HookEvent, MouseEvent, ButtonId, EventDisposition};

fn handle(event: HookEvent) -> EventDisposition {
    match event {
        HookEvent::Mouse(MouseEvent::Button { id, pressed, .. }) => {
            if id == ButtonId::RightClick && pressed {
                // Suppress right-clicks under specific conditions
                return EventDisposition::Suppress;
            }
        }
        _ => {}
    }
    EventDisposition::PassThrough
}

Enumerating Event Taps on macOS

The list_event_taps method, implemented in src/macos.rs (lines 75-98), allows inspection of active system event monitors:

let taps = openlogi_hook::Hook::list_event_taps();
for tap in taps {
    println!(
        "Tap {} at {:?} - active: {} enabled: {}",
        tap.tap_id, tap.location, tap.active, tap.enabled
    );
}

Synthesizing Input Events

While injection lives primarily in the openlogi-inject crate, the hook recognizes these events through the synthetic marker:

use openlogi_inject::Inject;

fn synthesize_click() {
    // Generate synthetic mouse events that the hook will ignore
    Inject::mouse_click(ButtonId::LeftClick, true).unwrap();  // press
    Inject::mouse_click(ButtonId::LeftClick, false).unwrap(); // release
}

The hook automatically ignores these events due to the SYNTHETIC_EVENT_USER_DATA check in the macOS translate function, preventing feedback loops.

Summary

  • The openlogi-hook crate abstracts macOS, Linux, and Windows input APIs behind a unified Hook façade defined in src/lib.rs.
  • Platform-specific backends implement the HookBackend trait, handling OS-specific details like macOS CGEventTap, Linux evdev, and Windows WH_MOUSE_LL hooks.
  • Synthetic events are marked with openlogi_inject::SYNTHETIC_EVENT_USER_DATA and filtered out during translation to prevent re-capture loops.
  • macOS requires Accessibility permissions, checked via AXIsProcessTrusted, with watchdog threads protecting against input system freezes.
  • The crate provides rich device identity information through EventDevice, exposing vendor and product IDs when available.

Frequently Asked Questions

How does OpenLogi prevent infinite loops when synthesizing input events?

The crate marks injected events with a special SYNTHETIC_EVENT_USER_DATA value in the openlogi-inject crate. When the macOS backend's translate function processes events in src/macos.rs, it checks this field using event.get_integer_value_field(EventField::EVENT_SOURCE_USER_DATA) and immediately drops any event carrying the synthetic marker. This ensures that events generated by Inject::mouse_click or similar functions never re-enter the hook's callback pipeline, preventing feedback loops.

What permissions are required to run the hook on macOS?

macOS requires Accessibility permissions for any application that monitors or injects input events. The hook checks for these permissions using AXIsProcessTrusted and a live probe via can_filter_events before starting. If permissions are missing, Hook::start returns HookError::AccessibilityDenied. Applications can check permission status using Hook::has_accessibility() and prompt users to grant access via Hook::prompt_accessibility().

How does the crate identify different input devices across platforms?

Each captured event includes an EventDevice structure containing optional vendor ID, product ID, and product name fields. Each platform backend populates these values from the native event source—macOS derives them from CGEvent metadata, while Linux extracts them from evdev device properties. This allows applications to distinguish between different keyboards, mice, or other HID devices even when they generate the same keycodes or button identifiers.

What safety mechanisms prevent system input from freezing if the hook crashes?

The macOS implementation in src/macos.rs runs two independent watchdog threads alongside the main event tap. These monitors detect stalled callbacks or taps that get disabled by the system (for example, if the process hangs). Upon detecting either failure condition, the watchdogs terminate the process with a specific exit code, ensuring the system can recover input functionality rather than leaving the user with a frozen cursor or unresponsive keyboard.

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 →