Platform-specific Input Handling in OpenLogi: macOS, Linux, and Windows Implementation
OpenLogi unifies cross-platform input capture and synthesis through a HookBackend trait that abstracts OS-native mechanisms including CGEventTap on macOS, evdev/uinput on Linux, and low-level Windows hooks.
OpenLogi implements a sophisticated input pipeline that bridges operating system differences while respecting platform security models. The openlogi-hook crate defines a common interface that enables consistent platform-specific input handling across macOS, Linux, and Windows without sacrificing low-level control. This article examines the architecture in the AprilNEA/OpenLogi repository, detailing how each backend captures hardware events and injects synthetic input.
The HookBackend Trait Abstraction
At the core of OpenLogi's input system lies the HookBackend trait defined in crates/openlogi-hook/src/lib.rs. This trait establishes a contract for platform-specific implementations, exposing methods for event iteration and input injection. The factory function HookBackend::new() selects the appropriate concrete backend at compile time, returning a unified interface regardless of the target operating system.
All backends implement the same event model using structs like HookEvent, MouseEvent, and KeyEvent. This design allows higher-level application code to process input uniformly while the underlying platform handles OS-specific nuances.
Platform-Specific Backend Implementations
macOS: CGEventTap and Core Graphics
The macOS backend in crates/openlogi-hook/src/macos.rs utilizes CGEventTap from Apple's Core Graphics framework. The implementation spawns a private thread running a CFRunLoop to host the event tap, which intercepts mouse and keyboard events at the HID (Human Interface Device) level.
To identify specific hardware sources, the code calls CGEventCopyIOHIDEvent and IOHIDEventGetSenderID to extract the originating device identifier. The backend includes a watchdog mechanism that monitors the tap lifecycle, automatically re-enabling it if macOS disables the tap during system sleep or permission changes. Users must grant Accessibility permissions, which the code verifies using AXIsProcessTrusted.
Linux: evdev and uinput Integration
The Linux implementation in crates/openlogi-hook/src/linux.rs employs the evdev kernel interface to grab physical mouse devices and uinput to re-inject synthetic events. The backend enumerates available input devices using evdev::enumerate(), then claims exclusive access to relevant hardware.
For output, the system constructs a virtual device via build_virtual_device and translates raw evdev::InputEvent structures into the common MouseEvent and KeyEvent abstractions. The implementation also supports high-resolution scrolling wheels. Unlike macOS, this backend requires no special user-grantable permissions beyond standard device file access for /dev/input/* nodes.
Windows: Low-Level Hook APIs
The Windows backend in crates/openlogi-hook/src/windows.rs installs WH_MOUSE_LL and WH_KEYBOARD_LL hooks via SetWindowsHookExW. These low-level hooks run on a dedicated thread that pumps a Windows message loop, capturing input before it reaches target applications.
For synthetic input generation, the backend uses the Win32 SendInput API. The implementation can distinguish between physical and synthetic events by examining parameters passed to the hook callbacks, preventing infinite loops when injecting events. No additional privacy permissions are required beyond standard user privileges.
Cross-Platform Event Model
All three backends translate native OS events into a shared representation. The HookEvent enum variants wrap platform-specific data into MouseEvent and KeyEvent structs containing normalized fields like position coordinates, key states, and device identifiers. This abstraction ensures that calling code in crates/openlogi-hook/src/lib.rs remains agnostic to the underlying platform while receiving rich, structured input data.
Implementing Input Capture and Injection
Initializing the Platform Backend
The following example demonstrates starting a hook and processing events across all supported platforms:
use openlogi_hook::{HookBackend, HookEvent, HookError};
fn main() -> Result<(), HookError> {
// Create the platform-specific backend; the concrete type is selected at compile time
let mut backend = HookBackend::new()?;
// Run the event loop; each iteration yields a high-level HookEvent
while let Some(event) = backend.next_event()? {
match event {
HookEvent::Mouse(m) => println!("mouse: {:?} at {:?}", m.kind, m.position),
HookEvent::Key(k) => println!("key: {:?} {}", k.kind, k.is_down),
}
}
Ok(())
}
Synthesizing macOS Input Events
For injecting keyboard shortcuts on macOS, the implementation uses Core Graphics events:
use openlogi_inject::inject::Inject;
use core_graphics::event::{CGEvent, CGEventFlags, CGEventTapLocation};
fn send_cmd_shift_c() {
// Create a source for HID events
let src = core_graphics::event_source::CGEventSource::new(
core_graphics::event_source::CGEventSourceStateID::HIDSystemState,
).unwrap();
// Command + Shift + C (keycode 8)
let mut ev = CGEvent::new_keyboard_event(&src, 8, true).unwrap();
ev.set_flags(CGEventFlags::CGEventFlagCommand | CGEventFlags::CGEventFlagShift);
ev.post(CGEventTapLocation::HID);
// Release the keys
let mut ev_up = CGEvent::new_keyboard_event(&src, 8, false).unwrap();
ev_up.set_flags(CGEventFlags::CGEventFlagCommand | CGEventFlags::CGEventFlagShift);
ev_up.post(CGEventTapLocation::HID);
}
Injecting Linux Input Events
On Linux, synthetic input utilizes the evdev and uinput interfaces:
use openlogi_inject::inject::Inject;
use evdev::{InputEvent, InputEventKind};
fn click_left() {
// Build a synthetic button press
let press = InputEvent::new(
evdev::EventType::KEY,
evdev::KeyCode::BTN_LEFT.0,
InputEventKind::Key(evdev::KeyState::Pressed),
);
// Followed by a release
let release = InputEvent::new(
evdev::EventType::KEY,
evdev::KeyCode::BTN_LEFT.0,
InputEventKind::Key(evdev::KeyState::Released),
);
// Forward these to the uinput device
Inject::inject_linux(&[press, release]).unwrap();
}
Summary
- OpenLogi uses a trait-based abstraction in
crates/openlogi-hook/src/lib.rsto unify platform-specific input handling across operating systems. - The macOS backend leverages CGEventTap with Core Graphics APIs and requires Accessibility permissions verified via
AXIsProcessTrusted. - The Linux implementation utilizes evdev for hardware capture and uinput for injection, requiring only standard device file access without elevated privileges.
- The Windows backend employs WH_MOUSE_LL and WH_KEYBOARD_LL hooks with SendInput for synthetic event generation and differentiates physical from synthetic input.
- All platforms share common event structs (
HookEvent,MouseEvent,KeyEvent) for consistent higher-level processing regardless of OS.
Frequently Asked Questions
Does OpenLogi require root privileges on Linux?
No, OpenLogi on Linux only requires standard device file access permissions for evdev and uinput nodes. While some distributions may require adding the user to the input group, the implementation does not require full root privileges to capture or synthesize events.
How does the macOS backend handle system sleep and accessibility permission changes?
The macOS implementation includes a watchdog mechanism that monitors the CGEventTap lifecycle. If the system disables the tap during sleep or due to permission revocation, the code automatically re-enables the tap to ensure continuous input handling without requiring application restart.
Can the Windows backend distinguish between physical and synthetic input events?
Yes, the Windows implementation in crates/openlogi-hook/src/windows.rs uses hook callback parameters to differentiate between real hardware events and synthetic events generated via the SendInput API. This prevents the system from processing its own injected input as new raw events.
What Rust trait enables cross-platform input handling in OpenLogi?
The HookBackend trait defined in crates/openlogi-hook/src/lib.rs provides the common interface for all platforms. The factory method HookBackend::new() selects the appropriate concrete implementation at compile time, enabling zero-cost abstraction across macOS, Linux, and Windows.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →