- niri 26.04 has no hotplug event: OutputConfigChanged is only an ACK (niri-ipc). Watch WorkspacesChanged, diff sorted output set, apply on change. Proven live: eDP-1 off/on -> 3 output-set transitions, each matched + applied + reloaded. - unit: %h/.local/bin/desk-autoswitch.sh, quoted Environment, drop User/Group (user unit); add desk-autoswitch.sh bin symlink - desk.sh: silence validate/load-config/notify stdout - deploy.sh: deploy repo symlinks (find -type l), so .local/bin links land - docs: event semantics corrected (displays diagram, log)
6.0 KiB
type, title, description, tags, verified
| type | title | description | tags | verified | |||||
|---|---|---|---|---|---|---|---|---|---|
| reference | Display Configuration | Niri output configuration on blankpad — EDID-identity-keyed desk profiles, hotplug auto-switching, portrait transforms, position pinning, and GPU notes (AMD Cezanne now; Intel Arc render-node pin is dankware-legacy). |
|
2026-10-09 |
Display Configuration
Niri output config on blankpad is a desk-profile system: the laptop
travels between desks, connector names (DP-2, HDMI-A-1) reshuffle per
dock, so panels are configured by EDID identity (manufacturer model
serial), and positions live in per-desk files. A hotplug watcher and a
manual picker keep the live layout matched to reality.
Mechanism: ~/.config/niri/cfg/display.kdl is a symlink to the
active profile (desk-1-home.kdl, desk-2-portable.kdl, …).
config.kdl includes it with include optional=true so a missing link is a
warning, not a parse failure.
Probe: niri msg outputs is authoritative (connector, full EDID long
name, modes, transform, scale, position, VRR). wlr-randr remains available.
The desk-1 rig (home)
┌──────────────┬──────────────────────────────┬──────────┐
│ PM1601 │ Sceptre M34 ultrawide │ eDP-1 │
│ portrait │ 3440x1440 @ 239.999 │ 1920x1080│
│ 1200x1920 │ landscape, scale 1 │ scale 1 │
│ rot 90 │ position 1581,-600 │ 5021,-240│
└──────────────┴──────────────────────────────┴──────────┘
HDMI-A-1 DP-2 (focus-at-startup) eDP-1 (fallback focus)
| Panel | EDID long name | Keyed by | Notes |
|---|---|---|---|
| Sceptre M34 ultrawide | Sceptre Tech Inc Sceptre M34 Unknown |
identity | mode "3440x1440@239.999", focus-at-startup |
| PM1601 portrait | not captured | connector HDMI-A-1 |
dock-dependent; run desk.sh capture when plugged in, then re-key |
| eDP-1 laptop panel | Lenovo Group Limited 0x40A9 Unknown |
identity | scale 1.0 (deliberate; Ghostty grid at 1.25 rejected 2026-10-09), second focus-at-startup |
VRR: DP-2 supports it; left disabled by decision (2026-10-09). Add
variable-refresh-rate per output if that ever changes.
focus-at-startup on both M34 and eDP-1: niri focuses in config order, so
the ultrawide wins when present and the laptop panel is the fallback.
Desk switching
graph LR
P[plug / unplug] --> E[niri event-stream<br/>WorkspacesChanged → output-set diff]
E --> M[desk-autoswitch.sh<br/>debounced matcher]
M --> R{profile requires<br/>subset of connected?}
R -->|best match| A[desk.sh apply:<br/>validate - relink - reload - notify]
R -->|none| K[keep current + notify]
D[Mod+Shift+D] --> C[desk.sh cycle] --> A
- Matcher: each profile declares
// requires: <long name>(all must be connected) and// optional:comment lines; the most-specific eligible profile wins. - Apply order is load-config-safe:
niri validatethe current config first, relinkdisplay.kdl, thenniri msg action load-config-file, then a noctalia notification names the active desk. A profile that fails validation can never go live. - Manual override:
Mod+Shift+Dcycles profiles (wrong auto-pick, or same panels, new layout). - New desk:
desk.sh captureprints connected long names → copy into a newdesk-N-<name>.kdl(seedesk-2-portable.kdlskeleton, portable monitor EDID pending first capture). - Unit:
blankpad-desk-autoswitch.service(systemd user,ConditionHost=blankpad).
The config pattern
// desk-1-home.kdl — one block per panel, keyed by identity
output "Sceptre Tech Inc Sceptre M34 Unknown" {
mode "3440x1440@239.999"
scale 1
transform "normal"
position x=1581 y=-600
focus-at-startup
}
Four rules learned the hard way:
- One output, one block. Two blocks matching the same output (connector name and long name) fight over properties — transform reverts are the classic symptom. Desk profiles are self-contained: a panel's identity, mode, transform, and position live in exactly one block per file.
- Key by EDID identity, not connector. Connector names swap between
docks; the EDID long name travels with the panel. Serials on these panels
read
Unknown, so the full long name (manufacturer model serial) is the match string — copy it verbatim fromniri msg outputs. - Positions are absolute, not relational. A disconnected panel leaves a hole; that is exactly why positions belong to the desk profile, not a global file.
- Modes must match what the panel reports (
niri msg outputs→Mode: current …). A mode the panel cannot drive leaves the output dark.
GPU assignment
blankpad is a single AMD Cezanne iGPU (/dev/dri/renderD128, PCI
07:00.0). No render-node pin exists in misc.kdl — and must not: the
blankpad override file deliberately deletes the pin.
The environment { render-node "/dev/dri/renderD129" } pin found in the
base misc.kdl is dankware-legacy (Intel Arc iGPU + discrete GPU
hybrid; pin kept the iGPU as display owner and sidesteps the suspend/resume
deadlock documented in the sleep-wake investigation). Deploying that base
file on blankpad pins a render node that does not exist — this is why
blankpad runs the zz-blankpad override layer (see [[index|the per-host
symlink matrix]]).
Reload
niri msg action load-config-file reloads live. Never SIGHUP the process
— see tips.
See Also
- Niri — compositor overview and config layout
- tips — reload path,
niri msg layers/casts, includes - noctalia-shell — notification shown on desk switch
- cachyos — operating system, including
wlr-randr