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

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
kitty/.config/kitty/ssh.conf~/.config/kitty/ssh.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)

Windows, tabs & layouts

kitty has its own multiplexing, independent of tmux. Terminology: an OS window holds tabs; each tab arranges windows (splits) using a layout. Every kitty shortcut uses cmd (macOS) or ctrl+shift (kitty_mod, e.g. on Linux) — a different namespace from the tmux C-a prefix, so the two never collide. Inside a tmux session, cmd+… drives kitty and C-a … drives tmux; they stack happily.

Keys below are macOS (the daily driver). On Linux cmd is inert and kitty’s ctrl+shift defaults apply — see the kitty docs.

Built-in defaults

ActionKey
New tab⌘T
Next / prev tab⌘] / ⌘[ (or ⌃Tab / ⇧⌃Tab)
Go to tab 1–10⌘1⌘9, ⌘0
Close tabctrl+shift+q
New OS window⌘N
Next / prev splitctrl+shift+] / ctrl+shift+[
Close split⌘W
Next layoutctrl+shift+l
Fullscreen toggle⌘↩

Custom maps (this config) — added to kitty.conf, macOS only. They repurpose the default ⌘D/⌘⇧D (new window) into iTerm-style directional splits:

KeyActionvim / tmux analog
⌘D / ⌘⇧Dsplit right / down (launch --location=vsplit/hsplit)tmux `prefix
⌘⌃H ⌘⌃J ⌘⌃K ⌘⌃Lfocus split left / down / up / rightvim ctrl-w hjkl, tmux prefix hjkl
⌘⌃⇧H ⌘⌃⇧J ⌘⌃⇧K ⌘⌃⇧Lmove / swap splitvim ctrl-w HJKL
⌘⌃Zzoom the active split (toggle stack)tmux prefix z
⌘Rresize mode → then arrows or hjkl, Enter/Esctmux prefix HJKL

(In kitty.conf the focus/move directions are left/right/top/bottom — kitty does not accept up/down.) These are safe alongside tmux because cmd never reaches the shell. The only chord that would clash — ctrl+a (the tmux prefix) — is deliberately never mapped in kitty.

Layoutsenabled_layouts splits,stack,tall,fat,grid, cycled with ctrl+shift+l:

  • splits — arbitrary horizontal/vertical splits; the default, and the only layout where the ⌘D/⌘⇧D direction applies.
  • stack — one split maximized (zoom); flip back with ctrl+shift+l.
  • tall / fat / grid — auto-tiled arrangements for quick even layouts.

In non-splits layouts, --location is ignored and a new window is just auto-placed by that layout.

ssh kitten

kitten ssh is kitty’s smarter ssh: it ships kitty’s terminfo and shell integration to the remote, so colors, prompt marking, and the clipboard work there even when kitty isn’t installed on the remote — and it can copy selected dotfiles over. It only works when your local terminal is kitty (allow_remote_control yes is already set above).

kitten ssh user@host          # older form: kitty +kitten ssh user@host
kssh user@host                # the alias in zsh/.zshrc (kitty-guarded)

kssh is preferred over aliasing ssh itself — it’s explicit, and ssh keeps working normally outside kitty (where kitten ssh can’t run anyway).

Per-host behavior lives in ssh.conf (tracked in this package, stowed to ~/.config/kitty/ssh.conf). It is not ~/.ssh/config — OpenSSH still owns hostnames, ports and keys; this only controls what kitten ssh carries over. The shipped starter enables remote shell integration and copies a few dependency-free configs (.vimrc, .editorconfig, .gitconfig), with a commented per-host block for heavier setups (nvim/zsh/tmux):

hostname *
shell_integration enabled
copy .vimrc
copy .editorconfig
copy .gitconfig
env EDITOR=vim

Caveat: wait for the shell prompt before typing on a fresh connection — early keystrokes during the bootstrap handshake can be dropped.

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

mise

mise manages language runtimes and CLI tools with per-project version pinning. This package stows one file:

SourceSymlinks to
mise/.config/mise/config.toml~/.config/mise/config.toml (mise’s global config)

What it’s for

The global [tools] list is the primary tool manager on Linux. There is no Homebrew on the Debian VMs (see the repo CLAUDE.md and bootstrap-debian.sh), so mise install from $HOME is what actually installs bat, eza, ripgrep, go-task, the Node/Go runtimes, etc. On macOS the runtimes and CLIs still come from the Brewfile, but the repo’s lint tools are installed via mise there too — bootstrap-macos.sh stows this package and mise installs just those (see the lint-tooling note below). The [settings] block silences mise’s “missing tool” nag for the brew-provided entries on macOS.

zsh/.zshrc runs mise activate zsh before the tool-integration blocks (zoxide, atuin, eza, task, …) so mise-managed binaries are on PATH when those command -v checks run.

Why these tools (and not others)

  • Runtimesnode (LTS) and go (latest). Python is left to Debian’s system python3; uncomment the pin in config.toml if you want mise to own it.
  • CLIs — the modern replacements the shell config references. Each is gated behind command -v in zsh/.zshrc, so adding/removing one here never breaks a machine that lacks it.
  • fzf and direnv are deliberately NOT here. Their oh-my-zsh plugins load during source $ZSH/oh-my-zsh.sh, which runs before mise activate. A mise-only fzf/direnv wouldn’t be on PATH yet, so those two come from apt (Linux) / brew (macOS) instead.
  • Repo lint tooling — the linters lefthook and CI run live here so one file tracks them and they match CI: shellcheck, shfmt, taplo, actionlint, editorconfig-checker, lefthook, plus npm:markdownlint-cli2 (needs node) and pipx:yamllint (needs python). These come from mise on macOS too — notably editorconfig-checker, whose aqua build ships the ec command lefthook/CI call (Homebrew’s ships editorconfig-checker, no ec). mdformat is the one exception: its gfm/tables plugins need pipx inject, which mise’s pipx backend can’t express, so task mise install-or-upgrades it via pipx.

How it’s used

mise install     # install everything in the global config (run from anywhere)
mise upgrade     # upgrade installed tools to the latest matching the config
mise outdated    # preview which tools have newer versions (nothing changes)
mise ls          # show installed tools + versions
mise doctor      # diagnose activation / shim problems
mise use -g go@latest   # add/pin another global tool (edits this config)

mise install only fetches what’s missing; mise upgrade re-resolves the latest/lts pins and moves them forward. The repo root’s Taskfile.yml has a task mise target that runs mise upgrade and also install-or-upgrades mdformat via pipx (see the bullet above); task update refreshes the vim and nvim plugins too.

The headless devbox flow does this automatically: it installs mise from apt, and after the dotfiles are stowed it runs mise install in $HOME with MISE_YES=1.

nvim

Stow package for ~/.config/nvim/. A from-scratch Lua config (lazy.nvim + native LSP + Treesitter), not a port of ~/.vimrc. The older vim/ package stays as-is and remains the default $EDITOR; this exists to migrate to Neovim incrementally without losing the muscle memory baked into the Vim config.

Table of contents

Leader is , (vim.g.mapleader = ','), set in init.lua before any plugin loads.

Layout

nvim/.config/nvim/
├── init.lua                 entry: leader, then require the config modules
└── lua/
    ├── config/
    │   ├── lazy.lua         bootstrap lazy.nvim, import lua/plugins/
    │   ├── options.lua      vim.opt settings ported from .vimrc
    │   ├── keymaps.lua      non-plugin maps (leader, visual-line j/k, …)
    │   ├── autocmds.lua     trailing-WS, cursor restore, text tw=80
    │   └── commands.lua     :W (SudoWrite alias), :DiffOrig
    └── plugins/             one spec file per concern; lazy imports them all
        ├── colorscheme.lua  dracula.nvim
        ├── editing.lua      tpope stack, autopairs, tmux-navigator
        ├── ui.lua           lualine, nvim-tree, aerial, undotree
        ├── finder.lua       fzf-lua
        ├── git.lua          fugitive, gitsigns
        ├── treesitter.lua   nvim-treesitter
        ├── completion.lua   blink.cmp + friendly-snippets
        ├── lsp.lua          mason, lspconfig, nvim-lint
        └── lang.lua         go.nvim, rust.vim

init.lua is Neovim’s $MYVIMRC, so ,ev / ,sv edit / source it.

Plugin map (Vim → Neovim)

Kept as-is — these run natively and have no better replacement: vim-surround, vim-repeat, vim-sleuth, vim-eunuch, vim-projectionist, vim-dispatch, vim-fugitive, splitjoin.vim, vim-tmux-navigator, undotree, rust.vim.

Replaced with a Lua-native equivalent:

Vim pluginNeovim replacementNotes
vim-airlinelualine.nvimtheme auto follows Dracula
nerdtreenvim-tree.lua,nt / ,nf unchanged
tagbaraerial.nvim,tt; LSP/Treesitter-backed, no ctags
fzf + fzf.vimfzf-luasame ,f ,b ,a ,* ,h maps
vim-gitguttergitsigns.nvim]c/[c hunks, ,gg toggle
auto-pairsnvim-autopairsintegrates with blink.cmp
ultisnips + vim-snippetsblink.cmp + friendly-snippetssnippets via the completion menu
alenvim-lint + LSP diagnosticspylint / shellcheck / yamllint
vim-gogo.nvim + goplscommands + maps preserved (see below)
dracula/vimdracula.nvimTreesitter- & LSP-aware

What Neovim made redundant

Dropped entirely — the capability is now built in:

DroppedReplaced by (built-in)
vim-commentarygc / gcc commenting (Neovim 0.10+)
vim-unimpaired]q [q, ]b [b, ]<Space>, ]d [d default maps (0.11+); ]c [c via gitsigns
editorconfig-vimnative .editorconfig support (Neovim 0.9+)
syntax on + g:go_highlight_*Treesitter highlighting
nocompatible, filetype … on, backspace, incsearch, wildmenu, hidden, autoindent, ttyfastNeovim defaults

LSP, completion, linting

  • Servers (auto-installed by mason-lspconfig): gopls (via go.nvim), rust_analyzer, pyright, lua_ls, bashls, yamlls. Enabled with the native vim.lsp.enable(); :Mason manages the binaries.
  • Built-in LSP maps (Neovim defaults, on attach): K hover, grr references, grn rename, gra code action, gri implementation, [d/]d diagnostics. Added on top: gd definition, ,e rename, ,ca code action.
  • Completion: blink.cmp<C-y> accept, <C-n>/<C-p> select, <C-space> toggle docs. Snippets come from friendly-snippets.
  • Format on save: Go uses go.nvim’s goimports; every other server that advertises formatting (rust_analyzer, lua_ls) runs vim.lsp.buf.format() on BufWritePre (built-in, no plugin). Servers that can’t format (pyright, bashls, yamlls) are simply skipped.
  • Linting: nvim-lint runs the same linters ALE did — pylint (Python), shellcheck (sh), yamllint (yaml). The CLI tools must be on PATH.

Leader-key cheat sheet

(Leader = ,. Maps shared with the Vim config keep the same keys.)

KeysAction
,<space>clear search highlight
,,switch to last file
,ev / ,svedit / source init.lua
,ln / ,nw / ,rntoggle number / wrap / relativenumber
,sc / ,rttoggle spell / retab
,tn / ,tcnew / close tab
,bd / ,bdadelete current / all buffers
,dc / ,doclose diff / :DiffOrig
,urlopen URL under cursor (system browser)
,utoggle undotree
,tttoggle aerial symbol outline
,nt / ,nftoggle / reveal nvim-tree
,f / <C-p>fzf-lua files
,b / ,befzf-lua buffers
,afzf-lua live grep
,*fzf-lua grep word under cursor
,hfzf-lua recent files
,ggtoggle gitsigns line highlight
,hs / ,hu / ,hpstage / reset / preview git hunk
,qclose quickfix
]q / [qnext / prev quickfix (built-in)
]c / [cnext / prev git hunk

Non-leader maps carried over verbatim from the .vimrc: jj→Esc, visual-line j/k, <M-j>/<M-k> move line/selection, < / > keep selection, <Tab>%, recentering n/N/*/#, very-magic /, Qgq, <C-h/j/k/l> window+tmux navigation. See the vim README for the rationale behind each.

Text objects (Treesitter)

From nvim-treesitter-textobjects (no built-in equivalent). Use in operator- or visual-pending position, e.g. daf deletes a whole function, vif selects its body, cia changes an argument:

KeySelects / moves to
af / ifa function / inside a function
ac / ica class / inside a class
aa / iaa parameter / inside a parameter
]m / [mnext / prev function start
]] / [[next / prev class start

Go workflow

go.nvim owns gopls and reproduces the vim-go commands. Maps active only in .go buffers:

KeysAction
,b:GoBuild
,t / ,rtest / run
,c:GoCoverage toggle
,ga:GoAlt (code ↔ test)
,gl / ,gdlint / doc
,i / ,shover (type/doc) / implements
,ds / ,dv / ,dtdefinition in split / vsplit / tab
,erename (LSP, global map)

go.nvim drives gopls (already on $PATH if you used vim-go — it installs to ~/go/bin). The extra tools it shells out to (gotests, iferr, …) come from :GoInstallBinaries — run it from inside a Go buffer, since go.nvim only loads for Go filetypes; nvim "+GoInstallBinaries" on an empty buffer fails with “not an editor command”. goimports runs on save.

Settings that differ from the .vimrc

Mostly faithful; the deliberate changes:

ChangeReason
cindent droppedTreesitter handles indentation per-filetype
signcolumn=yes addedkeep git/diagnostic signs from shifting text
runtime dirs under ~/.local/share/nvimNeovim’s stdpath('data'), not ~/.vim
viminfoshadaNeovim’s default shada is sensible; not overridden
statusline/laststatus/ruler unsetlualine owns the statusline

Everything else — colorcolumn 80+120 formula, clipboard, undofile, textwidth=200 (80 for text), ignorecase+smartcase, gdefault, trailing-whitespace highlight + strip-on-save for go/py/sh — is ported directly.

Runtime directories

init.lua/options.lua create these on startup (Neovim won’t):

DirContents
~/.local/share/nvim/undopersistent undo
~/.local/share/nvim/backupbackup files
~/.local/share/nvim/swapswap files
~/.local/share/nvim/lazylazy.nvim-managed plugins
~/.local/share/nvim/masonLSP servers / tools

~/.config/nvim/lazy-lock.json pins plugin commits — commit it after a :Lazy sync so installs are reproducible.

Fresh-machine setup

stow nvim                                   # symlinks ~/.config/nvim
nvim --headless "+Lazy! sync" +qa           # install plugins

Then, for the optional Go tools, open any .go file and run :GoInstallBinaries (gopls itself is already covered if vim-go installed it).

Treesitter compiles parsers on first run (needs a C compiler — clang on macOS, build-essential on Debian). :checkhealth flags anything missing.

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.

Instant-prompt mode

POWERLEVEL9K_INSTANT_PROMPT is set to quiet rather than the stock verbose. In a real terminal, init is silent and instant prompt works perfectly. But a non-tty shell — like the one Claude Code uses to snapshot the environment — can’t give p10k’s gitstatus job control or let atuin/zoxide bind zle widgets, so those steps print errors. With verbose, p10k then emits a console-output warning on every such launch. quiet suppresses that spurious warning and has no effect on real interactive sessions (which produce no output, so the prompt never “jumps”). ⚠️ Like the Dracula colors below, re-running p10k configure resets this to verbose — re-apply it.

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

task

Stow package for Taskwarrior and taskwarrior-tui. Taskwarrior is a console todo manager; taskwarrior-tui is a terminal UI on top of it. The TUI has no config file of its own — it reads everything from Taskwarrior’s taskrc, so both tools are configured from one place here.

Table of contents

Layout

FileStows to
task/.config/task/taskrc~/.config/task/taskrc
task/.config/task/dracula.theme~/.config/task/dracula.theme
task/.config/task/shortcuts/*.sh~/.config/task/shortcuts/*.sh

Task data (the TaskChampion SQLite DB) lives at ~/.local/share/task via data.location — XDG, matching the rest of the repo. It is not tracked here (it’s your actual todo data).

Config file resolution

Taskwarrior reads the first that exists, in order:

  1. rc:<path> on the command line
  2. $TASKRC
  3. ~/.taskrc (classic location)
  4. ~/.config/task/taskrc (XDG) ← what this package uses

Because there is no ~/.taskrc, the XDG path wins. include dracula.theme in taskrc is resolved relative to the taskrc file, so the two files must stay side by side.

Dracula theme

dracula.theme uses named ANSI colours (color1/red, color4/blue, …) and palette indices, not hex. Taskwarrior maps those to the terminal’s 16-colour palette, which kitty themes with Dracula (kitty/dracula.conf). So the colours render as true Dracula and stay correct on any Dracula-themed terminal — including the Debian box. This is the same --theme=ansi trick bat uses.

The one gotcha worth remembering: in Dracula’s ANSI map, color4 (“blue”) is purple #bd93f9 and color5 (“magenta”) is pink #ff79c6. The theme leans on those for headers, projects, and recurring tasks.

Taskwarrior colours can’t take hex directly (only names / colorN / rgbRGB / grayN), which is the other reason the named-ANSI approach is the right call.

Sensible defaults

Set in taskrc:

SettingValueWhy
data.location~/.local/share/taskXDG, consistent with the repo
news.version3.4.2skip the interactive “task news” prompt on first run
weekstartmondayweek starts Monday in reports/calendar
due4colour tasks as “due” within 4 days (default is 7)
nag(empty)silence the “you have more urgent tasks” scold
confirmationonstill prompt before bulk/destructive edits
dateformat*Y-M-DISO-8601 dates on input, reports, info, annotations

taskwarrior-tui settings

All under the uda.taskwarrior-tui.* namespace in taskrc:

  • Behaviour — opens on the next report (status:pending), shows the task detail pane (show-info), wraps top/bottom (looping), jumps to a task after adding it, pre-fills metadata when modifying, prompts before delete but not before done.
  • Cursor / marks selection indicator (bold), for multi-selected rows.
  • Quick-tagt toggles the +next tag for fast prioritising.
  • Dracula chrome — context bar, navbar, calendar title/today, command-line highlight, and the selection row all use the same named-ANSI palette as dracula.theme (e.g. active context = black on color4/purple).

taskwarrior-tui’s movement keys are already vim-style out of the box (j/k, g/G, / to filter, : for a command), so no key remapping is needed. Run it with taskwarrior-tui, press ? for the full key list.

Number-key shortcuts

Press 13 in the TUI to run a one-key action on the selected (or multi-marked) task(s). Each is a small script in shortcuts/, invoked with the task UUID(s) as arguments (rc.confirmation=off etc. so it’s a single keypress):

KeyScriptAction
1snooze.shwait:tomorrow — hide until tomorrow
2today.shdue:today — pull into today
3bump.shpriority:H +next — escalate

Add more by dropping an executable in shortcuts/ and pointing uda.taskwarrior-tui.shortcuts.N at it (keys 19). ~ is expanded; the script must be chmod +x with a shebang.

Shell access — TUI only, no Taskwarrior CLI aliases

Taskwarrior is driven here only through its TUI. zsh/.zshrc defines a single Taskwarrior alias, tt=taskwarrior-tui; there are intentionally no Taskwarrior t/ta/tl/… CLI aliases. (The task alias below is go-task, not Taskwarrior.)

The reason is a name clash: task is an overloaded command on this machine — it’s both Taskwarrior and go-task (the Taskfile.yml runner, installed at $GOBIN/task). The bare task command is bound to go-task with an explicit alias in zsh/.zshrc:

alias task="${GOBIN:-$HOME/go/bin}/task"

Without it, $(brew --prefix)/bin precedes $GOBIN in $PATH, so task would resolve to Taskwarrior unless a project prepends $GOBIN (e.g. via direnv) — fragile and directory-dependent. The alias makes it deterministic. There’s no per-directory dispatch to set up: go-task itself walks up parent directories to the nearest Taskfile.{yml,yaml}, so task <target> works anywhere inside a project tree (and task -g runs $HOME/{T,t}askfile.{yml,yaml}).

zsh/.zshrc also loads go-task’s own completion for task (cached at $XDG_CACHE_HOME/gotask-completion.zsh). That deliberately overrides Taskwarrior’s _task (linked in Homebrew’s site-functions, which auto-binds to task) — otherwise task <TAB> would fire Taskwarrior’s helpers (task _zshids, …) against go-task and error.

The OMZ taskwarrior plugin is also dropped from the plugin list: its stale 2022 _task errors on current zsh (_task_attributes:zregexparse: invalid regex : local, from an uninitialized word array), and we don’t want Taskwarrior CLI completion bound to task anyway.

Note: the alias only affects this interactive shell. taskwarrior-tui is a separate process that does its own $PATH lookup for task (it has no flag to point at a specific binary), so it never sees the alias and keeps talking to Taskwarrior — which wins in $PATH by default. The one caveat: launch tt from a directory that prepends $GOBIN (e.g. a direnv’d Taskfile project) and the TUI would shell out to go-task instead. Start it from elsewhere, or front tt with a Taskwarrior-first $PATH if that ever bites.

Things you might add

Personal enough that they’re left out by default:

# Contexts — scope every command to a project/tag set, toggle with `task context`
task context define work  +work or project:work
task context define home  -work
task context work          # activate;  task context none  to clear

# A leaner custom report
task config report.next.columns    id,start.age,priority,project,due.relative,description.count
task config report.next.labels     ID,Active,P,Project,Due,Description

# Sync (TaskChampion server / taskwarrior.org) — then set
# uda.taskwarrior-tui.background_process=sync to auto-sync from the TUI.

Verifying

task rc:task/.config/task/taskrc rc.data.location=/tmp/taskcheck show >/dev/null  # parses config + theme

Colours and the TUI need a real Dracula-themed terminal to eyeball — taskwarrior-tui can’t be checked headlessly.

Fresh-machine setup

brew install taskwarrior-tui   # in the Brewfile; pulls in task/Taskwarrior
stow task

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).

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
tig tmux

The OMZ taskwarrior plugin was deliberately not included: its stale 2022 _task completion errors on current zsh (_task_attributes:zregexparse: invalid regex). Taskwarrior is used only via its TUI (tt=taskwarrior-tui) — there are no task/t CLI aliases, and the bare task command is go-task, whose completion .zshrc loads instead. See task/README.md.

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.