# How OpenLogi Captures Operating-System Input Using `openlogi-hook`: Architecture Deep Dive

> Discover how OpenLogi captures OS input like mouse and keyboard events using openlogi-hook. Explore its architecture and platform-specific backends for seamless event handling.

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

---

**OpenLogi captures mouse, scroll, and keyboard events through a unified `Hook` API that delegates to platform-specific backends—CGEventTap on macOS, evdev+uinput on Linux, and WH_MOUSE_LL/WH_KEYBOARD_LL hooks on Windows.**

The `openlogi-hook` crate is OpenLogi's low-level input capture layer. It normalizes raw OS events into a common `HookEvent` type, allowing the agent system to react to user input regardless of platform. This article breaks down the architecture, key source files, and how each backend implements capture.

---

## The Core Abstraction: `Hook` and `HookBackend`

All platform logic is hidden behind two types in [`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs).

### `Hook` Struct

The `Hook` struct holds the running backend state as an opaque `Option<Backend::Running>`:

```rust
// crates/openlogi-hook/src/lib.rs#L14-L18
pub struct Hook<Backend: HookBackend> {
    running: Option<Backend::Running>,
}

```

This design lets `Hook` remain generic while each platform defines its own `Running` type—containing thread handles, run-loop references, or hook IDs.

### `HookBackend` Trait

Every platform implements the [`HookBackend`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/lib.rs#L31-L50) trait:

```rust
// crates/openlogi-hook/src/lib.rs#L31-L50
pub trait HookBackend: Sized {
    type Running;
    
    fn start<F>(callback: F) -> Result<Self::Running, HookError>
    where F: Fn(HookEvent) -> EventDisposition + Send + 'static;
    
    fn stop(running: Self::Running);
    fn is_running(running: &Self::Running) -> bool;
    
    // Platform-specific capabilities
    fn has_accessibility() -> bool;
    fn prompt_accessibility();
    fn list_event_taps() -> Vec<(u32, String)>;
    fn frontmost_app() -> Option<String>;
    fn cursor_position() -> (f64, f64);
}

```

The `start` method takes a **callback closure** that receives every `HookEvent` and returns an `EventDisposition`—`PassThrough` or `Suppress`—controlling whether the original event reaches other applications.

---

## macOS Backend: CGEventTap and CFRunLoop

The macOS implementation in [[`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/macos.rs) uses Core Graphics event taps.

### Accessibility Permission Check

Before capturing, the backend verifies the process has accessibility access:

```rust
// crates/openlogi-hook/src/macos.rs#L66-L80
fn has_accessibility() -> bool {
    // First check the standard API
    if !unsafe { AXIsProcessTrusted() } {
        return false;
    }
    
    // Also verify we can actually install a filtering tap
    probe_filtering_tap_available()
}

```

This dual check prevents false positives from `AXIsProcessTrusted` alone.

### Starting the Hook

[`Hook::start`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/lib.rs#L38-L42) delegates to the macOS backend, which:

1. **Spawns a watchdog thread** to detect stalls
2. **Spawns a hook thread** that creates a CGEventTap

```rust
// crates/openlogi-hook/src/macos.rs#L78-L98
let tap = CGEventTap::new(
    CGEventTapLocation::HID,           // Monitor system-wide HID events
    CGEventTapPlacement::HeadInsertFirst, // Intercept before other taps
    CGEventTapOptions::Default,
    hooked_event_types(),              // Mask of event types we want
    |_proxy, event_type, cg_event| {
        // Translate and dispatch...
    },
);

```

The `hooked_event_types()` function returns a mask including mouse button, scroll, move, and keyboard events.

### Run-Loop Management

The tap is added to a `CFRunLoop` source, then executed in slices:

```rust
// crates/openlogi-hook/src/macos.rs#L108-L113
loop {
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false);
    
    // Check if tap was disabled (accessibility revoked)
    if tap_is_disabled() {
        break;
    }
}

```

The 500ms timeout allows periodic checks for permission revocation while maintaining responsive event delivery.

### Event Translation

Mouse events translate via [`translate`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/macos.rs#L76-L89); keyboard via [`translate_key`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/macos.rs#L60-L74):

```rust
// crates/openlogi-hook/src/macos.rs#L60-L74
fn translate_key(event: &CGEvent) -> Option<KeyEvent> {
    let keycode = event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE) as u16;
    let pressed = event.get_type() == CGEventType::KeyDown;
    
    Some(KeyEvent {
        keycode,
        pressed,
        // ...modifiers, repeat flag
    })
}

```

The callback's return value determines event disposition:

```rust
// crates/openlogi-hook/src/macos.rs#L198-L206
let disposition = callback(event);
match disposition {
    EventDisposition::PassThrough => CallbackResult::PassThrough,
    EventDisposition::Suppress => CallbackResult::Suppress,
}

```

---

## Linux Backend: evdev and uinput

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) reads from kernel input devices directly.

### Device Enumeration

The backend scans `/dev/input/event*` for devices supporting the needed event types:

```rust
// Conceptual flow from linux.rs
for path in glob("/dev/input/event*")? {
    let device = evdev::Device::open(&path)?;
    
    // Check if device supports keys, relative, or absolute events
    if device.supported_events().contains(EventType::KEY) 
        || device.supported_events().contains(EventType::REL) {
        devices.push(device);
    }
}

```

### Event Reading Thread

Each device gets its own thread reading `input_event` structures:

```rust
// From linux.rs implementation
loop {
    match device.fetch_events()? {
        EventType::KEY => {
            let key_event = translate_key(&ev);
            let disposition = callback(HookEvent::Key(key_event));
            // Handle suppression...
        }
        EventType::REL => {
            let mouse_event = translate_relative(&ev);
            let disposition = callback(HookEvent::Mouse(mouse_event));
            // Handle suppression...
        }
        // EV_ABS for absolute positioning (touchpads, tablets)
    }
}

```

### Pass-Through with uinput

When the callback returns `PassThrough`, events must still reach applications. The backend creates a **uinput** virtual device and re-injects the original event:

```rust
// Conceptual uinput flow
if disposition == EventDisposition::PassThrough {
    uinput_device.emit(input_event.type_, input_event.code, input_event.value)?;
}

```

This mirrors the original event stream while allowing the hook to intercept and optionally suppress.

---

## Windows Backend: Low-Level 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 Win32 low-level hooks.

### Hook Registration

Separate hooks for mouse and keyboard:

```rust
// Conceptual from windows.rs
let mouse_hook = SetWindowsHookExW(
    WH_MOUSE_LL,
    Some(mouse_hook_proc),
    module_handle,
    0,
);

let keyboard_hook = SetWindowsHookExW(
    WH_KEYBOARD_LL,
    Some(keyboard_hook_proc),
    module_handle,
    0,
);

```

### Message Loop Thread

Hooks require a thread with a message pump:

```rust
// From windows.rs implementation
loop {
    let mut msg: MSG = mem::zeroed();
    let result = GetMessageW(&mut msg, null_mut(), 0, 0);
    
    if result == -1 || should_exit {
        break;
    }
    TranslateMessage(&msg);
    DispatchMessageW(&msg);
}

```

### Hook Procedures

The `mouse_hook_proc` translates `MSLLHOOKSTRUCT` to `MouseEvent`:

```rust
// Conceptual translation
unsafe extern "system" fn mouse_hook_proc(n_code: i32, w_param: WPARAM, l_param: LPARAM) -> LRESULT {
    if n_code >= 0 {
        let info = &*(l_param as *const MSLLHOOKSTRUCT);
        let event = translate_mouse(w_param, info);
        
        match CALLBACK.get()(event) {
            EventDisposition::Suppress => return 1, // Swallow event
            EventDisposition::PassThrough => {}     // Continue to CallNextHookEx
        }
    }
    CallNextHookEx(null_mut(), n_code, w_param, l_param)
}

```

Keyboard hooks follow the same pattern with `KBDLLHOOKSTRUCT`.

---

## Practical Usage Example

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // macOS: ensure accessibility permission
    if !Hook::has_accessibility() {
        eprintln!("Please grant Accessibility permission");
        Hook::prompt_accessibility();
        return Ok(());
    }

    let hook = Hook::start(|event| {
        match event {
            HookEvent::Mouse(MouseEvent::Button { id, pressed, .. }) => {
                println!("Mouse button {:?} {}", id, if pressed { "pressed" } else { "released" });
            }
            HookEvent::Mouse(MouseEvent::Scroll { delta, .. }) => {
                println!("Scroll: {:?}", delta);
            }
            HookEvent::Key(k) => {
                println!("Key {}: code {}", if k.pressed { "down" } else { "up" }, k.keycode);
            }
        }
        EventDisposition::PassThrough // Allow event to continue
    })?;

    std::thread::park(); // Keep main thread alive
    
    // Cleanup on shutdown
    hook.stop();
    Ok(())
}

```

---

## Watchdog Protection

All platforms use a watchdog in [[`crates/openlogi-hook/src/watchdog.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/watchdog.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/watchdog.rs) to prevent system freezes. If the hook thread stalls—due to a blocking callback or deadlock—the watchdog aborts the process rather than leaving the system without input.

---

## Summary

- **`Hook`** provides a unified API across macOS, Linux, and Windows via the **`HookBackend`** trait abstraction
- **macOS** uses **CGEventTap** with CFRunLoop, requiring explicit accessibility permission verification
- **Linux** uses **evdev** for reading and **uinput** for pass-through, handling device enumeration per input node
- **Windows** uses **WH_MOUSE_LL/WH_KEYBOARD_LL** low-level hooks with a dedicated message-pump thread
- All platforms normalize events to **`HookEvent`** and respect **`EventDisposition`** for suppression control
- A **watchdog thread** prevents system input hangs if the hook malfunctions

---

## Frequently Asked Questions

### What permissions does OpenLogi need for input capture?

On macOS, OpenLogi requires **Accessibility** permission (System Settings → Privacy & Security → Accessibility). The [`has_accessibility()`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/macos.rs#L66-L80) method verifies this with both `AXIsProcessTrusted()` and a live tap probe. Linux requires read access to `/dev/input/event*` devices; Windows requires no special permissions beyond standard user execution.

### Can OpenLogi suppress events so they don't reach other applications?

Yes. The callback passed to [`Hook::start`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/lib.rs#L38-L42) returns an [`EventDisposition`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/lib.rs)—either `PassThrough` or `Suppress`. On macOS this maps to CoreGraphics callback results; on Windows to `CallNextHookEx` or swallowing; on Linux to uinput re-injection or omission.

### How does OpenLogi handle multiple input devices on Linux?

The Linux backend enumerates all `/dev/input/event*` devices supporting `EV_KEY`, `EV_REL`, or `EV_ABS` events. It spawns a dedicated thread per device, merging their event streams into the single user callback. This handles mixed setups like USB mice, laptop trackpads, and external keyboards simultaneously.

### What happens if the hook callback blocks or crashes?

A **watchdog thread** in [[`watchdog.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/watchdog.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/watchdog.rs) monitors hook thread health. If the hook stalls beyond a timeout—due to a blocking callback or panic—the watchdog triggers a controlled process abort. This prevents the system-wide input freeze that would otherwise occur if a CGEventTap or low-level hook stopped processing events.