# dots Unified dotfiles for all machines: one shared base + per-host overlays, deployed with [GNU Stow](https://www.gnu.org/software/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 → `; 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: 1. the explicit argument (`./install.sh wm`), 2. else the name in **`~/.config/dots-host`** — written by `install.sh` itself on every run that resolved a real package, so a one-time `./install.sh ` is all the setup there is, 3. 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 ` — 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) ```fish sudo pacman -S stow git clone ~/.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: ```fish cd ~/.dots git status # make sure the repo is clean first (see the restore caveat) ./install.sh -a # --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 `/` package (at least a `host.fish`), commit, then `./install.sh ` (naming it once records it — later runs and dotsync need no argument). 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 = 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 | `/.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](https://github.com/HyDE-Project/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 `, 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` — 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 `. - `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--bar.service` (auto-restarting) — `systemctl --user stop ; systemctl --user mask `. - older (pre-unit) HyDE: `exec-once = $start.BAR` in HyDE's generated `hyprland.conf` — the hyprlang-era dots blanked it with `$start.BAR=` in `gui/.config/hypr/hyde.conf`; that file went with the chain (LUA-PORT.md step 8), and under Lua the lever is `hc.start.bar` in `hyprland.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` / `.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](https://github.com/HyDE-Project/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.conf` from the current HyDE theme. `kitty.conf` adds 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. - **`gui/.local/share/icons/dots-icons/`** ships only its `index.theme`. Its tray/status icons are symlinks into each host's own *Tela-circle-dracula* (by vinceliuice, installed by HyDE), made at install time by `bin/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.png` is 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 in `background.jpg` is an AI-altered rendition of the Open Source Initiative mark, used purely decoratively; no affiliation with or endorsement by the OSI is implied.