Linux Input Capture Architecture in OpenLogi: evdev and uinput Implementation Guide
OpenLogi implements a three-stage Linux input capture pipeline that discovers physical Logitech mice via evdev, filters events through user callbacks, and re-injects allowed events through virtual uinput devices.
The OpenLogi repository provides a Linux-only mouse hook designed specifically for Logitech hardware. This architecture leverages the evdev kernel interface for reading raw input events and uinput for creating virtual input devices, ensuring seamless event interception and replay without modifying system drivers.
Device Discovery and Filtering with evdev
OpenLogi begins input capture by enumerating all available input devices through the evdev subsystem. In crates/openlogi-hook/src/linux.rs, the find_mouse_devices() function scans /dev/input/event* nodes using evdev::enumerate() to identify candidate devices.
Each discovered device undergoes validation through is_hookable_mouse() to ensure compatibility:
- Vendor verification – Only devices matching
LOGITECH_VENDOR_IDare accepted - Capability checks – Devices must support standard mouse buttons (
BTN_LEFT) and relative axes (REL_X,REL_Y) - Exclusion filters – Touch-driven devices, pointing sticks, and hardware reporting multitouch
EV_ABScapabilities are rejected since the virtual uinput device cannot fully mirror these complex event types
This filtering ensures that only supported Logitech mice proceed to the capture stage, preventing interference with touchpads or hybrid input devices that could break the virtual device mirroring logic.
Virtual Device Construction via uinput
For every validated physical mouse, OpenLogi constructs a paired virtual device using the uinput subsystem. The implementation creates a VirtualDevice::builder() in crates/openlogi-hook/src/linux.rs and configures it to match the source device capabilities exactly.
The builder copies the original device's supported keys and relative axes through with_keys() and with_relative_axes() methods. This configuration ensures the virtual mouse can emit identical event types to the physical device it replaces. The resulting VirtualDevice is registered with the kernel as "OpenLogi virtual mouse" (defined by the constant VIRTUAL_DEVICE_NAME), making it visible to applications as a standard input device.
This mirroring approach allows OpenLogi to maintain full compatibility with existing software while sitting between the hardware and the operating system's input stack.
Per-Device Threading and Event Loop Architecture
OpenLogi spawns a dedicated thread for each discovered mouse using thread::Builder::spawn, isolating device I/O and preventing event latency between multiple input sources. Each thread executes an event loop with three critical stages:
Exclusive Device Access – The thread immediately calls device.grab() to establish an exclusive lock on the physical device file descriptor. This prevents the Linux input subsystem from delivering duplicate events to the desktop environment while OpenLogi processes them.
Event Polling – The loop uses poll() to monitor two file descriptors simultaneously: the evdev device fd for input events and a stop-pipe fd (wait_readable) for shutdown signals. This dual-channel approach enables responsive thread termination without dropping events.
Event Translation and Callback – When input arrives, the thread reads batches of evdev::InputEvent structures and translates them through the translate() function into the higher-level MouseEvent type representing buttons, movement deltas, and scroll actions. Each translated event passes to the user-provided callback function which returns an EventDisposition of either PassThrough or Suppress.
SYN_REPORT Batching and Event Re-injection
Event emission follows Linux input subsystem conventions by batching multiple event updates into atomic transactions. OpenLogi accumulates permitted events (those returning PassThrough from the callback) into a pending vector until encountering a SYN_REPORT synchronization event.
Upon receiving SYN_REPORT, the thread immediately flushes the pending batch through virtual_device.emit(&pending), injecting the captured events into the virtual uinput device. This batching mechanism ensures that multi-axis movements (simultaneous X and Y motion) and button state changes appear atomically to downstream applications, preserving input consistency.
High-Resolution Scroll Wheel Processing
Modern Logitech mice report scroll data using high-resolution relative axes (REL_WHEEL_HI_RES and REL_HWHEEL_HI_RES) delivering 120 units per physical detent. OpenLogi handles this precision through the HIRES_UNITS_PER_TICK constant, converting hi-res deltas into standard wheel ticks while suppressing redundant low-resolution wheel events when high-resolution data is present.
This translation occurs within the event loop logic, ensuring that applications receive consistent scroll increments regardless of whether the source hardware reports hi-res or legacy wheel events.
Graceful Shutdown and Resource Cleanup
OpenLogi implements cooperative thread termination using an AtomicBool named stop shared across all device threads. When the hook receives a shutdown command, the main thread:
- Sets the
stopflag to true - Writes to the
signal_pipeto wake any threads blocked inpoll() - Waits for threads to exit cleanly, releasing exclusive grabs on physical devices
This mechanism ensures that device.grab() is properly released on all physical mice before program termination, restoring normal input delivery to the desktop and preventing "stuck" input devices that would require manual intervention or replugging.
Required Permissions for evdev and uinput
Access to the input capture functionality requires specific capabilities documented in crates/openlogi-permissions/src/lib.rs. The process must have read access to /dev/input/event* nodes for discovery and exclusive grabbing, plus write access to /dev/uinput for virtual device creation.
Typically, these permissions are granted through udev rules adding the user to the input group, or by running the OpenLogi process with elevated privileges capable of accessing raw input device nodes.
Summary
- Device Discovery –
find_mouse_devices()filters/dev/input/event*nodes for Logitech hardware supportingBTN_LEFT,REL_X, andREL_Ywhile excluding touch and multitouch devices - Virtual Mirroring – Each physical mouse gets a paired uinput device built with
VirtualDevice::builder(), copying capabilities viawith_keys()andwith_relative_axes() - Exclusive Capture – Per-device threads call
device.grab()to intercept events before desktop delivery, usingpoll()for I/O multiplexing - Event Translation – The
translate()function convertsevdev::InputEventtoMouseEvent, passing results through user callbacks returningPassThroughorSuppressdispositions - Atomic Emission – Events batch until
SYN_REPORT, then emit viavirtual_device.emit()to maintain input atomicity - Clean Shutdown –
AtomicBoolstop flags and pipe signaling ensure threads release grabs and terminate without leaving devices locked
Frequently Asked Questions
How does OpenLogi prevent duplicate mouse events from reaching applications?
OpenLogi calls device.grab() on each physical mouse file descriptor immediately after thread spawning in crates/openlogi-hook/src/linux.rs. This exclusive grab instructs the kernel to deliver all events only to the OpenLogi process, blocking normal delivery to the X server or Wayland compositor. Allowed events are then re-injected through the virtual uinput device, creating a single controlled pathway for input flow.
What types of input devices are excluded from OpenLogi capture?
The is_hookable_mouse() function specifically rejects touch-driven devices, pointing sticks, and any hardware reporting EV_ABS (absolute positioning) capabilities. The virtual uinput device cannot accurately mirror multitouch or absolute coordinate systems, so OpenLogi restricts capture to relative-motion Logitech mice reporting BTN_LEFT, REL_X, and REL_Y events to ensure reliable mirroring behavior.
Why does OpenLogi use uinput instead of modifying evdev events in-place?
The uinput architecture allows OpenLogi to maintain a clean separation between capture and injection layers. By creating a virtual "OpenLogi virtual mouse" device through VirtualDevice::builder(), the system preserves the original evdev device for exclusive monitoring while generating a new input source that applications recognize as a standard mouse. This approach avoids kernel-level modifications and ensures compatibility with all Linux desktop environments.
How are high-resolution scroll wheel events normalized across different Logitech mice?
OpenLogi handles the REL_WHEEL_HI_RES and REL_HWHEEL_HI_RES axes by dividing accumulated delta values by HIRES_UNITS_PER_TICK (120 units) to calculate standard wheel ticks. When high-resolution events are present, the system suppresses corresponding low-resolution wheel events to prevent double-counting. This normalization occurs in the per-device event loop before callback processing, ensuring consistent scroll behavior regardless of hardware generation.
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 →