17 KiB
dots
Unified dotfiles for all machines: one shared base + per-host overlays, deployed with GNU Stow.
Layout
Every top-level directory is a Stow package (they all share one stow dir, which --override
requires). bin/ holds repo tooling and is not a package (never stowed):
common/ # shared everywhere (mirrors $HOME): fish, nvim, bat, btop, tmux, pi, claude, presenterm, ...
gui/ # GUI configs shared by graphical hosts: hypr, waybar, kitty, alacritty, mpv, zellij
wm/ # desktop: 4K monitors, NVIDIA, cloud LLM (duskadiy), work aliases
lw/ # laptop: no NVIDIA, roaming VPNs, network/LLM tools
fl/ # KDE Plasma box (no Hyprland session): llamacpp, whisper, evremap
bin/ # tooling (not a stow package): install.sh companions
install.sh
A headless (terminal-only) machine has no overlay package — it runs common only. Mark any box
headless with touch ~/.config/dots-headless (a machine-local, hostname-independent marker); it then
skips gui too, so the same terminal-only setup replicates across any number of machines. In other
words common is the headless baseline; gui and the host packages layer graphical machines on top.
Run it as a bare ./install.sh — never ./install.sh common (common/gui are layers, not hosts,
and install.sh refuses them).
How it deploys
Deploy order is common → gui → <host>; later layers win (the host package is stowed last with
--override, so a host file replaces the common/gui file at the same path). Stow runs with
--no-folding, so each file is symlinked individually (directories stay real) — this is what lets
a host override a single file in a shared dir without shadowing the rest.
Editing a config edits the repo copy directly (it's a symlink) — no deploy step. You only re-run
install.sh when files are added or removed (to create/prune symlinks). See Syncing below.
The repo must be cloned to ~/.dots (the relative ../.dots symlink convention).
Which host package a machine deploys
Hostname is not the authority — the machine remembers its own choice, so several boxes can deploy
the same overlay without sharing a hostname. install.sh resolves the host as:
- the explicit argument (
./install.sh wm), - else the name in
~/.config/dots-host— written byinstall.shitself on every run that resolved a real package, so a one-time./install.sh <host>is all the setup there is, - else
hostname -s, purely as a convenience guess for boxes where the two happen to agree.
~/.config/dots-host is a machine-local, hand-editable one-word file — the sibling of the
dots-headless marker above. It is what makes bin/dotsync correct: dotsync re-links
automatically and has no argument to pass, so without a recorded host it could only guess. Switching
a machine to another overlay is just ./install.sh <other> — the previously recorded host is
unstowed first, so the base layers don't collide with its leftover links.
Mismatched hostnames used to fail in a way worth recognizing: ./install.sh on a box whose hostname
matches no package deploys common, aborts gui on every file the real overlay owns
(existing target is stowed to a different package) and skips the overlay entirely — a loud
exit 1, but a half-applied one, where newly added files silently get no symlink.
Install (new machine)
sudo pacman -S stow
git clone <remote> ~/.dots
cd ~/.dots
./install.sh wm # name the host package once — it is remembered (see above)
./install.sh -h lists all options: -n dry-run, -a adopt (first run), -D unstow (revert),
-h help. GUI hosts also need HyDE installed first — see HyDE dependency below.
Once per machine afterwards: :Lazy restore inside nvim (restore obeys the shared
lazy-lock.json pins; sync/update would rewrite them).
nvim's treesitter (main branch) builds parsers via the tree-sitter CLI — pacman -S tree-sitter-cli first, or :TSInstallAll fails with ENOENT ... 'tree-sitter'.
First run on a machine that still has real config files
Stow aborts the whole package on the first conflict (one real file blocks everything in
common), and refuses to clobber real files without --adopt — you'll see
cannot stow … since neither a link nor a directory. Adopt them, review, then keep the repo's
versions:
cd ~/.dots
git status # make sure the repo is clean first (see the restore caveat)
./install.sh -a <host> # --adopt: pull existing files into the repo, symlink them back
git diff --stat # what this machine had vs the repo (review the drift!)
git restore . # discard adopted content, keep the repo versions
--adopt overwrites the repo copies with the machine's files, so git restore . is what keeps the
repo versions. Cautions: adopted changes can land under common//gui/ even when host-specific, and
git restore . drops all uncommitted edits — so commit/stash anything you want first. To keep a
genuine machine change (e.g. custom pyradio stations.csv), hand-copy it into the right package
instead of restoring that path.
Syncing (day-to-day)
Because configs are symlinks into the repo, you almost never run install.sh to sync:
- Pull/propagate edits:
cd ~/.dots && git pull— changes are live in place instantly. No re-stow, so no symlink churn and no app reloads (kitty etc. keep runtime state). bin/dotsyncdoes this safely:git pull, then re-link viainstall.shonly if the pull added/removed/renamed files (pure edits skip the re-stow).dotsync -npreviews incoming changes.install.shis a (re)deploy tool, not a sync tool — itsstow -Rdeletes+recreates every symlink, which is what makes apps reload. Only needed for the initial link or when files are added/removed.- Never
stowthe repo root (stow .dots,stow */) — it folds package dirs into~as stray~/common,~/gui, … symlinks.install.shonly ever links package contents. - New host: create a top-level
<name>/package (at least ahost.fish), commit, then./install.sh <name>(naming it once records it — later runs and dotsync need no argument). If it's terminal-only, skip the package and justtouch ~/.config/dots-headless(it runscommononly).
Tooling (bin/)
| Command | What it does |
|---|---|
./install.sh [host] |
Deploy common → gui → <host>. Host = arg, else ~/.config/dots-host, else hostname -s; the resolved host is recorded there. Flags: -n dry-run, -a adopt (first run), -D/--unstow revert (remove this host's symlinks; repo + real files kept), -h help. Self-heals: headless hosts get any stray gui unstowed, a previously recorded host is released before re-deploying; fails loudly (exit 1) if a package couldn't stow. |
./bin/dotsync [-n|host] |
Sync with least churn: git pull, re-stow only if files were added/removed (via install.sh, using the recorded host). |
./bin/reconcile-hyde.sh [-i|--relink] [host] |
After a HyDE update overwrote ~/.config/hypr, reconcile HyDE's new files against your dots versions (report / -i keep·take·merge each / --relink re-symlink). |
How per-host differences are expressed
| Difference | Mechanism |
|---|---|
| Monitors | <h>/.config/hypr/host.lua (hl.monitor + workspace pins; loaded by the shared hyprland.lua) |
| NVIDIA | the NVIDIA block in wm/.config/hypr/host.lua (lw has no NVIDIA) |
| Shell aliases | shared config.fish sources ~/.config/fish/host.fish (per host) |
| Bash extras | shared .bashrc sources ~/.bashrc.host (optional, per host) |
| Unique apps | in the owning host overlay (fl: llamacpp/evremap; lw: xremap) |
aichat, pi, and opencode share one config in common (LAN-first, with the cloud provider also
listed so any host can switch). lw and wm each carry a host overlay of all three that adds their
own llama.cpp models (lw: CPU; wm: the GTX 1080 Ti router, plus the duskadiy default) — mirror
edits between the common copy and both overlays. The opencode tree is documented
in common/.config/opencode/README.md.
HyDE dependency (important for GUI hosts)
The Hyprland config is HyDE-based, since 2026-08-21 through
HyDE 26.x's Lua chain (live on wm since then and on lw since 2026-11; merged into main
2026-11-09 — see LUA-PORT.md). The repo pins HYPRLAND_CONFIG at
~/.local/share/hypr/hyde.lua in two files — gui/.config/fish/conf.d/00-hypr-config.fish
(SDDM/fish session path) and gui/.config/uwsm/env-hyprland.d/10-dots.sh (uwsm path) — and hyde.lua
loads HyDE's lua/ modules, then the repo's override gui/.config/hypr/hyprland.lua, which pulls in
dots_rules.lua + dots_binds.lua (split out of it) and the per-host host.lua. So a GUI host must have HyDE installed — it ships hyde.lua and the
modules, both overwritten on HyDE updates; the dots layer lives only in ~/.config/hypr/*.lua, which
survives them. LUA-PORT.md has the full map, flip recipe and rollback.
The legacy hyprlang chain (gui/.config/hypr/*.conf, plus the two ~/.local/share/hyde/ {hyprland,hyde}.conf framework copies HyDE 26.x stopped shipping) was retired 2026-09-10
(LUA-PORT.md step 8) once the Lua soak closed — nothing reads a .conf in Lua mode. One line
survives: gui/.local/share/hyde/hyde.conf keeps $default.FONT, the fallback HyDE's rofi/menu
scripts resolve FONT through (get_hyprConf) when the theme's hypr.theme names no font. Rolling
back to hyprlang now means restoring those files from history (git restore --source=bddbbf3 -- gui/.config/hypr wm/.config/hypr lw/.config/hypr gui/.local/share/hyde), ./install.sh <host>,
repointing the two pins and relogging; d851213 is the last hyprlang main (the port merged
2026-11-09). After pulling the 2026-08-14 audit, a machine must run
./bin/hyde-materialize.sh before ./install.sh: the newly-untracked files leave dangling stow
symlinks otherwise, and hypr/hyprlock treat a missing source = as a hard parse error.
- Install/update HyDE with its own installer (
~/HyDE/Scripts/install.sh); it also pulls the required apps (xdg-desktop-portal-hyprland,xdg-desktop-portal-gtk,hyprpolkitagent). - HyDE's
-r/restore overwrites~/.config/hypr— usebin/reconcile-hyde.shafterward to fold its new defaults back against your dots configs. - Wallpapers and generated themes are HyDE-owned, never in dots (
**/hyde/themes/and**/wallpapers/are gitignored — they once bloated a source repo to 255 MB). A fresh machine gets them from HyDE, not from this repo.
Waybar (two worlds: HyDE-generated vs the standalone rig)
HyDE owns ~/.config/waybar/config.jsonc AND style.css — its waybar.py writes both on
any --set/--update, through the stow symlinks, i.e. into this repo's checkout. So dots
track neither. HyDE-managed customization goes via:
gui/.config/waybar/layouts/*.jsonc— dots-tracked layouts; switch withhyde-shell waybar --set <name>.gui/.config/waybar/modules/*.jsonc— module defs, globbed into the generated config.
The custom cava rig is NOT HyDE-managed: layouts/custom.jsonc + layouts/custom.css form a
self-contained two-bar config run by wbar (gui/.local/bin/wbar) as the supervised systemd
user unit dots-waybar.service (Restart=always — waybar 0.15.0 dies on every DP-2 link
drop/return, and as a bare process nothing relaunched it). hc.start.bar in hyprland.lua, the
Ctrl+Alt+W / Super+Shift+R binds and the wbar command all go through it; never killall waybar — systemd brings it back in seconds and a raw relaunch beside it means two bars
(wbar toggle|restart|reload|stop). Never route it through hyde-shell waybar --set — waybar.py would clobber the repo's files as above. HyDE only reads
from layouts/, which is what makes them safe to track. On a rig host, HyDE's own bar must be
disabled once, or two bars appear at every login — and the launcher differs by HyDE vintage:
- current HyDE: systemd user unit
hyde-<XDG_SESSION_DESKTOP>-bar.service(auto-restarting) —systemctl --user stop <unit>; systemctl --user mask <unit>. - older (pre-unit) HyDE:
exec-once = $start.BARin HyDE's generatedhyprland.conf— the hyprlang-era dots blanked it with$start.BAR=ingui/.config/hypr/hyde.conf; that file went with the chain (LUA-PORT.md step 8), and under Lua the lever ishc.start.barinhyprland.lua, which launches the rig itself — see HYDE-UPDATE.md step 4.
Note: the Arch waybar build has no native cava module — the rig uses the script module
custom/cava running cava-waybar.sh (self-contained, no HyDE dependency).
Secrets
Never committed. Put DUSKADIY_API_KEY etc. in ~/.config/fish/conf.d/secrets.fish (gitignored).
pi's auth.json is not tracked — it only ever holds $VAR references, but pi's /login
rewrites it with the literal key, so one stray /login would commit a real token. A new host
bootstraps it with cp common/.pi/agent/auth.json.example common/.pi/agent/auth.json, then
./install.sh. Also gitignored: fish_variables,
greet.txt, pi agent runtime/state, tmux/plugins/, .claude/ (Claude Code project dir/worktrees),
*.bak/*.log.
Merge notes (history — the one-time unification)
The four former per-machine repos had drifted; the merge made some judgment calls:
config.fish/.bashrcwere hand-reconciled into a neutral shared base + per-hosthost.fish/.bashrc.host(removed a committedfnm_multishellsruntime PATH, stray absolute home-dir paths, duplicate git aliases).pilives at~/.pi(repo.pi/);tmuxat~/.config/tmux/(delete a legacy~/.tmux.confif present after migrating).- Appearance drift was unified to one canonical (mostly wm): bat, btop, cava, fastfetch, lazygit, mpv, pyradio, waybar, zellij, most of nvim.
- nvim is shared entirely (no host overrides); plugin set /
lazy-lock.jsonare shared — run:Lazy restoreper machine once (obeys the lockfile pins —:Lazy updateon one machine + commit is how versions move forward; other machines converge withrestore). zellijis one shared config with vim keybinds (h/j/k/l) + theguvbox-darktheme.misc/{clis,tuis}.txtlauncher inventories use wm's lists incommon; a machine needing its own can override in its overlay.
Third-party content and licensing
The repo is GPL-3.0 (LICENSE). What that does and doesn't cover:
-
hypr/,waybar/and the other desktop configs are derived from HyDE (and, further back, legacy Hyprdots). HyDE is itself GPL-3.0, so the derivation and this repo's licence agree — no conflict, and the Hyprdots-era ASCII banners in those files are left in place as attribution. -
Colour themes: the repo ships no third-party theme files, so it needs no notices file. Each app gets its theme from somewhere else:
- kitty: HyDE renders
theme.conffrom the current HyDE theme.kitty.confadds only this repo's own background, foreground and tab bar. - aichat: One Half Dark (MIT, by Son A. Pham) is downloaded from upstream at install
time by
bin/dots-themes-fetch.sh, pinned by commit and checksum. - cava: it writes its own shaders and themes.
- zellij and bat: their gruvbox-dark and OneHalfDark themes are built in.
The pi, opencode and alacritty themes are the repo's own files. They hold colour values from the Catppuccin Mocha and SynthWave '84 palettes (both MIT, by Catppuccin and Robb Owen), mapped onto each app's keys. Credit, gladly given: a set of colour values isn't a copy of either licensed theme. Keep it that way: a theme file copied in brings its licence notice with it.
- kitty: HyDE renders
-
gui/.local/share/icons/dots-icons/ships only itsindex.theme. Its tray/status icons are symlinks into each host's own Tela-circle-dracula (by vinceliuice, installed by HyDE), made at install time bybin/dots-icons-link.sh. Tela-circle publishes no licence, so the repo doesn't redistribute its SVGs. -
common/.config/fastfetch/pngs/holds project and distro marks (Arch, the Open Source Initiative and Open Source Hardware logos). Trademarks aren't relicensable, so GPL-3 does not extend to them; they are bundled as nominative use, the way most dotfiles repos do. The dmz.rs rabbit (dmz-rabbit*.png) and the decentrala.org network logo (network*.png) are the community's own marks, used with permission.neonom.pngis the repo owner's own render of the Om glyph, offered as CC0. -
gui/.config/hypr/hyprlock/assets/— the lock screen background and logo are AI-generated by the repo owner (February 2026). They sit outside the GPL-3 grant and are offered as CC0. The keyhole inbackground.jpgis an AI-altered rendition of the Open Source Initiative mark, used purely decoratively; no affiliation with or endorsement by the OSI is implied.