# Linux Input Capture Architecture in OpenLogi: evdev and uinput Implementation Guide

> Explore OpenLogi's Linux input capture architecture. Learn how evdev and uinput work together to manage physical mouse events and re-inject them as virtual input for enhanced control. Get the implementation guide.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: architecture
- Published: 2026-09-02

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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_ID` are 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_ABS` capabilities 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

1. Sets the `stop` flag to true
2. Writes to the `signal_pipe` to wake any threads blocked in `poll()`
3. 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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 supporting `BTN_LEFT`, `REL_X`, and `REL_Y` while excluding touch and multitouch devices
- **Virtual Mirroring** – Each physical mouse gets a paired uinput device built with `VirtualDevice::builder()`, copying capabilities via `with_keys()` and `with_relative_axes()`
- **Exclusive Capture** – Per-device threads call `device.grab()` to intercept events before desktop delivery, using `poll()` for I/O multiplexing
- **Event Translation** – The `translate()` function converts `evdev::InputEvent` to `MouseEvent`, passing results through user callbacks returning `PassThrough` or `Suppress` dispositions
- **Atomic Emission** – Events batch until `SYN_REPORT`, then emit via `virtual_device.emit()` to maintain input atomicity
- **Clean Shutdown** – `AtomicBool` stop 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.