Files
dots/README.md
T
2026-08-12 23:49:33 +02:00

12 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: a few Hyprland overrides + llamacpp, 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.

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). A host's package name must match hostname -s (or pass the host explicitly, e.g. ./install.sh lw).

Install (new machine)

sudo pacman -S stow
git clone <remote> ~/.dots
cd ~/.dots
./install.sh            # auto-detects host via `hostname -s`

./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: bat cache --build (registers the repo's custom bat themes — without it every bat run warns Unknown theme 'onedark'), and :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/dotsync does this safely: git pull, then re-link via install.sh only if the pull added/removed/renamed files (pure edits skip the re-stow). dotsync -n previews incoming changes.
  • install.sh is a (re)deploy tool, not a sync tool — its stow -R deletes+recreates every symlink, which is what makes apps reload. Only needed for the initial link or when files are added/removed.
  • Never stow the repo root (stow .dots, stow */) — it folds package dirs into ~ as stray ~/common, ~/gui, … symlinks. install.sh only ever links package contents.
  • New host: create a top-level <name>/ package (at least a host.fish), commit, then ./install.sh. If it's terminal-only, skip the package and just touch ~/.config/dots-headless (it runs common only).

Tooling (bin/)

Command What it does
./install.sh [host] Deploy common → gui → <host>. 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; fails loudly (exit 1) if a package couldn't stow.
./bin/dotsync [-n] Sync with least churn: git pull, re-stow only if files were added/removed.
./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/monitors.conf (sourced by the shared hyprland.conf)
NVIDIA wm/.config/hypr/nvidia.conf, pulled in by wm's host.conf (fl/lw have no NVIDIA)
Shell aliases shared config.fish sources ~/.config/fish/host.fish (per host)
Bash extras shared .bashrc sources ~/.bashrc.host (optional, per host)
fl's Hyprland overrides ~9 hypr files (hyprland.conf, keybinds, windowrules, theme colors, monitors); inherits the rest from gui
Unique apps in the owning host overlay (fl: llamacpp/evremap; lw: xremap)

aichat, pi, and opencode are single shared configs in common (LAN-first, with the cloud provider also listed so any host can switch) — no per-host overrides.

HyDE dependency (important for GUI hosts)

The Hyprland config is HyDE-based. gui/.config/hypr/* is your customization layer, but it sources HyDE's framework: source = ~/.local/share/hyde/hyprland.conf — which defines $mainMod and other variables and is not tracked by dots (HyDE generates it). So a GUI host must have HyDE installed, or Hyprland fails to start (source globbing error + invalid mod $mainMod).

  • 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 — use bin/reconcile-hyde.sh afterward 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 with hyde-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 launched raw (waybar -c …custom.jsonc -s …custom.css — exec-once, the Ctrl+Alt+W / Super+Shift+R binds, and the wbar abbr). 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 (systemd user unit hyde-<XDG_SESSION_DESKTOP>-bar.service, auto-restarting) must be disabled once: systemctl --user stop <unit>; systemctl --user mask <unit>.

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 / .bashrc were hand-reconciled into a neutral shared base + per-host host.fish / .bashrc.host (removed a committed fnm_multishells runtime PATH, stray absolute home-dir paths, duplicate git aliases).
  • pi lives at ~/.pi (repo .pi/); tmux at ~/.config/tmux/ (delete a legacy ~/.tmux.conf if 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.json are shared — run :Lazy restore per machine once (obeys the lockfile pins — :Lazy update on one machine + commit is how versions move forward; other machines converge with restore).
  • zellij is one shared config with vim keybinds (h/j/k/l) + the guvbox-dark theme.
  • misc/{clis,tuis}.txt launcher inventories use wm's lists in common; 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.
  • common/.config/aichat/dark.tmTheme is One Half Dark by Son A. Pham, MIT. The licence header at the top of that file is the attribution — don't strip it.
  • 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.
  • gui/.config/hypr/hyprlock/assets/ — the lock screen background and logo. EXIF is stripped, so the files carry no provenance; confirm these are yours to redistribute before publishing, or swap them for something you know the origin of.