Displays
A laptop moving between a desk monitor and no monitor gets scale and
power that follow the connected set: dock, and the external
panel comes up at its own scale (and, with disabled, the lid panel
goes dark); undock, and the laptop panel takes over again, lit even
if the docked profile turned it off:
programs.scoot.desktop.displays.profiles = [ { name = "docked"; outputs = [ "eDP-1" "DP-1" ]; scale = { "DP-1" = 1.0; "eDP-1" = 2.0; }; } { name = "undocked"; outputs = [ "eDP-1" ]; scale = { "eDP-1" = 2.0; }; }];A profile sets scale and power, at hotplug. It
names the exact connected set it answers (connector names, as
scoot msg outputs lists them: eDP-1, DP-1 on real hardware) and,
per output, a scale and whether the panel stays off. It does not place
outputs (they pack left to right in connection order), does not set
modes, and cannot match on make or model (scoot’s IPC reports
connector names only). The watcher (scoot-displays, a user unit
bound to scoot-session.) re-matches once at session start
and on every output event: each plug (output_added, which fires for
a monitor’s first plug and every replug), each unplug, and each mode
change, settling 2 s after the first of a burst. First match wins, in
list order. With the idle policy on, it also
re-applies when the screens come back from idle.
How a match applies, all over scoot’s own IPC, with no config file written:
- Scale. Each output of the set runs at the profile’s scale
(
scoot msg output-scale NAME SCALE). An output the profile gives no scale goes back to your config’s scale (scoot msg output-scale NAME reset), so one profile’s scale never lingers into the next. - Power off. Each output the profile’s
disablednames is powered off (scoot msg output-power ID off), and the watcher records that it did. - Power back on. An output the watcher itself turned off comes
back on as soon as the matched profile stops disabling it, or no
profile matches at all. Undocking from a clamshell
profile:
dockeddarkenseDP-1, the monitor leaves, andeDP-1lights again whether or not anundockedprofile exists. - Every other power state is left alone. An output the watcher did not turn off is never turned on: screens the idle policy blanked stay dark through a monitor’s standby unplug, and so does one you turned off by hand.
- No match. Every connected output goes back to your config’s scale, and the watcher’s own offs come back on.
The record of its own offs is $XDG_RUNTIME_DIR/scoot-displays..
It goes when your last session logs out (unless lingering is on), but
it survives a compositor restart inside one login. Until the watcher’s
first apply in that session, an entry whose name and id match a live
output can turn that output on once. It only ever lights a screen,
never darkens one. Everything else is live runtime state, not config:
your config file (under home-manager, a read-only link into the Nix
store) is never touched. A successful
scoot msg reload or a restart goes back to the config’s scales until
the watcher applies again (at the next output event, idle resume or
login). A home-manager rebuild never reloads the session, so it never
drops an applied scale. The apply reports applied only when every
call succeeded. Otherwise it exits non-zero and names each failed
call, and the unit’s journal shows the line.
The watcher is scoot-native because stock kanshi does not work here:
kanshi runs its hooks only after the compositor answers an output
configuration with success, and scoot’s output-management write half
answers every configuration with failure, so kanshi matches the profile
and then fails it on every hotplug, applying nothing. The watcher uses
scoot’s own IPC (subscribe for events, outputs for matching,
output-scale and output-power for applying). It blocks on events,
with no polling and no idle wakeups. It is a stopgap until a writable
output-management protocol lets kanshi (and positions and modes) work
as-is; that work is tracked in the output-management reconfiguration entry.
scoot-displays statusscoot-displays applyscoot msg outputssystemctl --user status scoot-displaysEvery value is an option, applied on rebuild/switch (new profiles restart the watcher, which re-applies at start):
| Option | Type | Default | Meaning |
|---|---|---|---|
desktop. |
bool | true with the profile |
match the connected set and apply scale and power (with no profiles the watcher sets no scale and turns nothing off; it only turns back on an output it turned off itself under an earlier profile list) |
desktop. |
list of profiles | [] |
arrangement profiles, first match wins in list order |
profiles[]. |
string (unique) | — | the profile’s name, as status prints it |
profiles[]. |
list of strings (non-empty, unique set) | — | the exact connected connector-name set this profile answers |
profiles[]. |
attrset of number (0.5–4.0) | { } |
per-output scale, applied live; an output in outputs with no entry runs at your config’s scale (every key names an output in outputs) |
profiles[]. |
list of strings | [ ] |
outputs to power off under this profile, live, and back on once it no longer applies (empty disables nothing; every entry names an output in outputs, and at least one output of outputs stays out of it) |
A mode is not a profile field: a mode cannot change live, and a
profile that sets mode fails evaluation. Set one per connector in
programs. ({ name = "DP-1"; mode = "3840x2160"; }): it applies at session start, beside the watcher. A
static scale there is fine too. It is what an output runs at when no
profile scales it.
Troubleshooting, by symptom:
- Nothing applies on plug. Names first:
scoot msg outputslists the connected connectors, and the profile matches only the exact set. One extra monitor (or one missing letter) falls through.scoot-displays statusprints the match (noneplus the connected set) without changing anything. - The scale reverted after a reload. A successful reload goes back
to your config’s scales, and no event tells the watcher about it.
Run
scoot-displays applyto converge now, or wait for the next plug. - A scale I set by hand reverted. With profiles configured, every
apply (each plug and unplug, and each idle resume) sets every
connected output’s scale: the profile’s, or your config’s when the
profile gives none or nothing matches. A hand
scoot msg output-scalelasts until the next apply; put the scale in a profile (or inprograms.) instead.scoot. settings. outputs applyexits non-zero. Its stderr (and the unit’s journal) names the call that failed and scoot’s reason. An output unplugged mid-apply is the common one; the next output event applies again.- An output stays dark. Either the matched profile disables it
(
disabled;statusnames the profile), or something other than the watcher turned it off (the idle policy,wlopm, a handoutput-power), which the watcher deliberately leaves alone: the next input after idle lights it, orscoot msg output-power <id> ondoes (ids come fromscoot msg outputs, stable for the session, fresh after every unplug). An off the watcher made itself always comes back on when its profile stops matching. Turning the slot off (enable = false) stops the watcher where it stands, so an output it had off stays off until thatoutput-powercall or the next login. - The laptop panel lit up while idle, docked. A monitor that drops its connection in standby is an unplug to scoot, so with the docked profile no longer matching, the watcher lights the panel it darkened rather than risk leaving no screen on (it cannot tell that unplug from a real undock). The panel goes dark again when the monitor reconnects and the docked profile matches. The idle policy does not darken it again on its own: its screens-off fires once per idle stretch, so the panel stays lit until the monitor comes back or you return.
- A disabled panel lit up after idle. Without the idle policy,
something else ran
wlopm --on '*'(oroutput-power all on):scoot-displays applyturns it back off. With the idle policy on, its resume re-applies by itself. - Evaluation refuses a profile that “disables every output it
matches”. Its
disablednames every output of its ownoutputs, which would leave no screen on whenever it matched. Keep at least one out. - Two profiles could match. The first in list order wins; two profiles for one set fail evaluation instead of shadowing.
- The watcher is not running. It starts with the session scope:
systemctl --user status scoot-displays(andscoot-session.beside it). With the slot on but no session yet, it retries.target
