| name | tauri-plugin-user-input |
| description | Use the plugin for global keyboard/mouse event monitoring and input simulation in Tauri desktop apps, with JS bindings in tauri-plugin-user-input-api. |
Tauri Plugin User Input
Scope
This plugin is a desktop-first Tauri v2 plugin (tauri-plugin-user-input) with a JS API package (tauri-plugin-user-input-api).
Rust side:
- commands are registered in
src/lib.rs::init
- core API state is in
src/desktop.rs and src/mobile.rs
- IPC payload/types are in
src/models.rs
- command handlers are in
src/commands.rs
- permissions are generated from
build.rs → permissions/autogenerated
JS side:
- thin invoke wrappers in
guest-js/index.ts
- shared runtime types in
guest-js/types.ts
- published entry:
dist-js/index.js, dist-js/index.cjs, dist-js/index.d.ts
How to integrate
- Register the Rust plugin in your App setup:
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_user_input::init())
.run(tauri::generate_context!())
.expect("error while running");
}
- Depend on the Rust crate in
src-tauri/Cargo.toml (example uses local path):
tauri-plugin-user-input = { path = "../../" }
- Add JS binding dependency in the frontend package:
"tauri-plugin-user-input-api": "link:../../"
- Enable permissions in capabilities:
"user-input:default"
If you need stricter control, replace it with explicit allow-* or deny-* IDs under the same user-input namespace.
Command names and payloads
All commands are invoked via the plugin:user-input|{command} channel.
start_listening → { on_event: Channel<InputEvent> }
stop_listening
set_window_labels → { labels: string[] }
set_event_types → { eventTypes: EventType[] }
is_listening
key → { key: monio::Key, evtType: "KeyPress" | "KeyRelease" | "KeyClick", delayMs?: number }
text → { text: string }
button → { button: "Left" | "Middle" | "Right" | "Back" | "Forward" | "ScrollUp" | "ScrollDown" | "ScrollLeft" | "ScrollRight", direction: "Pressed" | "Released" | "Clicked" }
move_mouse → { x: number, y: number, coordinate: "Abs" | "Rel" }
scroll → { length: number, axis: "Horizontal" | "Vertical" }
Recommended JS usage pattern
Set filters before starting listening:
setEventTypes(["KeyPress", "KeyRelease", "MouseMove", ...])
startListening((evt) => { ... })
stopListening() when done
InputEvent arrives through the callback with:
eventType (KeyPress, KeyRelease, ButtonPress, ButtonRelease, MouseMove, MouseDragged, Wheel)
time
- optional
key, button, position, deltaPosition
Event model details to remember
InputEvent is generated from monio::Event in src/models.rs:
- key events become
KeyPress and KeyRelease
- mouse press/release/click becomes
ButtonPress/ButtonRelease (MouseClicked is merged into release)
- mouse move/drag emit
MouseMove/MouseDragged with position
- wheel emits
Wheel with deltaPosition
- time is milliseconds since Unix epoch
- unknown/edge variants currently default to best-effort values (
Unknown(0) key/button)
How the example app uses it (examples/tauri-app)
examples/tauri-app/src-tauri/src/lib.rs registers .plugin(tauri_plugin_user_input::init())
- it stores permissions in
examples/tauri-app/src-tauri/capabilities/default.json with user-input:default
- frontend calls:
setEventTypes in reactive state
startListening / stopListening
key(...) and text(...) to simulate input
moveMouse(...), button(...), scroll(...)
platform() from @tauri-apps/plugin-os is used to choose macOS vs Windows shortcuts in the demo
Design quirks to follow
- Event simulation split:
- key press/release/click uses
monio::key_*
- text, button, mouse move and scroll use shared
enigo singleton (ENIGO) protected by a global mutex
- Keep this plugin as singletons: do not create additional enigo instances.
set_event_types and set_window_labels must run before listening if you want immediate filtering/targeting.
- On mobile, core runtime is currently stubbed; only plugin registration exists, and desktop APIs are not fully implemented there.
Safety notes for future edits
- Add new commands in all three places:
- handler in
src/commands.rs
- command list in
build.rs (COMMANDS)
- plugin handler list in
src/lib.rs
- Prefer
Result<T, Error> over stringly errors when changing existing signatures for consistency.
- Avoid assuming no values can drop before channel/emit send: using
let _ = ... is intentionally tolerated.