2 releases
Uses new Rust 2024
| 0.1.1 | Feb 6, 2026 |
|---|---|
| 0.1.0 | Feb 5, 2026 |
#359 in Hardware support
61 downloads per month
Used in tauri-plugin-user-input
240KB
5.5K
SLoC
monio-rs
A pure Rust cross-platform input hook library with proper drag detection.
Features
- Cross-platform: macOS, Windows, and Linux (X11/evdev) support
- Proper drag detection: Distinguishes
MouseDraggedfromMouseMovedevents - Event grabbing: Block events from reaching other applications (global hotkeys)
- Async/Channel support: Non-blocking event receiving with std or tokio channels
- Event recording & playback: Record and replay macros (requires
recorderfeature) - Input statistics: Analyze typing speed, mouse distance, etc. (requires
statisticsfeature) - Display queries: Get monitor info, DPI scale, system settings (multi-monitor support)
- Pure Rust: No C dependencies (uses native Rust bindings)
- Event simulation: Programmatically generate keyboard and mouse events
- Thread-safe: Atomic state tracking for reliable button/modifier detection
The Problem This Solves
Most input hooking libraries report all mouse movement as MouseMoved, even when buttons are held down. This makes implementing drag-and-drop, drawing applications, or gesture recognition difficult.
monio-rs tracks button state globally and emits MouseDragged events when movement occurs while any mouse button is pressed:
Button Down → Move → Move → Button Up
↓ ↓ ↓ ↓
Pressed Dragged Dragged Released
Installation
Add to your Cargo.toml:
[dependencies]
monio = "0.1"
Feature Flags
# Default (X11 on Linux)
monio = "0.1"
# Async channel support with Tokio
monio = { version = "0.1", features = ["tokio"] }
# Event recording and playback (macro scripts)
monio = { version = "0.1", features = ["recorder"] }
# Input statistics collection
monio = { version = "0.1", features = ["statistics"] }
# All features
monio = { version = "0.1", features = ["tokio", "recorder", "statistics"] }
# Linux: evdev support (works on X11 AND Wayland)
monio = { version = "0.1", features = ["evdev"], default-features = false }
Quick Start
Listening for Events
use monio::{listen, Event, EventType};
fn main() {
listen(|event: &Event| {
match event.event_type {
EventType::KeyPressed => {
if let Some(kb) = &event.keyboard {
println!("Key pressed: {:?}", kb.key);
}
}
EventType::MouseDragged => {
if let Some(mouse) = &event.mouse {
println!("Dragging at ({}, {})", mouse.x, mouse.y);
}
}
EventType::MouseMoved => {
if let Some(mouse) = &event.mouse {
println!("Moved to ({}, {})", mouse.x, mouse.y);
}
}
_ => {}
}
}).expect("Failed to start hook");
}
Grabbing Events (Block Keys/Mouse)
Use grab() to intercept events and optionally prevent them from reaching other applications.
Return None to consume an event, or Some(event) to pass it through.
use monio::{grab, Event, EventType, Key};
fn main() {
grab(|event: &Event| {
// Block the F1 key
if event.event_type == EventType::KeyPressed {
if let Some(kb) = &event.keyboard {
if kb.key == Key::F1 {
println!("Blocked F1!");
return None; // Consume - don't pass to other apps
}
}
}
Some(event.clone()) // Pass through
}).expect("Failed to start grab");
}
Platform Support for Grabbing:
| Platform | Grab Support | Notes |
|---|---|---|
| macOS | ✅ Full | Via CGEventTap |
| Windows | ✅ Full | Via low-level hooks |
| Linux/X11 | ⚠️ Limited | Falls back to listen mode (XRecord cannot grab) |
| Linux/Wayland | ⚠️ Limited | See Wayland Limitation below |
Channel-Based Listening (Non-Blocking)
For background processing, use channels instead of callbacks:
use monio::channel::listen_channel;
use monio::EventType;
use std::time::Duration;
fn main() {
// Start hook with bounded channel (capacity 100)
let (handle, rx) = listen_channel(100).expect("Failed to start hook");
// Process events without blocking
loop {
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(event) => {
if event.event_type == EventType::KeyPressed {
println!("Key pressed!");
}
}
Err(_) => {
// Timeout - do other work
}
}
}
}
With Tokio (requires tokio feature):
use monio::channel::listen_async_channel;
#[tokio::main]
async fn main() {
let (handle, mut rx) = listen_async_channel(100).unwrap();
while let Some(event) = rx.recv().await {
println!("{:?}", event.event_type);
}
}
Simulating Events
use monio::{key_tap, mouse_move, mouse_click, Key, Button};
fn main() -> monio::Result<()> {
// Move mouse to position
mouse_move(100.0, 200.0)?;
// Click
mouse_click(Button::Left)?;
// Type a key
key_tap(Key::KeyA)?;
Ok(())
}
Using the Hook Struct (Non-blocking)
use monio::{Hook, Event};
use std::thread;
use std::time::Duration;
fn main() -> monio::Result<()> {
let hook = Hook::new();
// Start in background thread
hook.run_async(|event: &Event| {
println!("{:?}", event.event_type);
})?;
// Do other work...
thread::sleep(Duration::from_secs(10));
// Stop the hook
hook.stop()?;
Ok(())
}
Display & System Properties
Query display information and system settings:
use monio::{displays, primary_display, system_settings};
fn main() -> monio::Result<()> {
// Get all displays
let all_displays = displays()?;
for display in all_displays {
println!("Display {}: {}x{} @ {:?}Hz",
display.id,
display.bounds.width,
display.bounds.height,
display.refresh_rate
);
}
// Get primary display
let primary = primary_display()?;
println!("Primary scale factor: {}", primary.scale_factor);
// Get system settings
let settings = system_settings()?;
println!("Double-click time: {:?}ms", settings.double_click_time);
Ok(())
}
Recording & Playback (Macros)
Record user actions and replay them later (requires recorder feature):
use monio::recorder::{EventRecorder, Recording};
use std::time::Duration;
fn main() -> monio::Result<()> {
// Record for 5 seconds
println!("Recording for 5 seconds...");
let recording = EventRecorder::record_for(Duration::from_secs(5))?;
recording.save("macro.json")?;
// Playback with original timing
println!("Replaying...");
let recording = Recording::load("macro.json")?;
recording.playback()?;
// Or playback at 2x speed
recording.playback_with_speed(2.0)?;
Ok(())
}
Input Statistics
Collect and analyze input patterns (requires statistics feature):
use monio::statistics::StatisticsCollector;
use std::time::Duration;
fn main() -> monio::Result<()> {
println!("Collecting statistics for 60 seconds...");
let stats = StatisticsCollector::collect_for(Duration::from_secs(60))?;
println!("{}", stats.summary());
println!("Typing speed: {:.1} keys/min", stats.keys_per_minute());
println!("Mouse distance: {:.0} pixels", stats.total_mouse_distance);
if let Some((key, count)) = stats.most_frequent_key() {
println!("Most pressed key: {:?} ({} times)", key, count);
}
if stats.needs_break(Duration::from_secs(30)) {
println!("You've been typing for 30+ seconds. Consider taking a break!");
}
Ok(())
}
Event Types
| Event Type | Description |
|---|---|
HookEnabled |
Hook started successfully |
HookDisabled |
Hook stopped |
KeyPressed |
Key pressed down |
KeyReleased |
Key released |
KeyTyped |
Character typed (after dead key processing) |
MousePressed |
Mouse button pressed |
MouseReleased |
Mouse button released |
MouseClicked |
Button press + release without movement |
MouseMoved |
Mouse moved (no buttons held) |
MouseDragged |
Mouse moved while button held |
MouseWheel |
Scroll wheel rotated |
Platform Notes
macOS
Requires Accessibility permissions. The app will prompt for permission on first run, or you can grant it manually in System Preferences → Security & Privacy → Privacy → Accessibility.
Windows
No special permissions required for hooking. Simulation may require the app to be running as Administrator in some contexts.
Linux
Two backends are available:
X11 (default): Uses XRecord for event capture and XTest for simulation. Works only on X11.
evdev: Reads directly from /dev/input/event* devices. Works on both X11 and Wayland!
# Use evdev backend (for Wayland support)
cargo build --features evdev --no-default-features
evdev permissions: Requires membership in the input group:
sudo usermod -aG input $USER
# Log out and back in for changes to take effect
Wayland Limitation
On Wayland, the grab() function has a fundamental limitation due to how Wayland compositors handle input:
- ✅ Blocking events works: Events you choose to consume (return
None) are properly blocked - ❌ Pass-through events fail: Events you want to pass through (return
Some(event)) may not reach other applications
Why this happens: Wayland compositors use libinput, which takes exclusive access to physical input devices. When we grab via evdev, we intercept events before libinput sees them. When we re-inject events via uinput (virtual device), libinput typically ignores them for security reasons.
Workarounds:
- Use X11 instead of Wayland for full grab support
- Use grab only for consuming/blocking events, not for selective pass-through
- For global hotkeys on Wayland, consider using your compositor's native hotkey system
This limitation affects all input libraries using evdev+uinput on Wayland, not just monio.
Examples
# Basic event logging
cargo run --example basic
# Drag detection demo
cargo run --example drag_detection
# Event simulation
cargo run --example simulate
# Event grabbing (block specific keys)
cargo run --example grab
# Display information
cargo run --example display
# Channel-based (sync)
cargo run --example channel_sync
# Channel-based (async with tokio)
cargo run --example channel_async --features tokio
# Record and playback macros (requires recorder feature)
cargo run --example recorder --features recorder -- record macro.json
cargo run --example recorder --features recorder -- playback macro.json
# Input statistics (requires statistics feature)
cargo run --example statistics --features statistics
Dependencies
~0.2–41MB
~531K SLoC