OpenLogi Event Hook Architecture: How CGEventTap, evdev+uinput, and WH_MOUSE_LL Enable Cross-Platform Input Capture
OpenLogi implements a unified, cross-platform OS-level input hook that captures mouse and keyboard events before they reach applications by abstracting three platform-specific backends: CGEventTap on macOS, evdev+uinput on Linux, and WH_MOUSE_LL on Windows.
The openlogi-hook crate in the AprilNEA/OpenLogi repository provides this event hook architecture, enabling per-device remapping and event suppression across all major desktop operating systems. This article examines how each backend works, how they share a common Rust API, and how you can leverage them in your own applications.
Common Hook API in lib.rs
The entry point for all platforms is defined in [crates/openlogi-hook/src/lib.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/lib.rs). The Hook struct exposes a minimal, consistent interface regardless of underlying implementation:
pub struct Hook { /* opaque */ }
impl Hook {
pub fn start<F>(callback: F) -> Result<Self>
where F: FnMut(HookEvent) -> EventDisposition + Send + 'static;
pub fn stop(self);
pub fn has_accessibility() -> bool;
}
Key behaviors:
Hook::startspawns a platform-specific thread that installs the native hook and pumps a message/event loop.- The callback receives a
HookEvent(mouse or keyboard) and returns anEventDisposition: eitherPassThroughorSuppress. - Dropping the
Hookhandle automatically stops the hook and cleans up resources.
macOS: CGEventTap Implementation
On macOS, OpenLogi uses CGEventTap—the same low-level tap mechanism employed by Logitech Options+. This is implemented in the macOS-specific module gated by #[cfg(target_os = "macos")].
How CGEventTap Works in OpenLogi
The tap is created at CGEventTapLocation::HIDEventTap, the lowest accessible level for hardware events. This allows OpenLogi to intercept events before WindowServer processes them, enabling both suppression and modification.
Event handling flow:
- Raw
CGEventobjects arrive through the tap callback. - The implementation builds high-level
MouseEventorKeyEventstructs with device attribution via IOKit. - The user callback determines
EventDisposition. - For
PassThrough, the event continues normally; forSuppress, it is dropped. - Synthetic events can be re-posted via
CGEvent::postfor remapping scenarios.
Device attribution uses IOKit to populate EventDevice with vendor/product IDs and human-readable names, enabling per-device logic.
Linux: evdev Grab + uinput Re-injection
The Linux implementation in [crates/openlogi-hook/src/linux.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/linux.rs) takes a different approach that enables full event modification.
The evdev+uinput Architecture
Linux input handling in OpenLogi follows this pattern:
- Open the appropriate
/dev/input/event*devices for target hardware. - Exclusive grab via
EVIOCGRABioctl prevents other processes (including X11/Wayland) from receiving original events. - Read and translate evdev events into
HookEventstructures. - Callback execution determines disposition and any modifications.
- Re-injection through a shared uinput virtual device emits the final events to the display server.
This architecture uniquely allows event modification—not just suppression. Because the original events never reach the system, OpenLogi has complete control over what gets re-injected.
// Conceptual flow in linux.rs
let device = evdev::Device::open(path)?;
device.grab()?; // EVIOCGRAB
// Read loop
for event in device.fetch_events()? {
let hook_event = translate(event);
match callback(hook_event) {
EventDisposition::PassThrough => uinput.emit(event),
EventDisposition::Suppress => {}, // Drop silently
}
}
Device attribution is straightforward on Linux since evdev exposes vendor/product IDs directly in the device hierarchy.
Windows: WH_MOUSE_LL and WH_KEYBOARD_LL Hooks
The Windows implementation in [crates/openlogi-hook/src/windows.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/windows.rs) uses Windows' low-level hook APIs.
WH_MOUSE_LL Hook Mechanics
OpenLogi installs two hooks via SetWindowsHookExW:
WH_MOUSE_LLfor mouse eventsWH_KEYBOARD_LLfor keyboard events
Critical characteristics of this approach:
| Aspect | Behavior |
|---|---|
| Execution context | Runs in user space on a dedicated thread |
| Modification capability | Limited—cannot change originating device identity |
| Installation requirement | Requires message pump on hook thread |
| Device attribution | Often None in EventDevice; Windows doesn't expose HID details to user-mode hooks |
The hook callback extracts position, button state, and scroll information from the MSLLHOOKSTRUCT, then constructs a normalized MouseEvent. Because WH_MOUSE_LL operates at a relatively high abstraction layer, per-device remapping relies on heuristics rather than direct hardware identification.
Thread lifetime management ensures UnhookWindowsHookEx is called on cleanup, preventing resource leaks.
Event Disposition and Device Attribution
Every callback returns an EventDisposition that controls event flow:
PassThrough— Original system event continues to its target application.Suppress— Event is dropped; the target never receives it.
Platform-specific suppression mechanisms:
- macOS: Return
NULLfromCGEventTapCallbackto suppress; callCGEventTapPostEventto allow. - Linux: Original grab already prevents delivery; simply don't re-inject through uinput.
- Windows: Return non-zero from
CallNextHookExchain to suppress the message.
Device Safety: The source_is_remappable Check
OpenLogi guarantees that trackpad-like devices are never remapped. The source_is_remappable function filters input sources using platform-specific heuristics:
- macOS: Checks
CGEventGetIntegerValueField(kCGEventSourceStateID)against trackpad source constants. - Linux: Examines evdev device capabilities for
ABS_MT_POSITION_X(multitouch). - Windows: Uses
GetRawInputDeviceInfowhen available, falls back to heuristics.
This safety mechanism prevents accidental capture of built-in laptop trackpads that users expect to function normally.
Practical Code Examples
Basic Hook Initialization (All Platforms)
use openlogi_hook::{Hook, HookEvent, EventDisposition};
fn main() -> anyhow::Result<()> {
if !Hook::has_accessibility() {
eprintln!("Grant Accessibility/Input permission first.");
return Ok(());
}
let hook = Hook::start(|event| {
match event {
HookEvent::Mouse(mouse) => {
println!("Mouse: {:#?}", mouse);
// Suppress right-clicks as example
if let openlogi_hook::MouseEvent::Button {
id, pressed, ..
} = mouse {
if id == openlogi_core::binding::ButtonId::Right && pressed {
return EventDisposition::Suppress;
}
}
}
HookEvent::Key(key) => println!("Key: {:#?}", key),
}
EventDisposition::PassThrough
})?;
// Hook runs until drop or explicit stop()
std::thread::park();
Ok(())
}
Conditional Suppression on Linux
HookEvent::Mouse(openlogi_hook::MouseEvent::Scroll {
delta, from_trackpad, device
}) => {
if let Some(dev) = device {
// Only suppress physical wheel scroll, not trackpad gestures
if dev.is_logitech() && !from_trackpad {
return EventDisposition::Suppress;
}
}
EventDisposition::PassThrough
}
Logging Raw Motion on Windows
// Same API works; WH_MOUSE_LL provides the data
Hook::start(|event| {
if let HookEvent::Mouse(
openlogi_hook::MouseEvent::Moved { delta_x, delta_y }
) = event {
println!("Raw move: dx={}, dy={}", delta_x, delta_y);
}
EventDisposition::PassThrough
})?;
Key Implementation Files
| File | Purpose |
|---|---|
[crates/openlogi-hook/src/lib.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/lib.rs) |
Public Hook API, platform dispatch, and shared types |
[crates/openlogi-hook/src/linux.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/linux.rs) |
evdev grab + uinput re-injection implementation |
[crates/openlogi-hook/src/windows.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/windows.rs) |
WH_MOUSE_LL/WH_KEYBOARD_LL hook implementation |
[crates/openlogi-core/src/binding.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/binding.rs) |
ButtonId and core binding definitions |
Summary
- OpenLogi's event hook architecture unifies three distinct platform APIs—CGEventTap, evdev+uinput, and WH_MOUSE_LL—behind a single Rust interface.
- macOS provides the lowest-level access with CGEventTap, enabling full event modification and reliable device attribution through IOKit.
- Linux achieves maximum flexibility via evdev exclusive grab and uinput re-injection, allowing both suppression and arbitrary event manipulation.
- Windows uses WH_MOUSE_LL for broad compatibility but sacrifices device-level identification due to user-mode hook limitations.
- All platforms support per-device remapping where hardware identification is available, with automatic exclusion of trackpad-like devices.
Frequently Asked Questions
What permission does OpenLogi require to capture input events?
OpenLogi requires Accessibility permission on macOS, root access or input group membership for evdev access on Linux, and runs with standard user privileges on Windows (though UIPI may affect certain scenarios). The Hook::has_accessibility() method checks platform-specific prerequisites before hook installation.
Can OpenLogi modify events, or only suppress them?
Full modification is supported on Linux and macOS; Windows is limited to suppression. On Linux, the evdev+uinput pipeline allows arbitrary event reconstruction. On macOS, CGEvent::post enables synthetic event injection. WH_MOUSE_LL on Windows does not provide APIs to modify event parameters in the originating hardware stream.
Why does the Windows implementation use WH_MOUSE_LL instead of Raw Input?
WH_MOUSE_LL provides global hooking with minimal setup and guaranteed delivery order. Raw Input requires window message handling and doesn't offer a callback-based suppression mechanism. The trade-off is reduced device attribution—WH_MOUSE_LL exposes less hardware information than Raw Input or the HID APIs.
How does OpenLogi prevent remapping laptop trackpads?
Each platform module implements source_is_remappable using capability detection: multitouch axes on Linux evdev, source state IDs on macOS CGEvent, and heuristics on Windows. This hardcoded safety check ensures built-in pointing devices remain unmodified regardless of user configuration.
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 →