How OpenLogi Captures Input on Linux Using evdev and uinput
OpenLogi uses a two-stage pipeline on Linux: raw mouse events are captured via the evdev API and re-injected through a uinput virtual device, with each physical device handled by a dedicated thread in the openlogi-hook crate.
The openlogi-hook crate provides the cross-platform input capture foundation for OpenLogi, an open-source mouse automation and logging tool. On Linux, this crate implements a user-space interception layer that requires no kernel modules—only standard permissions to read /dev/input/event* nodes. This article explains the complete evdev-to-uinput mechanism with direct reference to the source code in AprilNEA/OpenLogi.
The evdev and uinput Architecture
OpenLogi's Linux implementation runs entirely in user space through a carefully designed two-device pipeline. As documented in [crates/openlogi-hook/src/lib.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/lib.rs#L8-L11), the architecture follows this principle: one thread per physical mouse device reads evdev events and re-injects pass-through events via a uinput virtual device.
The separation of capture (evdev) and injection (uinput) allows OpenLogi to inspect, modify, or block events before they reach the display server.
Step 1: Device Enumeration with evdev
The backend first discovers available input devices. In [crates/openlogi-hook/src/linux/mod.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/linux/mod.rs), the Backend type scans /dev/input/event* nodes to identify mouse-class devices. Each discovered device spawns its own thread for isolation—failure of one physical device does not affect others.
Step 2: Raw Event Reading via evdev
Each dedicated thread opens its assigned device using the Linux evdev API through evdev::Device. The thread continuously reads kernel-generated EV_REL (relative movement) and EV_KEY (button) events. These raw events carry timestamps, device IDs, and unprocessed coordinate deltas.
Step 3: Event Translation and Callback Dispatch
Every evdev event converts to OpenLogi's internal HookEvent type, defined in [crates/openlogi-hook/src/event.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/event.rs). The user's callback function receives this structured event and returns an EventDisposition:
EventDisposition::Pass— forward the event to the systemEventDisposition::Consume— drop the event entirelyEventDisposition::Mutate— replace with modified values
Step 4: Re-injection Through uinput
For events marked Pass or Mutate, the thread writes to a uinput virtual mouse device created via uinput::Device. This virtual device registers with the kernel's input subsystem and appears to X11/Wayland as ordinary hardware. The uinput device merges seamlessly with the standard input stack, preserving compatibility with all applications.
Practical Implementation Example
The following Rust code demonstrates the complete capture pipeline. When executed, it starts the evdev-uinput hook and filters right-click events:
use openlogi_hook::{Hook, HookEvent, EventDisposition};
fn main() -> Result<(), openlogi_hook::HookError> {
// Start the hook; the closure receives every mouse event.
let hook = Hook::start(|event: HookEvent| {
// Example: block right-clicks
if let HookEvent::ButtonPress { button, .. } = event {
if button == 3 {
return EventDisposition::Consume; // drop the event
}
}
EventDisposition::Pass // let the event pass through
})?;
// The hook runs in background threads; keep the main thread alive.
std::thread::sleep(std::time::Duration::from_secs(60));
// Explicitly stop the hook (optional; Drop does this automatically).
hook.stop();
Ok(())
}
The Hook::start() call initiates the evdev enumeration, spawns per-device threads, and establishes the uinput virtual device. All heavy I/O occurs asynchronously; the callback executes synchronously to minimize latency.
Permission Requirements and Security
Unlike macOS, which requires explicit accessibility permission grants, OpenLogi's Linux implementation needs only filesystem access. The user must have read permissions on /dev/input/event* nodes—typically granted by membership in the input group or custom udev rules. No kernel modules load, no root processes spawn, and no display server integration is required.
This design choice prioritizes portability and security auditing: the entire interception logic resides in auditable Rust code within the openlogi-hook crate.
Key Source 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 abstraction, documentation of evdev+uinput flow |
[crates/openlogi-hook/src/linux/mod.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/linux/mod.rs) |
Linux Backend implementation, thread spawning, device lifecycle |
crates/openlogi-hook/src/linux/ |
Helper modules for evdev handling, uinput creation, device enumeration |
[crates/openlogi-hook/src/event.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/event.rs) |
HookEvent and EventDisposition definitions |
Summary
- evdev provides raw, low-latency access to physical mouse events through
/dev/input/event*devices - uinput creates a virtual mouse device that transparently injects modified events into the X11/Wayland stack
- The
openlogi-hookcrate implements one thread per device for isolation and performance - User callbacks determine event disposition: pass, consume, or mutate
- No kernel modules or elevated privileges beyond
inputgroup membership are required
Frequently Asked Questions
Does OpenLogi require root privileges on Linux?
No. OpenLogi runs as an unprivileged user provided the account has read access to /dev/input/event* nodes. Grant this through the input group membership or targeted udev rule configuration.
Can OpenLogi capture events from multiple mice simultaneously?
Yes. The Backend in [crates/openlogi-hook/src/linux/mod.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/linux/mod.rs) spawns an independent thread for each discovered mouse device. Each thread maintains its own evdev reader and uinput injector.
What happens if the callback function is slow?
The evdev reader blocks until the callback returns. Slow callbacks introduce latency but do not drop events—the kernel buffers evdev events up to its queue limit. For minimal latency, keep callback execution fast and defer heavy processing to other threads.
How does uinput differ from alternative injection methods?
uinput creates a genuine kernel input device visible to all consumers of the input subsystem. Alternative methods like XTest (X11) or wayland protocols are display-server specific. uinput works uniformly across X11, Wayland, and console environments without protocol dependencies.
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 →