wdotool
An xdotool-compatible input automation CLI for Wayland, built on the protocols that were actually designed for this.
Why
- xdotool is X11-only and does not work on Wayland.
- ydotool writes to
/dev/uinput, which means root (or careful udev rules), no focus awareness, and no window management. It bypasses the compositor entirely, which breaks in sandboxed sessions and loses any security boundary. - wdotool uses the protocols Wayland already provides for this: libei (via the XDG RemoteDesktop portal), wlroots' virtual-keyboard/pointer, and foreign-toplevel-management. It respects compositor focus and permissions, and only falls back to uinput when nothing better is available.
Status
Early but usable. Tested on Hyprland. Current surface:
| Feature | libei | wlroots | uinput |
|---|---|---|---|
key / keydown / keyup |
✅ | ✅ | ✅ |
type (Unicode via keymap) |
partial¹ | ✅ | partial² |
mousemove (relative) |
✅ | ✅ | ✅ |
mousemove (absolute) |
✅ | ✅³ | ✅ |
click / mousedown / mouseup |
✅ | ✅ | ✅ |
scroll |
✅ | ✅ | ✅ |
search / getactivewindow |
— | ✅ | — |
windowactivate / windowclose |
— | ✅ | — |
¹ libei is a sender context; the EIS server owns the keymap. Characters not in the active layout are skipped with a warning. ² uinput has the same limitation as libei — the kernel doesn't know about keymaps. Best-effort via the env-default xkb layout. ³ wlroots absolute pointer needs output geometry to be meaningful; currently uses a 10,000×10,000 logical extent as a placeholder.
Install
# Arch Linux (AUR) — three flavors, pick one:
# Prebuilt binary via shell installer (any glibc x86_64 Linux)
|
# Nix (flake)
# or add to a NixOS/home-manager config:
# inputs.wdotool.url = "github:cushycush/wdotool";
# From crates.io (any Linux arch; builds on install)
# From source
&&
Runtime deps: libxkbcommon and libwayland-client. Both are universally present on Wayland systems.
Usage
Drop-in xdotool replacement for the common commands:
Global flags:
||||
Backends
libei (preferred on GNOME / KDE)
Opens an org.freedesktop.portal.RemoteDesktop session via the XDG portal, negotiates a libei socket, handshakes as a sender context, and emits input through the compositor's own libei implementation. Full focus awareness and permission prompts.
Requirements:
xdg-desktop-portal+ a backend that exportsorg.freedesktop.portal.RemoteDesktop. GNOME (46+) and KDE Plasma 6 both ship this.- On Hyprland specifically,
xdg-desktop-portal-hyprland1.3.11 does not yet expose RemoteDesktop. Use the wlroots backend instead.
wlroots (preferred on Sway, Hyprland, river, Wayfire)
Binds zwp_virtual_keyboard_v1, zwlr_virtual_pointer_v1, and zwlr_foreign_toplevel_management_v1 directly. Uploads a transient xkb keymap at type time (one keycode per unique character as a Level-0 Unicode keysym) — this is how arbitrary Unicode works without a server-side keymap change.
Requirements:
- A wlroots-based compositor that exposes the three protocols above. Sway, Hyprland, river, and Wayfire all do by default.
kde (KDE Plasma 5/6)
Composes libei (for input) with KWin-scripting window management over D-Bus. Generates small JS snippets, hands them to org.kde.KWin.Scripting.loadScriptFromText, runs them, and receives results on a transient com.wdotool.KdeBridge D-Bus service the backend registers at startup. Same trick kdotool uses, adapted to both the Plasma 6 (workspace.windowList, workspace.activeWindow) and Plasma 5 (workspace.clientList, workspace.activeClient) APIs.
Requirements:
xdg-desktop-portal-kdefor the libei input path.- KWin 5.22+ for the Scripting D-Bus interface.
uinput (last resort)
Creates a virtual input device via /dev/uinput and writes raw input_event structs through the kernel. Compositor-agnostic — works on Wayland, X11, or a bare framebuffer session — but has no focus awareness.
Requirements:
- Write access to
/dev/uinput. The usual setup isusermod -aG uinput $USER(orinputon some distros) plus a udev rule if the device isn't created with the group by default:
On Arch withKERNEL=="uinput", GROUP="uinput", MODE="0660"systemd-tmpfiles,uaccesstags work too. - xkb configuration present on the system (for keysym → keycode translation). Installed by default on every Wayland/X11 install.
Supported compositors
| Compositor | Input backend | Window backend |
|---|---|---|
| Hyprland | wlroots | wlroots |
| Sway | wlroots | wlroots |
| river | wlroots | wlroots |
| Wayfire | wlroots | wlroots |
| GNOME (46+) | libei | planned (extension) |
| KDE Plasma 6 | libei | KWin scripting (D-Bus) |
| Anything else | uinput | — |
Backend selection is automatic based on XDG_CURRENT_DESKTOP and compositor hints (SWAYSOCK, HYPRLAND_INSTANCE_SIGNATURE, etc.). The detector tries the preferred backend first and falls through to alternatives if it fails to bootstrap — use --backend to force a specific one.
Building
Requires Rust 1.75+ (for async-in-traits via async_trait). Builds cleanly on stable.
System libraries: libxkbcommon (runtime), libwayland-client. Both are universally present on Wayland systems.
Known limitations
- Unicode
typeworks only on wlroots today; libei falls back to the server's keymap and skips characters it can't find. - Absolute mouse coordinates on wlroots use a 10,000×10,000 logical grid because the backend doesn't yet track
wl_outputgeometry. - The only window backend is wlroots'
foreign-toplevel. KDE and GNOME window management are planned. - No pointer region / multi-seat handling yet — the first seat gets everything.
Project layout
src/
main.rs # CLI entry (tokio)
cli.rs # clap subcommand definitions
error.rs # WdoError
types.rs # Capabilities, KeyDirection, MouseButton, WindowInfo
keysym.rs # ctrl+shift+a parser
backend/
mod.rs # Backend trait (async_trait)
detector.rs # runtime backend selection
libei.rs # RemoteDesktop portal + reis
wlroots.rs # virtual-keyboard/pointer + foreign-toplevel
stub.rs # PendingBackend placeholder
Every real backend runs on a dedicated OS thread because the underlying event streams (reis::EiConvertEventStream, wayland_client::EventQueue) aren't Send. Input ops are dispatched to that thread via channels.
License
MIT OR Apache-2.0