Drive Hyprland
Operate the local Hyprland compositor (Lua config, Hyprland 0.55+). This is
the Linux counterpart of windows-ui-inject. It is not
agent-workstation-drive (that loop is Windows mstsc + a GNOME/KDE container).
Helper (next to this file): scripts/hypr-drive
After harness install: ~/.grok/skills/drive-hyprland/scripts/hypr-drive
Legacy hyprctl dispatch workspace 5 / focuswindow address:0x… fails on
Lua config. Always dispatch Lua. hl.dsp.* returns a dispatcher table and
does nothing until hl.dispatch(...).
Setup (host)
Needs: Hyprland session, hyprctl, python3, grim. Clicks need ydotool +
ydotoold. Typing needs wtype.
# from this skill directory (clone of agent-skills)
./scripts/install.sh host
./scripts/install.sh verify
What install.sh host does (and what you must run with sudo when it prints):
- Packages:
ydotoolwtypegrim(omarchy pkg addif present, elsepacman -S). - User in group
input(sudo usermod -aG input "$USER"— re-login if newly added). /dev/uinput→0660 root:input. The package ships/usr/lib/udev/rules.d/80-uinput.rulesbut the node often boots asroot:root 600.udevadm trigger --name-match=uinputmay printInvalid argument;chgrp/chmodis the reliable fix.systemctl --user enable --now ydotool.service- Socket:
$XDG_RUNTIME_DIR/.ydotool_socket(dotfile;ls -lthat path).
Manual equivalent:
# packages
omarchy pkg add ydotool wtype grim # or: sudo pacman -S --needed ydotool wtype grim
sudo usermod -aG input "$USER" # then log out/in if this is new
sudo udevadm control --reload
sudo chgrp input /dev/uinput
sudo chmod 0660 /dev/uinput
ls -l /dev/uinput # expect crw-rw---- root input
systemctl --user reset-failed ydotool.service
systemctl --user enable --now ydotool.service
ls -l "$XDG_RUNTIME_DIR/.ydotool_socket"
ydotoold logs Permission denied on /dev/uinput until step 3. After chmod,
reset-failed then enable --now if it hit start-limit.
Install (harness)
Point the harness at this clone, or symlink the skill folder:
./scripts/install.sh harness # ~/.grok/skills, ~/.claude/skills, ~/.cursor/skills
./scripts/install.sh harness grok # Grok only
Preferred Grok load-from-clone (no copy):
# ~/.grok/config.toml
[skills]
paths = ["~/agent-skills"]
install.sh harness creates a symlink to this folder so the clone stays
source of truth.
Inspect
./scripts/hypr-drive windows
./scripts/hypr-drive workspaces
./scripts/hypr-drive monitors
./scripts/hypr-drive active
hyprctl clients -j: class, title, pid, address, at [x,y], size
[w,h], workspace.id / .name, monitor, mapped, hidden, floating,
xwayland. at can be negative (monitor left of origin).
Mutate (verified)
Switch workspace (change_id renames an id; do not use it to switch):
hyprctl dispatch 'hl.dsp.focus({ workspace = 1 })'
# or
hyprctl repl 'return hl.dispatch(hl.dsp.focus({ workspace = 1 }))'
Focus:
./scripts/hypr-drive focus-class google-chrome
./scripts/hypr-drive focus-title 'timsonner.com'
./scripts/hypr-drive focus-workspace 1
Cursor (layout coords, same space as at):
./scripts/hypr-drive cursor -100 200
URL in the existing Chrome/Chromium session (not omnibox typing):
./scripts/hypr-drive open-url https://example.com
Screenshot (grim; negative geometry is valid):
./scripts/hypr-drive screenshot-class google-chrome
Launch: hyprctl dispatch 'hl.dsp.exec_cmd("ghostty")'
Close: hl.dispatch(hl.dsp.window.close({ window = w }))
Compositor binds vs client keys
wtype / hypr-drive combo injects into the focused client (zwp_virtual_keyboard_v1). Hyprland never sees Super. hypr-drive combo super+s will not toggle the scratchpad.
Look up the bind, then run that action:
# Omarchy defaults: $OMARCHY_PATH/default/hypr/bindings/*.lua
# Live list: hyprctl binds -j (Lua binds show dispatcher=__lua — you cannot
# re-invoke by that index; copy the action from the lua source.)
Do not wrap every Omarchy key in hypr-drive. Full list: omarchy menu keybindings --print or ~/Documents/omarchy-shortcuts-discussed.md. Copy the action, then hl.dispatch(...) or run the command.
Stock Omarchy (modmask 64 = SUPER). Helpers only for the two we keep hitting:
| Key | Action |
|---|---|
| Super+S | ./scripts/hypr-drive scratchpad → toggle_special("scratchpad") |
| Super+Alt+S | hl.dsp.window.move({ workspace = "special:scratchpad", follow = false }) |
| Super+Space | ./scripts/hypr-drive menu → omarchy-menu toggle |
| Super+1–0 | hl.dsp.focus({ workspace = N }) — 0 is workspace 10 |
| Super+Tab / Shift+Tab | hl.dsp.focus({ workspace = "e+1" }) / "e-1" |
| Super+Ctrl+Tab | hl.dsp.focus({ workspace = "previous" }) |
| Super+Shift+N | hl.dsp.window.move({ workspace = N }) (follows). Add follow = false for Shift+Alt |
| Super+Left/Right/Up/Down | hl.dsp.focus({ direction = "l" }) — l/r/u/d |
| Ctrl+Alt+Tab | hl.dsp.focus({ monitor = "+1" }) |
| Super+W | hl.dsp.window.close() |
| Super+T | hl.dsp.window.float({ action = "toggle" }) |
| Super+F | hl.dsp.window.fullscreen({ mode = "fullscreen" }) |
| Super+Alt+F | hl.dsp.window.fullscreen({ mode = "maximized" }) |
| Super+J | hl.dsp.layout("togglesplit") |
| Super+G | hl.dsp.group.toggle() |
| Super+Alt+Tab | hl.dsp.group.next() |
| Super+Return | omarchy-launch-terminal / { omarchy = "terminal" } |
| Super+O | this machine: ~/.config/hypr/window-pop-fit.sh (not stock omarchy-hyprland-window-pop) |
| Alt+Tab | cycle_next + bring_to_top unless active class matches freerdp — then do not dispatch (Windows gets the keys) |
| Super+Shift+Alt+Left | hl.dsp.workspace.move({ monitor = "l" }) |
Always hl.dispatch(...). Example:
hyprctl dispatch 'hl.dsp.focus({ direction = "l" })'
hyprctl dispatch 'hl.dsp.window.fullscreen({ mode = "maximized" })'
Not dispatchers (in-app / wtype after focus): Ghostty splits (Ctrl+Shift+E/O), Herdr prefix (Ctrl+Space, release, then key), Omawrite Ctrl+P, font zoom.
Do not add a helper for Ctrl+Alt+Del (omarchy-hyprland-window-close-all).
ydotool key (uinput) might reach Hyprland as a real Super; prefer the dispatcher/omarchy-menu so the action does not depend on focus or key timing.
Click and type
Pointer: move with Hyprland layout coords, then click at the current
pointer. Do not ydotool mousemove --absolute with Hyprland at values
(multi-monitor origin ≠ ydotool origin).
./scripts/hypr-drive cursor -803 313
./scripts/hypr-drive click left
Click codes: left 0xC0, right 0xC1, middle 0xC2.
Keyboard: wtype (zwp_virtual_keyboard_v1) on Hyprland. Focus first.
Use this for in-app keys (Ctrl+L, Return, typing). Not for Super+… window-manager binds.
./scripts/hypr-drive focus-class google-chrome
./scripts/hypr-drive type 'hello'
./scripts/hypr-drive key Return
./scripts/hypr-drive combo ctrl+l
Prefer app IPC (open-url, exec_cmd) over synthesizing Chrome Ctrl+L.
Related: ~/claude-screen-agent/actuator.py (YdotoolActuator) for evdev
keycodes if you need ydotool key (GNOME; optional here). Super = 125.
Do not
hyprctl dispatch workspace 5orfocuswindow address:0x…- Call
hl.dsp.focus(...)withouthl.dispatch - Use
hl.dsp.workspace.change_idto switch workspaces - Edit
~/.config/hypr/from this skill - Drive this session with
agent-workstation-drive/windows-ui-inject wtype/combo super+…for compositor binds (scratchpad, menu, workspaces)
Dispatchers
Wiki: https://wiki.hypr.land/Configuring/Basics/Dispatchers/
| Goal | Call |
|---|---|
| Workspace | hl.dsp.focus({ workspace = N }) also "+1", "previous", "special:name" |
| Window | hl.dsp.focus({ window = w }) |
| Direction | hl.dsp.focus({ direction = "l" }) — l/r/u/d |
| Monitor | hl.dsp.focus({ monitor = "DP-2" }) |
| Move window to ws | hl.dsp.window.move({ workspace = 3, window = w, follow = true }) |
| Cursor | hl.dsp.cursor.move({ x, y }) |
| Shortcut | hl.dsp.send_shortcut({ mods = "CTRL", key = "L", window = w }) |
After a mutate, re-read hypr-drive active. Do not assume focus stuck.