Files
dotfiles/.config/dot/CLAUDE.md
2026-07-04 13:50:00 -04:00

6.4 KiB

Dotfiles

This machine's dotfiles are a bare git repo at ~/.dotfiles, checked out with $HOME as its work-tree. The dot fish function wraps that invocation (git --git-dir=~/.dotfiles --work-tree=$HOME $argv, declared with --wraps=git), so every git subcommand works through it: dot status, dot add, dot commit, dot push, etc.

This directory (~/.config/dot) holds the dot CLI's custom subcommands, tests, and package lists, but the repo tracks files across $HOME — fish config, git identity, the dot function itself, and more. To see everything tracked, run dot ls-tree -r --name-only HEAD from $HOME (paths are shown relative to cwd, so running it from elsewhere silently truncates the list).

Always add by explicit path

status.showUntrackedFiles=no is set locally (see dot init below), and .gitignore only excludes .dotfiles itself plus OS/editor cruft — it is not a whitelist. That means virtually everything under $HOME reads as untracked, and git status deliberately hides all of it.

Always run dot add <specific-path>. Never dot add -A, dot add ., or any wildcard add — that would try to stage the entire home directory (caches, secrets, everything).

Stage automatically after changes. Once a tracked file is edited, run dot add <specific-path> for it right away rather than waiting to be asked — one explicit path per changed file, still never a wildcard. This does not extend to dot commit or dot push, which still require an explicit request.

The dot CLI

Architecture

dot is defined in one file: ~/.config/fish/functions/dot.fish. It holds three functions:

  • dot (--wraps=git) — dispatches init, help, and any file found under ~/.config/dot/commands/, otherwise forwards everything to git --git-dir=~/.dotfiles --work-tree=$HOME $argv (full passthrough).
  • __dot_init — the bootstrap logic, inlined in the same file rather than autoloaded separately, because it's the one subcommand that must work before the dotfiles repo has ever been cloned onto a machine.
  • __dot_help — prints usage: the built-in commands plus whatever is currently found under ~/.config/dot/commands/, generated by globbing that directory rather than a hardcoded list, so it can't drift from reality.

__dot_help's glob over ~/.config/dot/commands/*.fish is duplicated in ~/.config/fish/completions/dot.fish's __dot_custom_subcommands rather than shared: fish only autoloads a function from a file named after that function, so a helper defined inside dot.fish would be undefined if tab-completion ran before dot had ever been sourced in the session. Keep both copies in sync when the listing logic changes.

dot init:

  • refuses to run if ~/.dotfiles already exists (no re-init support)
  • clones the bare repo from --url (default: the hardcoded Gitea remote) — if the clone fails, it errors out; it never falls back to git init
  • backs up any pre-existing file that checkout would clobber into ~/.dotfiles-backup/<timestamp>/, then retries the checkout
  • explicitly sets status.showUntrackedFiles=no after cloning — this is a local-only git setting, so a fresh git clone never carries it over

Adding a subcommand

Beyond init, dot looks for ~/.config/dot/commands/<name>.fish, sources it, and calls _dot_<name>. These files are deliberately kept out of ~/.config/fish/functions/ (fish's autoload path) so they never become independently invokable top-level commands or clutter tab-completion outside of dot itself.

  1. Create ~/.config/dot/commands/<name>.fish defining a _dot_<name> function.
  2. Confirm dot <name> dispatches to it. No other wiring is needed — ~/.config/fish/completions/dot.fish and __dot_help both discover new command files by globbing that directory, and --wraps=git still covers raw git subcommands.
  3. Implement a help subcommand: check for help as _dot_<name>'s first positional argument before argparse, and call a _dot_<name>_usage function that prints usage and every flag. If _dot_<name> itself dispatches to nested subcommands, apply this same check-then-dispatch pattern at that level too — there's no central --help handling in dot.fish to lean on; each level is responsible for its own. _dot_<name>_usage should print its text as a single multi-line echo "..." string (fish preserves literal newlines inside double quotes) rather than one echo per line.
  4. Add a row to ~/.github/README.md's command table for it — one row per distinct use case, with paths written relative to $HOME (~/.config/dot/...), not relative to the README's own location.
  5. Add a case to ~/.config/dot/tests/dot.fish covering it, including its help output, and run fishtape ~/.config/dot/tests/dot.fish until it passes.

Testing

Tests live at ~/.config/dot/tests/dot.fish, run with fishtape ~/.config/dot/tests/dot.fish. Fishtape is installed via Fisher (fisher install jorgebucaran/fishtape) and tracked in ~/.config/fish/fish_plugins — a real, restorable dependency for developing dot, but never required just to use it.

  • Each scenario overrides $HOME (set -gx HOME (mktemp -d)) before calling dot, so tests never touch the real ~/.dotfiles.
  • Build a throwaway bare "remote" fixture with git init --bare plus a seeded commit, and explicitly set its HEAD (git --git-dir=$remote symbolic-ref HEAD refs/heads/main). Pushing with git push origin HEAD:main does not update the bare repo's HEAD symref — skip this and a clone of the fixture can end up "on a branch yet to be born."
  • Don't use .gitconfig as a fake pre-existing "conflict" file in a fixture — git parses $HOME/.gitconfig as its own global config on every invocation, and garbage content there spams "key does not contain a section" errors that drown out the real assertion. Use a harmless file like .bashrc instead.
  • @test "description" <expr> <op> <expected> mirrors fish's test builtin (-eq, -ne, =, -e, -f, -d, -n, -z); -a/-o combinators aren't supported.

Keybindings

Whenever a keybind is added, changed, or removed in any config on this machine (tmux, KDE, neovim, fish, whatever), add or update its row in ~/.github/keybindings.md in the same change. That file is the single reference for every keybind across tools — it drifts the moment a bind changes somewhere without a matching edit there.