How OpenLogi-Hook Captures OS Input Events on macOS, Linux, and Windows
OpenLogi-Hook uses platform-specific native APIs—CGEventTap for macOS, evdev/uinput for Linux, and low-level Win32 hooks for Windows—to intercept mouse and keyboard events before they reach applications, forwarding them to a user-supplied callback that can choose to pass through or suppress each event.
OpenLogi-Hook, part of the AprilNEA/OpenLogi repository, provides a unified Rust interface for capturing low-level input events across desktop operating systems. The library abstracts the significant differences between platform APIs into a common Hook interface that delivers HookEvent structures to a user callback, allowing applications to monitor or intercept mouse movements, clicks, and keystrokes. Each platform implementation lives in its own source file under crates/openlogi-hook/src/, selected at compile time via cfg attributes.
macOS: CGEventTap and IOKit Integration
On macOS, OpenLogi-Hook installs a CGEventTap in the Core Graphics event stream to receive every mouse and keyboard event before it reaches the Window Server. This mechanism allows the library to inspect or suppress events at the system level.
The implementation resides in crates/openlogi-hook/src/macos.rs. The Backend struct implements the HookBackend trait, providing the start, frontmost_app, and cursor_position methods. The event tap is created using CGEventTapCreate and enabled with CGEventTapEnable, running on a dedicated CFRunLoop thread to prevent blocking the main application (see lines 1-10).
For scroll events, the tap queries the underlying IOHIDEvent to determine the originating device—distinguishing between a physical mouse wheel and a trackpad—via the IOKit registry. This device-origin lookup occurs in the event_sender_id and sender_device_info functions (lines 60-80). To ensure reliability, crates/openlogi-hook/src/watchdog.rs implements a monitoring mechanism that restarts the tap if it stops unexpectedly.
Linux: evdev Capture and uinput Replay
The Linux implementation takes a different approach, utilizing the evdev interface to open Logitech mice exclusively while replaying events through a uinput virtual device. This ensures suppressed events never reach the desktop, while allowed events are faithfully re-injected into the system stream.
Found in crates/openlogi-hook/src/linux.rs, the Backend::start function begins by enumerating available devices via find_mouse_devices. It then creates a virtual uinput device using build_virtual_device and spawns a dedicated thread for each physical device via device_thread. Each thread reads raw evdev events, translates them into HookEvent structures, and writes them back through the virtual device if the callback returns PassThrough (see lines 1-10).
Clean shutdown is handled through a pipe-based wake-up mechanism that signals all device threads to terminate gracefully (lines 45-55). This prevents resource leaks when the hook stops.
Windows: Low-Level Win32 Hooks
On Windows, OpenLogi-Hook employs the low-level mouse and keyboard hooks (WH_MOUSE_LL and WH_KEYBOARD_LL) installed via the Win32 API. These hooks run on a dedicated thread that maintains a classic message pump to receive system input notifications.
The implementation in crates/openlogi-hook/src/windows.rs spawns a hook_thread that registers the callbacks using SetWindowsHookExW. The thread then enters a GetMessageW/DispatchMessageW loop to receive MSLLHOOKSTRUCT (mouse) and KBDLLHOOKSTRUCT (keyboard) records. These raw structures are converted into HookEvent enums, and the callback's EventDisposition determines whether the event is passed through or suppressed (see lines 1-10).
To stop the hook, the library posts a WM_QUIT message to the dedicated thread (lines 90-100), causing the message pump to exit and the hooks to unhook cleanly.
Shared API Architecture
All three backends conform to a common trait defined in crates/openlogi-hook/src/lib.rs. The public entry point is Hook::start, which instantiates the appropriate Backend implementation based on the target OS.
The callback receives a HookEvent enum containing either Mouse or Key variants and must return an EventDisposition—either PassThrough to allow the event to continue or Suppress to block it from reaching other applications.
Practical Implementation Example
The following example demonstrates starting the hook and printing all captured events across platforms:
use openlogi_hook::{Hook, HookEvent, EventDisposition};
fn main() {
// Ensure the OS hook has the required permissions (e.g. Accessibility on macOS)
if !Hook::has_accessibility() {
eprintln!("Please grant Accessibility access and restart.");
return;
}
// Start the hook; the closure runs for each captured event.
let hook = Hook::start(|event: HookEvent| {
println!("{event:?}");
EventDisposition::PassThrough // let the event continue to its original target
})
.expect("Failed to start OpenLogi hook");
// Keep the process alive while the hook runs.
std::thread::park();
// hook.stop() will be called automatically on drop.
}
To query the frontmost application using platform-specific implementations:
use openlogi_hook::Hook;
fn main() {
if let Some(app) = Hook::frontmost_app() {
println!("Frontmost application: {app}");
} else {
println!("No frontmost app information available");
}
}
Summary
- macOS: Uses
CGEventTapCreateincrates/openlogi-hook/src/macos.rsto tap the Core Graphics event stream, with IOKit registry lookups inevent_sender_idandsender_device_info(lines 60-80) to identify scroll event sources, and a watchdog to monitor tap health. - Linux: Implements exclusive evdev device access with uinput replay in
crates/openlogi-hook/src/linux.rs, using per-device threads and pipe-based signaling (lines 45-55) for clean shutdown. - Windows: Leverages
WH_MOUSE_LLandWH_KEYBOARD_LLhooks incrates/openlogi-hook/src/windows.rs, running a dedicated message pump thread that can be terminated viaWM_QUIT(lines 90-100). - Common Interface: All platforms expose the same
Hook::startAPI fromcrates/openlogi-hook/src/lib.rs, returningHookEventstructures and respectingEventDispositionresponses.
Frequently Asked Questions
How does OpenLogi-Hook distinguish between trackpad and mouse wheel events on macOS?
According to the crates/openlogi-hook/src/macos.rs implementation, when processing scroll events, the tap extracts the underlying IOHIDEvent and queries the IOKit registry via event_sender_id and sender_device_info (lines 60-80). This allows the hook to identify the physical device type, distinguishing between a traditional mouse wheel and a trackpad gesture before forwarding the event to the callback.
Why does the Linux backend use both evdev and uinput instead of simple interception?
The Linux implementation in crates/openlogi-hook/src/linux.rs grabs input devices exclusively using evdev to ensure events cannot reach the display server while the hook is processing them. However, to maintain system functionality for events the user wishes to allow, the library re-injects them through a uinput virtual device. This dual-device approach ensures clean suppression of blocked events while preserving the behavior of permitted ones.
What system permissions are required to run OpenLogi-Hook on macOS?
macOS requires the application to have Accessibility permissions granted in System Preferences. The Hook::has_accessibility() method checks for these rights before initializing the CGEventTap. Without this permission, CGEventTapCreate will fail silently or return null, preventing the hook from receiving system events.
How do I safely stop the hook on Windows without crashing the application?
The Windows backend in crates/openlogi-hook/src/windows.rs handles shutdown by posting a WM_QUIT message to the dedicated hook_thread (lines 90-100). When the Hook object is dropped, it triggers this mechanism automatically, causing the GetMessageW loop to exit and SetWindowsHookExW hooks to unhook. Manual interruption of the thread is unnecessary and potentially unsafe; rely on the RAII pattern via hook.stop() or allow the Hook to drop naturally.
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 →