Files
serverAppConfigs/serverAppConfigs-docs/Desktop/Noctalia and Niri.md
T
Frank 33eae71d33 docs: distill the managed skill library into the vault (waves 1-7)
All ~173 managed skills read in full and merged, one polished reference
page per skill-cluster:

- LLM cross-cutting (17 pages): GGUF Compatibility, Llama Cpp Serving
  Sizing, Llama Cpp Multi-Device Splits, PCIe Link and Atomics, RDNA4
  Container Serving, RDNA4 MoE Expert Offload VRAM Budget, vLLM Boot
  Forensics, vLLM Porting and Compile Triage, LLM Bench Windows, LLM
  Performance Diagnosis, Model Swap and Rollback, Tcclaviger Fork Ops,
  Heterogeneous Split Triage, FreeToken Builds + deltas folded into the
  retired runbooks (GLM DFlash2 port laws, Lucebox dual-R9700, R9V
  qualification pipeline, FLM pack registration, ik hybrid envelope,
  frank-medium split rebalance, llama.cpp GGUF preflight, batch-ubatch
  split section, bench-window D-state/stale-read notes, second-endpoint
  bring-up).
- Architecture (3): Stale Signal Audit, Quadlet Lifecycle, Deploy State
  Triage.
- Operations (6 + index): Remote Job Runner, Payload and Disk Reclaim,
  Container Host Reachability, Fleet Ops Playbook, Fleet Audit and
  Posture, SSH and Remote Access.
- Infrastructure: Observability Provisioning + Homepage Status Probes;
  updates to Authentik, Anubis, LAN-DNS (per-client DoH bypass, Porkbun
  zone ops), Swap Configuration, OpenWebUI, Hermes (pre-existing page
  preserved and extended).
- FrankBook-Pro: ARR-Stack filled (API split, schema-first probing,
  auth-posture, auth-config trap); qBittorrent E2E verification.
- Desktop/Agent/Research: Noctalia and Niri, OMP Harness, Agent Docs and
  Edits, Command Center Dotfiles, PICO-8 Asset Standards.
- Indexes: LLM Cross-Cutting References section, Operations/ and Agent/
  index pages, root-index Skill Distillation section, AGENTS.md pointer.
- log.md carries the wave plan.

check-vault-links.py 0 unresolved (185 pages); ./tests/run.sh 366/366.
2026-10-05 21:19:15 -07:00

16 KiB

type, title, description, tags, timestamp
type title description tags timestamp
reference Noctalia and Niri The noctalia v5 desktop shell on niri (workstation + blankpad) — config layers, remote IPC over ssh, custom-palette recolor, offline template rendering, and output rotation/pinning.
desktop
noctalia
niri
blankpad
workstation
ghostty
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):

    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:

# 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):
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:

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.