- 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)
136 lines
6.0 KiB
Markdown
136 lines
6.0 KiB
Markdown
---
|
|
type: reference
|
|
title: Display Configuration
|
|
description: 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).
|
|
tags:
|
|
- niri
|
|
- display
|
|
- monitors
|
|
- rotation
|
|
- desk-profiles
|
|
verified: 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
|
|
|
|
```mermaid
|
|
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 validate` the *current* config
|
|
first, relink `display.kdl`, then `niri 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+D` cycles profiles (wrong auto-pick, or
|
|
same panels, new layout).
|
|
- **New desk:** `desk.sh capture` prints connected long names → copy into a
|
|
new `desk-N-<name>.kdl` (see `desk-2-portable.kdl` skeleton, portable
|
|
monitor EDID pending first capture).
|
|
- Unit: `blankpad-desk-autoswitch.service` (systemd user,
|
|
`ConditionHost=blankpad`).
|
|
|
|
## The config pattern
|
|
|
|
```kdl
|
|
// 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:
|
|
|
|
1. **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.
|
|
2. **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 from `niri msg outputs`.
|
|
3. **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.
|
|
4. **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](/tools/niri/index.md) — compositor overview and config layout
|
|
- [[tips|tips]] — reload path, `niri msg layers/casts`, includes
|
|
- [[noctalia-shell|noctalia-shell]] — notification shown on desk switch
|
|
- [[cachyos|cachyos]] — operating system, including `wlr-randr`
|