The vault was 155 pages / 31,210 lines, most of it measurement ledgers for payloads that no longer run. A throughput figure for a runtime nobody operates is not a record: AI moves faster than the vault, so the number is wrong before anyone reads it, and a wrong number in a runbook gets believed. Per the new purpose test in `AGENTS.md` → *Documentation Policy*, it is destroyed. Gone (`.AGENTTRASH/20261019-destruction/`, KILL-LIST.txt is the manifest): - the `frank-medium` endpoint and its five era pages, `frank-npu`, FastFlowLM, the ikllama/kingjones/dealign/frank-redo eras, the ling/swift15/freetoken/ubatch campaign pages, qwen3.8 spec-matrix and strix-optimizations, the two "Running Qwen3.6 …" pages; - the whole `Pipeline Parallelism/` folder (no live script passes a pipeline flag) and the multi-device/x4-pair/hybrid/offload experiment pages; - every vLLM page — no vLLM endpoint exists on this fleet; - `LLM/deployment.md` (`Architecture/Deployment Flow.md` is the test-referenced owner) and the external surveys (SGLang, Lemonade, llama.cpp forks/diff, self-hosted app census, the TTS survey) that were obsolete by construction; - `log.md`, 2,380 lines. Git holds the chronology; a causal law belongs to the page that owns the thing it governs, never to a diary. Survivors: 101 pages, 17,542 lines, 0 OKF violations, 0 unresolved links. Eras tables keep the window, payload, runtime, reason and rollback pointer; the rate columns are blanked. The live shapes stay verbatim — those numbers are what the deployer and `models.yml` consume.
304 lines
16 KiB
Markdown
304 lines
16 KiB
Markdown
---
|
|
type: reference
|
|
title: Noctalia and Niri
|
|
description: "The noctalia shell on niri across workstations: config layers, remote IPC, custom palettes, output rotation and pinning."
|
|
tags: [desktop, noctalia, niri, blankpad, workstation, ghostty]
|
|
timestamp: 2026-10-05T00:00:00-07:00
|
|
---
|
|
|
|
# Noctalia and Niri
|
|
|
|
Noctalia (`/usr/bin/noctalia`) is the desktop shell on niri, running on both
|
|
the workstation and blankpad. Since the 2026-09-10 unification both run the
|
|
**bare `noctalia` v5 binary** (niri `spawn-at-startup "noctalia"`); the old
|
|
Quickshell-hosted v4 architecture (`qs -c noctalia-shell`, JSON config) is
|
|
retired on both hosts.
|
|
|
|
## Config layers
|
|
|
|
- Hand-edit declarative layer: `~/.config/noctalia/<name>.toml` (e.g.
|
|
`config.toml`). Main config: `~/.config/noctalia/config.toml`; the niri
|
|
config includes `noctalia.kdl` for display settings.
|
|
- Runtime state file `~/.local/state/noctalia/settings.toml` loads LAST and
|
|
**overrides declarative values for the same keys — never hand-edit it**;
|
|
UI/IPC writes land there.
|
|
- Main reference: `noctalia-reference` facts — IPC `noctalia msg <command>`
|
|
replaced the `qs -c noctalia-shell ipc call` wrapper; app templates live at
|
|
`/usr/share/noctalia/assets/templates/` (gtk, ghostty, alacritty, hyprland,
|
|
sway, wezterm, helix, btop, starship, qt, kde, cava, scroll, mango,
|
|
umbriel, foot, emacs, kitty); palette generation `noctalia theme [image]`.
|
|
|
|
## Remote IPC over ssh (the D-Bus trap)
|
|
|
|
`noctalia msg <command>` resolves the running instance via the D-Bus session
|
|
bus. sshd-spawned non-login shells lack `DBUS_SESSION_BUS_ADDRESS`, so plain
|
|
ssh exec prints `error: noctalia is not running` even when the shell IS
|
|
running — do not conclude "shell dead" from this error. Working remote
|
|
procedure (runs in the systemd user-manager context where the bus address
|
|
exists):
|
|
|
|
```
|
|
systemd-run --user --pipe --wait noctalia msg <command> [args]
|
|
```
|
|
|
|
Verified: `msg status` returns live JSON state; unknown panel ids return an
|
|
error listing all valid panel ids (clipboard, control-center, launcher,
|
|
polkit, session, setup-wizard, test, tray-drawer, wallpaper).
|
|
|
|
Verified v5 msg verb grammar (keybinds-safe): `panel-toggle <id>`
|
|
`session lock|suspend|lock-and-suspend|logout|reboot|shutdown`
|
|
`volume-up [step]`, `volume-down`, `volume-mute`, `mic-mute`
|
|
`media next|previous|playPause` (graceful no-op, rc 0, with no active
|
|
player), `brightness-up [target] [step]`, `brightness-down`
|
|
`settings-open [context]`, `settings-toggle [context]`, `config-reload`
|
|
`wallpaper-get [connector]`, `wallpaper-set <path>`, `wallpaper-next`
|
|
`color-scheme-get|set`, `theme-mode-get`, `templates-apply`
|
|
`screenshot-fullscreen`.
|
|
|
|
**Wallpaper directory semantics:** empty/unset `[wallpaper] directory`
|
|
resolves to XDG Pictures via user-dirs.dirs; an explicit directory key
|
|
overrides. Fleet default pattern: point `directory` at the repo-tracked
|
|
wallpapers dir so git-tracked wallpapers serve the shell without copying
|
|
files. After writing config.toml over ssh the file watcher hot-reloads
|
|
automatically; `config-reload` via systemd-run forces it immediately. Note:
|
|
the *current* wallpaper stays the built-in default until the next rotation
|
|
or an explicit `wallpaper-next`; `wallpaper-get` then reporting a path
|
|
inside the configured directory is the confirmation the config took effect.
|
|
|
|
**Lock chain (unified):** swayidle-lock.service ExecStart on both hosts:
|
|
`/usr/bin/swayidle -w timeout 300 '/usr/bin/noctalia msg session lock'
|
|
before-sleep '…' after-resume '…'`. `msg` resolves via the session bus, so
|
|
no WAYLAND_DISPLAY pinning is needed in unit context.
|
|
|
|
## Custom-palette recolor (without restarting the session)
|
|
|
|
Why custom, never the community cache:
|
|
`~/.local/state/noctalia/community-palettes/*.json` is a **sync cache** —
|
|
`.catalog/palettes.json` carries an `md5` per palette and noctalia re-fetches
|
|
from `https://api.noctalia.dev/palettes`, so edits there get reverted.
|
|
Custom palettes live at `~/.config/noctalia/palettes/<Name>.json`
|
|
(`NOCTALIA_CONFIG_HOME` then `XDG_CONFIG_HOME` honored) and are never
|
|
synced. Missing/invalid file → silent fallback to the builtin palette.
|
|
|
|
Palette JSON shape = the community file's shape: `dark`/`light` maps of
|
|
`mPrimary`, `mOnPrimary`, `mSecondary`, `mTertiary`, `mError`, `mSurface`
|
|
`mSurfaceVariant`, `mOutline`, `mShadow`, `mHover` plus a `terminal` block
|
|
(`foreground`, `background`, `normal.{black..white}`, `bright.{…}`
|
|
`cursor`, `cursorText`, `selectionFg`, `selectionBg`). NOTE: `noctalia theme
|
|
-o` emits M3 snake_case tokens (`on_primary_container`, …) — that is the
|
|
*generator* format, not this one. Copy an existing community JSON and mutate
|
|
with `jq`.
|
|
|
|
Procedure:
|
|
|
|
1. Read the live scheme first — never assume: `noctalia msg color-scheme-get`
|
|
(e.g. `community Gruber Darker`); `noctalia msg theme-mode-get`.
|
|
2. Back up settings: `cp -a ~/.local/state/noctalia/settings.toml
|
|
/tmp/noctalia-settings-backup-$(date +%s).toml`.
|
|
3. Build the palette from the current one (jq keeps structure):
|
|
|
|
```sh
|
|
mkdir -p ~/.config/noctalia/palettes
|
|
jq '.dark.mPrimary = "#f43841"
|
|
| .dark.terminal.normal.yellow = "#f43841"
|
|
| .dark.terminal.bright.yellow = "#ff4f58"
|
|
| .dark.terminal.foreground = "#f43841"' \
|
|
"$HOME/.local/state/noctalia/community-palettes/Gruber%20Darker.json" \
|
|
> ~/.config/noctalia/palettes/"Gruber Red.json"
|
|
```
|
|
|
|
Prefer a hex already present in the palette (its own `mError` red) so the
|
|
theme stays internally consistent. Set BOTH `normal` and `bright`
|
|
variants, and both `dark` and `light` if the user toggles modes.
|
|
4. Switch (persists `theme.source = custom` + `theme.custom_palette`):
|
|
`noctalia msg color-scheme-set custom "Gruber Red"` → `ok`; confirm with
|
|
`color-scheme-get`.
|
|
5. Re-render every configured template: `noctalia msg templates-apply`.
|
|
Regenerates `~/.config/ghostty/themes/noctalia`
|
|
`~/.config/gtk-{3,4}.0/noctalia.css`, kcolorscheme, foot, wezterm,
|
|
starship, etc. Symlinked-into-dotfiles paths show up as modified files
|
|
worth committing.
|
|
6. Reload surfaces: compositor — `niri msg action load-config-file`
|
|
(`reload-config` is NOT a valid niri subcommand); ghostty — see the trap
|
|
below.
|
|
|
|
Verification: `ghostty +show-config | grep -E '^(foreground|palette)'`
|
|
(proves the theme file resolves — note it prints only the delta from
|
|
defaults; cross-check with `ghostty +show-config --default` before
|
|
concluding a key is unset); `noctalia config validate` → `✓ Config is
|
|
valid`; visual via `noctalia msg screenshot-fullscreen` (saves to
|
|
`~/Pictures/screenshot_<ts>.png` and ALSO overwrites the clipboard with the
|
|
PNG — warn the user); `grep -c <old-hex> ~/.config/noctalia/palettes/*.json`
|
|
→ 0 for no leftovers.
|
|
|
|
Track it in the dotfiles repo (`~/.omp`, the command-center —
|
|
`scripts/deploy.sh` is a stow-equivalent over `dotfiles/<pkg>/` trees):
|
|
mirror the shape for the new palette so `git pull` + `deploy.sh` is the
|
|
whole propagation story — move the JSON into
|
|
`~/.omp/dotfiles/noctalia/.config/noctalia/palettes/` and symlink it back
|
|
into `~/.config/noctalia/palettes/`; re-set the custom palette (re-reads
|
|
through the symlink). Do the link by hand for one file instead of running
|
|
`deploy.sh` — sync mode quarantines and replaces every divergent live file.
|
|
|
|
Traps:
|
|
|
|
- **NEVER `kill -USR1` ghostty to reload.** It does not reload config on
|
|
this fleet — it kills/respawns the terminal (took out Frank's window
|
|
2026-09-13). Correct: the default keybind `ctrl+shift+,` = `reload_config`
|
|
(per `ghostty +show-config --default`). Existing windows keep old colors
|
|
until reloaded; new windows inherit immediately.
|
|
- Palette hex values are NOT a proxy for what the user sees: in
|
|
`Gruber Darker` the *accent* (`mPrimary`) is yellow `#ffdd33`, so "change
|
|
the yellow" means the accent too, not just terminal index 3.
|
|
- Saturating terminal text makes ANSI 1/9 indistinguishable from default
|
|
text (grep highlights flatten) — flag it and offer a lighter red.
|
|
- Contrast sanity: `#f43841` on `#181818` ≈ 4.6:1 (dark-on-color beats
|
|
white-on-color here, ~3.9:1 — keep `mOnPrimary` dark).
|
|
- Revert = `noctalia msg color-scheme-set community "<original name>"`; the
|
|
community cache is untouched by the whole flow.
|
|
- Generated theme files symlinked into the dotfiles repo (`gtk-3.0`/`gtk-4.0`
|
|
`noctalia.css`, ghostty theme) show up as tracked modifications after
|
|
`templates-apply`; commit them together with the new palette JSON so every
|
|
machine gets the same recolor.
|
|
|
|
## Offline template render (when templates-apply never ran)
|
|
|
|
Failure signature: niri `failed to read included config from
|
|
~/.config/niri/noctalia.kdl: No such file or directory` — the desktop runs
|
|
but with DEFAULT config (niri 26.04 falls back, does not exit): no custom
|
|
keybinds/rules, no noctalia autostart, default output placement. ghostty:
|
|
`theme "noctalia" not found, tried ~/.config/ghostty/themes/noctalia`.
|
|
alacritty: import of `~/.config/alacritty/themes/noctalia.toml` missing.
|
|
|
|
Root cause pattern: upstream dotfiles commits theme switches (niri include,
|
|
ghostty `theme = noctalia`, alacritty import) that depend on noctalia
|
|
TEMPLATE-GENERATED files. Those files are per-host outputs (rendered from
|
|
the local palette) and are intentionally NOT in the repo; on any host where
|
|
`templates-apply` never ran, they don't exist.
|
|
|
|
Fix (offline, works while a session runs) — built-in templates live at
|
|
`/usr/share/noctalia/assets/templates/` (declared in `builtin.toml`); render
|
|
each needed one with the live palette:
|
|
|
|
```bash
|
|
# niri include (plain host-local file; do NOT symlink into repo — regenerated per host)
|
|
noctalia theme --theme-json ~/.config/noctalia/colors.json \
|
|
--render "/usr/share/noctalia/assets/templates/niri/niri.kdl:$HOME/.config/niri/noctalia.kdl"
|
|
mkdir -p ~/.config/ghostty/themes && \
|
|
noctalia theme --theme-json ~/.config/noctalia/colors.json \
|
|
--render "/usr/share/noctalia/assets/templates/ghostty/ghostty:$HOME/.config/ghostty/themes/noctalia"
|
|
mkdir -p ~/.config/alacritty/themes && \
|
|
noctalia theme --theme-json ~/.config/noctalia/colors.json \
|
|
--render "/usr/share/noctalia/assets/templates/alacritty/alacritty.toml:$HOME/.config/alacritty/themes/noctalia.toml"
|
|
```
|
|
|
|
All template tokens (primary, surface, error, primary_container, …) resolve
|
|
from the flat 16-key colors.json; container tokens are derived, never blank.
|
|
|
|
Verify: `niri validate` → rc 0; `ghostty +validate-config` → rc 0; next
|
|
login journal shows `loaded config from config.kdl` with NO parse WARN,
|
|
custom output placement (`niri msg outputs` vs display.kdl), noctalia
|
|
autostarted (`pgrep noctalia`).
|
|
|
|
Traps: **`noctalia msg templates-apply` on 5.0.1 returns "ok" but applies
|
|
NOTHING** (this build's opt-in config schema does not act on it) — do not
|
|
trust its exit code, use the explicit `--render` calls above. Niri config
|
|
errors at boot are NOT fatal — the compositor keeps running on defaults,
|
|
which reads as "some settings silently stopped working", not a crash.
|
|
Regenerate after any palette change; the rendered files are static
|
|
snapshots.
|
|
|
|
## Niri output rotation and pinning
|
|
|
|
Verified on blankpad, niri 26.04 (two PM1601 externals + eDP-1 laptop panel).
|
|
|
|
**Ground truth first:** `niri msg outputs` (connector names, current mode,
|
|
scale, transform, logical pos/size) and `niri --version`. Output **names are
|
|
connector names** (`eDP-1`, `DP-2`, `HDMI-A-1`), not EDID model strings —
|
|
the EDID strings printed before them are decoration. On blankpad the live
|
|
config splits via `include "./cfg/*.kdl"` from `~/.config/niri/config.kdl`
|
|
and `cfg/display.kdl` is a **symlink into the dotfiles repo** — edit the
|
|
repo path (that is what gets committed); check with `ls -ld`.
|
|
|
|
**Transform direction is counter-clockwise** (`niri msg output <NAME>
|
|
transform --help`): `90` = content rotated 90° CCW ⇒ the panel's **top
|
|
bezel sits on the LEFT**; `270` = top bezel RIGHT; `180` = upside down;
|
|
`flipped-90`/`flipped-270` = mirrored variants. `90` vs `270` is a
|
|
physical-stand fact no tool can read (portable monitors expose no
|
|
orientation in EDID/MCCS) — pick one, apply, and hand the user the one-liner
|
|
to flip; don't stall on the question.
|
|
|
|
Two traps when editing the config:
|
|
|
|
1. **An `output "X" { … }` block resets every property you don't restate to
|
|
its default.** Pinning position on `eDP-1` without repeating `scale 1.25`
|
|
silently drops the laptop panel to scale 1.0. Restate `scale` (and
|
|
`mode`) in every block you touch.
|
|
2. **Niri re-runs auto-layout for outputs with no pinned position.** Pinning
|
|
only the externals shoved `eDP-1` from `0,0` to `3936,0`. Pin every
|
|
output's position to keep the left-to-right order. Positions are logical
|
|
pixels at each output's own scale; x-offsets chain (eDP-1 logical width
|
|
1536 → DP-2 at 1536 → +1200 portrait width → HDMI-A-1 at 2736):
|
|
|
|
```kdl
|
|
output "DP-2" { scale 1 transform "90" position x=1536 y=0 }
|
|
output "HDMI-A-1" { scale 1 transform "90" position x=2736 y=0 }
|
|
output "eDP-1" { scale 1.25 position x=0 y=0 } // MUST restate scale
|
|
```
|
|
|
|
Apply and verify:
|
|
|
|
```bash
|
|
niri validate -c ~/.config/niri/config.kdl # exit 0
|
|
niri msg action load-config-file # live reload; NOT reload-config
|
|
sleep 1; niri msg outputs | grep -E '^Output|Transform|Logical|Scale'
|
|
```
|
|
|
|
Success looks like `Logical size: 1200x1920` (swapped) plus
|
|
`Transform: 90° counter-clockwise` on each rotated panel, positions matching
|
|
the intended chain. One-off rotation without touching config (temporary):
|
|
`niri msg output DP-2 transform 90`. Commit via the dotfiles repo
|
|
(`~/.omp`, `main`; push to the local Gitea permitted): `git pull --ff-only`
|
|
→ add the display.kdl → commit → push. Pitfalls: `reload-config` is not a
|
|
niri subcommand; never SIGUSR1 a terminal to "reload" (niri config reloads
|
|
do not touch ghostty themes); no man page for niri on this host — `niri
|
|
<sub> --help` and `/usr/share/doc/niri/default-config.kdl` are the docs.
|
|
|
|
## GPU RGB over I2C (EVGA iCX3)
|
|
|
|
Turn RGB on/off (or read it back) on an EVGA FTW3 / iCX3-controller GPU
|
|
(the 3090 on dankclaw) using the GPU's own nvidia i2c bus. No display
|
|
engine, no X, no daemon — the mode is EEPROM-saved and survives reboot.
|
|
|
|
- **Find the controller:** the nvidia i2c driver exposes the card's lighting
|
|
ports as `/dev/i2c-N`; on the 3090 FTW3 that is ports 1,5,6,7,8 →
|
|
i2c-1..i2c-5, and only i2c-1 carries a live slave (NVIDIA port 1). Probe
|
|
each bus. The iCX3 MCU answers at address **0x2D**. `/dev/i2c-*` are
|
|
`root:i2c 660` — run with `sudo` (dank has passwordless sudo).
|
|
- **Protocol (raw I2C_RDWR, NOT the SMBus ioctls)** — write the register
|
|
byte first, then the payload:
|
|
- enable `0xB2` write `04 C6 EB EA 15` (required once before mode writes)
|
|
- mode `0xC0` write `09 m0 m1 m2 m3 .. .. .. .. SYNC`; m0..m3 are the four
|
|
zone modes, `MODE_OFF = 0x00`; OFF = `09 00 00 00 00 FF FF FF FF 00`
|
|
- save `0x90` write `04 9E EB 00 90` (commit to EEPROM — the
|
|
reboot-persistence step)
|
|
- fw `0xB1` block read (firmware version)
|
|
- Read the mode back: write reg `0xC0`, then read 16 bytes; bytes 1..4 are
|
|
the zone modes (0x00 = OFF), bytes 6..9 the per-zone LED counts.
|
|
- **Verify persistence:** after a reboot, read the mode — all four zone
|
|
modes `0x00` means the EEPROM save held (confirmed 2026-09-19 post-reboot).
|
|
- **Tool:** a ~90-line I2C_RDWR reader/writer lived at `/tmp/evga_raw.c`
|
|
(`evga_raw <bus> mode|off|save|enable|fw`) — `/tmp` is wiped by reboot, so
|
|
recreate it if needed; OpenRGB's `EVGAGPUv3Controller.cpp` carries the
|
|
same protocol plus the full colour API.
|
|
- Gotchas: raw I2C_RDWR only (the iCX3 answers the raw form, not the SMBus
|
|
ioctls); the `enable` write must precede the first mode write in a
|
|
session; without the `save` write the mode is RAM-only and resets on power
|
|
cycle.
|
|
|
|
## Related
|
|
|
|
- [[Command Center Dotfiles]] — the stow-equivalent deploy this page links into
|
|
- [[Hermes]] — the agent that drove the CDP daemon alongside this desktop
|
|
- [[GGUF Compatibility]] — unrelated but shares the "read the file, not the name" lesson
|