# OpenLogi Event Hook Architecture: How CGEventTap, evdev+uinput, and WH_MOUSE_LL Enable Cross-Platform Input Capture

> Explore OpenLogi's event hook architecture, a unified input capture system using CGEventTap evdev+uinput and WH_MOUSE_LL for cross-platform mouse and keyboard event handling before they reach apps.

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

---

**OpenLogi implements a unified, cross-platform OS-level input hook that captures mouse and keyboard events before they reach applications by abstracting three platform-specific backends: CGEventTap on macOS, evdev+uinput on Linux, and WH_MOUSE_LL on Windows.**

The `openlogi-hook` crate in the [AprilNEA/OpenLogi](https://github.com/AprilNEA/OpenLogi) repository provides this event hook architecture, enabling per-device remapping and event suppression across all major desktop operating systems. This article examines how each backend works, how they share a common Rust API, and how you can leverage them in your own applications.

---

## Common Hook API in [`lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/lib.rs)

The entry point for all platforms is defined in [[`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/lib.rs). The `Hook` struct exposes a minimal, consistent interface regardless of underlying implementation:

```rust
pub struct Hook { /* opaque */ }

impl Hook {
    pub fn start<F>(callback: F) -> Result<Self>
    where F: FnMut(HookEvent) -> EventDisposition + Send + 'static;
    
    pub fn stop(self);
    pub fn has_accessibility() -> bool;
}

```

Key behaviors:

- `Hook::start` spawns a platform-specific thread that installs the native hook and pumps a message/event loop.
- The callback receives a `HookEvent` (mouse or keyboard) and returns an `EventDisposition`: either `PassThrough` or `Suppress`.
- Dropping the `Hook` handle automatically stops the hook and cleans up resources.

---

## macOS: CGEventTap Implementation

On macOS, OpenLogi uses **CGEventTap**—the same low-level tap mechanism employed by Logitech Options+. This is implemented in the macOS-specific module gated by `#[cfg(target_os = "macos")]`.

### How CGEventTap Works in OpenLogi

The tap is created at `CGEventTapLocation::HIDEventTap`, the lowest accessible level for hardware events. This allows OpenLogi to intercept events before WindowServer processes them, enabling both suppression and modification.

Event handling flow:

1. Raw `CGEvent` objects arrive through the tap callback.
2. The implementation builds high-level `MouseEvent` or `KeyEvent` structs with device attribution via IOKit.
3. The user callback determines `EventDisposition`.
4. For `PassThrough`, the event continues normally; for `Suppress`, it is dropped.
5. Synthetic events can be re-posted via `CGEvent::post` for remapping scenarios.

Device attribution uses IOKit to populate `EventDevice` with vendor/product IDs and human-readable names, enabling per-device logic.

---

## Linux: evdev Grab + uinput Re-injection

The Linux implementation in [[`crates/openlogi-hook/src/linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/linux.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/linux.rs) takes a different approach that enables full event modification.

### The evdev+uinput Architecture

Linux input handling in OpenLogi follows this pattern:

1. **Open** the appropriate `/dev/input/event*` devices for target hardware.
2. **Exclusive grab** via `EVIOCGRAB` ioctl prevents other processes (including X11/Wayland) from receiving original events.
3. **Read and translate** evdev events into `HookEvent` structures.
4. **Callback execution** determines disposition and any modifications.
5. **Re-injection** through a shared **uinput** virtual device emits the final events to the display server.

This architecture uniquely allows **event modification**—not just suppression. Because the original events never reach the system, OpenLogi has complete control over what gets re-injected.

```rust
// Conceptual flow in linux.rs
let device = evdev::Device::open(path)?;
device.grab()?; // EVIOCGRAB

// Read loop
for event in device.fetch_events()? {
    let hook_event = translate(event);
    match callback(hook_event) {
        EventDisposition::PassThrough => uinput.emit(event),
        EventDisposition::Suppress => {}, // Drop silently
    }
}

```

Device attribution is straightforward on Linux since evdev exposes vendor/product IDs directly in the device hierarchy.

---

## Windows: WH_MOUSE_LL and WH_KEYBOARD_LL Hooks

The Windows implementation in [[`crates/openlogi-hook/src/windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/windows.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/windows.rs) uses Windows' **low-level hook APIs**.

### WH_MOUSE_LL Hook Mechanics

OpenLogi installs two hooks via `SetWindowsHookExW`:

- `WH_MOUSE_LL` for mouse events
- `WH_KEYBOARD_LL` for keyboard events

Critical characteristics of this approach:

| Aspect | Behavior |
|--------|----------|
| Execution context | Runs in user space on a dedicated thread |
| Modification capability | Limited—cannot change originating device identity |
| Installation requirement | Requires message pump on hook thread |
| Device attribution | Often `None` in `EventDevice`; Windows doesn't expose HID details to user-mode hooks |

The hook callback extracts position, button state, and scroll information from the `MSLLHOOKSTRUCT`, then constructs a normalized `MouseEvent`. Because `WH_MOUSE_LL` operates at a relatively high abstraction layer, per-device remapping relies on heuristics rather than direct hardware identification.

Thread lifetime management ensures `UnhookWindowsHookEx` is called on cleanup, preventing resource leaks.

---

## Event Disposition and Device Attribution

Every callback returns an `EventDisposition` that controls event flow:

- **`PassThrough`** — Original system event continues to its target application.
- **`Suppress`** — Event is dropped; the target never receives it.

Platform-specific suppression mechanisms:

- **macOS**: Return `NULL` from `CGEventTapCallback` to suppress; call `CGEventTapPostEvent` to allow.
- **Linux**: Original grab already prevents delivery; simply don't re-inject through uinput.
- **Windows**: Return non-zero from `CallNextHookEx` chain to suppress the message.

### Device Safety: The `source_is_remappable` Check

OpenLogi guarantees that **trackpad-like devices are never remapped**. The `source_is_remappable` function filters input sources using platform-specific heuristics:

- macOS: Checks `CGEventGetIntegerValueField(kCGEventSourceStateID)` against trackpad source constants.
- Linux: Examines evdev device capabilities for `ABS_MT_POSITION_X` (multitouch).
- Windows: Uses `GetRawInputDeviceInfo` when available, falls back to heuristics.

This safety mechanism prevents accidental capture of built-in laptop trackpads that users expect to function normally.

---

## Practical Code Examples

### Basic Hook Initialization (All Platforms)

```rust
use openlogi_hook::{Hook, HookEvent, EventDisposition};

fn main() -> anyhow::Result<()> {
    if !Hook::has_accessibility() {
        eprintln!("Grant Accessibility/Input permission first.");
        return Ok(());
    }

    let hook = Hook::start(|event| {
        match event {
            HookEvent::Mouse(mouse) => {
                println!("Mouse: {:#?}", mouse);
                
                // Suppress right-clicks as example
                if let openlogi_hook::MouseEvent::Button { 
                    id, pressed, .. 
                } = mouse {
                    if id == openlogi_core::binding::ButtonId::Right && pressed {
                        return EventDisposition::Suppress;
                    }
                }
            }
            HookEvent::Key(key) => println!("Key: {:#?}", key),
        }
        EventDisposition::PassThrough
    })?;

    // Hook runs until drop or explicit stop()
    std::thread::park();
    Ok(())
}

```

### Conditional Suppression on Linux

```rust
HookEvent::Mouse(openlogi_hook::MouseEvent::Scroll { 
    delta, from_trackpad, device 
}) => {
    if let Some(dev) = device {
        // Only suppress physical wheel scroll, not trackpad gestures
        if dev.is_logitech() && !from_trackpad {
            return EventDisposition::Suppress;
        }
    }
    EventDisposition::PassThrough
}

```

### Logging Raw Motion on Windows

```rust
// Same API works; WH_MOUSE_LL provides the data
Hook::start(|event| {
    if let HookEvent::Mouse(
        openlogi_hook::MouseEvent::Moved { delta_x, delta_y }
    ) = event {
        println!("Raw move: dx={}, dy={}", delta_x, delta_y);
    }
    EventDisposition::PassThrough
})?;

```

---

## Key Implementation Files

| File | Purpose |
|------|---------|
| [[`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/lib.rs) | Public `Hook` API, platform dispatch, and shared types |
| [[`crates/openlogi-hook/src/linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/linux.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/linux.rs) | `evdev` grab + `uinput` re-injection implementation |
| [[`crates/openlogi-hook/src/windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/windows.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/windows.rs) | `WH_MOUSE_LL`/`WH_KEYBOARD_LL` hook implementation |
| [[`crates/openlogi-core/src/binding.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/binding.rs) | `ButtonId` and core binding definitions |

---

## Summary

- **OpenLogi's event hook architecture** unifies three distinct platform APIs—CGEventTap, evdev+uinput, and WH_MOUSE_LL—behind a single Rust interface.
- **macOS** provides the lowest-level access with CGEventTap, enabling full event modification and reliable device attribution through IOKit.
- **Linux** achieves maximum flexibility via evdev exclusive grab and uinput re-injection, allowing both suppression and arbitrary event manipulation.
- **Windows** uses WH_MOUSE_LL for broad compatibility but sacrifices device-level identification due to user-mode hook limitations.
- All platforms support **per-device remapping** where hardware identification is available, with automatic exclusion of trackpad-like devices.

---

## Frequently Asked Questions

### What permission does OpenLogi require to capture input events?

OpenLogi requires **Accessibility permission** on macOS, **root access or input group membership** for evdev access on Linux, and runs with standard user privileges on Windows (though UIPI may affect certain scenarios). The `Hook::has_accessibility()` method checks platform-specific prerequisites before hook installation.

### Can OpenLogi modify events, or only suppress them?

**Full modification is supported on Linux and macOS**; Windows is limited to suppression. On Linux, the evdev+uinput pipeline allows arbitrary event reconstruction. On macOS, `CGEvent::post` enables synthetic event injection. WH_MOUSE_LL on Windows does not provide APIs to modify event parameters in the originating hardware stream.

### Why does the Windows implementation use WH_MOUSE_LL instead of Raw Input?

WH_MOUSE_LL provides **global hooking with minimal setup** and guaranteed delivery order. Raw Input requires window message handling and doesn't offer a callback-based suppression mechanism. The trade-off is reduced device attribution—WH_MOUSE_LL exposes less hardware information than Raw Input or the HID APIs.

### How does OpenLogi prevent remapping laptop trackpads?

Each platform module implements `source_is_remappable` using capability detection: multitouch axes on Linux evdev, source state IDs on macOS CGEvent, and heuristics on Windows. This hardcoded safety check ensures built-in pointing devices remain unmodified regardless of user configuration.