Skip to content

Requests and replies

Every request, type vs key, and what the replies carry. All of these work as scoot msg <request>.

Request What it does
version Version and IPC protocol of the running compositor — needs a session. scoot --version answers locally with no session (scoot <version> (ipc protocol <N>)). <version> is the release (0.2.1), or the last release plus commits since (0.2.1+14.g615608c); the reply’s version field carries the same string. Compare the protocol number, not the version.
outputs Every output’s name, rectangle, usable rectangle, scale and power state.
windows Every window: id, app id, title, icon, output, workspace, adoption, focus, popup grab.
keyboard The active keyboard layout’s name and index — what a layout indicator shows, and which layout the next type will produce.
locked Whether the session is locked — the side-effect-free lock probe, answered locked or not ({"type":"locked","locked":false}). What tells an agent whether injected input would reach the lock screen instead of the desktop.
binds [--json] The live keymap: every combo in canonical spelling, its action string, where it came from, its repeat/allow_when_locked flags, plus skipped config binds with reasons — see Show the keymap. The human view is an aligned table grouped like the keybindings page; --json is the same reply as JSON. Answered locked or not.
output-power ID|all on|off Switch an output’s panel off or on — the IPC half of zwlr_output_power_v1 (see protocols.md). all powers every output at once; an unknown id is refused. Session-level like outputs, not an action: it applies while locked (the idle cycle is off-after-lock, on-at-resume). On the wire an additive request tag, with no PROTOCOL_VERSION change ({"type":"output_power","output":1,"powered":false}, output omitted for all).
output-scale ID|NAME SCALE|reset Set an output’s scale live, by id or connector name (DP-1), or reset it to the config file’s. Range 0.5 to 4 (resolved to 1/120 like the file’s); a value outside it, or not a number, is refused instead of clamped; an unknown id or name is refused too. Runtime state, like output-power: it beats the file’s scale for that connector (a replugged monitor comes back at it), and a successful reload or restart returns to the file’s scales. A repeat set, or a reset with nothing to reset, changes nothing. Applies while locked; refused under --nested. What the display profiles watcher drives. On the wire an additive request tag: output is a number (id) or a string (name), scale a number or null for reset, required either way ({"type":"output_scale","output":"DP-1","scale":1.5}); answered with the existing ok; an older server refuses the unknown tag with an error.
action ACTION [ARGUMENT...] Run a layout action — see Actions.
reload [--look] [--wait-wallpaper] Re-read the config file the session started from and re-apply what can be re-applied live (layout, output scale – the default and each [[outputs]] entry’s, [[outputs]] mode (a live resize per backend, refused under --nested), appearance, keybindings, new autostart spawn entries) — see configuration.md. A successful reload also drops every live output-scale scale, and each one the file disagrees with is listed in applied as outputs.<name>.scale. Answers reloaded with applied-vs-refused field lists plus the shadowed file keys that override the active look (and the invalid ones whose bad values fell back to it), or error (running config untouched, live scales kept) when the file cannot load or validate. With --look only the look layer is re-read, under a snapshot of the file from the last successful plain load: appearance and wallpaper swap, pending edits wait, no autostart entry spawns and no bind swaps. The reply carries the scope that ran ("scope":"look" on a scoped reload, absent on a plain one). With --wait-wallpaper the same plain reload parks its reloaded reply until the wallpaper run lands (or 30 s passes, reporting handed-over-but-unwaited-on) instead of answering once handed over — see below. --look with --wait-wallpaper is refused.
look-apply NAME Swap the active look and wait for the swap to land — what scoot look apply does to local files, for a client with no files (a Mac driving a VM). Answers look_applied when the swap’s child exits, or error when the swap is refused (unknown look, a path as the name, a second swap while one runs) with nothing touched. Applies while locked.
looks The look catalog in search order, the active look’s name, and the session’s state directory — what scoot look list and scoot look current read from local files, for a client with no files.
shortcut-register APP_ID SHORTCUT_ID [--trigger COMBO] [--description TEXT] Register a portal global shortcut (the stage-1 half of the org.freedesktop.portal.GlobalShortcuts portal — see Global shortcuts). The trigger is a hint only, stored and listed but never auto-bound; the user assigns the chord in [global_shortcuts] or with shortcut-bind. Idempotent by (app_id, shortcut_id); answers shortcuts, re-listing. Bounded: at most 64 per connection and 512 compositor-wide, trigger at most 256 bytes, description at most 1024 bytes — past any of those the register is refused with an ordinary error and nothing is stored (a re-register of an owned id never counts against the count caps).
shortcut-unregister APP_ID SHORTCUT_ID Remove one registration (unknown ids are refused; any same-user connection may drop any id). Answers shortcuts.
shortcuts List every registered shortcut: app and shortcut id, the app’s description and preferred trigger, the user-assigned chord if bound, and whether it is shadowed (conflicted: a live compositor bind holds the chord, or another registration won the grab for it). Bounded by the register caps above.
shortcut-bind APP_ID SHORTCUT_ID COMBO|reset Bind a registered shortcut to a user chord (reset, the wire null, clears it). A shortcut bound to a chord a live compositor bind holds stays listed but conflicted and never fires. Colliding with another registration’s chord does the same to the loser. Unknown ids and bad chords are refused. Any same-user connection may bind any id. Answers shortcuts.
screenshot [--output ID] [--out FILE] [--no-cursor] Capture the screen as PNG. Without --out, the PNG goes to stdout. --output names which output to capture, and the capture is that output’s own pixels. An id naming no output is refused rather than answered with another output’s pixels. Omitting it means the primary output. The pointer is drawn in unless --no-cursor — see The pointer in a screenshot.
pointer move X Y Move the pointer to logical coordinates.
pointer click X Y [left|right|middle|back|forward] Move, then press and release (default button left).
pointer button left|right|middle|back|forward press|release Half a click, for drags.
pointer scroll DX DY Scroll by a delta. Sent as a wheel scroll that also carries its detents (eight v120 units per delta unit, so pointer scroll 0 15 is exactly one detent), so clients that only listen for steps still see it. Runs scroll binds the way a wheel does, so a bind a held modifier (or none) matches takes the scroll and no client sees it; the request presses no modifier of its own, so it fires a scroll bind with no modifier (or one whose modifier is physically held).
key COMBO Press one key combination — see type vs key.
type TEXT Type text on the active keyboard layout.
wait-idle [--quiet-ms N] [--timeout-ms N] Block until nothing on screen has redrawn for --quiet-ms (default 200), giving up after --timeout-ms (default 5000).
subscribe [EVENT...] Dedicate this connection to events of the named kinds (output, keyboard, workspace, lock, look, global-shortcut; scoot msg subscribe with no names sends output; an empty events list on the raw socket is refused), streaming them until the session ends or drops the subscription — see Events. Every subscription starts silent. For a baseline, issue one keyboard query, read windows once (workspace), read locked once (lock) or read shortcuts once (global-shortcut), then apply events after it. A look subscription has no baseline: nothing has changed until the first look_changed arrives.
Terminal window
scoot msg windows
scoot msg action focus-column left
scoot msg reload
scoot msg reload --wait-wallpaper
scoot msg look-apply moonrise
scoot msg looks
scoot msg screenshot --out /tmp/shot.png
scoot msg type "hello"
scoot msg keyboard
scoot msg locked
scoot msg binds
scoot msg wait-idle --quiet-ms 200
scoot msg subscribe
scoot msg subscribe keyboard
scoot msg subscribe lock
scoot msg subscribe look
scoot msg subscribe global-shortcut
scoot msg shortcuts
scoot msg shortcut-bind org.example.recorder toggle-record super+ctrl+r

scoot msg type TEXT types text the way a person would, on the session’s keyboard layout (ask scoot msg keyboard which one that is): for each character it finds the key that carries it and holds whatever modifiers that key’s level needs — Shift for A or !, AltGr for a German layout’s @ — so a client receives the same key and modifier events as from a real keyboard. \n and \t send as Return and Tab.

A character no single keypress produces goes through a second path: the two-key dead-led sequence from the session-locale compose table (dead_acute then e for é on a German layout), pressed as the two keypresses a person would type, each with its own level’s modifiers. All 95 printable ASCII characters type on each of these fourteen Latin layouts (us, us(intl), gb, de, de(neo), fr, fr(oss), es, it, pt, se, no, dk, pl).

  • A character the active layout can’t produce is an error naming it (no key for `é` in this layout). The compose table counts as well as the keymap: a character with no two-key dead-led sequence there stays refused. Plain us carries no dead keys and no Compose key, so é is still “no key” on it, and three-key Multi_key sequences are never driven. A character on an inactive layout group is refused too: nothing here switches the session’s layout to find it. A character on a level the layout only reaches through a locking or latching modifier gets its own message ([character] needs a modifier this layout only locks or latches); scoot will not press Caps Lock, because that would leave it on afterwards. Sequences come from your own XCompose file if you have one ($XCOMPOSEFILE, then $XDG_CONFIG_HOME/XCompose, then ~/.XCompose), otherwise from the session locale’s table (LC_ALL, then LC_CTYPE, then LANG, else C, as the compositor process sees them — like setlocale, an empty variable counts as unset). In every case the characters before the failure are already typed: the request stops at the first character it can’t type rather than rolling back.
  • Keybindings apply to what it types, exactly as they would to a real keypress. That only matters for a bind with no modifiers, or one on Shift plus a key; if a character does hit a bind, the compositor logs a warning naming it rather than swallowing it silently.

scoot msg key COMBO is not the same. It presses exactly the combination named and holds exactly the modifiers named. Name the key as it is with nothing held, plus the modifiers: shift+1, not exclam; shift+a, not A. A name this layout only carries above its unmodified level is refused — scoot msg key exclam would press the 1 key and deliver 1. Some characters can’t be named as a combination at all (@ on a German layout needs AltGr, which key has no name for). type works the modifiers out from the layout; use it for text and key for chords. Modifiers resolve from the active layout too: whichever key actually holds Shift/Control/Alt/Super is what gets held (either hand’s key, or the one a layout option like grp:lshift_toggle left in place), so a combo is refused for its modifier only when no key on the layout can hold it.

outputs, one entry per output:

Field Meaning
id The output’s id, which a window’s output names.
name HDMI-A-1, eDP-1, … under --tty — the DRM connector name; headless, headless-2, … otherwise.
rect The output’s full rectangle, in logical pixels.
usable The full output minus whatever a bar reserved at its edges (layer-shell exclusive zones) — where windows actually go.
scale This output’s own scale. Outputs need not share one (see [[outputs]]); rect and usable are logical, screenshots are physical.
powered Whether the output is powered on — false while output-power (or wlopm over zwlr_output_power_v1) has it switched off. A powered-off output does no render work and takes no screenshots (refused, naming the recovery).
live Whether the output is currently driven — false while a --tty session has lost DRM master (usually a VT switch seatd failed to re-acquire master for; see VT switching). Always true off --tty, and on servers predating the field. The composited frame outlives master, so a screenshot of a live: false output is stale.

windows, one entry per window, in layout order:

Field Meaning
id The window id. What action focus-window-id N takes, and what follows the last dash of an ext-foreign-toplevel-list-v1 identifier.
app_id The toplevel’s app id, or "" before the toolkit has sent one. Under --xwayland, an X11 window’s is its WM_CLASS class (XTerm for xterm), falling back to the instance; X windows are listed like any other, with nothing on the wire to mark them.
title The toplevel’s title, same caveat. An X11 window’s is _NET_WM_NAME, falling back to WM_NAME. An X title or class is cut at its first NUL byte (X allows one; a Wayland string cannot carry it).
icon The freedesktop icon name the client committed through xdg-toplevel-icon-v1, or null — read off the surface’s current state when asked, so never stale. A client that supplied raw pixel buffers instead of a name reads as null, and so does every X11 window (_NET_WM_ICON is not read).
output The id of the output the window is on.
workspace Which workspace of that output the window sits on, 0-based — the same numbering focus-workspace-index N and move-window-to-workspace-index N take. (Bars see the 1-based twin over ext-workspace-v1.)
adopted Whether the window’s workspace was adopted from an unplugged monitor — with origin, where an unplugged monitor’s windows went.
origin Which connector the window’s workspace was adopted from ("DP-1"), or null for a workspace that was never adopted.
rect Where the window is, in logical pixels — what you click: the slot’s top-left corner, and the smaller of the slot and what the window last committed on each axis. Normally the whole slot. Smaller for a window that draws less than it was given (a fixed-size dialog, a video player keeping its size), and for a frame or two after its slot grows until the larger frame arrives. The same area the focus ring surrounds and rounded corners cut; never past the slot. A window that has drawn nothing yet reports its whole slot. Toplevel surface only: open menus and popups can draw outside rect. Only the part inside its own output’s rect (scoot msg outputs) is shown and clickable: a column scrolled past its output’s edge is cut there (as is a menu crossing it, unless it lets the compositor adjust it — toolkit menus do — when it is flipped or slid back onto the window’s output on open; see protocols.md), and a click past that edge lands on whatever the neighbouring output shows there — or on nothing, past the last output. A window that is not visible reports its layout frame unchanged — except one stacked in the same column as a fullscreen window, which reports that fullscreen window’s frame until the fullscreen ends.
visible false when the window is scrolled out of view, on an inactive workspace, or hidden behind a fullscreen window (every other window on an output a fullscreen window covers, including windows stacked in its own column, and every floating window on it). A floating window is also false before its first frame, and while it is fullscreen without focus.
focused Compositor window focus — not necessarily where keystrokes go; see below.
popup_grab Whether this window’s own popup tree holds the keyboard — see below.
fullscreen Whether the window is fullscreen. While its column is focused it covers its output: rect equals that output’s rect (for a window that draws its whole frame, as fullscreen windows do; see rect), and every other window on the output reports visible: false. Focused away, it keeps that size and sits in the strip where a column that wide would, one ordinary gap from its neighbours — it may still be partly visible beside the focused window, never overlapping it, and like any window is drawn and clickable only within its own output, however far its rect reaches past that output’s edge.
maximized Whether the window is maximized. While its column is focused (and no fullscreen window covers the output) it fills the output’s usable area: rect equals that usable area minus the layout gap, and every other tiled window on the output reports visible: false (floating windows stay above it). Focused away, it keeps that size and sits in the strip where a column that wide would, one ordinary gap from its neighbours. Fullscreen wins while both hold.
floating Whether the window floats above its workspace’s strip: a dialog, transient or fixed-size window floated as it mapped, a [[window_rule]] match, or toggle-floating/set-floating. Its rect is where it really is: where last moved (by a drag or move-floating), else centred on its parent (when visible on the same workspace) or its output, inside the output’s usable area, at the size it drew. Drawn above, and taking clicks before, every tiled window on its output. A floating window with no first frame yet is visible: false, as is one on an inactive workspace or under a covering fullscreen window.

keyboard, the seat keyboard’s currently effective layout (xkb group):

Field Meaning
index The active group, 0-based — the same numbering type resolves each character in, so an agent reads off which layout its next type will produce.
name The keymap’s own name for that group (us reads as "English (US)", ru as "Russian") — what a bar shows.

Read live off the compositor’s keymap on every request. Read-only: scoot has no layout-switch bind, option or action, so nothing over IPC switches the layout. The group moves only through the keymap’s own mechanics (a toggle key from the XKB_DEFAULT_OPTIONS the session started with, e.g. grp:caps_toggle).

locked, whether the session is locked:

Field Meaning
locked true while the session is locked, false otherwise, read live off the compositor’s lock state on every request.

Unlike every action, this answers locked or not. Read-only: nothing over IPC locks or unlocks the session.

Terminal window
$ scoot msg locked
{
"type": "locked",
"locked": false
}

binds, the live keymap as the running compositor uses it — what the default Super+Shift+/ bind opens in a terminal:

Field Meaning
combo The combo in canonical spelling: modifiers in super+shift+ctrl+alt order, then the xkb keysym name (super+shift+slash).
action The action string: the same grammar scoot msg action takes and [binds] uses (focus-column left, spawn foot, show-keymap; change-vt 3 for the session-managed VT-switch binds, which have no config spelling).
source Where the row came from: default (a built-in bind, unchanged), config (a combo the defaults leave unbound), config (replaces default: <old action>), config (unbinds default: <old action>) (a config unbind — action names what was removed), or session (VT switch) (the --tty recovery binds).
repeat Whether the bind re-fires while its key is held. Never true on a default row.
allow_when_locked Whether a spawn bind fires while the session is locked. Only ever true beside a spawn action.

One row per effective binding — built-ins first in their canonical order, then config binds (fresh combos and overrides alike) sorted by combo, then any session-managed rows — followed by one row per default a config unbind removed. Sorting config rows keeps --json stable from run to run. A second list, skipped, holds every config entry that never made it in, sorted by combo — each with its bind (as written), its value (in TOML form), and its reason (a parse error, a colliding group, or an unbind with nothing to remove).

Terminal window
$ scoot msg binds --json
{
"type": "binds",
"bindings": [
{
"combo": "super+h",
"action": "focus-column left",
"source": "default",
"repeat": false,
"allow_when_locked": false
}
],
"skipped": []
}

Derived by diffing the live table against the defaults on every request, so it is always the merged result — never a re-read of the file. Like locked, this answers locked or not: listing keys is not a window-management operation (the Super+Shift+/ bind itself never fires while locked). binds is a new reply variant and moved PROTOCOL_VERSION 8 → 9. An older client handed one would fail its decode, but only a client new enough to have asked receives one; a binds sent to an older server answers an ordinary error.

Every ok reply also carries a locked field: the session-lock state it was built under. An agent typing a password over IPC learns the unlock landed from the next reply.

locked, usable, scale, icon, popup_grab, fullscreen, maximized and floating are additive and defaulted: an older server omits them, with no PROTOCOL_VERSION bump, so read each asymmetrically. locked: true, popup_grab: true, fullscreen: true, maximized: true and floating: true are always truthful, while false means “no, or a server predating the field”; an all-zero usable means the same (fall back to rect), and a missing scale decodes as 1.0.

reloaded, the answer to reload, is a new reply variant, not a defaulted field, and moved PROTOCOL_VERSION 2 → 3. An older client handed one would fail its decode, but only a client new enough to send reload ever receives one. A reload sent to a server predating the request answers an ordinary error, not a kill: an unknown request tag is a decode error the server answers and keeps serving.

{ "type": "reloaded", "applied": ["layout.gap", "binds"],
"refused": ["tty.gpu (takes effect on restart: the session already drives its device)"] }

A look-scoped reload ({"type":"reload_look"}, sent as scoot msg reload --look) answers the same shape with "scope":"look": only the look layer was re-read, so applied names appearance and wallpaper fields only, and a half-written file cannot turn it into an error. A plain reload omits scope. The scoped tag is additive (PROTOCOL_VERSION did not move). An older compositor answers it with an ordinary unknown-request error (the client prints the error and exits non-zero, the running config untouched), never a silent plain reload.

A wallpaper-wait reload ({"type":"reload_wait_wallpaper"}, sent as scoot msg reload --wait-wallpaper) runs the same plain reload but parks its reloaded reply until the wallpaper run lands instead of answering once handed over. The reply shape is the same reloaded (no new variant, scope absent like a plain reload); past 30 s it answers anyway with one extra refused entry (wallpaper (timed out waiting for the wallpaper run; handed over, exit unwaited-on)), and the run’s exit is still logged when it comes. A wait with no wallpaper run in flight answers at once, like a plain reload; an error (unreadable file, bad layer) also answers at once, never parked. Two waits park two replies, each answered when its own run (or the newest, if superseded) exits; a disconnected client drops its park. SIGHUP never parks: it has no reply channel, so it runs the plain path. The tag is additive like the scoped one (PROTOCOL_VERSION does not move). An older compositor answers it with an ordinary unknown-request error, never a silent immediate answer (which would read as “landed” when it only handed over).

applied names the fields re-applied live (including autostart.commands when new spawn entries started, where a spawn entry that fails to start is refused by name instead and stays pending for the next reload, and wallpaper / wallpaper.command when the [wallpaper] section or its command changed, which is handed to scootbg: its outcome is in the compositor log, not in this reply), refused the ones that differed but cannot be (each with its reason: the two restart fields, a non-spawn autostart entry by name, a locked-skipped autostart delta, an unusable [[window_rule]] by its position in the file, or a [wallpaper] section with a problem, named – an unknown key inside [wallpaper] is this refusal, not an error). Both name only fields that differed: two empty lists together mean the reload changed nothing it was asked to – except an unusable window rule and a [wallpaper] section with a problem, each refused on every reload that finds it, since neither is ever in effect. A reload that could not load or validate the file answers error with the running config untouched (scoot msg exits non-zero).

look_applied, the answer to look-apply, names the look now active (name), the apps reloaded in order (applied: compositor, bar <display>, …), and every user-file key that overrides a look token (shadowed). refused is empty on this reply: a refusal fails the swap and answers error instead, naming the rollback. The reply arrives when the swap lands: the compositor runs scoot look apply in a child and answers when the child exits, so only one swap runs at a time (a second is refused). Ring, background and cursor are on screen with the reply. The wallpaper follows through scootbg (handed over, never waited on; its outcome is in the compositor log), so screenshot after wait-idle to see it.

A timed preview ({"type":"look_apply","name":"moonrise","for_ms":20000} on the raw socket; scoot msg sends a stay) swaps back when the hold expires, unless a newer choice moved meanwhile. A swap still running when the hold expires holds the revert until it finishes, then re-checks, so the preview never sticks with no timer left. Zero holds nothing (a stay, like omitting it). A restart drops a pending revert; the swap stays.

{ "type": "look_applied", "name": "moonrise",
"applied": ["compositor"], "refused": [],
"shadowed": ["appearance.focus_ring_active_color"] }

looks, the answer to looks, carries the catalog (looks: one entry per look with name, title, polarity, origin and source), the active look’s name (active, or null with no recorded choice), and the session’s state directory (state_dir).

{ "type": "looks", "active": "moonrise",
"state_dir": "/run/user/1000/scoot-look-wayland-1",
"looks": [{ "name": "moonrise", "title": "Moonrise",
"polarity": "dark", "origin": "shipped",
"source": "/prefix/share/scoot/looks/moonrise" }] }

look_applied and looks are new reply variants and moved PROTOCOL_VERSION 11 → 12. An older client handed one would fail its decode, but only a client new enough to have asked receives one. The request halves are additive ({"type":"look_apply","name":"…"} optionally with "for_ms", {"type":"looks"}): an older server answers either with an ordinary error, never a misapplied swap.