rdev Rust Library

repository·main·Indexed 20 days ago

https://github.com/narsil/rdev

A Rust library for listening to and simulating global keyboard and mouse events across Windows, Linux (X11), and macOS. It provides functionality to listen to global input via `listen`, inject events using `simulate`, and intercept or cancel events using the `grab` function (via the `unstable_grab` feature). The library distinguishes between physical events (`EventType`) and interpreted characters (`Event`), and includes a `Keyboard` struct for tracking keyboard state and character mapping.

Tokens
4.6K
Snippets
13
Records
18
Agent score
70%

What's inside rdev

  1. Understand the Event and EventType structs

    main

    rdev distinguishes between physical key presses and interpreted characters:

    • EventType: Represents a physical event (e.g., a QWERTY layout key press). This is what you use for simulation.
    • Event: Represents the actual event received from the OS. It includes the EventType and an Event.name field, which reflects the character interpreted by the OS based on the current keyboard layout.

    Warning: Event.name can be None, an empty string, or contain non-displayable Unicode characters. Always perform sanity checks before using it.

    EventType Variants

    • KeyPress(Key)
    • KeyRelease(Key)
    • ButtonPress(Button)
    • ButtonRelease(Button)
    • MouseMove { x: f64, y: f64 }
    • Wheel { delta_x: i64, delta_y: i64 } (Note: On Linux, delta_x is ignored; only the sign of delta_y is used to determine direction.)
  2. Simulate global keyboard and mouse events

    main

    Use the simulate function to inject global input events into the system.

    Note on Timing: On some operating systems (like macOS), it is recommended to add a small delay (e.g., 20ms) between simulated events to allow the OS to catch up.

    Limitations: Not all keys are mapped to an OS code; simulate may return a SimulateError if you attempt to send an unmapped key.

    use rdev::{simulate, Button, EventType, Key, SimulateError};
    use std::{thread, time};
    
    fn send(event_type: &EventType) {
        let delay = time::Duration::from_millis(20);
        match simulate(event_type) {
            Ok(()) => (),
            Err(SimulateError) => {
                println!("We could not send {:?}", event_type);
            }
        }
        // Let the OS catch up
        thread::sleep(delay);
    }
    
    send(&EventType::KeyPress(Key::KeyS));
    send(&EventType::KeyRelease(Key::KeyS));
    
    send(&EventType::MouseMove { x: 0.0, y: 0.0 });
    send(&EventType::MouseMove { x: 400.0, y: 400.0 });
    send(&EventType::ButtonPress(Button::Left));
    send(&EventType::ButtonRelease(Button::Right));
    send(&EventType::Wheel {
        delta_x: 0,
        delta_y: 1,
    });
  3. Listen to global keyboard and mouse events

    main

    Use the listen function to start a blocking loop that executes a callback whenever a global input event occurs. The callback receives an Event struct.

    Important OS Caveats:

    • macOS: The process must be the parent process (no forking before calling listen). You must grant the application (e.g., Terminal.app) access to the Accessibility API in System Preferences > Security & Privacy > Privacy > Accessibility. If access is not granted, listen will fail silently without triggering callbacks.
    • Linux: listen uses X11 APIs and will not work in Wayland or the Linux kernel virtual console.
    use rdev::{listen, Event};
    
    // This will block.
    if let Err(error) = listen(callback) {
        println!("Error: {:?}", error)
    }
    
    fn callback(event: Event) {
        println!("My callback {:?}", event);
        match event.name {
            Some(string) => println!("User wrote {:?}", string),
            None => (),
        }
    }
  4. Intercept and cancel global events with `grab`

    main

    By enabling the unstable_grab feature, you can use the grab function to intercept events before they reach other applications or the window manager.

    How it works: Provide a callback that returns Option<Event>:

    • Return None to consume/cancel the event (it will not reach other apps).
    • Return Some(event) to let the event pass through normally.

    OS Caveats:

    • macOS: Requires Accessibility API access. If access is denied, grab will fail with an EventTapError.
    • Linux: Uses evdev and works with both X11 and Wayland. The process must run as root or as a user in the input group (or plugdev group on some distributions).

    Warning: The grab API is marked unstable and is subject to change.

    #[cfg(feature = "unstable_grab")]
    use rdev::{grab, Event, EventType, Key};
    
    #[cfg(feature = "unstable_grab")]
    let callback = |event: Event| -> Option<Event> {
        if let EventType::KeyPress(Key::CapsLock) = event.event_type {
            println!("Consuming and cancelling CapsLock");
            None  // CapsLock is now effectively disabled
        }
        else { Some(event) }
    };
    
    // This will block.
    #[cfg(feature = "unstable_grab")]
    if let Err(error) = grab(callback) {
        println!("Error: {:?}", error)
    }
  5. Understand the `Event` and `EventType` structures

    main

    The library distinguishes between physical keys and interpreted characters:

    • EventType: Represents a physical event (e.g., KeyPress(Key::KeyS)). These correspond to a standard QWERTY layout.
    • Event: Represents the actual event received from the OS. The Event.name field contains the interpreted character (respecting the user's OS keyboard layout).

    Warning on Event.name: This field may be None, an empty string, or contain non-displayable Unicode characters. Always perform sanity checking before using it.

    EventType Variants:

    • KeyPress(Key)
    • KeyRelease(Key)
    • ButtonPress(Button)
    • ButtonRelease(Button)
    • MouseMove { x: f64, y: f64 }
    • Wheel { delta_x: i64, delta_y: i64 } (Note: On Linux, delta_x is ignored; delta_y sign determines direction).
  6. Handle keyboard and mouse events with Event and EventType

    main

    The Event struct is the primary data container for all input received from the OS. It includes a time (SystemTime), an optional name (the character produced based on the OS layout), and an event_type.

    EventType categorizes the input into one of the following:

    • KeyPress(Key) / KeyRelease(Key): Keyboard interactions using the Key enum.
    • ButtonPress(Button) / ButtonRelease(Button): Mouse button interactions.
    • MouseMove { x: f64, y: f64 }: Mouse movement in pixels (top-left is 0,0).
    • Wheel { delta_x: i64, delta_y: i64 }: Scroll wheel movement (positive is up/right, negative is down/left).

    Note on Keys: Key values represent physical locations. For character-based input, use Event.name instead of mapping Key to characters manually, as Key is layout-agnostic and does not account for modifiers or layout logic.

    // Example of the structure of an Event
    let event = Event {
        time: SystemTime::now(),
        name: Some("a".to_string()),
        event_type: EventType::KeyPress(Key::KeyA),
    };
  7. Get the main screen size

    main

    Use display_size() to retrieve the width and height of the primary display in pixels.

    use rdev::{display_size};
    
    let (w, h) = display_size().unwrap();
    assert!(w > 0);
    assert!(h > 0);
  8. Track keyboard state and character mapping

    main

    The Keyboard struct allows you to simulate adding EventTypes to a virtual keyboard to see what string they would produce based on the current OS layout.

    Caveats:

    • This is layout-dependent. If your application needs to support layout switching, do not rely on this.
    • On Linux, dead keys are not implemented.
    • Only Shift and dead keys are implemented; Alt+Unicode on Windows is not supported.
    use rdev::{Keyboard, EventType, Key, KeyboardState};
    
    let mut keyboard = Keyboard::new().unwrap();
    let string = keyboard.add(&EventType::KeyPress(Key::KeyS));
    // string == Some("s")
  9. Troubleshoot ListenError and GrabError

    main

    When capturing or grabbing OS events, you may encounter errors. Note that on macOS, failing to set Accessibility permissions often does not trigger an error; instead, the library will simply ignore events.

    ListenError (Capturing events)

    • EventTapError (macOS)
    • LoopSourceError (macOS)
    • MissingDisplayError (Linux)
    • KeyboardError (Linux)
    • RecordContextEnablingError (Linux)
    • RecordContextError (Linux)
    • XRecordExtensionError (Linux)
    • KeyHookError(u32) (Windows)
    • MouseHookError(u32) (Windows)

    GrabError (Grabbing/Intercepting events)

    • EventTapError (macOS)
    • LoopSourceError (macOS)
    • MissingDisplayError (Linux)
    • KeyboardError (Linux)
    • KeyHookError(u32) (Windows)
    • MouseHookError(u32) (Windows)
    • SimulateError: Occurs when trying to simulate an event fails.
    • IoError(std::io::Error): Standard I/O errors.
  10. Simulate keyboard state with KeyboardState

    main

    The KeyboardState trait allows you to maintain a virtual keyboard state to predict what characters would be emitted by a sequence of EventTypes. This is useful for testing or layout simulation without interacting with the actual OS.

    Caveats:

    • Layout Dependency: The behavior is dependent on the currently used layout.
    • Linux Limitation: Dead keys are not currently implemented on Linux (X11); you will receive the raw letter instead of the accentuated character.
    • Windows Limitation: Alt+unicode code combinations are not supported.
    use rdev::{Keyboard, EventType, Key, KeyboardState};
    
    let mut keyboard = Keyboard::new().unwrap();
    let string = keyboard.add(&EventType::KeyPress(Key::KeyS));
    // string == Some("s")
  11. Simulate keyboard and mouse events with `simulate`

    main

    The simulate function allows you to programmatically trigger input events.

    Best Practice: On macOS, it is recommended to add a small delay (e.g., 20ms) between simulated events to allow the OS to catch up.

    use rdev::{simulate, Button, EventType, Key, SimulateError};
    use std::{thread, time};
    
    fn send(event_type: &EventType) {
        let delay = time::Duration::from_millis(20);
        match simulate(event_type) {
            Ok(()) => (),
            Err(SimulateError) => {
                println!("We could not send {:?}", event_type);
            }
        }
        // Let the OS catchup (at least MacOS)
        thread::sleep(delay);
    }
    
    fn main() {
        send(&EventType::KeyPress(Key::KeyS));
        send(&EventType::KeyRelease(Key::KeyS));
    
        send(&EventType::MouseMove { x: 0.0, y: 0.0 });
        send(&EventType::MouseMove { x: 400.0, y: 400.0 });
        send(&EventType::ButtonPress(Button::Left));
        send(&EventType::ButtonRelease(Button::Right));
        send(&EventType::Wheel {
            delta_x: 0,
            delta_y: 1,
        });
    }