Skip to content

Drive scoot over IPC

Drive scoot from a script or an agent: the socket, the rules that keep automation honest, and where the rest lives. The binaries document themselves for agents: scootctl --help (topics requests, actions, exit-codes, environment; help <verb> for one verb’s row) and scootctl --help --json for the machine-readable form — see Generated CLI pages for the contract every binary meets. The client is scootctl; scoot msg ... is the same client kept as a permanent alias on the compositor binary — every example works with either, byte for byte.

Everything a keybinding can do, and everything a user can type or click, is also a request on a Unix socket. This page is the reference for scripts and for agents doing computer-use tasks.

The client is scootctl; scoot msg ... is the same client kept as a permanent alias on the compositor binary. Every scootctl example below works with scoot msg in its place, byte for byte.

The socket can inject any keystroke, so it is a privileged channel: it lives in $XDG_RUNTIME_DIR/scoot.sock, is created 0600, and serves only connections from the same user as the compositor. A missing $XDG_RUNTIME_DIR is a one-line startup error.

Override the path with $SCOOT_SOCKET (read by the compositor and scootctl) or with --socket PATH on the compositor. Running two compositors at once means giving each its own socket.

scootctl opens one connection per invocation and closes it as soon as it has its answer. A client that wants many requests should pipeline them on one connection rather than open a connection per request — the per-connection bounds below are what keep the socket fair, and reconnecting resets them.

scootctl prints an error reply and exits non-zero. The client builds on every platform – scootctl is the macOS package – so a Mac can drive a compositor running in a VM.

Window focus and keyboard focus are separate. scootctl windows’ focused flag, the focus ring and scootctl action focus-* all mean the window. A layer surface holding the keyboard (a launcher like fuzzel or wofi, a bar’s search field) never appears there — what windows reports is where focus returns to once that surface goes away. So if a launcher is up, type and key go to the launcher, while windows still names the window behind it. There is no IPC request that reports a layer surface.

The focus-family actions do take the keyboard back. focus-column, focus-window, focus-window-id, focus-workspace and focus-workspace-index – with or without --output ID – each spend a click that had given a click-focused (on_demand) layer surface the keyboard, so after one, keystrokes go to the window windows reports as focused. The other actions — move-*, close, spawn, cycle-column-width, set-column-width, toggle-fullscreen, set-fullscreen, toggle-maximize, set-maximized, quit — change arrangement rather than where focus is reported to be, so they leave a deliberate keyboard placement alone, as does a keybinding. An exclusive layer surface keeps the keyboard through all of them, by protocol, until it unmaps. The one exception is the top layer under a fullscreen window: while a fullscreen window covers an output, that output’s top-layer surfaces are not drawn, so neither a click-focused nor an exclusive one there holds the keyboard until the output is uncovered again (overlay surfaces are unaffected).

popup_grab is the subtler case of the same split. An explicit xdg_popup.grab routes every keystroke to the menu until it is dismissed, and compositor focus still moves underneath it — focus keybindings keep firing with a menu open, by design. So after such a keybinding, windows can report window B as "focused": true while every key still reaches window A’s menu, possibly off-screen, with no error. Each window therefore reports popup_grab while its own popup tree holds the keyboard:

{ "id": 1, "focused": false, "popup_grab": true }

The rule: if any window reports "popup_grab": true, keystrokes go to that window’s menu, not to the focused window — wait for the menu to close (Escape or a click dismisses it) before typing at anything else. false means no grab, or a server predating the field. Two limits: a grab rooted at a layer surface — a bar’s own dropdown — belongs to no window and leaves every window false; and while the session is locked there is never a grab to report, because locking dismisses any open one and refuses new ones.

Screenshots are physical pixels; layout coordinates are logical. screenshot captures the framebuffer at full physical resolution, while windows and outputs report logical rectangles. Convert with physical = logical * scale, rounded down where a rectangle’s edge lands mid-pixel (the logical size is ceil(physical / scale), so a full-output logical * scale can overshoot by under one pixel). outputs reports each output’s scale for exactly this – and it is per output: with [[outputs]] entries two screens can run at different scales, so convert a window’s rectangle with the scale of the output the window is on (its output), never with the first output’s. X windows (--xwayland) follow the same rule: their rect is logical and a click at a logical point lands on the X widget drawn there, whatever the scale – the X server’s own pixels (ceil(scale) per logical pixel by default, floor(scale) with [xwayland] fractional = "light", see protocols.md) are never what an agent reads or sends.

A pointer lock freezes injected motion, and still answers ok. While a client holds an active pointer lock (zwp_pointer_constraints_v1 — a game or 3D app), pointer move and click answer ok but move nothing: the lock owns the pointer until its client releases it. Clicks still reach whatever surface holds pointer focus. A session lock ends the freeze — locking deactivates the held constraint, so injected motion and clicks reach the lock surface exactly like a real mouse, and unlocking re-arms the game’s lock. Do not assume a lock you observed survives a session lock.

wait-idle waits for nothing on screen to have redrawn, and a bar redraws on its own schedule. With a waybar clock ticking once a second, a short --quiet-ms settles normally while a long one never does and times out. Keep --quiet-ms below whatever your bar’s own redraw interval is — the same caveat an animated cursor carries.