How OpenLogi Captures and Synthesizes Input Events: Low-Level Hooks and Synthetic Injection

OpenLogi separates input capture from synthesis into two dedicated crates: openlogi-hook intercepts hardware events using platform-specific low-level APIs (CGEventTap, evdev, WH_MOUSE_LL), while openlogi-inject generates synthetic input via OS-native injection functions (CGEventPost, uinput, SendInput).

OpenLogi is an open-source input remapping and automation framework that provides comprehensive control over mouse and keyboard hardware. Understanding how it captures and synthesizes input events requires examining its dual-crate architecture, which cleanly separates event interception from programmatic event generation. This design enables advanced features like button remapping, custom gestures, and DPI cycling while maintaining cross-platform compatibility across macOS, Linux, and Windows.

Architecture Overview: The Hook and Inject Crates

OpenLogi organizes its input pipeline into two distinct Rust crates that handle opposing concerns. The capture mechanism lives in openlogi-hook, while the synthesis logic resides in openlogi-inject.

  • openlogi-hook: Captures raw hardware events before they reach applications, using platform-specific low-level hooks.
  • openlogi-inject: Generates synthetic input events that appear identical to hardware-generated events at the OS level.

This separation allows the system to observe user input, process it through custom logic, and optionally generate new events without creating infinite feedback loops.

Input Capture with openlogi-hook

The capture flow begins when user code initiates a hook via Hook::start, defined in crates/openlogi-hook/src/lib.rs. This function accepts a callback that receives every intercepted event along with a disposition directive.

Platform-Specific Hook Mechanisms

Each operating system implements the hook using native low-level APIs:

Event Translation and Disposition

Once captured, raw OS events are normalized into the high-level HookEvent enum, which provides variants for Mouse(MouseEvent) and Key(KeyEvent). The callback receives both the event and an EventDisposition value that determines whether the event should be suppressed or passed through to the original target.

For debugging purposes, the hook optionally feeds events into EventMonitor (crates/openlogi-agent-core/src/event_monitor.rs), a bounded lock-free buffer that the GUI polls via EventMonitor::record and EventMonitor::poll methods.

Capture Example

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

// Start the OS-level hook and print every captured event
let hook = Hook::start(|event| {
    match event {
        HookEvent::Mouse(m) => println!("mouse: {m:?}"),
        HookEvent::Key(k)   => println!("key: {k:?}"),
    }
    // Allow the event to continue to its original target
    EventDisposition::PassThrough
})?;

/* … later … */
hook.stop(); // Clean shutdown

Input Synthesis with openlogi-inject

The injection crate exposes a minimal API surface for generating synthetic events. Located in crates/openlogi-inject/src/lib.rs, it provides functions like execute, post_horizontal_scroll, and ax_navigate_browser.

Synthetic Event Generation

Platform-specific implementations in crates/openlogi-inject/src/inject/{macos,linux,windows}.rs wrap OS APIs to generate mouse clicks, cursor movements, scrolls, and keyboard actions:

  • macOS: Calls CGEventCreateMouseEvent and CGEventPost to inject events into the system event stream.
  • Linux: Writes synthetic event structures to uinput device nodes.
  • Windows: Uses the SendInput API to submit hardware-input simulation structures.

Distinguishing Synthetic from Real Input

Injected events carry the SYNTHETIC_EVENT_USER_DATA flag in their metadata. When the hook subsequently captures these events, it identifies them as synthetic, preventing feedback loops where generated input re-triggers the capture callback.

Synthesis Examples

Generate a left-click at the current cursor position:

use openlogi_inject::execute;
use openlogi_core::binding::ButtonId;

let click = openlogi_inject::inject::SyntheticMouseEvent::Button {
    id: ButtonId::Left,
    pressed: true,
    device: None, // None indicates synthetic origin
};
execute(click)?;

Send horizontal scroll events for side-scrolling gestures:

use openlogi_inject::post_horizontal_scroll;

// Scroll 3 "ticks" to the right
post_horizontal_scroll(3)?;

Key Source Files

Summary

  • OpenLogi uses a dual-crate architecture to separate input observation from generation, with openlogi-hook handling capture and openlogi-inject handling synthesis.
  • Platform-native APIs provide low-level access: macOS uses CGEventTap and CGEventPost, Linux uses evdev and uinput, and Windows uses WH_MOUSE_LL and SendInput.
  • Events are normalized to the HookEvent enum with EventDisposition controlling whether they propagate to the original target.
  • Synthetic events are tagged with SYNTHETIC_EVENT_USER_DATA to distinguish them from hardware-generated input, preventing infinite loops.
  • Debugging capabilities include EventMonitor, a bounded lock-free buffer for real-time event inspection without impacting the hot path.

Frequently Asked Questions

How does OpenLogi prevent infinite loops when synthesizing events?

The system marks every synthetic event with the SYNTHETIC_EVENT_USER_DATA flag during injection. When openlogi-hook captures events via platform-specific taps, it checks for this flag and identifies the event as synthetic, allowing the callback logic to ignore or specially handle self-generated input.

What privileges are required to run the input hooks?

Privilege requirements vary by platform according to the underlying APIs used. macOS requires Accessibility permissions for CGEventTap, Linux requires root access or membership in the input group to open evdev device nodes, and Windows requires standard user privileges for WH_MOUSE_LL hooks but may need elevation for certain device interactions.

Can OpenLogi distinguish between different physical input devices?

On Linux, the implementation uses evdev device grabs that target specific device nodes, enabling per-device capture. The macOS and Windows implementations capture global events, though additional filtering logic can identify source devices through extended event metadata where the OS provides it.

How does EventMonitor impact system performance?

EventMonitor uses a bounded lock-free ring buffer to record events, minimizing latency impact on the capture hot path. The GUI polls this buffer asynchronously via EventMonitor::poll, ensuring that monitoring and debugging activities do not block the real-time input processing pipeline.

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 →