Skip to content

Events

Subscribe instead of polling: dedicate a connection to the event stream and learn about outputs, layouts and workspaces as they change.

A connection that wants push notifications subscribes instead of polling. subscribe names the event kinds it wants (output, keyboard, workspace — the three kinds; naming none is refused, and bare scootctl subscribe sends output); the reply echoes the subscription; and afterwards that connection carries events until the session ends:

Terminal window
$ scootctl subscribe
{"type":"subscribed","events":["output"]}
{"type":"output_removed","output":2,"name":"DP-1","adopter":1,"adopted_start":2,"adopted_count":2,"adopter_prev_active":0,"adopter_active":2,"origin":"DP-1"}
{"type":"output_restored","output":3,"name":"DP-1","adopter":1,"adopted_start":2,"adopted_count":2,"adopter_prev_active":2,"adopter_active":0,"origin":"DP-1","moved":3}
{"type":"output_changed","output":1,"name":"DP-1","width":2952,"height":1660,"scale":1.5}
$ scootctl subscribe keyboard
{"type":"subscribed","events":["keyboard"]}
{"type":"keyboard_changed","name":"Russian","index":1}
$ scootctl subscribe workspace
{"type":"subscribed","events":["workspace"]}
{"type":"workspaces","output":1,"name":"DP-1","active":0,"counts":[2,0,1]}

scootctl subscribe prints the answer, then one compact JSON object per line per event, until killed or the connection ends — so a desktop notification is one pipe away (scootctl subscribe | ... notify-send), and an agent learns about a monitor leaving without polling windows. It exits 0 at a clean end of stream; re-run to resubscribe.

Three rules, matching the request/reply contract beside them:

  • A subscribed connection serves no further requests. After the subscribed answer it carries events only; any other request on it is refused with an error naming the rule. Open another connection for requests — they pipeline, so one is enough for any number of them.
  • Filtering is by kind, not by field. The server sends every event of the subscribed kinds, and the client filters or debounces further itself. In particular the removal/restore pair below fires on every monitor standby too (a routine unplug to scoot) — that is accepted, and it is the difference from a notification pushed at the user unconditionally, which is why scoot still draws and sends nothing itself. output_changed fires only on an applied resize, never on standby (the mode is unchanged then), and a refused resize fires nothing.
  • The same socket and the same credentials. There is no second channel: a subscriber connects to the same 0600, same-user control socket every other client uses.

output_removed — an output was removed and its workspaces adopted:

Field Meaning
output The removed output’s id, as outputs reported it.
name Its connector name (DP-1 under --tty, headless-2 otherwise) — the identity a later restore matches on.
adopter The output that adopted its workspaces, or null when none did.
adopted_start / adopted_count The adopted block on the adopter: the 0-based workspace index it starts at, in the post-removal list, and how many workspaces it holds (0 when the removed output held no windows).
adopter_prev_active / adopter_active The adopter’s active workspace before and after the removal, 0-based — where it was already looking, or the adopted workspace the switch moved it to when focus was on the removed output. null with no live adopter to read off.
origin Which connector the adopted workspaces are tagged as coming from (DP-1) — what bars show and what windows reports each adopted window adopted from. null when nothing was adopted.

output_restored — an output came back under a matching identity (note the fresh output id: ids are stable for the session, not across unplug cycles). The adoption fields describe the record this restore consumed, as the removal filed it; moved says how many still-open windows actually went back — windows moved by hand or closed in between stay where they are. adopter_prev_active / adopter_active are the adopter’s view before the restore and after it returns to its pre-adopt view (null when the adopter itself is gone, e.g. a chained unplug whose middle monitor never returned — then nothing moved either).

output_changed — an output’s mode changed in place: the same connector at a new framebuffer size, with the scale it keeps running at (a mode change never changes the scale). This is the re-probe resize — a --tty hotplug offering a new mode for a connector that stays connected (a VM window moving between displays of different densities), or a --nested host window being resized — not a removal: no workspace is adopted, nothing moves, and no restore follows. A script watching density recomputes the scale it wants from width / height and applies it with a config reload (output.scale and outputs.<name>.scale apply live); polling outputs gives the same numbers, this just says when to look. It fires once per applied resize, after the windows are re-laid-out at the new size; a resize the render target refuses fires nothing.

Field Meaning
output The output’s id, as outputs reports it.
name Its connector name (DP-1 under --tty, headless-2 otherwise) — the same string outputs names it by.
width / height The new framebuffer size in physical pixels.
scale The scale the output keeps running at — the scale to recompute from.

keyboard_changed — the seat keyboard’s effective layout (xkb group) changed: the same name and index the keyboard query answers with (see What the replies carry), naming the layout now in effect. No standard Wayland protocol reports this to an unfocused client — wl_keyboard sends the keymap and the modifier group only to the client holding keyboard focus, which a bar never does — so this event (and the query) is the channel a layout indicator reads.

It fires once per change, never per keypress: typing on one layout sends nothing, and one group switch sends exactly one event. Rapid successive switches each send their own — every event names the layout in effect when it was sent, so a reader that processes them in order ends where the keyboard is. As with the query, this is read-only: nothing over IPC switches the layout, and the bar’s click action has nothing to call — the group moves only through the keymap’s own mechanics (a toggle key from the XKB_DEFAULT_OPTIONS the session started with). Layout switching UI and per-window layouts are out of scope.

A subscriber that stops reading is disconnected rather than buffered without bound: past the same 1 MiB queued-reply bound a connection observes, or with no byte leaving for the same 10-second stall window, the compositor shuts the connection down and drops the subscription. Output removal never waits for a subscriber. A client that disconnects itself leaves no record behind.

workspaces — one output’s workspace occupancy changed: which of its workspaces hold windows, as a full snapshot rather than a delta, so a subscriber that missed one is never wrong. What a bar’s workspace module draws (dimming the empty ones) without polling windows, and what tells an agent “workspace 3 now has windows” the same way. No standard Wayland protocol reports this to an unfocused client — ext-workspace-v1 carries the list, positions and the one active bit, but no “holds windows” bit, and no other standard protocol maps a toplevel to a workspace.

Field Meaning
output The output’s id, as outputs reports it.
name Its connector name (DP-1 under --tty, headless-2 otherwise) — the same string outputs names it by.
active The output’s active workspace, 0-based — the same numbering focus-workspace-index N takes.
counts One entry per workspace, in order: how many windows sit on it (counts[i] > 0 is the occupied flag a bar draws; the number itself is what an agent reads).

One event per output whose snapshot moved — a window opened, closed or moved between workspaces, the active workspace switched, or an output added (a removed output sends nothing: its removal is already an output_removed, and there is no occupancy left to report). Coalesced to at most one event per output per frame tick, however fast windows churn: a client opening and closing windows at its maximum rate is one event per tick, not an event stream. A fresh subscription starts silent, like keyboard — read windows once for the baseline (the counts are the histogram of each window’s workspace on its output) and apply snapshots after it — so subscribing never replays the unsubscribed interval as one change.

Versioning: the subscription is IPC protocol 5 — the subscribed, output_removed and output_restored tags under the 3 → 4 bump, plus the output_changed tag under 4 → 5 — the keyboard half is protocol 6: the keyboard reply and the keyboard_changed tag — and the workspace occupancy event is protocol 7: the workspaces tag. A client that never sends subscribe (or keyboard) never receives any of them. An unknown event kind in a subscribe is answered with an ordinary error like any unknown request tag, so an older server meets a newer subscriber with an error, not a kill.