How OpenLogi Implements the Linux Input Hook Mechanism Using evdev and uinput
OpenLogi captures Logitech mice exclusively via the evdev crate, transforms raw kernel events into high-level mouse actions, and re-injects permitted events through a uinput virtual device to intercept or modify input transparently.
The openlogi-hook crate in the AprilNEA/OpenLogi repository provides a low-level input interception layer for Linux systems. By combining the evdev crate for hardware access with the uinput subsystem for virtual device creation, OpenLogi achieves exclusive capture of Logitech mice while maintaining transparent pass-through behavior for unmodified events.
Architecture Overview of the Linux Input Hook
The Linux backend creates one dedicated thread per physical Logitech mouse. This design ensures isolated, non-blocking event streams for each device. The architecture follows a strict pipeline: discovery, exclusive grab, virtual device setup, and event loop processing.
Device Discovery with evdev
Device enumeration begins in find_mouse_devices() within crates/openlogi-hook/src/linux.rs. The function scans every node under /dev/input/, using the evdev crate to query device capabilities. It filters for Logitech-branded relative-pointer devices that support safe exclusive access. The predicate logic in is_hookable_mouse() validates vendor IDs and device classes before the hook attempts a grab.
Exclusive Grab and Isolation
Once identified, each physical device undergoes an exclusive grab via device.grab(). This system call prevents the Linux input subsystem from delivering the hardware events to the desktop environment or other applications. If the grab fails—typically due to permissions or existing grabs—the device is bypassed and left un-hooked.
Virtual Device Creation via uinput
To maintain cursor functionality, build_virtual_device() constructs a virtual mouse using the uinput kernel interface. The virtual device mirrors the original mouse's keys and relative axes, registering under the constant name VIRTUAL_DEVICE_NAME = "OpenLogi virtual mouse". This virtual endpoint becomes the conduit for re-injecting events that the user callback permits to pass through.
The Event Processing Pipeline
The core logic resides in device_thread(), where raw kernel events flow through translation, user callback evaluation, and conditional re-injection.
The Main Device Thread
Each thread blocks on a poll() call that monitors two file descriptors: the physical mouse's evdev node and a pipe endpoint used for shutdown signaling (wait_readable). When data arrives, the thread reads batches of evdev::InputEvent structures from the hardware. This dual-fd approach ensures the thread remains responsive to termination signals while maintaining low-latency input capture.
Event Translation and Remapping
Raw evdev events undergo transformation via translate() in crates/openlogi-hook/src/linux.rs. The function maps kernel primitives to the crate's MouseEvent enum:
- Button events (
EV_KEY) becomeMouseEvent::Button - Relative axis deltas (
EV_REL) becomeMouseEvent::Moved - Wheel events map to
MouseEvent::Scroll
High-resolution scrolling is explicitly supported through recognition of REL_WHEEL_HI_RES and REL_HWHEEL_HI_RES codes, normalized to 120 units per tick (HIRES_UNITS_PER_TICK).
Callback Disposition and Re-injection
After translation, the user-provided closure receives a HookEvent::Mouse(me) and returns an EventDisposition. PassThrough queues the original evdev::InputEvent into a pending buffer; Suppress drops the event immediately. Upon receiving a SYN_REPORT synchronization event, the buffer flushes to the virtual uinput device via virtual_device.emit(&pending), ensuring atomic event delivery to the operating system.
Graceful Shutdown Mechanism
Termination coordination relies on an Arc<AtomicBool> flag (HookInner::stop) paired with Unix pipes (stop_pipes). Invoking signal_pipe() writes a byte to the pipe, waking any blocked poll() calls across all device threads. The threads check the atomic flag, exit their loops, and join in shutdown(), releasing the exclusive grabs and closing file descriptors cleanly.
Implementing the Hook in Practice
The public API in crates/openlogi-hook/src/lib.rs exposes the Hook struct. Instantiate the hook with a closure that inspects events and returns dispositions.
use openlogi_hook::{Hook, EventDisposition, HookEvent};
let hook = Hook::start(|event| {
match event {
HookEvent::Mouse(me) => {
// Suppress right-clicks while allowing other events
if let openlogi_hook::MouseEvent::Button { id, .. } = me {
if id == openlogi_hook::ButtonId::RightClick {
return EventDisposition::Suppress;
}
}
EventDisposition::PassThrough
}
_ => EventDisposition::PassThrough,
}
})?;
The underlying Linux implementation demonstrates the low-level flow:
// Simplified excerpt from linux.rs
fn device_thread(device: Device, callback: impl Fn(HookEvent) -> EventDisposition) {
// Acquire exclusive access
device.grab().expect("evdev grab failed");
let mut pending = Vec::new();
loop {
if !wait_readable(device.fd, stop_fd) {
break;
}
for ev in device.fetch_events().unwrap() {
if let Some(mouse_event) = translate(&ev, hires_scroll) {
match callback(HookEvent::Mouse(mouse_event)) {
EventDisposition::PassThrough => pending.push(ev),
EventDisposition::Suppress => {}
}
// Flush on SYN_REPORT
if ev.event_type() == EventType::SYNCHRONIZATION {
virtual_device.emit(&pending).unwrap();
pending.clear();
}
}
}
}
}
Explicit shutdown ensures resources release properly:
// Signal termination and wait for threads
hook.stop();
Summary
- OpenLogi uses the evdev crate to enumerate and exclusively grab Logitech mice under
/dev/input/. - A uinput virtual device (
"OpenLogi virtual mouse") mirrors the hardware to maintain system cursor functionality. - The
device_thread()pipeline polls both the hardware descriptor and a shutdown pipe, translating raw events intoMouseEventenums. - User callbacks return
PassThroughorSuppressto control event flow; permitted events buffer untilSYN_REPORTtriggers atomic emission. - Graceful shutdown uses an atomic flag and pipe-based signaling to unblock threads and release device grabs.
Frequently Asked Questions
What is evdev and why does OpenLogi use it?
evdev is the Linux kernel's generic input event interface, exposing hardware devices as files under /dev/input/. OpenLogi uses the evdev Rust crate to read raw InputEvent structures directly from the kernel, bypassing X11 or Wayland abstraction layers. This direct access enables exclusive grabs and high-resolution scroll wheel detection unavailable through higher-level APIs.
How does the exclusive grab prevent duplicate cursor movement?
When device.grab() succeeds, the kernel detaches the physical device from all other input handlers, including the display server. Without this grab, both the physical device and the virtual uinput device would emit cursor events, causing doubled or erratic movement. The grab ensures only the virtual device—controlled by OpenLogi's callback logic—influences the cursor position.
Can OpenLogi hook keyboards or other input devices?
The current implementation in crates/openlogi-hook/src/linux.rs specifically filters for Logitech relative-pointer devices via is_hookable_mouse(). While the evdev and uinput mechanisms support any input class, OpenLogi's translation layer and callback API currently focus exclusively on mouse events. Extending support would require adding keyboard event types to the translate() function and relaxing the device filter predicate.
What happens to suppressed events in the pipeline?
Suppressed events return EventDisposition::Suppress from the user callback and are immediately dropped without entering the pending buffer. They never reach the virtual_device.emit() call, meaning the uinput virtual device never reports them to the operating system. Consequently, the desktop environment receives no indication that the input occurred, effectively blocking the action at the kernel-input layer.
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 →