# dotfiles Shell + tooling setup for a fresh Ubuntu box: **fish · starship · atuin · eza · macchina**, plus the Claude Code statusline. Two layers, deliberately split: | Layer | Owns | Tool | |---|---|---| | Provisioning | apt packages, rust toolchain, starship, default shell | `setup.sh` (root) | | Dotfile state | everything under `$HOME` | chezmoi (user) | `setup.sh` does the root work and then hands off to chezmoi. Nothing that needs `sudo` lives inside chezmoi, and nothing under `$HOME` is touched by the script. ## Fresh box ```bash git clone https://git.domverse-berlin.eu/domverse/dotfiles.git cd dotfiles sudo bash setup.sh ``` Run it as your normal user via `sudo` — the script reads `SUDO_USER` to know whose home to set up, and refuses to run as root directly. Expect **10–20 minutes**: atuin, eza, macchina and oxker are built from source with `cargo install --locked`, which needs the rust toolchain. That's the deliberate trade for matching the currently-running versions exactly rather than taking whatever apt ships. ## What gets installed Everything `setup.sh` puts on the box, and where it comes from. ### apt packages | Package | What for | Upstream | |---|---|---| | `fish` | the shell itself; `setup.sh` `chsh`es to it | [fish-shell/fish-shell](https://github.com/fish-shell/fish-shell) | | `bat` | binary is **`batcat`** on Ubuntu; `config.fish` aliases `cat` to it | [sharkdp/bat](https://github.com/sharkdp/bat) | | `ripgrep` | `rg` | [BurntSushi/ripgrep](https://github.com/BurntSushi/ripgrep) | | `jq` | required by `statusline-command.sh` | [jqlang/jq](https://github.com/jqlang/jq) | | `git` | chezmoi clones the source repo with it | [git/git](https://github.com/git/git) | | `curl` | fetches the rustup / starship / chezmoi installers | [curl/curl](https://github.com/curl/curl) | | `nano` | `$EDITOR`; the `nanox` alias forces `TERM=xterm-256color` under Ghostty | [nano-editor.org](https://www.nano-editor.org/) | | `build-essential` | cc/linker — cargo builds need it | [GNU GCC](https://gcc.gnu.org/) | | `pkg-config` | build dep for the cargo tools | [pkg-config](https://gitlab.freedesktop.org/pkg-config/pkg-config) | | `libssl-dev` | OpenSSL headers — atuin links against them | [openssl/openssl](https://github.com/openssl/openssl) | The last three exist only to compile the cargo tools below. Nothing uses them at runtime. ### Built from source with `cargo install --locked` | Tool | What for | Upstream | |---|---|---| | `atuin` | shell history w/ sync + encryption | [atuinsh/atuin](https://github.com/atuinsh/atuin) | | `eza` | `ls` replacement; backs the `l` / `ll` / `lt` / `la` aliases | [eza-community/eza](https://github.com/eza-community/eza) | | `macchina` | the banner `config.fish` prints on interactive start | [Macchina-CLI/macchina](https://github.com/Macchina-CLI/macchina) | | `oxker` | terminal Docker container UI | [mrjackwills/oxker](https://github.com/mrjackwills/oxker) | This is the slow part — **10–20 minutes cold**. Built from source deliberately, to match the versions running on the origin box rather than whatever apt ships. That makes the rust toolchain a mandatory build dependency: | Tool | What for | Upstream | |---|---|---| | `rustup` + cargo | toolchain for the four tools above | [rust-lang/rustup](https://github.com/rust-lang/rustup) | Installed via `sh.rustup.rs` with `--no-modify-path` — `conf.d/rustup.fish` already sources `~/.cargo/env.fish`, and the installer would otherwise add a second PATH edit. Lands in `~/.cargo`, owned by the user, not root. ### Installed via upstream install scripts | Tool | Where it lands | Upstream | |---|---|---| | `starship` | `/usr/local/bin` (root) | [starship/starship](https://github.com/starship/starship) | | `chezmoi` | `~/.local/bin` (user) | [twpayne/chezmoi](https://github.com/twpayne/chezmoi) | Both are `curl … | sh` from the projects' own domains (`starship.rs`, `get.chezmoi.io`). ### Not installed here `fnm` (node) and `dtop`/opencode are out of scope — their own installers write `conf.d/fnm.fish` and `conf.d/dtop.env.fish`, which are `.chezmoiignore`d. `config.fish` adds `~/.opencode/bin` to `PATH` only if that directory already exists. ## chezmoi on another VPS `setup.sh` already does all of this. Use this path only when you want the dotfiles *without* the provisioning layer (no apt, no rust, no `chsh`) — e.g. a box where the tools are already there, or where you don't have root. ```bash # 1. install chezmoi (user-local, no root) sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" export PATH="$HOME/.local/bin:$PATH" # 2. pin the umask BEFORE the first apply — see Notes, this is load-bearing mkdir -p ~/.config/chezmoi printf 'umask = 0o022\n' >> ~/.config/chezmoi/chezmoi.toml # 3. pull the source state (private repo — needs Gitea credentials) chezmoi init https://git.domverse-berlin.eu/domverse/dotfiles.git # 4. look before you leap chezmoi diff # 5. write it out chezmoi apply ``` Notes on the above: - **Step 2 must come before step 4.** The umask is read at apply time; applying first and fixing the config after leaves wrong modes on disk. - **Step 3 needs repo access.** The repo is private. Either have `credential.helper=store` already populated, or clone over SSH: `chezmoi init git@git.domverse-berlin.eu:domverse/dotfiles.git`. - The source clone lands in `~/.local/share/chezmoi` (chezmoi's default). This box is the exception — it sets `sourceDir = /home/crabix/projects/dotfiles` in `~/.config/chezmoi/chezmoi.toml` because the repo predates chezmoi. On a fresh box, don't set `sourceDir`; the default is what `setup.sh` expects. - `.chezmoiroot` contains `home`, so the source root is `home/`, not the repo root. `setup.sh` and `README.md` sit outside chezmoi's view by design. - If `~` already has real dotfiles, `chezmoi diff` in step 4 shows exactly what gets clobbered. Back them up with modes intact first: `cp -a ~/.config/fish ~/.config/atuin ~/backup/dotfiles-preapply-$(date -u +%Y%m%dT%H%M%SZ)/` - Then do the **Manual steps** below (atuin key, git identity) — they are not specific to `setup.sh`. ## Day-to-day Configs are managed by chezmoi, so the file in `~/.config` is a *copy*, not a symlink. Edit through chezmoi or your change gets overwritten on the next apply: ```bash chezmoi edit ~/.config/fish/config.fish # edit source chezmoi diff # what drifted on this box? chezmoi apply # write source -> $HOME chezmoi cd # jump to the source repo, then git push ``` The `fishconfig` alias is wired to `chezmoi edit` for this reason. ## What's tracked ``` home/ dot_bashrc ~/.bashrc (stock + atuin init) dot_gitconfig ~/.gitconfig dot_config/fish/config.fish.tmpl aliases, eza, init lines dot_config/fish/conf.d/rustup.fish sources ~/.cargo/env.fish dot_config/atuin/config.toml dot_config/starship.toml stock-equivalent, pinned dot_claude/executable_statusline-command.sh statusline (needs jq) ``` ## What's deliberately NOT tracked See `home/.chezmoiignore`. The load-bearing exclusions: - **`~/.local/share/atuin/key`** — the sync encryption key. Tracked by hand in `secrets.yml`. Restore it *before* the first `atuin sync`, or history encrypted on other hosts will not decrypt. - **`~/.git-credentials`** — plaintext tokens (`.gitconfig` sets `helper = store`). - **`~/.claude/settings.json`** — pins the model and sets `skipDangerousModePermissionPrompt`; a per-box decision, set by hand. - **`conf.d/fnm.fish`, `conf.d/dtop.env.fish`** — node/opencode toolchains, out of scope. Their installers write these themselves. - **`fish_variables`** — machine-local state, fish rewrites it. ## Manual steps after setup.sh The script prints these; repeated here because they're easy to miss: 1. **atuin** — restore key from `secrets.yml` into `~/.local/share/atuin/key`, then `atuin login -u ` and `atuin sync`. 2. **git identity** — not set globally, only `credential.helper=store`: ```bash git config --global user.name '' git config --global user.email '' ``` 3. **Log out and back in** for the fish shell change to take effect. ## chezmoi gotchas Learned the hard way while building this. Don't rediscover them. - **chezmoi does not preserve file modes.** It tracks only the `executable_` and `private_` source attributes and derives the actual mode from the *umask at apply time*. With `umask 002` (the default for this user), `~/.config/atuin/` silently goes `0700` → `0775` and `config.toml` `0600` → `0664`. Correct modes need **both** halves: - the source attribute — `private_atuin/private_config.toml`, and - `umask = 0o022` in `~/.config/chezmoi/chezmoi.toml` (`setup.sh` writes this). Always `chezmoi diff` before trusting a dotfiles repo on a new box; the mode lines are quiet and easy to scroll past. - **Never pipe `chezmoi apply` into a truncating command.** `chezmoi apply --verbose | head -20` SIGPIPEs chezmoi partway through, leaving the apply **incomplete** — files sorting after the cutoff are never written. Worse, `$?` reports *head's* exit status (0), so it looks like it succeeded. Redirect to a file and read that instead: ```bash chezmoi apply --verbose > /tmp/apply.log 2>&1; echo "exit=$?" ``` - **The file in `~` is a copy, not a symlink.** Editing `~/.config/fish/config.fish` directly gets silently clobbered on the next apply. Use `chezmoi edit`. The `fishconfig` alias points at `chezmoi edit` for exactly this reason. - **`chezmoi init` on a box with existing dotfiles overwrites without asking** once you `apply`. `chezmoi diff` first, always. ## Notes - `config.fish` aliases `cat` to `batcat` — on Ubuntu the bat binary really is `batcat`, so the apt `bat` package is a hard dependency of the fish config. - rustup is installed with `--no-modify-path`: `conf.d/rustup.fish` already sources `~/.cargo/env.fish`, and the installer would otherwise add a second PATH edit. - `starship.toml` is currently equivalent to stock defaults. It exists so the prompt is pinned to a known config rather than tracking version defaults.