rdev sends and listens to real keyboard and mouse events
Simple library to listen and send events to keyboard and mouse (MacOS, Windows, Linux)
At a glance
- What is it?
- rdev is a small Rust crate that injects and observes input at the operating system level on macOS, Windows and Linux, and the part of it that will decide your integration is the list of caveats, because macOS permissions and Wayland support are not optional extras.
- Who is it for?
- rdev suits a developer writing a test harness, a remote control utility or a macro tool on macOS or Windows, and it suits anyone who needs to know what a user physically typed rather than what a web page received. It is the wrong choice on Linux under Wayland, where the listen function uses X11 APIs and will not work, and the wrong choice for a macOS tool that you expect to work without a permissions prompt.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 165 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Global input means the operating system, not your window
rdev is a simple library to listen and send events globally to the keyboard and mouse on macOS, Windows and Linux with X11, and the word global is doing the heavy lifting. A normal library reports events to the process that asked for them. This one hooks the operating system's input device stream, so a callback fires for keystrokes and clicks anywhere on the machine, including in other applications and, with the grab feature, before those events are delivered. The basic listening loop is short.
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 => (),
}
}The listen call blocks, which is what a background input watcher wants, and the callback receives an Event carrying the event type and, where the operating system could interpret one, the name of the key. The author is candid about the motivation, saying the crate is so far a pet project to understand the Rust ecosystem, and it credits the Enigo crate as the project that helped.
EventType is physical, Event is what the OS made of it
The two types in the library are deliberately different, and understanding the difference is what makes the library usable. EventType corresponds to a physical event on a standard QWERTY layout, so the key codes do not correspond to the actual letter a user would use, which requires layout logic to be added. Event corresponds to an actual event received, and its name field reflects what the key was interpreted as at that time, so it respects the layout in use. The Event struct carries the time the event was received, the optional name and the event type. The readme is blunt about the name field, warning that it might be None, might also be an empty string, and might contain characters that are not displayable, since the library sends exactly what the operating system sends it, so do some sanity checking before using it. There is also a caveat that dead keys do not function yet on Linux.
Sending events means the OS can refuse the key
The other half of the library is simulation, and the pattern in the examples is press, release, pause, which is the important detail. Not all keys are mapped to an operating system code, so simulate may fail if you try to send an unmapped key, and sending Unknown variants always works, though the operating system might still reject them. The Unknown variant of the enum exists for a related reason, as a safe mechanism to detect events regardless of what the operating system supports, holding some OS specific value. The send helper in the examples also sleeps for twenty milliseconds after each event, with a comment that it lets the OS catch up, and the comment names macOS specifically. A wheel event carries two deltas, and the enum documents that on Linux there is no actual delta, the values are ignored for delta_x, and only the sign of delta_y is used to simulate wheel up or wheel down, which is a small example of a platform difference leaking into the API.
Reading the keyboard state, with three caveats
There is a separate mechanism for turning physical events into strings, built on a dummy Keyboard that you feed events and which returns the character the layout would produce.
use rdev::{Keyboard, EventType, Key, KeyboardState};
let mut keyboard = Keyboard::new().unwrap();
let string = keyboard.add(&EventType::KeyPress(Key::KeyS));
// string == Some("s")Three caveats come with it. It is layout dependent, and if your application needs to support layout switching you should not use it. On Linux the dead keys mechanism is not implemented. And only shift and dead keys are implemented, so Alt+Unicode code on Windows will not work. The readme also notes that the currently used layout is what the Keyboard gives you, and the whole mechanism exists because detecting what a user types means plugging into the operating system level management of keyboard state, including modifiers like shift and control and dead keys where they exist.
Grabbing is behind a feature flag named for its instability
The most aggressive capability is grabbing, and it is deliberately opt-in behind a feature flag whose name is a warning. Installing with the unstable_grab feature adds a grab function which hooks into the global input device event stream, and by supplying it with a callback you can intercept all keyboard and mouse events before they are delivered to applications and window managers. Returning None from the callback ignores the event, and returning the event lets it pass, and the readme is clear that there is no modification of the event possible here yet. The note about the word unstable refers specifically to the grab API being unstable and subject to change. The Cargo.toml features make the packaging explicit, with a default feature set of nothing, and separate flags for serialize, unstable_grab, wayland and x11, and the unstable_grab flag pulls in evdev-rs, epoll, inotify and serde_json on top of the serialize flag.
Where this will fail on you, and how the crates are organised
The caveats are the real content of this project and they are split by platform. On macOS, the process running the blocking listen or grab function has to be the parent process, so no fork before it, and the process needs to be granted access to the Accessibility API, meaning Terminal.app itself has to be added under System Preferences, Security and Privacy, Privacy, Accessibility if you are running inside it. The nasty part is what happens when that permission is missing, because macOS silently ignores the listen callback and will not trigger it with events, and no error is generated. On Linux, the listen function uses X11 APIs and so will not work in Wayland or in the Linux kernel virtual console, and the wayland feature flag in Cargo.toml pulls in the input, input-linux and xkbcommon crates, so the Wayland path is being worked on. The crate also has a display_size function for the main screen size, and the repository ships examples for channel, display, grab, keyboard_state, listen, serialize, simulate and tokio_channel, with a flake.nix for Nix development.
Editorial conclusion
rdev suits a developer writing a test harness, a remote control utility or a macro tool on macOS or Windows, and it suits anyone who needs to know what a user physically typed rather than what a web page received. It is the wrong choice on Linux under Wayland, where the listen function uses X11 APIs and will not work, and the wrong choice for a macOS tool that you expect to work without a permissions prompt. Before building on it, read the OS caveats section, because the macOS Accessibility requirement fails silently with no error, and check the feature flags in Cargo.toml, since the default feature set is empty and the interesting capabilities are opt-in. The last push was on 2026-05-12 and the crate version is 0.6.0 on edition 2024.
Frequently asked questions
How do I listen for global keyboard and mouse events in Rust?
Call rdev's listen function with a callback. The call blocks until it errors, and the callback receives an Event carrying the event type, the receipt time and the optional key name as the operating system interpreted it. The README notes that Event::name might be None, might be an empty string, and might contain characters that are not displayable, so it should be sanity checked before use.
Does rdev work on Wayland?
Not through the listen function as shipped, because it uses X11 APIs and so will not work in Wayland or in the Linux kernel virtual console. The Cargo.toml does define a wayland feature that pulls in the input, input-linux and xkbcommon crates, so there is work in that direction.
Why does the rdev listen callback never fire on macOS?
The process needs access to the Accessibility API, so Terminal.app must be added under System Preferences, Security and Privacy, Privacy, Accessibility if you run inside it, and the process running the blocking listen function must be the parent process with no fork before it. When the permission is missing macOS silently ignores the callback and no error is generated.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/narsil-rdev)Was this useful?