docs: design for multi-role dotfiles

The repository stopped matching reality in April 2021. It describes an i3/sway
desktop that no longer exists - the display menu drives xrandr outputs the
machine does not have - while .zshrc alone drifted by 360 lines. Checking out
master would overwrite five years of work rather than update anything.

Three machines, no shared base: beastix (Arch/KDE), shron (Arch, headless), a
Mac. Common .zshrc lines between all three: four.

Keeps the existing bare-repo-and-alias method. The role is resolved when the
shell starts, not when files are deployed, so no deployment tooling is needed
at all; every machine carries every file and sources one of them.

Package lists are derived from usage data - shell history, the KDE activity
database, autostart, flatpak - because each signal alone has a blind spot and
pacman -Qqe lists 512 packages including years of experiments. That yields 62.

Plain files and plain lists throughout, so a later move to home-manager stays
cheap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
thomas.kopp
2026-08-06 23:43:59 +02:00
co-authored by Claude Opus 5
parent cd6dd42178
commit 94b6ba3c33
@@ -0,0 +1,162 @@
# Multi-role dotfiles — design
2026-08-06
## Problem
The repository stopped tracking reality in April 2021. It describes an i3/sway
desktop that no longer exists: beastix runs KDE on X11, and the `xrandr` calls
in the old display menu address outputs (`DVI-D-0`, `HDMI-0`, `DP-0`..`DP-5`)
that the machine does not have. Meanwhile the live files kept evolving —
`.zshrc` alone differs by 360 lines.
A `config checkout` of the current master would therefore not update a machine,
it would overwrite five years of work.
Three machines exist and none of them shares a base with the others:
| | beastix | shron | Mac |
|---|---|---|---|
| Platform | Arch, KDE/X11 | Arch, headless | macOS 26.5.2, arm64 |
| Prompt | oh-my-zsh + powerlevel10k | oh-my-zsh | oh-my-zsh |
| `.zshrc` | 240 lines | 159 | 218 |
| Packages | 512 explicit | 106 | 34 leaves + 36 casks |
Shared `.zshrc` lines: beastix↔Mac 51, beastix↔shron 6, all three 4.
The Mac config was clearly copied from the desktop at some point; shron is the
outlier.
## Decisions
**Start from the live state, not from master.** The 2021 content is preserved
on a branch. Nothing of value is lost: every keybinding unique to the old i3
config is either present under a different key (`$mod+f``$mod+z`), available
in another form (the system menu is now an i3-nagbar), or dead (the display
menu targets outputs that do not exist).
**The base is not the intersection.** Four common lines would be a worthless
base. The base is the good part of the desktop configuration; the server role
leaves out what it does not need. Defining it the other way round lets the
least-equipped machine set the standard for all of them.
**Keep the bare repo and the alias.** The existing method already works:
```sh
alias config='/usr/bin/git --git-dir=$HOME/.cfg/ --work-tree=$HOME'
```
No stow, no wrapper tool. The earlier objection to this approach — that
`git status` drowns in untracked files and that roles need branches — is wrong
on both counts: `status.showUntrackedFiles no` handles the first, and the
second only applies if the role is resolved at deploy time.
**Resolve the role at runtime, not at deploy time.** Every machine checks out
every file; which one gets sourced is decided when the shell starts. Dotfiles
are kilobytes, so carrying an unused i3 config on the server costs nothing and
removes the need for any deployment tooling.
**Stay portable to Nix.** NixOS is the likely long-term direction. Plain files
and plain package lists map onto `home-manager` and `environment.systemPackages`
almost unchanged; chezmoi templates would not. This costs nothing today.
## Layout
```
.zshrc stub: source rc.d, then the role file
.config/zsh/rc.d/
10-aliases.zsh base, everywhere
20-functions.zsh
30-path.zsh
50-desktop.zsh exactly one of these is sourced
50-server.zsh
50-mobile.zsh
90-local.zsh machine-local, gitignored, sourced last
.config/dotconfs/role one word, gitignored
packages/
arch-base.txt arch-desktop.txt arch-server.txt
brew-mobile.txt brew-cask-mobile.txt
flatpak-desktop.txt
.scripts/
packages.sh check | install
curate.sh derive candidates from usage data
```
The stub:
```sh
ROLE=$(cat ~/.config/dotconfs/role 2>/dev/null || echo desktop)
for f in ~/.config/zsh/rc.d/[0-4]*.zsh; do source "$f"; done
source ~/.config/zsh/rc.d/50-$ROLE.zsh
[ -r ~/.config/zsh/rc.d/90-local.zsh ] && source ~/.config/zsh/rc.d/90-local.zsh
```
`90-local.zsh` is sourced last on purpose: it can override anything without
touching the repository.
## Prompt
powerlevel10k on all three, same segments, colour by role:
- desktop — blue
- mobile — green
- server — **red**
This is a safety measure, not decoration. shron carries mail, web and cloud. A
red prompt is the cheapest effective guard against running a command in the
wrong terminal.
## Packages
Package selection is derived from usage data rather than from
`pacman -Qqe`, which lists 512 packages on beastix and includes years of
one-off experiments. Four signals, because each alone has a blind spot:
1. **Shell history** (10332 entries) — covers CLI tools, blind to anything
started from the desktop menu.
2. **KDE activity database** (`kactivitymanagerd/resources/database`) — covers
applications that open documents, blind to chat clients.
3. **Autostart and running processes** — covers the daily drivers
(Signal, Discord, Thunderbird, KeePassXC, Nextcloud).
4. **`flatpak list`** — these never appear in `pacman -Qqe` at all.
Combined, this reduces 512 to 62 candidates. `curate.sh` stays in the
repository so the list can be regenerated rather than maintained by hand.
`packages.sh check` reports drift in both directions. `packages.sh install`
adds what is missing. **Nothing is ever removed automatically** — individual
packages on shron carry mail and web services, and a tool that tidies up on its
own is a tool that eventually takes the mail server down.
## Secrets
The existing `mailsecrets` pattern becomes the rule: credentials live in a
separate file that is imported and never committed. `.gitignore` covers
`mailsecrets.py`, `.netrc`, `.pgpass`, `*.key`, `*.pem`, `id_*` (public keys
excepted), the role file and `90-local.zsh`.
`config add -f` still bypasses this. The ignore list guards against
carelessness, not against intent.
## Rollout order
`config checkout` overwrites existing files, so the sequence matters.
1. **beastix** — richest configuration, becomes the source. Split `.zshrc` into
stub and `rc.d` fragments, write the role file, commit, push.
2. **shron** — clone, checkout with conflicts moved to `.config-backup` as the
existing installer already does. Then review its 57 unique lines: what
belongs in `50-server.zsh`, what is obsolete.
3. **Mac** — same, role `mobile`. The 51 lines it shares with beastix are
already covered by the base.
The role file is written **before** the first shell start on each machine.
Otherwise the default applies and shron comes up with a desktop prompt.
`.config-backup` stays until the result is confirmed. Nothing is deleted.
## Out of scope
- Migration to NixOS. The design keeps the path open; it does not take it.
- macOS `defaults` (Finder, keyboard) — deferred until the shell layer works.
- Removing packages anywhere.