Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

dotfiles

Personal dotfiles, managed with GNU Stow. Each chapter in the sidebar covers one stow package — what the tool is, what’s been changed from the defaults, and the everyday commands worth knowing.

How this book is built

The book is generated from the per-package READMEs in the repo. Every chapter is a thin wrapper that pulls in the matching README.md via mdBook’s {{#include}} directive — so the GitHub repo and this site never drift.

If you want install instructions (Debian / macOS), the modern-CLI tour (zoxide, eza, dust, hyperfine, etc.), or the full subdirectory table, those live in the repo README on GitHub.

Conventions

  • Every top-level directory in the repo is a stow package mirroring the layout it should have under $HOME. For example, zsh/.zshrc is symlinked to ~/.zshrc by running stow zsh from the repo root.
  • The same bootstrap-debian.sh script handles a fresh Debian/Ubuntu box; brew bundle covers macOS.
  • Repo-wide conventions for editing live in CLAUDE.md.

Quick install

git clone https://github.com/jacob-delgado/dotfiles ~/dotfiles
cd ~/dotfiles
# Debian / Ubuntu
./bootstrap-debian.sh
# macOS
brew bundle && stow zsh vim tmux fzf   # pick whichever packages you want

atuin

Stow package for ~/.config/atuin/config.toml. Atuin is a SQLite-backed shell history with full-text search, per-directory context, and optional E2E-encrypted sync.

Table of contents

Layout

FileStows to
atuin/.config/atuin/config.toml~/.config/atuin/config.toml

Active settings

enter_accept = true       # Enter runs the selected line immediately
search_mode  = "fuzzy"    # fuzzy match (explicit; matches modern default)
filter_mode  = "global"   # Ctrl+R searches across all sessions/hosts
workspaces   = true       # treat git repos as a scope for filter_mode = "workspace"
style        = "compact"  # denser TUI layout

[sync]
records = true            # newer record-based sync protocol

The rest of the file is atuin’s commented default template, kept around as a cheat-sheet of tunable options.

enter_accept = true gotcha: the default behavior is Enter inserts the line for editing, Tab runs it. This config flips them so Enter runs the selection immediately — faster but easier to misfire on a dangerous command. Flip back if that bites you.

Why the file is so long

When atuin first generates config.toml it writes the entire schema with every option commented out. We keep the full file rather than trimming to the two active lines so the comments stay around as a cheat-sheet for what’s tunable.

Regenerating

If you change settings via the atuin TUI (some are configurable there), atuin rewrites this file. Diff it against the tracked version:

git -C ~/dotfiles diff atuin/.config/atuin/config.toml
git -C ~/dotfiles commit -am "Update atuin config"

If atuin’s config schema gains new fields in a future release, you may see a flood of diff noise from the new commented entries — review and commit if you want the updated template.

Fresh-machine setup

brew install atuin          # in the Brewfile
stow atuin
atuin import auto           # one-time: backfill existing zsh history into atuin's DB

Run atuin register if you want sync across machines; otherwise everything stays local.

zsh/.zshrc initializes atuin via eval "$(atuin init zsh)".

bat

Stow package for ~/.config/bat/config. bat is a cat clone with syntax highlighting, line numbers, and git change markers.

Table of contents

Layout

FileStows to
bat/.config/bat/config~/.config/bat/config

Settings

--theme="ansi"                                          # terminal palette (themed by p10k)
--style="numbers,changes,header,grid,header-filesize"   # line nums, git markers, file header + size, gutter grid
--pager="less -RFX"                                     # raw colors, quit-if-one-screen, no alt-screen

--theme="ansi" is intentional — it picks up whatever color scheme the terminal/Dracula gives us instead of locking to a fixed bat theme. Run bat --list-themes to see alternatives.

BAT_THEME=ansi is also set in zsh/.zshrc as a belt-and-suspenders default for tools that invoke bat via env (e.g., MANPAGER).

--pager="less -RFX" matches LESS='-RFX' in zsh/.zshrc so the pager behaves identically whether bat invokes it or LESS is exported.

Syntax mappings

bat recognizes most filenames by extension/shebang, but a few common files need help:

--map-syntax="Dockerfile.*:Dockerfile"   # e.g. Dockerfile.dev
--map-syntax="*.jenkinsfile:Groovy"
--map-syntax="*.tfvars:HCL"              # Terraform variable files
--map-syntax=".envrc:Bash"               # direnv files
--map-syntax=".ignore:Git Ignore"        # ripgrep / fd ignore files

Add more as new conventions show up.

Regenerating the default template

bat ships a fully commented default config:

bat --generate-config-file        # writes ~/.config/bat/config if missing

If you want to start over with the full template, delete the symlink and run the above, then move the new file into bat/.config/bat/config and re-stow.

Fresh-machine setup

brew install bat        # in the Brewfile
stow bat

btop

Stow package for ~/.config/btop/btop.conf. btop is a process / CPU / memory / network monitor.

Table of contents

Layout

FileStows to
btop/.config/btop/btop.conf~/.config/btop/btop.conf

Settings

color_theme      = "dracula"   # matches git/, lazygit/, overall palette
theme_background = False        # transparent — inherit terminal bg
truecolor        = True         # 24-bit color (enabled by tmux too)
graph_symbol     = "braille"    # denser/prettier than block glyphs
vim_keys         = True         # hjkl navigation

Cycle themes inside btop with p (uppercase P reverses). Press ? for full keybindings. Other built-in themes worth trying: gruvbox_dark, gruvbox_dark_v2, gruvbox_material_dark, nord, tokyo-night, tokyo-storm.

Churn warning

btop rewrites this file every time you change a setting via its TUI (theme cycling, sort order, etc.). Diff before committing:

git -C ~/dotfiles diff btop/.config/btop/btop.conf

Fresh-machine setup

brew install btop      # in the Brewfile
stow btop

direnv

Stow package for ~/.config/direnv/direnvrc. Per-user direnv setup — .envrc files in your projects can call any of the helpers defined here.

Table of contents

Layout

FileStows to
direnv/.config/direnv/direnvrc~/.config/direnv/direnvrc

What direnv is

direnv hooks into your shell so that whenever you cd into a directory containing .envrc, it loads that file as environment for the directory (and unloads on leaving). Activated via the OMZ direnv plugin in zsh/.zshrc. Trust new .envrc files explicitly with direnv allow.

Helpers defined here

HelperUsage in .envrcWhat
layout python_venv [python]layout python_venv python3.12create/activate ./.venv/; pin python binary
layout uvlayout uvactivate ./.venv/ created by uv venv (faster pip-replacement)
use go <version>use go 1.21use Homebrew’s go@1.21 (run brew install go@1.21 first)
use node <version>use node 20use Homebrew’s node@20
op_env <item>op_env "GitHub PAT"inject env vars from a 1Password item (needs op signed in)
use_flake / use_nix(provided by nix-direnv when sourced)activate a nix flake / shell

Example .envrc files

Python project with uv:

layout uv
export PYTHONDONTWRITEBYTECODE=1

Go service with a version pin:

use go 1.22
export CGO_ENABLED=0
PATH_add bin

Project that needs secrets from 1Password:

op_env "production-postgres"   # injects DATABASE_URL, etc.

Then direnv allow once and the env loads automatically when you cd in.

nix-direnv integration

If nix-direnv is installed (Mac: brew install nix-direnv; Linux: distro package or via nix itself), the direnvrc sources it automatically — use_flake / use_nix become available to .envrc files. Without nix-direnv installed, the source-attempt is silent.

Fresh-machine setup

brew install direnv          # in the Brewfile
stow direnv
# OMZ's direnv plugin (in zsh/.zshrc) hooks into the shell on startup.

editorconfig

Stow package for ~/.editorconfig. Vim picks it up via editorconfig/editorconfig-vim (in vim/.vimrc); most editors / IDEs support it natively.

Table of contents

Layout

FileStows to
editorconfig/.editorconfig~/.editorconfig

Defaults

[*]
charset                  = utf-8
end_of_line              = lf
indent_style             = space
indent_size              = 2
insert_final_newline     = true
trim_trailing_whitespace = true
max_line_length          = 100

Per-language overrides:

PatternIndentNotes
*.gotabGo convention
Makefile, *.mktabMake requires tabs
*.py4 spacesPEP 8
*.{rs,c,cpp,h,hpp,java,kt,scala}4 spaces
*.mdtrim_trailing_whitespace = false, no max_line_length

Per-project overrides

This file has root = true so editorconfig stops walking up the filesystem when it finds it. Any project’s own .editorconfig overrides this one — drop a more specific file in your project root when you need different rules.

Fresh-machine setup

stow editorconfig

Vim’s editorconfig-vim plugin (in vim/) reads this automatically. For other editors: VS Code, JetBrains, Sublime, Neovim, and most modern editors support .editorconfig natively or via a free plugin.

fzf

Stow package for ~/.fzf.zsh. Sourced automatically by the oh-my-zsh fzf plugin on shell startup.

Table of contents

Defaults

FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
FZF_TMUX=1

FZF_TMUX=1 makes fzf pop up in a tmux popup pane when invoked inside tmux — much nicer than taking over the whole terminal.

Colors (Dracula)

FZF_DEFAULT_OPTS also carries the official Dracula --color palette so the picker matches kitty/tmux/vim:

--color=fg:#f8f8f2,bg:#282a36,hl:#bd93f9
--color=fg+:#f8f8f2,bg+:#44475a,hl+:#bd93f9
--color=info:#ffb86c,prompt:#50fa7b,pointer:#ff79c6
--color=marker:#ff79c6,spinner:#ffb86c,header:#6272a4

Unlike bat/tig (which inherit the terminal’s ANSI palette), fzf’s colors are pinned to Dracula hex here.

fd integration

If fd is on PATH, replace fzf’s built-in walker with it. fd is faster than the default find invocation and respects .gitignore:

if command -v fd >/dev/null; then
  export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
  export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
  export FZF_ALT_C_COMMAND='fd --type d --hidden --follow --exclude .git'
fi

Guarded so machines without fd still get a working fzf.

Keybinding-specific options

OMZ’s fzf plugin binds:

KeysActionOptions set here
Ctrl+Tinsert a selected file path into the command lineFZF_CTRL_T_OPTS — bat preview, falling back to cat
Ctrl+Rhistory searchFZF_CTRL_R_OPTS — 3-line wrapped preview pane
Alt+Ccd to a selected directoryFZF_ALT_C_COMMAND — fd-based directory walker (above)
export FZF_CTRL_T_OPTS="--preview 'bat --color=always --style=numbers --line-range :500 {} 2>/dev/null || cat {}'"
export FZF_CTRL_R_OPTS="--preview 'echo {}' --preview-window down:3:wrap"

frg — live ripgrep + fzf

A function for project-wide content search. Type to refine; rg re-runs on every keystroke. Enter opens the chosen line in $EDITOR.

frg            # start with an empty query, type to search
frg "TODO"     # seed the query

Implementation:

frg() {
  local rg_prefix='rg --column --line-number --no-heading --color=always --smart-case'
  fzf --ansi --disabled --query "${*:-}" \
      --bind "start:reload:$rg_prefix {q} || true" \
      --bind "change:reload:sleep 0.1; $rg_prefix {q} || true" \
      --delimiter : \
      --preview 'bat --color=always --style=numbers --highlight-line {2} -- {1}' \
      --preview-window 'right,60%,border-left,+{2}+3/3,~3' \
      --bind "enter:become(${EDITOR:-vim} +{2} {1})"
}

Also bound to prefix + g in tmux — see tmux/README.md.

Fresh-machine setup

stow fzf       # symlinks ~/.fzf.zsh

The OMZ fzf plugin (in zsh/.zshrc’s plugins list) auto-sources the symlink. No manual source line needed.

gh

Stow package for ~/.config/gh/config.yml only. The GitHub CLI’s companion file hosts.yml is deliberately not tracked — it carries OAuth tokens.

Table of contents

Layout

FileStows toTracked?
gh/.config/gh/config.yml~/.config/gh/config.ymlyes
(~/.config/gh/hosts.yml)no (gitignored)

Active customization

prefer_editor_prompt: disabled  # prompt inline in the terminal, not in $EDITOR
pager: delta                    # syntax-highlight `gh pr diff` and similar
aliases:
    co: pr checkout
    prs: pr list --author "@me"
    prv: pr view --web
    prc: pr create --fill --web
    issues: issue list --assignee "@me"
    watch: run watch
    compare: '!gh repo view --web --branch "$(git branch --show-current)"'

Alias quick-ref:

AliasExpands toUse
gh co <#>gh pr checkout <#>check out a PR locally
gh prsgh pr list --author "@me"list my open PRs in this repo
gh prvgh pr view --webopen this branch’s PR in browser
gh prcgh pr create --fill --webnew PR from commits, open in browser
gh issuesgh issue list --assignee "@me"issues assigned to me
gh watch <run-id>gh run watch <run-id>tail a GH Action run
gh compareshells out — opens the GH compare view for the current branch

The leading ! on compare is a shell-out alias; everything after runs in a subshell.

Extensions

Installed via gh extension install; they live in ~/.local/share/gh/extensions/ (not in this repo). List with gh extension list, update with gh extension upgrade --all.

ExtensionCommandUse
dlvhdr/gh-dashgh dashTUI dashboard of PRs/issues across repos with filters
seachicken/gh-poigh poiprune local branches whose PRs have been merged
kyanny/gh-pr-draftgh pr-drafttoggle the draft state of a PR

Reinstall on a fresh machine:

gh extension install dlvhdr/gh-dash
gh extension install seachicken/gh-poi
gh extension install kyanny/gh-pr-draft

Why hosts.yml stays out

~/.config/gh/hosts.yml is where gh stores per-host auth state. On machines using token auth (Linux/CI) it contains the literal token; on macOS gh typically uses Keychain but the file still holds the user identity. Either way, it’s per-machine state, not config, so:

  • gh/.config/gh/hosts.yml is listed in the repo root .gitignore.
  • The file lives at ~/.config/gh/hosts.yml as a regular file (not a symlink) and stow gh leaves it alone.

If you ever see hosts.yml show up in git status, something has gone wrong — investigate before committing.

Schema churn warning

gh rewrites config.yml when new schema fields are added in a release (it adds new commented-out entries). Expect occasional noisy diffs after brew upgrade gh. Either commit the new schema or git checkout to keep the previous version.

Fresh-machine setup

brew install gh             # in the Brewfile
stow gh
gh auth login               # populates hosts.yml (which stays untracked)

git

Stow package for ~/.gitconfig.

Table of contents

Layout

FileStows to
git/.gitconfig~/.gitconfig

Local / private overrides

The tracked .gitconfig ends with:

[include]
  path = ~/.gitconfig.local

Anything machine-specific or sensitive (work email, signing keys, work remotes, credential helpers) lives in ~/.gitconfig.local, which is not stowed and not in the repo. Create it by hand per machine.

For example, gh auth setup-git writes a [credential] helper with a host-specific path (/opt/homebrew/bin/gh on macOS) — that belongs here, not in the tracked .gitconfig.

Delta integration

git-delta is wired in as the pager for git diff, show, log, blame:

[core]
  pager = delta
[interactive]
  diffFilter = delta --color-only
[delta]
  navigate = true        # n / N jumps between hunks
  side-by-side = true    # split view (drop for narrow terminals)
  line-numbers = true
[merge]
  conflictstyle = zdiff3 # better 3-way conflict markers; needs git ≥ 2.35

brew install git-delta provides the binary (already in the Brewfile).

Behavior tuning

[init]    defaultBranch = main
[pull]    rebase = true; ff = only             # rebase on pull, error on divergence
[push]    default = current; autoSetupRemote = true; followTags = true
[fetch]   prune = true                          # drop refs to deleted remote branches
[rebase]  autosquash = true; autoStash = true   # fixup! commits flow; uncommitted work stashes
[diff]    algorithm = histogram; colorMoved = default; renames = copies
[rerere]  enabled = true                        # remember conflict resolutions

These set sane defaults for git’s nagging warnings (divergent branches, missing upstream) and unlock features that aren’t on by default but should be (rerere, histogram diff, move-block detection).

Aliases

AliasExpands toUse
git ststatus -sbshort status with branch line
git cocheckout
git brbranch
git cmcommit
git cacommit --amend
git canecommit --amend --no-editamend without re-editing the message
git lgpretty-graph logone-line graph with colors and ago-style dates
git lastlog -1 HEAD --statwhat did the last commit touch?
git stageddiff --cachedreview what’s about to be committed
git unstagereset HEAD --undo git add
git undoreset --soft HEAD^un-commit (keeping changes)
git wipadd -A && commit -m 'WIP'snapshot to step away
git prune-branchesdelete every merged branch (except current / main / master)safe local cleanup
git cpcherry-pick
git sw / git swcswitch / switch -cmodern branch switch / create
git cobcheckout -bcreate + switch (older style)
git aa / git apadd -A / add -pstage all / interactively by hunk
git fafetch --all --prunerefresh all remotes, drop stale branches
git pfpush --force-with-leasesafe force-push (won’t clobber others’ work)
git rbirebase -i --autosquashinteractive rebase, auto-orders fixups
git rbc / git rbarebase --continue / --abort
git fixupcommit --fixup <sha>marks a fixup commit for rbi to squash
git recentlast 10 branches by commit date
git rootrev-parse --show-toplevelprint the repo root
git aliaseslist every configured alias

Dracula color scheme

The [color "*"] sections set a Dracula-inspired palette for branches, diffs, grep, interactive prompts, and status. Most slots are explicit; empty values mean “use the default.”

URL aliases

[url "https://github.com/dracula/"]
  insteadOf = dracula://

Lets you git clone dracula://vim instead of typing the full URL.

Companion tools

Installed via Brewfile, no extra config required:

ToolUse
git-deltapager (wired in above)
git-absorbgit absorb — auto-creates fixup! commits targeting the right earlier commit based on what lines your unstaged changes touch. Pair with git rebase -i --autosquash (which [rebase] autosquash = true runs by default).
git-branchlessadds porcelain inspired by Mercurial/Sapling: git smartlog, git move, git restack, git undo (cross-operation undo). Power-user; read git help branchless first.

Fresh-machine setup

stow git

Create ~/.gitconfig.local for any per-machine bits (work email, signing keys, etc.) that shouldn’t live in the repo. git-delta, git-absorb, and git-branchless come in via the Brewfile.

gitignore_global

Stow package for ~/.gitignore_global. Wired into git via [core] excludesfile = ~/.gitignore_global in git/.gitconfig.

Table of contents

Layout

FileStows to
gitignore_global/.gitignore_global~/.gitignore_global

What’s in it

OS, editor, and tool noise that shouldn’t appear in any project:

  • macOS: .DS_Store, ._*, .Spotlight-V100, .fseventsd, …
  • Linux: .directory, .Trash-*, .nfs*
  • Windows: Thumbs.db, Desktop.ini
  • Vim: *.swp, *~, .netrwhist
  • IDEs: .idea/, .vscode/, *.sublime-workspace, .history/
  • ctags / LSP caches: tags, .ccls-cache/
  • direnv: .direnv/
  • pre-commit / lefthook caches
  • Generic: *.bak, *.orig, *.rej

What’s not in it

Per-project artifacts (node_modules/, __pycache__/, dist/, target/, *.pyc, build dirs, etc.) stay in each project’s own .gitignore. A global ignore for those would mask bugs in repos that should be excluding them deliberately.

Fresh-machine setup

stow gitignore_global
git config --global core.excludesfile ~/.gitignore_global   # already in git/.gitconfig

hadolint

Stow package for ~/.config/hadolint/config.yaml. hadolint is a Dockerfile linter. Used by lefthook on pre-commit and by hand.

Table of contents

Layout

FileStows to
hadolint/.config/hadolint/config.yaml~/.config/hadolint/config.yaml

Settings

Ignored rules:

CodeRuleWhy ignored
DL3008apt-get without version pinunpinned apt is fine in scratch/dev images
DL3018apk add without version pinsame logic for Alpine
DL3015apt-get without --no-install-recommendsstylistic preference

Trusted registries (skip “use a versioned tag” warnings for these): docker.io, gcr.io, quay.io, ghcr.io, registry.k8s.io, public.ecr.aws.

failure-threshold: warning makes hadolint exit non-zero only for warning/error rules — informational findings don’t fail builds.

Per-project overrides

A repo can drop a .hadolint.yaml at the root; it wins. Per-line disables also work:

# hadolint ignore=DL3007
FROM nginx:latest

Fresh-machine setup

brew install hadolint    # in the Brewfile
stow hadolint

i3

Stow package for i3wm and i3status configs. Linux/X11 only — not stowed by default on macOS.

SourceTarget
i3/.config/i3/config~/.config/i3/config
i3/.config/i3status/config~/.config/i3status/config

Table of contents

i3wm config

Started from i3-config-wizard defaults; deviations called out below.

Mod key

set $mod Mod4 — i.e., the Super (Windows/Command) key. floating_modifier $mod lets you Mod-drag floating windows.

Vim-style:

KeysAction
Mod+h/j/k/lfocus left/down/up/right
Mod+Shift+h/j/k/lmove focused window
Mod+Left/Down/Up/Rightsame as above (arrow alternatives)
Mod+spacetoggle focus between tiling and floating
Mod+Shift+spacetoggle the focused window’s floating state
Mod+afocus parent container

Layouts and splits

KeysAction
Mod+gsplit horizontally (next window opens to the right)
Mod+vsplit vertically
Mod+ztoggle fullscreen for focused container
Mod+s / Mod+w / Mod+estacked / tabbed / split (toggle) layout

Mod+g is the non-default — i3’s wizard uses Mod+h for horizontal split, which conflicts with the focus binding above. This config remaps horizontal-split to g.

Workspaces

Mod+1..0 switch; Mod+Shift+1..0 move the focused container to that workspace. Standard.

KeysAction
Mod+Shift+creload i3 config
Mod+Shift+rrestart i3 in place (keeps session)
Mod+Shift+eexit i3 (i3-msg exit)
Mod+Shift+qkill focused window
Mod+Returnopen i3-sensible-terminal
Mod+ddmenu_run

Multimedia / system keys

KeyAction
XF86AudioLowerVolumeamixer set Master 8%-
XF86AudioRaiseVolumeamixer set Master 8%+
XF86AudioMuteamixer set Master toggle
XF86AudioPrev/Next/Plaympc prev / mpc next / mpc_toggle
XF86MonBrightnessUp/Downxbacklight -inc/-dec 10

(These assume ALSA + mpd + xbacklight are installed.)

Modes

  • Mod+r enters resize modeh/j/k/l shrink/grow by 10px; Enter or Esc to exit.
  • Caps_Lock and Num_Lock enter named modes solely so i3 displays a visual indicator on the bar when those locks are on. Pressing the same key again exits.

Autostart

Run by i3 on session start:

exec --no-startup-id start-pulseaudio-x11
exec --no-startup-id syndaemon -t -k -i 2 -d        # disable touchpad while typing
exec --no-startup-id feh --bg-scale '/home/jacob/wallpapers/arch1080.jpg'
exec --no-startup-id unclutter -root -visible       # hide idle cursor
exec --no-startup-id dunst                          # notification daemon
exec --no-startup-id xrandr --dpi 165               # HiDPI

Note the hardcoded wallpaper path under /home/jacob/ — update for the host or replace with a relative path.

The status bar runs i3status:

bar { status_command i3status }

focus_follows_mouse no — focus only on click, not on hover.

Theme (Dracula)

The official Dracula palette, matching the rest of the dotfiles:

  • client.* window-border colors (focused #6272A4, urgent #FF5555, unfocused #282A36).
  • bar { colors { … } } for the i3bar (background #282A36, statusline #F8F8F2, urgent #FF5555).
  • dmenu launch colors on Mod+d (-nb #282A36 -sb #6272A4 -sf #F8F8F2).
  • i3status color_good/color_degraded/color_bad = #50FA7B/#F1FA8C/#FF5555 (see below).

i3status config

Modules in display order:

wireless wlp1s0   # SSID + signal + IP
battery 0         # percentage; charging icon ⚡, discharging ⚇, full ☻
load              # 1-min load avg
cpu_usage         # %
volume master     # ALSA Master percentage
tztime local      # %I:%M%p %b %d %Y

Refresh interval: 5 seconds. Battery threshold for “low” warning: 10%. Battery path is hard-coded to /sys/class/power_supply/BAT%d/uevent (%d is replaced by the battery number).

Wireless interface is hard-coded to wlp1s0. Adjust per machine.

Commented-out modules: ipv6, disk /, disk /home, DHCP run-watch, VPNC pidfile, VPN tun0 path-exists check.

Fresh-machine setup

apt install i3 i3status dmenu feh dunst                # plus pulseaudio, alsa, mpd, xbacklight as needed
stow i3                                                # symlinks ~/.config/i3/ and ~/.config/i3status/

bootstrap-debian.sh does not stow this by default — only if the machine actually runs an X session.

kitty

Stow package for ~/.config/kitty/. Kitty is the GPU-accelerated terminal — used here on macOS (/Applications/kitty.app, installed via the kitty.sh installer, not Homebrew).

Table of contents

Layout

FileStows to
kitty/.config/kitty/kitty.conf~/.config/kitty/kitty.conf
kitty/.config/kitty/dracula.conf~/.config/kitty/dracula.conf
kitty/.config/kitty/diff.conf~/.config/kitty/diff.conf

Reload after editing: kitty @ load-config (needs allow_remote_control yes), or Cmd+Ctrl+, (macOS) / Ctrl+Shift+F5 (Linux).

Main config

kitty.conf pulls in the Dracula palette, sets the font, and tunes a handful of defaults. Sections:

FontJetBrainsMono Nerd Font Mono is the patched build with Nerd Font glyphs (install via the font-jetbrains-mono-nerd-font cask, or grab from nerdfonts.com).

Quality of life

SettingWhy
enable_audio_bell no / visual_bell_duration 0Silence the bell entirely
confirm_os_window_close 0Don’t prompt when closing a window with a running process
scrollback_lines 10000Bigger in-memory scrollback
scrollback_pager_history_size 100100 MB disk-backed scrollback for the pager kitten
repaint_delay 8 / input_delay 2 / sync_to_monitor yesSnappier rendering on Apple Silicon

macOS

SettingWhy
macos_option_as_alt leftLeft-Option behaves as Alt so ⌥←/⌥→ word-jump works in shells
macos_titlebar_color backgroundTitle bar adopts the bg color (no white strip)
macos_quit_when_last_window_closed yesQuitting the last window quits the app

Remote control + shell integration

SettingWhy
allow_remote_control yesUnlocks kitty @ ls, kitty @ set-tab-title, the ssh kitten, etc.
listen_on unix:/tmp/kitty-{kitty_pid}Per-process control socket
shell_integration enabledClick-to-move cursor, prompt marking, jump-by-prompt, etc.

Visual

SettingWhy
window_padding_width 6Subtle gutter between text and window edge
tab_bar_edge topTabs on top, away from prompt output
tab_bar_style powerline + tab_powerline_style slantedCurved powerline separators (uses JetBrainsMono Nerd Font glyphs)

Dracula theme

dracula.conf is the upstream palette from https://draculatheme.com/kitty, unmodified. It sets:

  • Foreground/background and the full ANSI palette (color0color15)
  • Cursor, selection, URL underline
  • Tab bar foreground/background (active + inactive)
  • Split/window border colors
  • Mark colors (kitty’s scrollback marks feature)

Matches the rest of the dotfiles’ Dracula-ish palette ([[../tig/README.md]], [[../lazygit/README.md]]).

Diff colors

diff.conf styles kitty +kitten diff (kitty’s built-in side-by-side diff viewer) with the Dracula palette — green for added, red for removed, purple hunk separators, cyan search, yellow selection.

Invoke with kitty +kitten diff old new or alias it.

Fresh-machine setup

# kitty itself: NOT in Brewfile — installed standalone on macOS via
#   curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin
# On Debian: apt install kitty (or use the same installer).

stow kitty

lazygit

Stow package for ~/.config/lazygit/config.yml.

Table of contents

Layout

FileStows to
lazygit/.config/lazygit/config.yml~/.config/lazygit/config.yml

Find the live config dir on any machine with lazygit --config-dir.

Theme

The official Dracula theme (copied verbatim from upstream), matching git/.gitconfig:

ElementColor
activeBorderColor#FF79C6 bold (pink)
inactiveBorderColor#BD93F9 (purple)
searchingActiveBorderColor#8BE9FD bold (cyan)
selectedLineBgColor#6272A4 (comment)
optionsTextColor#6272A4 (comment)
unstagedChangesColor#FF5555 (red)
defaultFgColor#F8F8F2 (foreground)

Plus cherry-pick / marked-base-commit colors — see the file for the full upstream block.

Other settings

gui:
  nerdFontsVersion: "3"             # use NF v3 glyphs (font-hack-nerd-font in Brewfile)
  showRandomTip: false              # silence the splash tips
  showCommandLog: false             # hide the bottom command-log panel
  showFileTree: true                # tree view in the files panel
  experimentalShowBranchHeads: true # show branch heads in the commit log

git:
  paging:
    colorArg: always
    pager: delta --paging=never     # diff pane uses delta, matching `git diff` in the terminal

os:
  editPreset: "vim"                 # `e` opens files in vim

The delta pager means lazygit’s diff pane renders with the same side-by-side, syntax-highlighted view you see from git diff. Both read settings from git/.gitconfig’s [delta] block.

gh integration (custom commands)

KeyContextAction
Oglobalgh pr view --web — open this branch’s PR in browser
Cglobalgh pr create --fill --web — create PR for current branch
Vcommitsgh pr view --web {SHA} — open PR for selected commit
Lglobalgh pr list --author "@me" — list my PRs (terminal output)
Xglobalgh poi — prune branches whose PRs are merged (terminal output)

{{.SelectedLocalCommit.Sha}} is lazygit’s template syntax for the selected entry in the commits panel.

Schema reference

Full options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md

Keep this file lean — lazygit can write to it when settings change via its UI, which creates noisy diffs when you only meant to tweak one thing.

Fresh-machine setup

brew install lazygit       # in the Brewfile
stow lazygit

lefthook

Per-project git-hooks manager template. Lefthook has no global config — this directory ships a lefthook.yml you can copy or extends: into new repos.

Table of contents

Layout

FilePurpose
lefthook/lefthook.ymlGo-dev baseline template (no symlink)

This package is not stowed — lefthook.yml is meant to live inside your project repos, not in $HOME.

Two ways to use it

1. Copy into a new repo

cp ~/dotfiles/lefthook/lefthook.yml ./lefthook.yml
lefthook install

lefthook install writes the .git/hooks/* shims that invoke lefthook. You only need to do this once per clone.

2. Extend it from your project’s lefthook.yml

# project/lefthook.yml
extends: ~/dotfiles/lefthook/lefthook.yml

pre-commit:
  commands:
    # project-specific addition
    api-spec-check:
      glob: "openapi.yaml"
      run: spectral lint {staged_files}

Then lefthook install and additions stack on top of the baseline.

What the template runs

pre-commit (parallel):

HookGlobWhat
gofmt*.gofail if any file isn’t gofmt’d
goimports*.gofail if imports aren’t sorted
golangci-lint*.gorun lint, only new findings since HEAD
govet*.gogo vet ./...
shellcheck*.{sh,bash}lint staged shell
shfmt*.{sh,bash}format-diff shell (2-space, switch-case indent)
yamllint*.{yml,yaml}YAML lint
hadolintDockerfile*Dockerfile lint

commit-msg: reject empty subjects.

pre-push (parallel):

HookGlobWhat
go-test*.gogo test ./...
go-build*.gogo build ./...

All the linters / formatters are in the Brewfile.

Adding repo-specific hooks

The most common pattern: extends: the baseline, then add or override. Anything you add lives in the project repo (versioned with the code).

Skipping a hook

LEFTHOOK_EXCLUDE=golangci-lint git commit -m "wip"
# or skip all hooks (use sparingly):
git commit --no-verify

Fresh-machine setup

brew install lefthook   # in the Brewfile
# In each repo where you want hooks:
lefthook install

p10k

Stow package for ~/.p10k.zsh — the configured powerlevel10k prompt.

Table of contents

Why this is in the repo

The theme itself ships with sensible defaults, but the prompt I actually use is a customized version produced by running p10k configure. Without this file, a fresh machine gets the wizard the first time zsh starts — which is fine, but I prefer the deployed prompt to match what I’m used to.

The file is ~1700 lines and mostly auto-generated; treat it as a snapshot, not as hand-tuned config. Edits are fine, but re-running p10k configure overwrites them.

How it’s loaded

zsh/.zshrc ends with:

[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh

Plus the instant-prompt preamble at the top of .zshrc (must stay close to the top to be effective):

if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
  source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi

The instant-prompt cache file is regenerated automatically as you use the shell; it isn’t in this repo.

Colors (Dracula)

This is a rainbow-style config with the official Dracula color values merged in (colors only — segments, layout, and options are mine). The Dracula theme is itself rainbow-based and mostly uses ANSI slots 0–7, so it relies on the terminal palette being Dracula (kitty’s dracula.conf) rather than hardcoding hex; only ~11 segment colors are overridden (os_icon, dir text, time, status, prompt char, multiline gap).

⚠️ Re-running p10k configure overwrites these. After regenerating, re-merge the Dracula values — pull files/.p10k.zsh from dracula/powerlevel10k and copy across only the *_FOREGROUND/*_BACKGROUND/*_COLOR values, or re-run the merge script used originally.

Regenerating

If you change the prompt with p10k configure, the wizard writes the new config to ~/.p10k.zsh — which is symlinked here. Commit the resulting diff (then re-apply the Dracula colors per the section above):

p10k configure          # interactive wizard
git -C ~/dotfiles diff p10k/.p10k.zsh
git -C ~/dotfiles commit -am "Regenerate p10k prompt"

Fresh-machine setup

stow p10k       # symlinks ~/.p10k.zsh

Theme itself (cloned as an OMZ custom theme) is installed by bootstrap-debian.sh:

git clone --depth=1 https://github.com/romkatv/powerlevel10k \
  "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k"

You’ll want a Nerd Font installed in your terminal for the prompt’s glyphs to render. The Brewfile pulls in font-hack-nerd-font.

ripgrep

Stow package for ~/.config/ripgrep/config. The config file is only read if RIPGREP_CONFIG_PATH points at it — rg does not look for it by default.

Table of contents

Layout

FileStows to
ripgrep/.config/ripgrep/config~/.config/ripgrep/config

Required env var

zsh/.zshrc sets:

export RIPGREP_CONFIG_PATH="$HOME/.config/ripgrep/config"

Without that variable, rg ignores the config file completely.

Settings

--smart-case               # case-insensitive unless the query has uppercase
--hidden                   # search dotfiles (still respects .gitignore)
--max-columns=200          # cap matched-line width
--max-columns-preview      # show a truncation message on long lines

Glob excludes (additional to whatever .gitignore says):

--glob=!.git/*
--glob=!node_modules/*
--glob=!.venv/*
--glob=!__pycache__/*
--glob=!dist/*
--glob=!build/*
--glob=!target/*           # Rust + JVM build dirs
--glob=!vendor/*           # Go vendored deps
--glob=!*.min.js
--glob=!*.min.css
--glob=!*.map              # source maps

Custom filetype aliases — rg --type <alias> "pattern":

--type-add=tf:*.{tf,tfvars}
--type-add=k8s:*.{yaml,yml}
--type-add=helm:*.{yaml,yml,tpl}
--type-add=proto:*.proto

--hidden makes rg walk into .config/, .local/, etc. — the --glob exclusions then prune the noisy ones.

Anything you override on the CLI wins over the config file.

Colors (Dracula)

The official Dracula palette, so rg output matches fzf/kitty/etc:

--colors=path:fg:0xbd,0x93,0xf9     # purple paths
--colors=line:fg:0x50,0xfa,0x7b     # green line numbers
--colors=column:fg:0x50,0xfa,0x7b   # green column numbers
--colors=match:fg:0xff,0x55,0x55    # red match highlights

Verifying it’s loaded

rg foo --debug 2>&1 | head -5

The first line of debug output names the config file path being read.

Companion: ripgrep-all (rga)

brew install ripgrep-all provides rga, a wrapper that runs rg across non-text files by extracting their content first: PDFs, ebooks (EPUB, MOBI), Office docs, archives (zip, tar, 7z), and even SQLite databases:

rga "encryption" ~/Documents/papers   # grep across PDFs
rga "TODO" project.zip                # grep inside an archive without extracting

rga reads its own config dir (~/.config/ripgrep-all/) — it does not share this ripgrep/config file. Run rga --help to set up adapter-specific options.

Fresh-machine setup

brew install ripgrep ripgrep-all   # both in the Brewfile
stow ripgrep
# RIPGREP_CONFIG_PATH is exported by zsh/.zshrc already

shellcheck

Stow package for ~/.shellcheckrc. shellcheck is the de-facto shell script linter. Used by lefthook on pre-commit, ALE in vim, and by hand.

Table of contents

Layout

FileStows to
shellcheck/.shellcheckrc~/.shellcheckrc

What’s enabled / disabled

enable=all
shell=bash
external-sources=true

disable=SC2034   # variable appears unused (false positive in sourced libs)
disable=SC1090   # can't follow non-constant source
disable=SC1091   # can't find source file

The enable=all turns on shellcheck’s optional checks (literal numeric comparison, etc.) — more signal at the cost of slightly more verbose output. The three disabled codes are the ones that fire most often on legitimate code (utility scripts that source siblings, library files that define helpers their callers use, etc.).

Per-project overrides

Each rule can be re-enabled in a single file via a comment:

#!/usr/bin/env bash
# shellcheck enable=SC2034

Or per-line:

foo=bar  # shellcheck disable=SC2034

A repo can also drop its own .shellcheckrc at the root; it wins.

Fresh-machine setup

brew install shellcheck    # in the Brewfile
stow shellcheck

tig

Stow package for ~/.tigrc. tig is the text-mode interface for Git — the read/navigate companion to lazygit’s interactive UI.

Table of contents

Layout

FileStows to
tig/.tigrc~/.tigrc

Display options

set git-colors    = no                 # don't double-style on top of git's color.* output
set diff-options  = -m --first-parent  # render merges as patches; follow first parent only
set show-changes  = yes                # staged/unstaged appear as faux commits in main view
set blame-options = -C -C -C           # `tig blame` follows copies across files
set wrap-lines    = yes                # wrap long lines instead of clipping

show-changes = yes is especially nice: in the main commit view, the top entries show your current HEAD state with unstaged + staged changes — no need to bounce back to git status.

Color scheme

Dracula-ish, using ANSI color slots (named colors) rather than hex. Each terminal’s Dracula theme maps the ANSI palette consistently (magenta → pink, blue → comment-purple, etc.), so the same .tigrc renders right on macOS iTerm/Terminal.app and Linux URxvt/Wezterm without per-platform tweaks.

ElementColor
Selected line (cursor)bold magenta (pink)
Search resultsblack on yellow
Line numbersblue (comment-purple), subtle
Active pane titlebold magenta on black
Inactive pane titleblue on black
diff +/- bodiesgreen / red
diff +/- highlightinverse green / red
Commit headersbold yellow
Index linescyan
Hunk markersmagenta
Author columncyan
Date columnblue (comment-purple)
HEAD markerbold magenta
Tagsbold yellow
Remote refsgreen
Trailers (Reported-by:, Signed-off-by:)green

Key bindings

KeyAction
YCopy the selected commit SHA to the system clipboard (pbcopy / xclip / wl-copy, whichever is available)

All of tig’s built-in bindings (Enter view commit, R refresh, q quit, ? help, etc.) remain unchanged.

Fresh-machine setup

brew install tig            # in the Brewfile
stow tig

tmux

Stow package for ~/.tmux.conf. Installs via stow tmux from the dotfiles root. TPM auto-bootstraps on first run, so a fresh machine just needs tmux itself.

Table of contents

Quick reference

Prefix is Ctrl+A (not the tmux default Ctrl+B). Everything below assumes you press the prefix first.

Sessions, windows, panes

KeysAction
prefix + aToggle to last window
prefix + cNew window
prefix + ,Rename window
prefix + sSession picker (built-in)
prefix + Ffzf picker for sessions / windows / panes (tmux-fzf)
prefix + gRipgrep+fzf popup over the current pane’s directory (runs frg; opens result in $EDITOR)
prefix + dDetach session
prefix + rReload ~/.tmux.conf

Splits and navigation (tmux-pain-control)

KeysAction
`prefix +`
prefix + -Split pane vertically (top/bottom)
prefix + h/j/k/lMove focus left/down/up/right
prefix + H/J/K/LResize pane (repeatable)
prefix + < / >Swap window with previous/next

Copy / paste (vi mode + tmux-yank)

KeysAction
prefix + [Enter copy mode
vBegin visual selection
yCopy selection to system clipboard (pbcopy / xclip / wl-copy)
qExit copy mode
prefix + ]Paste

Session persistence (tmux-resurrect + tmux-continuum)

KeysAction
prefix + Ctrl-sSave current session manually
prefix + Ctrl-rRestore last saved session

Continuum also auto-saves every 15 minutes and auto-restores the last session whenever tmux starts (@continuum-restore 'on').

Misc

KeysAction
prefix + Ctrl-s (this config)Toggle synchronize-panes for the current window
MouseEnabled — click to focus panes, drag to resize, scroll to scrollback

SYNC shows in the right status bar while pane sync is active.

Plugins

Managed by TPM. Install or update with prefix + I (capital i) inside tmux.

PluginPurpose
tmux-sensibleSaner defaults (history-limit, escape-time, focus-events, etc.)
tmux-pain-control`
tmux-yankCopy from tmux to the OS clipboard
tmux-resurrectSave / restore tmux sessions
tmux-continuumAuto-save every 15 min, auto-restore on tmux start
tmux-fzfprefix + F opens an fzf picker for sessions/windows/panes/keybindings
dracula/tmuxStatus-bar theme

Optional plugins commented out in the config (uncomment + prefix + I): tmux-logging, tmux-sessionist, tmux-menus, tmux-sidebar.

Fresh-machine setup

# tmux must be installed (Brewfile handles macOS; apt install tmux on Debian)
stow tmux              # from the dotfiles root
tmux                   # TPM auto-clones and installs plugins on first launch

vim

Stow package for ~/.vimrc and ~/.vim/. Vim 8+ with vim-plug managing the plugin set.

Table of contents

Leader is , (let mapleader=',').

Plugins

Managed by vim-plug. Install/update with :PlugInstall / :PlugUpdate. Clean orphans with :PlugClean!.

Editing essentials (tpope stack)

PluginUse
vim-commentarygcc toggles a line, gc{motion} toggles a range
vim-surroundcs'" → change 'foo' to "foo"; ys{motion}{char}; ds{char}
vim-repeatmakes the above two .-repeatable
vim-sleuthauto-detect indent settings from the file
vim-fugitive:Git, :Gdiffsplit, :Gblame, :Git log
vim-dispatchasync build/test runner
vim-projectionistproject-aware :A-style file switching
vim-unimpairedbracket-pair mappings: ]q/[q quickfix, ]b/[b buffers, ]c/[c (shared with gitgutter), ]<Space> add blank line, etc.
vim-eunuchshell-command Ex wrappers: :Rename, :Move, :Delete, :SudoWrite, :Chmod

UI

PluginUse
vim-airlinestatusline with mode/branch/lint
bufexplorer<leader>be/bt/bs/bv — list/toggle/split buffers
preservim/nerdtree<leader>nt toggle, <leader>nf reveal current file
tagbar<leader>tt outline of symbols (needs ctags)
undotree<leader>u visualize undo branches (paired with undofile)

Fuzzy finder

PluginUse
junegunn/fzf + junegunn/fzf.vim:Files, :Rg, :Buffers, :GFiles?, :History

Editor integrations

PluginUse
editorconfig-vimhonor .editorconfig files
vim-tmux-navigator<C-h/j/k/l> jumps across vim splits and tmux panes
vim-gitguttergutter diff markers; ]c/[c next/prev hunk
splitjoin.vimgS/gJ split/join one-liners ↔ blocks
auto-pairsauto-close brackets/quotes
ultisnipssnippet engine
vim-snippetsthe snippet library ultisnips expands (engine ships none on its own)

Linting

PluginUse
dense-analysis/aleasync linting (pylint, shellcheck, yamllint). Go is owned by vim-go.

Languages

PluginUse
fatih/vim-goGo IDE-ish features. See Go workflow.
rust-lang/rust.vimRust filetype + rustfmt

Colors

dracula is the scheme for all filetypes, including Go.

Leader-key cheat sheet

(Leader = ,)

KeysAction
,<space>clear search highlight
,,switch to last file
,ev / ,svedit / source $MYVIMRC
,ln / ,nw / ,rntoggle number / wrap / relativenumber
,sctoggle spell check
,tn / ,tcnew / close tab
,bd / ,bdaclose current buffer / close all buffers
,dc / ,doclose diff / :DiffOrig (diff against unsaved)
,rtretab
,urlopen URL under cursor (OS default browser)
,ggtoggle gitgutter line highlights
,tttoggle tagbar
,nt / ,nftoggle / reveal NERDTree
,utoggle undotree
,f / <C-p>fzf :Files
,bfzf :Buffers
,afzf :Rg (prompt for pattern)
,*fzf :Rg word under cursor
,hfzf :History
,qclose quickfix
<C-n> / <C-m>:cnext / :cprevious

Other mappings

Non-leader bindings worth knowing:

KeysAction
jj (insert)<Esc> — escape without leaving home row
<Down> / <Up> (insert)move by visual line (gj/gk), so wrapped lines work intuitively
j / k (normal)move by visual line (gj/gk)
<M-j> / <M-k> (normal + visual)move the current line / selection down / up
< / > (visual)indent left / right and keep the selection (gv re-select)
<tab> (normal + visual)jump to matching bracket (rebinds %)
n / N / * / #re-center the screen on the match (Nzz)
/starts a “very magic” search (/\v — most punctuation is regex metachar)
Qgq — reformat with motion (the default Q ex mode is rarely useful)

Custom Ex commands:

CommandAction
:W / :SudoWritesudo-save the current buffer (:W is an alias for vim-eunuch’s :SudoWrite)
:DiffOrigvertical diff between the buffer and the on-disk file (,do triggers it)

Non-default settings

SettingReason
hiddenallow buffer switching with unsaved changes
clipboard=unnamed,unnamedplussystem clipboard on Mac (unnamed) and Linux (unnamedplus)
termguicolorstrue color; pairs with tmux terminal-overrides ",xterm-256color:Tc"
splitbelow, splitrightnew splits open in more intuitive positions
completeopt=menu,menuone,noselectdropdown without auto-selecting first match
mouse=amouse on
undofile, undolevels=500persistent undo across sessions
backup, backupdir=~/.vim/backupkeep backup files in a known place
directory=~/.vim/tmp, undodir=~/.vim/undomove swap/undo out of the working dir
colorcolumn="80," + range(120,999)line marker at 80, solid color from 120+
textwidth=200global default; overridden to 80 for FileType text
ignorecase + smartcasecase-insensitive unless query has uppercase
tags=tags;/walk up looking for ctags tags file

Trailing whitespace is highlighted in red and stripped on save for *.go/*.py/*.sh. The motion/search rebindings that pair with these settings (visual-line j/k, recentering n/*, <tab> for bracket matching) are listed under Other mappings.

Window/pane navigation across vim and tmux

<C-h/j/k/l> is bound in both. With vim-tmux-navigator and tmux’s matching keybindings, hitting <C-l> from a rightmost vim split jumps into the tmux pane to the right. Same in reverse.

Ripgrep integration

  • :Rg foo — interactive fzf picker over rg results (from fzf.vim).
  • ,*:Rg for the word under the cursor.
  • :grep foo — batch search; populates the quickfix list. Uses rg via:
    set grepprg=rg\ --vimgrep\ --smart-case\ --hidden
    set grepformat=%f:%l:%c:%m
    
  • For “live rg + fzf, open at line in editor” from outside vim, see the frg function in fzf/README.md.

Go workflow

vim-go is configured with:

  • g:go_fmt_command="goimports" — format + organize imports on save
  • g:go_metalinter_autosave=1 — run linter on save
  • g:go_auto_sameids=1, g:go_auto_type_info=1 — highlight same identifiers, show types
  • Many g:go_highlight_*=1 flags for richer syntax

Go-specific leader maps (active only in .go files):

KeysAction
,b:GoBuild or :GoTestCompile based on filename
,ctoggle coverage
,ttest
,rrun
,erename (gopls)
,iinfo under cursor
,simplements
,ds/,dt/,dvdef in split/tab/vsplit
,gd/,gv/,gbdoc / doc-vertical / doc-browser
,glmetalinter
,ga:GoAlternate (jump between code and test file)

Runtime directories

The repo includes empty .gitignore files in these dirs to keep them present in stow targets without committing the runtime data:

DirWhat goes there
~/.vim/backup*~ backup files (gitignored repo-wide)
~/.vim/tmpswap files
~/.vim/undopersistent undo blobs
~/.vim/pluggedvim-plug-managed plugins (not in repo)

Fresh-machine setup

stow vim                    # symlinks ~/.vimrc and ~/.vim/
vim +"PlugInstall --sync" +qall

For headless (CI/script) install, vim-plug needs a real pty — run inside a temporary tmux session (see CLAUDE.md at the repo root).

Xresources

Stow package for ~/.Xresources. Linux/X11 only — used by URxvt, xterm, and other X11 apps that read the resource database.

Table of contents

Palette: Wombat

A dark-background terminal palette derived from the Wombat color scheme.

SlotColor
*background#161616 (near-black)
*foreground#ffffff
color0..7normal palette (black, red, green, yellow, blue, magenta, cyan, white)
color8..15bright palette

Defined as full *colorN: rgb:RR/GG/BB entries so they apply to any X client reading the resource database, not just URxvt.

URxvt

Non-default settings:

ResourceValueWhy
URxvt*font / boldFontxft:Consolas:size=12Consolas-based; needs MS fonts installed
URxvt*depth32enable ARGB visuals (real transparency)
URxvt*transparenttrue + shading: 10translucent terminal
URxvt*bufferedtruedouble-buffered redraw
URxvt*cursorBlinktrue
URxvt*saveLines65535large scrollback
URxvt*scrollBarfalse
URxvt*scrollTtyOutputfalsedon’t jump on program output
URxvt*scrollTtyKeypresstruejump on keypress
URxvt*scrollWithBuffertruescrollback follows new lines while at the bottom
URxvt*secondaryScreentrue + secondaryScroll: falsealt-screen apps don’t pollute scrollback
URxvt*loginShelltruespawn as a login shell
URxvt*inheritPixmaptrueinherit root pixmap (for true transparency vs. shading)

xterm

Lighter touch — xterm is mostly used as a fallback.

ResourceValue
xterm*faceNameConsolas:style=Regular:size=12
xterm*loginShelltrue
xterm*saveLines65535
xterm*charClasscustom word-class string so double-click selects URLs and paths
xterm*boldModefalse
xterm*eightBitInputfalse (so Alt doesn’t get encoded)

Xft (font rendering)

Xft.antialias:  true
Xft.autohint:   false
Xft.dpi:        92
Xft.hinting:    true
Xft.hintstyle:  hintslight
Xft.lcdfilter:  lcddefault
Xft.rgba:       rgb

The DPI is hard-coded to 92 here, but i3/.config/i3/config runs xrandr --dpi 165 on autostart, which can override. Reconcile per host if you care.

Activating changes

xrdb -merge ~/.Xresources       # apply without restarting X
# or
xrdb ~/.Xresources              # replace the database wholesale

Already-running terminals won’t pick up changes until restart.

Fresh-machine setup

apt install rxvt-unicode-256color   # for URxvt; xterm ships with most X installs
stow Xresources                     # symlinks ~/.Xresources
xrdb -merge ~/.Xresources

Not stowed by default by bootstrap-debian.sh — only relevant on graphical Linux machines.

yamllint

Stow package for ~/.config/yamllint/config. yamllint is a YAML linter — invoked by lefthook on pre-commit, by ALE in vim, and by hand.

Table of contents

Layout

FileStows to
yamllint/.config/yamllint/config~/.config/yamllint/config

yamllint only reads this file when run without -c <path> and when the project has no .yamllint/.yamllint.yml/.yamllint.yaml of its own.

Rule tweaks

Starting from extends: default:

RuleChangeWhy
line-length: 120 (warning)bump from 80, demote to warningnobody wraps YAML at 80
document-startdisable--- is rarely useful in single-doc files
truthy.check-keys: falsedon’t flag on: keysGitHub Actions uses on: as a top-level key
comments.min-spaces-from-content: 1down from 2the default is overly strict
comments-indentationdisablemis-indented comments are common in real YAML and rarely matter
indentation.indent-sequences: consistentdon’t require nested under keymatches both common styles
braces / brackets max-spaces-inside: 1allow { k: v }flow style is fine

Per-project overrides

Drop a .yamllint in your repo root — it wins. Lefthook’s pre-commit hook (template at lefthook/lefthook.yml) will use whichever yamllint finds first.

Fresh-machine setup

brew install yamllint    # in the Brewfile
stow yamllint

zsh

Stow package for ~/.zshrc. Oh My Zsh + powerlevel10k + a curated plugin set.

Table of contents

Theme: powerlevel10k

ZSH_THEME="powerlevel10k/powerlevel10k". The instant-prompt preamble is at the top of .zshrc and must stay there. Configured prompt lives in ~/.p10k.zsh (see p10k/ stow package); .zshrc sources it if present.

Plugins

aliases colored-man-pages command-not-found cp dirhistory direnv docker
extract fzf gitfast golang helm kind kube-ps1 kubectl skaffold sudo
taskwarrior tig tmux

Highlights of non-obvious ones:

PluginWhy
gitfastReplaces git’s built-in completion with the official upstream Git completion — faster on big repos.
command-not-foundSuggests how to install missing commands (apt install foo / brew install foo).
extractx file.{zip,tar.gz,7z,...} — one command for any archive.
sudoEsc Esc prepends sudo to the current line (or recalls the previous one with sudo).
dirhistoryAlt+←/→ walks back/forward through cd history. Pairs with AUTO_PUSHD.
golangAdds Go aliases (g, gob, goc, gor, …) and completion.
kube-ps1Current k8s context/namespace in the prompt.

Custom plugins (conditionally loaded)

Loaded only if the directory exists under ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/.

PluginEffect
you-should-useNags when you type the long form of an alias.
zsh-autosuggestionsGreyed-out command completion from history; right-arrow to accept.
fzf-tabReplaces zsh’s default completion menu with an fzf picker. Must load last (after every plugin that registers completions).

Install on a fresh machine:

ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions      "$ZSH_CUSTOM/plugins/zsh-autosuggestions"
git clone --depth=1 https://github.com/Aloxaf/fzf-tab                     "$ZSH_CUSTOM/plugins/fzf-tab"
git clone --depth=1 https://github.com/MichaelAquilina/zsh-you-should-use "$ZSH_CUSTOM/plugins/you-should-use"

The bootstrap-debian.sh script does this automatically.

Completion

Before OMZ runs compinit, the config prepends Homebrew completion dirs to FPATH so brew-installed tools (gh, lazygit, yq, stern, etc.) get proper tab-completion:

FPATH="$(brew --prefix)/share/zsh-completions:$(brew --prefix)/share/zsh/site-functions:$FPATH"
ZSH_DISABLE_COMPFIX=true

ZSH_DISABLE_COMPFIX=true skips OMZ’s complaint about Homebrew’s group-writable share/ directory (which is intentional on Homebrew, not fixable without fighting brew).

The zsh-completions brew formula provides extra completion definitions beyond what individual tools ship.

zstyle polish (after OMZ’s compinit):

zstyle ':completion:*' menu select                          # arrow-key menu nav
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'   # case-insensitive
zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}"     # colored menu
zstyle ':completion:*' rehash true                          # pick up new $PATH binaries

PATH

export PATH=$PATH:$HOME/bin
export GOPATH=$HOME/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN
export PATH=$HOME/.local/bin:$PATH        # pipx / user-installed binaries
export PATH=$PATH:/usr/local/go/bin

GEM_HOME is set only if ruby is on PATH (guarded).

On macOS, Homebrew’s GNU coreutils/findutils/sed/tar/which/grep/binutils get prepended to PATH so GNU semantics win over the BSD defaults:

if [[ "$OSTYPE" == darwin* ]]; then
  for gnu_tool in binutils coreutils findutils gnu-sed gnu-tar gnu-which grep; do
    gnu_bin="/opt/homebrew/opt/$gnu_tool/libexec/gnubin"
    [[ "$gnu_tool" == "binutils" ]] && gnu_bin="/opt/homebrew/opt/$gnu_tool/bin"
    [[ -d "$gnu_bin" ]] && export PATH="$gnu_bin:$PATH"
  done
fi

History

HISTFILE  = ~/.zsh_history
HISTSIZE  = 500000      # in-memory
SAVEHIST  = 500000      # on disk

Setopts beyond OMZ defaults:

  • HIST_FCNTL_LOCK, HIST_FIND_NO_DUPS, HIST_IGNORE_ALL_DUPS, HIST_LEX_WORDS, HIST_NO_FUNCTIONS, HIST_NO_STORE, HIST_REDUCE_BLANKS, HIST_SAVE_NO_DUPS
  • HISTORY_IGNORE="(ls|cd|pwd|exit)" — never store these

OMZ’s defaults provide SHARE_HISTORY, EXTENDED_HISTORY, HIST_EXPIRE_DUPS_FIRST, HIST_IGNORE_SPACE, HIST_VERIFY — re-affirmed in the config for clarity.

atuin replaces Ctrl+R with a SQLite-backed TUI on top of this history.

Shell behavior

EXTENDED_GLOB        # ^pat (negate), ~pat (exclude), **/* (recursive)
NO_BEEP              # silence the bell
INTERACTIVE_COMMENTS # # comments allowed in interactive lines
AUTO_CD              # `dirname` alone cd's there
AUTO_PUSHD           # every cd pushes the old dir onto the stack
PUSHD_IGNORE_DUPS
PUSHD_SILENT
COMPLETE_IN_WORD     # complete from cursor position, not only end
ALWAYS_TO_END        # cursor goes to end after completion

Environment

EDITOR              = vim
LANG                = en_US.UTF-8
LESS                = -RFX                                  # raw colors, quit-if-one-screen, no alt-screen
LESSHISTFILE        = -                                     # don't write ~/.lesshst
MANPAGER            = sh -c 'col -bx | bat -l man -p'       # syntax-highlighted man pages
BAT_THEME           = ansi                                  # bat respects terminal palette
RIPGREP_CONFIG_PATH = $HOME/.config/ripgrep/config          # rg won't read its config without this

Try man ssh after a fresh shell — colorized.

Tool integrations

ToolWiring
zoxideeval "$(zoxide init zsh)" (guarded on command -v)
atuineval "$(atuin init zsh)" (guarded; replaces Ctrl+R)
ezawhen installed: ls is a function (falls back to real ls for short flag bundles with t, e.g. ls -ltrh), plus ll/la/lt/ltrh aliases
gitcurated shell shortcuts: gst ga gaa gc gca gco gcb gd gds gp gpf glg (names match the OMZ git plugin, which isn’t enabled — gitfast gives completion only)
kittykt/kw rename + kgo <title> jump-by-name + ktabs listing (uses jq) when installed
fzfOMZ fzf plugin auto-sources ~/.fzf.zsh (see fzf/)
zsh-syntax-highlightingsourced at the end; tries Linux, Apple Silicon brew, Intel brew paths in order

OMZ behavior

zstyle ':omz:update' mode reminder   # prompt when updates are available
zstyle ':omz:update' frequency 14    # check every 2 weeks
DISABLE_UNTRACKED_FILES_DIRTY=true   # faster git status in big repos
COMPLETION_WAITING_DOTS=true

What’s loaded from elsewhere

  • ~/.fzf.zsh (auto-sourced by OMZ fzf plugin) — fzf options + the frg ripgrep+fzf function. See fzf/README.md.
  • ~/.p10k.zsh — prompt configuration. See p10k/README.md.