Events
Subscribe instead of polling: dedicate a connection to the event stream for outputs, layouts, workspaces, the session lock, the active look and portal global shortcuts.
Events
Section titled “Events”subscribe names the event kinds it wants (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); the reply echoes the subscription,
and afterwards that connection carries events until the session ends:
$ scoot msg 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_added","output":3,"name":"DP-1","width":3840,"height":2160,"scale":1.5}{"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}$ scoot msg subscribe keyboard{"type":"subscribed","events":["keyboard"]}{"type":"keyboard_changed","name":"Russian","index":1}$ scoot msg subscribe workspace{"type":"subscribed","events":["workspace"]}{"type":"workspaces","output":1,"name":"DP-1","active":0,"counts":[2,0,1]}$ scoot msg subscribe lock{"type":"subscribed","events":["lock"]}{"type":"lock_changed","locked":true}$ scoot msg subscribe look{"type":"subscribed","events":["look"]}{"type":"look_changed","name":"moonrise","source_hash":"9f2c"}$ scoot msg subscribe global-shortcut{"type":"subscribed","events":["global_shortcut"]}{"type":"shortcut_activated","app_id":"org.example.recorder","shortcut_id":"toggle-record","timestamp_ms":4242}{"type":"shortcuts_changed"}scoot msg subscribe prints the answer, then one compact JSON object per
line per event, until killed or the connection ends. A desktop
notification is one pipe away (scoot msg subscribe | ... notify-send).
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
subscribedanswer 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. The removal/add pair below (plus a restore when
the monitor held windows) fires on every monitor standby too.
output_changedfires only on an applied resize, never on standby, and a refused resize fires nothing. - The same socket and the same credentials. A subscriber connects to
the same
0600, same-user control socket every other client uses.
output_added — an output was added: a monitor plugged in (or back
in) under --tty. It fires on every add: a first plug, a replug
that held no windows, and a replug that brings
windows back, which sends output_added first and output_restored
after it. “The connected set changed” is output_added plus
output_removed; output_restored alone misses a first plug and an
empty monitor’s replug. It is sent
once the output is laid out, so an outputs query answered after it
lists the output. The outputs a session starts with come before any
subscriber can exist: read outputs once for the baseline, then apply
events after it. (--headless adds its --outputs N at startup only,
so it never sends one at runtime.)
| Field | Meaning |
|---|---|
output |
The new output’s id, as outputs reports it — a fresh one on every add, never a removed output’s. |
name |
Its connector name (DP-1 under --tty, headless-2 otherwise) — the same string outputs names it by. |
width / height |
The framebuffer size in physical pixels. |
scale |
The scale it came up at: its live output-scale scale, else the config file’s. |
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 — then nothing moved either).
output_changed — an output’s mode changed in place: the same
connector at a new framebuffer size, keeping its scale. This is the re-probe resize: a
--tty hotplug offering a new mode for a connected
connector (a VM window moving between displays of different densities), or a
--nested host window being resized. It is 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. and outputs. apply live).
It fires once per applied resize, after the windows are re-laid-out at the new
size; a refused resize 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,
naming the layout now in effect. No standard Wayland protocol reports this
to an unfocused client, so this event and the query are 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 scoot has no layout-switch bind or action. The
group moves only through the keymap’s own mechanics (a toggle key from the XKB_DEFAULT_OPTIONS the session started with).
A subscriber that stops reading is disconnected rather than buffered without bound: past the same 1 MiB queued-reply bound, 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. A bar’s workspace module draws
from it without polling windows. No standard Wayland
protocol reports the “holds windows” bit to an unfocused client:
ext-workspace-v1 carries the list, positions and the one active bit but not that 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, because its removal is already an
output_removed). Events are coalesced to
at most one per output per frame tick, however fast windows churn. 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.
lock_changed — the session locked or unlocked: the same locked
flag the locked query answers with,
naming the state it moved to. No standard Wayland protocol
reports this to an unfocused client, so this event and the query are the
channel that watches the lock.
It fires once per transition, never per request: a refused lock
changes nothing and sends nothing, and
a takeover of an already-locked session is no transition either. A fresh
subscription starts silent: read
locked once for the baseline and apply changes after it. As
with the query, this is read-only: nothing over IPC locks or unlocks the
session.
look_changed — the active look layer changed: the rendered look’s
name (null when the layer went away) and its source_hash from the
render’s render.. Fires once per identity change, as seen by a
reload; a reload that changes nothing emits nothing. A fresh
subscription starts silent: the current look is whatever the last
look_changed said, or no look if none arrived yet.
shortcut_activated / shortcut_deactivated — a portal global
shortcut’s chord was pressed / released: app_id and shortcut_id
naming the registration, timestamp_ms the compositor clock at
intercept time. Sent only to global-shortcut subscribers. Compositor binds always win, so a chord the live
keymap holds never produces these; nothing fires while locked; key
repeat never re-fires. shortcuts_changed — the registered set
moved (register, unregister, bind, unbind, reload, registrant
disconnect): re-list with shortcuts for the new state. A fresh
subscription starts silent: read shortcuts once for the baseline.
Protocol versions, by the version that introduced each part:
- 5: the subscription (
subscribed,output_removedandoutput_restoredtags under the 3 → 4 bump, plusoutput_changedunder 4 → 5). - 6: the
keyboardreply and thekeyboard_changedtag. - 7: the
workspacestag. - 8: the
lockedreply and thelock_changedtag. - 10: the
output_addedtag, on the existingoutputkind. Anoutputsubscriber built against protocol 9 or older fails to decode it: rebuild it (scoot msgfrom the same build as the session is always in step). - 11: the
look_changedtag on the newlookkind. - 12: the look swap. The
look_applyandlooksrequest tags with theirlook_appliedandlooksreplies. An older server answers either request with an ordinaryerror, never a misapplied swap. - 13: global shortcuts. The
shortcut_register,shortcut_unregister,shortcutsandshortcut_bindrequest tags with theshortcutsreply, plus theshortcut_activated,shortcut_deactivatedandshortcuts_changedtags on the newglobal-shortcutkind.
A client that never sends subscribe (or keyboard, or locked) 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.
