# How OpenLogi Implements the Linux Input Hook Mechanism Using evdev and uinput

> Discover how OpenLogi uses Linux input hooks evdev and uinput to transparently intercept and modify Logitech mouse input. Learn about raw event transformation and virtual device re-injection.

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

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/linux.rs). The function maps kernel primitives to the crate's `MouseEvent` enum:

- Button events (`EV_KEY`) become `MouseEvent::Button`
- Relative axis deltas (`EV_REL`) become `MouseEvent::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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs) exposes the `Hook` struct. Instantiate the hook with a closure that inspects events and returns dispositions.

```rust
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:

```rust
// 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:

```rust
// 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 into `MouseEvent` enums.
- User callbacks return `PassThrough` or `Suppress` to control event flow; permitted events buffer until `SYN_REPORT` triggers 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.