Files
command-center/omp-docs/tools/dotfiles-deploy.md
T
Frank 9ebf5fe85f Untrack a retired niri config that was committed as trash; correct two census rows
The repo still tracked `dotfiles/niri/.config/niri/cfg/.AGENTTRASH/display.kdl.superseded`
— a retired output config committed 2026-10-09, five days before `.gitignore` grew the
`.AGENTTRASH/` rule. Tracked-then-ignored: an ignore rule never reaches an already-tracked
path, so `git rm --cached` is the only repair. The working copy joins the rest of today's
tracked debt.

Found while answering "does this repo still track my noctalia and niri settings", which
also falsified two rows of the census committed yesterday: niri is 9 tracked files, not
12; noctalia is 3 authored files against 2672 files of app state and downloaded catalog,
not 2/0. The page's own classifier command reproduces the new numbers — it never
reproduced the old ones, which is the only reason they were wrong and not merely
different.

Two laws recorded alongside, both from the same probe: `config.kdl`'s
`include "noctalia.kdl"` is not `optional=true`, so a fresh box cannot start niri until
the theme pipeline has run once; and `cfg/display.kdl -> desk-1-home.kdl` is desk state a
fresh box does not receive — by design, but nowhere written down.

Refs #40
2026-10-20 17:00:20 -07:00

93 lines
4.9 KiB
Markdown

---
type: reference
title: Dotfiles deployment tier
description: How dotfiles/ packages become live configuration — stow-equivalent symlinks, host override packages, and the two laws about theme media and systemd enable symlinks.
tags:
- dotfiles
- deployment
- systemd
- noctalia
timestamp: 2026-10-20
verified: 2026-10-20
---
# Dotfiles deployment tier
`dotfiles/` holds home-relative trees and `scripts/deploy.sh` is a stow-equivalent: the
repo checkout **is** the deployment, and deploying replaces live files with symlinks into
the repo, so `git pull` + `deploy.sh` is the whole propagation story. One directory per
tool (`niri`, `ghostty`, `gtk-3.0`, `systemd-user`, …); a package named `zz-$(hostname)`
is stowed last and its files replace symlinks coming from base packages.
| Mode | Effect |
|---|---|
| `deploy.sh --check` | dry run — adopts identicals, reports what would be linked, lists divergents, writes nothing. Run this first, always |
| `deploy.sh` (sync) | adopt identicals, link new, quarantine divergents into `.quarantine/` and replace them |
| `deploy.sh --adopt` | like sync, but divergents are reported and skipped rather than quarantined |
Two package shapes are not symlinks: `dconf/dump.txt` is loaded with `dconf load /`, and
`systemd-user` units get `daemon-reload` plus `enable --now` only when their `ExecStart`
binary resolves on the current machine, so a unit belonging to another box stays inert
instead of crash-looping.
**The tracked unit is the file, not the directory.** A live directory can be a real
directory and still be fully tracked, because every file inside it is a symlink into the
repo (`niri` is 9 tracked / 1 box-only; `gtk-3.0` is 3/0). Any audit that classifies by
directory mislabels the tree — classify per file with `readlink -f`. The live census and
its classifier command are in [home-config-census](/inventory/home-config-census.md).
## Law: media the theme pipeline consumes is content, not config
Wallpapers live at `dotfiles/noctalia/.local/share/noctalia/wallpapers/` and are
gitignored. They were tracked until 2026-10-20 — 23 files, 80 MiB — which meant every
clone of a *configuration* repo carried a media library no diff can review, and the
payload only ever grows because deleting a tracked blob from the tree does not delete it
from history. [Noctalia](noctalia-shell.md) reads
`~/.local/share/noctalia/wallpapers/`, which is a set of symlinks into that directory, so
the files stay on disk and untracking them broke nothing on this box.
Restore on a fresh box: every filename is `wallhaven_<id>.<ext>` and the ID is the whole
address — `curl -L -o <name> "https://wallhaven.cc/w/<id>"` puts the library back, then
`deploy.sh` relinks it. Losing the set is an acceptable outcome; 80 MB per clone forever
is not.
## Law: never `systemctl --user enable` a unit from a worktree
`enable` writes `~/.config/systemd/user/default.target.wants/<unit>` as a symlink to the
unit path it was handed, and that path is absolute. The heartbeat reaps `wt/*` worktrees
older than seven days ([live-sync](live-sync.md)), so a unit enabled from a worktree turns
into a dangling symlink while `systemctl --user is-enabled` still answers `enabled` — the
unit is dead and the status lies about it. Found on 2026-10-20:
`blankpad-desk-autoswitch.service` pointed into the reaped `wt/de-redesign-20261009`, so
desk auto-switching had been silently off since the reap.
Check the link target before the unit file:
```bash
ls -l ~/.config/systemd/user/default.target.wants/
# every target must resolve; a dangling one is a reaped worktree, not a config bug
```
Fix is to relink into the deployment checkout (`~/.omp/dotfiles/…`), `systemctl --user
daemon-reload`, then confirm with `is-active` — `is-enabled` proves nothing.
## Law: a generated file must never be a required include
`~/.config/niri/config.kdl` ends with `include "noctalia.kdl"` — no `optional=true`.
That file is noctalia's palette output (`#ff8c00`, `#f43841`, one re-stamp
`2026-10-18 17:36:27`), so it is machine state and correctly untracked. But on a fresh
box `deploy.sh` links `config.kdl` first, and niri then refuses to start until noctalia
has run once and written the file — the compositor is bricked by the order of operations,
not by a bad config. Close it at the include, which survives someone skipping the theme
pipeline: mark generated includes `optional=true`, as the `display.kdl` and
`switch-events.kdl` lines in the same file already are.
## Desk choice is live state, never config
`~/.config/niri/cfg/display.kdl` is a relative symlink to whichever profile is active
(`desk-1-home.kdl`, `desk-2-portable.kdl`), written by `desk.sh`. Both profiles are
tracked in `dotfiles/zz-blankpad/`; the pointer is not, because which desk the box sits on
is not a fact about the repo. A fresh box therefore starts with no output configuration —
`config.kdl` includes `display.kdl` with `optional=true`, so niri comes up usable but with
default monitors until `desk.sh` is run once.