Unified desktop control for Hyprland / Wayland — one small CLI that wraps
grim (screenshots), ydotool (mouse), wtype (keyboard) and hyprctl
(windows) behind a consistent interface, so scripts and LLM agents can see and
drive the real desktop.
desktopctrl screenshot # capture the screen -> PNG path
desktopctrl windows # list windows + geometry
desktopctrl focus chrome # focus a window by substring
desktopctrl moveclick 1110 513 # click at logical pixel (1110,513)
desktopctrl type 'hello' # type into the focused window
desktopctrl key ctrl+l # send a chordIt comes with an optional MCP server (mcp/) and a Claude Code
skill so an agent can control the machine through the same tool.
Wayland deliberately has no global "type/click anywhere" API like X11's XTEST.
The pieces exist (grim, ydotool+uinput, wtype, hyprctl) but each has
its own flags, quirks, and coordinate space. desktopctrl unifies them and
handles the annoying bits: ydotool's absolute-coordinate scaling, the
ydotoold daemon lifecycle, and rootless /dev/uinput setup.
| Tool | Purpose | Arch package |
|---|---|---|
hyprctl |
window management (ships with Hyprland) | hyprland |
grim |
screenshots | grim |
ydotool + ydotoold |
mouse/keyboard injection via /dev/uinput |
ydotool |
wtype |
typing / key chords | wtype |
jq, awk, bash |
glue | jq (awk/bash are usually present) |
Plus a running ydotoold daemon with an accessible socket. desktopctrl doctor validates all of the above and tells you what's missing.
git clone git@github.com:highercomve/desktopctrl.git
cd desktopctrl
./install.shinstall.sh will:
- check dependencies (and print the install command for any that are missing),
- symlink
bin/desktopctrlinto~/.local/bin, - set up rootless
ydotoold: install a udev rule for/dev/uinput, add you to theinputgroup, load theuinputmodule, - install + enable a
systemd --userservice soydotooldstarts with your session, - install the desktop-control agent skill to
~/.agents/skills/desktop-controland symlink it into every detected AI agent's skills dir (Claude Code, opencode, Cursor, …), - register the MCP server with Claude Code (
claude mcp add desktopctrl …at user scope, so it's available in every project) — just like installing playwright-mcp. Reconnect Claude Code afterwards and call thedaemon_statustool to verify.
Flags: --cli-only (just deps + symlink), --no-systemd, --no-rootless,
--no-skill (skip the agent skill), --no-mcp (skip MCP registration),
--only-skill (install only the skill), --only-mcp (register only the MCP
server), --mcp-scope <local|user|project> (MCP config scope, default user).
After the first install you must reboot (or
loginctl terminate-user $USERand log back in) so thesystemd --usersession picks up the newinputgroup membership — anewgrp inputshell alone won't fix the user manager's/dev/uinputaccess. Then rundesktopctrl doctor.
desktopctrl daemon prefers the systemd --user service, then a rootless
daemon if /dev/uinput is writable. It will never auto-escalate to root —
a root ydotoold creates an unmanaged virtual input device that can perturb the
compositor (dropped bars/keybinds). If you really need a one-off root daemon,
opt in explicitly:
DESKTOPCTRL_SUDO=1 desktopctrl daemon
# equivalent: sudo ydotoold -p /tmp/.ydotool_socket -P 0666 &If you're already in the input group but still get a /dev/uinput access
error, your session predates the group change — reboot and try again.
desktopctrl finds the socket automatically ($YDOTOOL_SOCKET, then
$XDG_RUNTIME_DIR/.ydotool_socket, then /tmp/.ydotool_socket).
All coordinates are logical pixels — the same space as hyprctl cursorpos
and a scale=1 grim screenshot. ydotool's absolute axis is offset by a divisor
(default 2, auto-derived by desktopctrl calibrate), applied internally, so you
always pass logical coordinates. If clicks land off-target, run calibrate.
screenshot [path] full screen -> png (prints path)
shot-window [path] active window -> png
move <x> <y> move cursor
click [left|right|middle] click at current position
moveclick <x> <y> [btn] move then click
doubleclick <x> <y>
drag <x1> <y1> <x2> <y2>
type <text...> type text
key <combo> e.g. ctrl+l, Return, Escape, alt+Tab
cursorpos print cursor position
windows list mapped windows (focused marked *)
activewindow focused window info
locate <substr> print centre "x y" of a matching window
focus <match> focus window (class:/title:/address:/substr)
exec <cmd...> launch an app
workspace <id> | close
daemon | daemon-stop | daemon-status ydotoold lifecycle
calibrate derive the ydotool coordinate divisor
doctor validate the environment (exit non-zero if not ready)
version
desktopctrl doctor # is everything ready?
desktopctrl windows # what's open + where
desktopctrl focus chrome # focus a window
read x y < <(desktopctrl locate chrome) # its centre
desktopctrl moveclick "$x" "$y" # click it
desktopctrl key ctrl+l # address bar
desktopctrl type 'http://...' ; desktopctrl key Return
SHOT=$(desktopctrl screenshot) # then view "$SHOT" to verifyskill/ is a portable "desktop-control" skill that teaches an AI agent
how to use desktopctrl correctly (sandbox/daemon requirements, the coordinate
model, finding targets, the command reference). install.sh places it at
~/.agents/skills/desktop-control and symlinks it into each detected agent.
mcp/ is a FastMCP server exposing the CLI as MCP tools (screenshots
return images). It self-bootstraps — point your MCP client at
mcp/run.sh, which creates a venv, installs deps, and runs the
server. See mcp/README.md.
bin/desktopctrl the CLI
install.sh installer / environment setup
systemd/ydotoold.service systemd --user unit for the daemon
udev/99-uinput.rules rootless /dev/uinput access
skill/SKILL.md agent skill (installed to ~/.agents/skills)
mcp/ FastMCP server (run.sh bootstraps venv + deps)
desktopctrl doctoris the first stop — it checks session, deps, uinput access, daemon, socket, and calibration.- "No such file or directory" for ydotool: the daemon socket is missing or
ydotoolisn't installed —desktopctrl daemon/doctor. - Clicks land in the wrong place:
desktopctrl calibrate. - Mouse does nothing:
ydotooldisn't running, or/dev/uinputisn't accessible — re-run./install.shand reboot, or force a root daemon withDESKTOPCTRL_SUDO=1 desktopctrl daemon.
MIT — see LICENSE.