Personal config for Ghostty + fish + tmux + vim — multi-theme (Solarized Dark / Mocha / Frappé / Dracula / Gruvbox / Tokyo Night Storm / Nord / Catppuccin Latte / Rose Pine / Rose Pine Moon via theme-set) and multi-font (17 Nerd Fonts via font-set). Solarized Dark and JetBrains Mono are the defaults.
Solarized-themed quick references — also browseable at martinciu.github.io/dotfiles:
- Terminal — eza, bat, less wrapper, git-delta, difftastic, glow, vivid, xh, duf, dust, dua, fzf
- tmux — prefix
C-amap, sessions/windows/panes, tmux-sessionx picker, status bar, copy mode - Neovim — LazyVim leader map, picker, LSP, neotest, Mason/Lazy
Detailed conventions and reasoning live in CLAUDE.md. This section is the
operational checklist.
- Install Homebrew (https://brew.sh).
- Export
PROJECTS_HOME(e.g.export PROJECTS_HOME="$HOME/code") and clone this repo to$PROJECTS_HOME/dotfiles. - Install brew packages:
brew bundle --file=$PROJECTS_HOME/dotfiles/Brewfile. - Run the symlinker:
$PROJECTS_HOME/dotfiles/bootstrap.sh(idempotent; safe to re-run). Also installs uv-managed CPython 3.13 as the globalpython3(~/.local/bin/python3); Apple's stays at/usr/bin/python3for system use. - Apply the manual extras below —
bootstrap.shcannot automate these. - Open tmux and press
<prefix> I(capital I, prefix =C-a) to install TPM plugins.
1. Login shell — fish. bootstrap.sh installs the fish config and copies
15-local.fish / 99-secrets.fish from templates, but it does not change
your login shell. Enable manually:
# One-time: register fish as a valid login shell
echo /opt/homebrew/bin/fish | sudo tee -a /etc/shells
# Edit the per-machine fish overlays:
$EDITOR ~/.config/fish/conf.d/15-local.fish # PROJECTS_HOME, PATH overrides
$EDITOR ~/.config/fish/conf.d/99-secrets.fish # API keys etc.
# Switch login shell to fish
chsh -s /opt/homebrew/bin/fishAfter chsh, open a new Ghostty tab (or exec /opt/homebrew/bin/fish to
hot-swap an existing pane).
2. ~/.config/sesh/sesh.local.toml — bootstrap.sh copies the template
on first run. Edit it to add machine-local project sessions; the shared
sesh.toml is the wrong place for them.
3. Wire delta + shared aliases into git (one-time, global). The delta
include picks up theme + chip tweaks from the active theme (see "Switching
themes" below — theme-set flips the included file). The aliases include
pulls in the tracked, portable [alias] block (e.g. git lo); bootstrap.sh
symlinks the file into ~/.config/git/, and this line wires it into git.
git config --global core.pager delta
git config --global interactive.diffFilter "delta --color-only"
git config --global delta.navigate true
git config --global delta.line-numbers true
git config --global include.path "~/.config/themes/delta-current.gitconfig"
git config --global --add include.path "~/.config/git/aliases.gitconfig"4. atuin import fish — one-time backfill of existing fish history
into the atuin sqlite store. Run after brew bundle (which installs
atuin) and after bootstrap.sh (which symlinks ~/.config/atuin/):
atuin import fishSafe to re-run (idempotent for already-imported rows). After this,
Ctrl-R opens atuin's picker (config at .config/atuin/config.toml).
Up stays on fish's native history-search.
| Tool | Source path | Target |
|---|---|---|
| Ghostty | .config/ghostty/ |
~/.config/ghostty |
| tmux | .config/tmux/ |
~/.config/tmux |
| nvim | .config/nvim/ |
~/.config/nvim |
| vim | .vimrc, .vim/colors/ |
~/.vimrc, ~/.vim/colors |
| fish | .config/fish/ |
~/.config/fish/ |
| sesh | .config/sesh/sesh.toml |
~/.config/sesh/sesh.toml |
| worktrunk | .config/worktrunk/ |
~/.config/worktrunk |
| glow | .config/glow/ |
~/.config/glow |
| tailspin | .config/tailspin/ |
~/.config/tailspin |
| lnav | .config/lnav/{configs,formats}/installed/ |
~/.config/lnav/{configs,formats}/installed |
| btop | .config/btop/btop.conf.template |
~/.config/btop/btop.conf (seeded) |
| procs | .config/procs/ |
~/.config/procs |
| xh | .config/xh/ |
~/.config/xh |
| ccstatusline | .config/ccstatusline/ |
~/.config/ccstatusline |
| Claude | .claude/CLAUDE.md |
~/.claude/CLAUDE.md |
| user bin | bin/* (e.g. s) |
~/.local/bin/* |
Ten themes are wired: Solarized Dark (default), Catppuccin Mocha, Catppuccin Frappé, Dracula, Gruvbox Dark Medium, Tokyo Night Storm, Nord, Catppuccin Latte (the only light theme), Rose Pine, and Rose Pine Moon.
Swap via the fish function theme-set:
theme-set mocha # switch to Catppuccin Mocha
theme-set frappe # switch to Catppuccin Frappé (lifted bg, mauve accent)
theme-set dracula # switch to Dracula
theme-set gruvbox # switch to Gruvbox Dark Medium
theme-set tokyo-night # switch to Tokyo Night Storm
theme-set nord # switch to Nord (Frost-teal pastel prompt)
theme-set latte # switch to Catppuccin Latte (light, partial coverage)
theme-set rose-pine # switch to Rose Pine Main (light-on-accent, rose prompt)
theme-set rose-pine-moon # switch to Rose Pine Moon (2.5-contrast accessibility variant)
theme-set solarized # switch backLatte caveat: Catppuccin Latte is the only light theme and ships with partial tier-1 coverage — only Ghostty, tmux, and starship have Latte variants. delta, glow, gh-dash, lnav, and nvim keep their previous (dark) theme during a Latte session. Issue #215 tracks the full-coverage follow-up.
The function flips per-tool symlinks under ~/.config/themes/,
~/.config/ghostty/theme.ghostty, ~/.config/starship.toml,
~/.config/glow/glamour.json, ~/.config/gh-dash/config.yml,
~/.config/lnav/configs/installed/theme.json, plus a
delta-current.gitconfig snippet included via the gitconfig
include.path directive set up during Setup above. It also sets
$BAT_THEME and $VIVID_THEME as fish universal variables (read by
bat and vivid generate at shell start, respectively). fzf colors are
palette-symbolic in FZF_DEFAULT_OPTS and auto-adapt via Ghostty's
16-color palette — no env var needed.
Reloads live: tmux (status bar + helpers, instant), ghostty,
starship (next prompt render), glow, delta (next git diff).
Needs restart: open shells ($BAT_THEME and $VIVID_THEME are
read at fish startup; affects bat + ls/eza file colors), nvim,
gh-dash, lnav.
Add another theme: drop a new .config/themes/<name>.tmux palette
file (mirror the role keys from the existing palettes) plus per-tool
*-<name>.<ext> variant configs, then extend the switch statement
in .config/fish/functions/theme-set.fish.
Smoke test: scripts/test-theme-switch.sh.
Run CLI/TUI tools you don't fully trust in an isolated Linux container that shares your portable dotfiles env (fish, nvim, bat, eza, fzf, wt, …):
sandbox <name> # create/reattach — drops into fish
sandbox <name> cmd ... # run a one-shot command
sandbox --rm fish # throwaway ephemeral shellTwo modes:
| Mode | Command | Isolation |
|---|---|---|
| 🔒 Container (safe) | sandbox <name> |
No host mounts. Named volume holds state. |
sandbox machine create <name> |
Mounts your Mac home. TRUSTED CODE ONLY. |
🎨 The sandbox inherits your current Mac theme at entry — the named-theme
tools (bat, delta, glow, vivid, lnav, nvim, starship) match the host; switch
with theme-set on the Mac and re-enter, no rebuild needed.
Lifecycle verbs:
sandbox build # (re)build the image
sandbox ls # list all sandboxes
sandbox stop <name> # stop (keep volume/state)
sandbox rm <name> # remove container + volume
sandbox reup <name> # recreate container with new flags (keep volume/state)
sandbox machine create <name> # OrbStack machine — Mac home mounted
sandbox machine ssh <name> # SSH into machine (fish shell)
sandbox machine ls # list OrbStack machines
sandbox machine rm <name> # delete machineContainer names round-trip: sandbox ls shows the bare <name>, which is what
stop/rm/reup take (a pasted sandbox-<name> is accepted too).
Flags (for sandbox <name> and sandbox reup <name>):
--rm ephemeral throwaway (no named volume)
-p PORT publish PORT to 127.0.0.1 only (LAN off)
--mount DIR bind-mount ONE dir at /home/dev/mnt
--env-file FILE inject env vars (e.g. ~/sandbox/<name>/.env)
-e KEY=VAL inject a single env var (repeatable)
--memory V memory limit (default 2g)
--cpus V CPU limit (default 2)
Flags only take effect when a container is created. To change them on an
existing sandbox without losing /home/dev state, use sandbox reup <name> <flags> — it docker rm -fs the container only, then recreates it re-attached
to the same volume. Passing flags to a bare sandbox <name> whose container
already exists prints a warning pointing you at reup (the flags are otherwise
silently ignored).
Multi-shell: open N Mac-tmux panes and run sandbox <name> in each —
they all docker exec into the same persistent container.
Per-project runtimes: the image bakes node (lts) and go (latest)
alongside the dotfiles tool set, but a project that pins a specific runtime
version in its own .mise.toml needs mise install to fetch it — the baked
latest/lts won't satisfy an exact pin. Inside a sandbox, clone a repo and
let its .mise.toml drive the toolchain:
git clone <repo> && cd <repo>
mise trust && mise install # fetches the version this project pins
make # that version is now on $PATHIf a freshly installed tool isn't found yet (e.g. make: go: No such file or directory because the project pins a version other than the baked one),
cd . re-fires mise's dir hook to load it.
Secret injection: use --env-file ~/sandbox/<name>/.env (or -e KEY=VAL)
to pass secrets at runtime. Never baked into the image.
Named-volume config-snapshot caveat: a persistent sandbox keeps the config
snapshot from when it was created. To pick up a rebuilt image:
sandbox rm <name> && sandbox <name>.
Building the image: sandbox build (or auto-triggered by sandbox <name>
when sources change). The build fetches pre-built binaries from GitHub releases,
so it needs a token to dodge the API rate limit — but it auto-uses
gh auth token when GITHUB_TOKEN isn't already set, so a gh auth login is
all you need. (sandbox machine create pulls the token from gh the same way.)
Smoke test: scripts/test-sandbox.sh.
- tmux prefix:
C-a - session switcher (tmux-sessionx):
<prefix> t(clock-mode moved to<prefix> T) - pane nav:
<prefix> h/j/k/l(prefix by choice; left-Option in Ghostty acts as Alt, right-Option still types Polish) - splits:
<prefix> |(right) /<prefix> -(down) - window cycling:
<prefix> ,(prev) /<prefix> .(next) — repeatable. Defaultsn/pandtmux-sensible'sC-p/C-nare unbound; this is the only cycle pair. - session cycling:
<prefix> Tab(next) /<prefix> S-Tab(prev) /<prefix> Space(last) — repeatable. Defaults(/)/Lunbound. - window reorder:
<prefix> <(left) /<prefix> >(right) — repeatable, focus follows the moved window. - reload tmux:
<prefix> r - TPM plugin install:
<prefix> I(capital I) - worktree+session command (any shell):
s [<project>] [<name>]— inside tmux 1 arg = worktree name in current project; outside tmux 1 arg = project name (attach), 0 args = fzf picker. Branch name verbatim — theworktree-prefix is reserved for theEnterWorktreeworkflow.
<project> · <git/worktree>
- Project chip (violet) is the top-level dir under
$PROJECTS_HOME. - Git chip is cyan in main checkout, yellow in a worktree.
Worktree label
wt:NAMEonly shows when branch name differs from worktree dir name.
-
URLs in tmux panes open with Shift+Cmd+click, not Cmd+click.
-
Why: with
set -g mouse on, Ghostty defers all mouse interactions (incl. URL hover/click detection) to tmux. Ghostty's defaultmouse-shift-capture = falsemakes Shift the bypass modifier — Shift releases the click from tmux and Cmd reaches Ghostty's URL handler. -
File-reference links printed by Claude Code (OSC 8 hyperlinks to
file:///abs/path) open with Shift+Cmd+click — same modifier as URLs. Routing to VS Code (or whichever editor) happens via the existing macOS file-type defaults (Finder → Get Info → "Open with" → "Change All"); no extra config in this repo. -
Smoke-test that the chain works end-to-end:
printf '\e]8;;file://%s/.config/fish/config.fish\e\\config.fish\e]8;;\e\\\n' "$HOME"
Shift+Cmd+click the rendered "
config.fish" — your default editor for that file type should open it.