`[[page|Label]]` is Obsidian syntax, not OKF. `kb-lint` resolves it Obsidian-style — by filename, anywhere in the bundle — so it has never tripped the link gate, while every other consumer of the bundle (an OKF renderer, a Gitea wiki export, `grep` for reachability) sees bracketed text where a link should be. That is how two generated pages ended up with no inbound link at all and nobody noticed: the my-first link census of the bundle reported them as orphans because their only inbound edges were wikilinks. Mechanical conversion, 52 links across 17 pages, each resolved against the same page set `kb-lint` uses: an explicit `|Label` is kept verbatim, an unlabelled link takes the target's frontmatter `title`. Targets are emitted bundle-absolute (`/tools/niri/tips.md`), the form the spec recommends and the form 99 existing links in this bundle already use. Two links were `[[index]]`, which resolves by filename to 21 pages — the ambiguity that filename-resolution invites. Both meant their own directory's index, so both are now written out in full: `/tools/gitea/index.md`, `/tools/niri/index.md`. The linter still tolerates wikilinks; whether it should reject them outright is an open question on #64, because that would replace the two tests pinning today's tolerance. Refs #64
10 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, the phantom-mode fallback trap, and GPU notes (AMD Cezanne now; Intel Arc render-node pin is dankware-legacy). |
|
2026-10-21 |
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). Per-output changes go live with
niri msg output <name> <mode|scale|transform|position|off|on> and are
temporary — never written to the config file. wlr-randr is not installed
on blankpad (pacman -Qs wlr matches nothing, verified 2026-10-21); pages that
list it as an installed tool are stale.
The desk-1 rig (home)
┌──────────────┬──────────────────────────────┬──────────┐
│ PM1601 │ Sceptre M34 ultrawide │ eDP-1 │
│ portrait │ 3440x1440 @ 180.000 │ 1920x1080│
│ 1200x1920 │ landscape, scale 1 │ scale 1 │
│ rot 90 │ position 1581,-600 │ 5021,-240│
└──────────────┴──────────────────────────────┴──────────┘
HDMI-A-1 DP-1 (focus-at-startup) eDP-1 (fallback focus)
Connector names are today's (`niri msg outputs`, 2026-10-21). They reshuffle with
the dock and the cable, which is why the config keys on the EDID long name instead.
| Panel | EDID long name | Keyed by | Notes |
|---|---|---|---|
| Sceptre M34 ultrawide | Sceptre Tech Inc Sceptre M34 Unknown |
identity | mode "3440x1440@180.000", deliberate — the panel also offers 3440x1440@239.998, 144.001 and 120.000; 180 is driven while the link is degraded (see the law below) — 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: the M34 supports it (its connector today is DP-1); 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@180.000"
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.
desk-1-home.kdlstill carries legacy connector-keyedDP-1/DP-2blocks next to the identity block. Their modes were aligned to 180 Hz on 2026-10-21 so a connector-named reconnect cannot drop the panel to 120, but they belong folded into the identity block. - 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.
- Ask only for modes the panel reports, and copy the string exactly. The M34's
Available modes(niri msg outputs, 2026-10-21) are3440x1440@239.998,3440x1440@180.000,144.001,120.000, … with59.998marked preferred. niri matches mode strings verbatim and does not round, so239.999— one digit from the real mode, which is what this file and the config both carried for weeks — is not239.998. A mode it cannot find does not leave the output dark: niri logsconfigured mode … could not be found, falling back to preferredand quietly drives the panel at its EDID preferred mode — 60 Hz. Nothing looks broken except the refresh rate, and every reconnect repeats the fallback. Check Hz, not just resolution, after any "monitor is acting up". - 180 Hz here is a decision, not a ceiling. The panel offers 239.998 and is driven
at 180.000 deliberately: the link trained at
x8 32.0 GT/sagainst a max ofx16, and the display engine is throwingDMCUBerrors withEINVALpage flips after s2idle, so the 240-class mode is not driven until a reboot clears that state. Before changing the mode, re-readniri msg outputsand the link sysfs; treat "restore 240" as a change to verify on a healthy link, never as the obvious fix. Refs #60.
"The monitors are broken" — three layers, three signatures
A display complaint belongs to one of three layers. Sort it before editing config, because the config is usually innocent — and when it is guilty, the guilt may have been copied from this page.
| Layer | Log signature | Reality | Move |
|---|---|---|---|
| Config | configured mode … could not be found, falling back to preferred |
a mode/scale the panel never offered | re-read the mode list with niri msg outputs and correct the file; do not trust a documented mode |
| Link | disconnecting connector: "DP-1" followed seconds later by new connector: DP-1, recurring |
the DP link itself dropping — cable, dock, the monitor's DP-version/DSC setting, or panel power saving | every drop re-runs mode selection, so a config error is re-enacted in public; swap the cable before blaming the compositor |
| Driver | Page flip commit failed … (Invalid argument) plus kernel *ERROR* … DMCUB error |
amdgpu's display engine wedged, typically after an s2idle resume | survives until a reboot; niri keeps running while the panel keeps dropping. No config edit clears it |
Probes, in this order:
niri msg outputs # connector, EDID long name, real mode list, current mode
journalctl --user -b | grep -E 'disconnecting connector|Page flip commit failed|falling back to preferred'
journalctl -k -b | grep -iE 'amdgpu.*(ERROR|DMCUB)|PM: suspend exit'
Correlate the drop timestamps against PM: suspend exit first. A resume a
minute before the first drop is the cause; the config file is not.
To fix a refresh rate without disturbing the session, use the temporary per-output path — it touches one output, applies no config, reloads nothing:
niri msg output "Sceptre Tech Inc Sceptre M34 Unknown" mode "3440x1440@180.000"
It is temporary by design, so correct the profile too and let the next
reconnect or load-config-file make it permanent. With the display engine
already throwing EINVAL, prefer the per-output path over a full config
reload; on a healthy machine either is fine.
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 the per-host symlink
matrix).
Reload
niri msg action load-config-file reloads live. Never SIGHUP the process
— see Niri Tips and Gotchas.
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