Lists every apt package, cargo tool, and install-script binary with its upstream, grouped by how it arrives — the three build deps are separated out since they exist only to compile the cargo tools and nothing uses them at runtime, and rustup is placed with the tools that force it rather than listed as a peer. Records the reasons that are invisible from the package list: bat's binary is batcat on Ubuntu so the cat alias depends on it, jq backs the Claude statusline, and rustup uses --no-modify-path because conf.d/rustup.fish already sources cargo's env. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
229 lines
10 KiB
Markdown
229 lines
10 KiB
Markdown
# 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 <user>` and `atuin sync`.
|
||
2. **git identity** — not set globally, only `credential.helper=store`:
|
||
```bash
|
||
git config --global user.name '<name>'
|
||
git config --global user.email '<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.
|