Skip to content

refactor: migrate dotfiles from stow to chezmoi, modernize the shell - #1

Merged
ptaranat merged 24 commits into
mainfrom
modernize
Jul 19, 2026
Merged

refactor: migrate dotfiles from stow to chezmoi, modernize the shell#1
ptaranat merged 24 commits into
mainfrom
modernize

Conversation

@ptaranat

Copy link
Copy Markdown
Owner

chore: add Brewfile, prune unused packages, rename claude alias

Rename the Claude Code alias cc -> cl. As cc it shadowed the C compiler
outright: command -v cc resolved to the alias rather than /usr/bin/cc,
which breaks any ./configure, make, or native gem/npm build that shells
out to cc.

Add a Brewfile so a fresh machine can be provisioned with brew bundle.
Generated from the installed state after auditing every formula against
actual invocation counts in ~/.zsh_history (with shell aliases resolved,
so ls->eza and k->kubectl are credited to the right package).

Drop 16 formulae that the audit found were never invoked and that nothing
else depends on: buildifier, cffi, ffmpeg, fontforge, git-filter-repo,
librsvg, meson, mupdf, mysql@8.0, poppler, qemu, yq, docutils, sphinx-doc,
gobject-introspection and pyenv. Removing them let brew autoremove reclaim
their orphaned dependencies too, taking the formula count from 91 to 75.

pyenv is the notable one: zero invocations against 253 for uv, yet
01-environment.zsh still ran both pyenv init --path and pyenv init -
on every shell start. The zsh side of that is removed separately.

Kept several packages the audit scored at zero, because they are invoked
by other tools rather than typed: git-delta (gitconfig sets it as pager),
tree-sitter-cli (nvim-treesitter compiles parsers with it), gopls,
shellcheck and rust.

Declare the cursor and visual-studio-code casks. Both were installed by
hand, so the 51 vscode extension entries had no editor to install into on
a clean machine.

fix(zsh): drop pyenv, remove dead config, guard gt completions

pyenv was uninstalled in the previous commit but 01-environment.zsh still
ran pyenv init --path and pyenv init -, so every new shell printed two
"command not found: pyenv" errors. Remove both evals along with PYENV_ROOT
and its $PATH entry.

Drop PYTHONPATH. It was set to /opt/homebrew/bin/python3, which is an
interpreter, not a module search path. Python treats PYTHONPATH entries as
directories to search for imports, so pointing it at a binary contributed
nothing and risked confusing imports.

Drop the ~/Library/Python/3.9/bin path entry: that is the system Python
3.9 user-scripts directory, unused now that uv manages Python.

Remove the commented-out NVM block and NVM_DIR export. nvm is not
installed; fnm has replaced it.

Wrap the pasted-in Graphite (gt) yargs completion block in a
(( $+commands[gt] )) guard, and fix its embedded literal newline, which
had been pasted in as a raw line break inside the IFS assignment.

refactor(zsh): XDG layout, starship, mise and atuin; 6x faster startup

Startup drops from ~0.90s to ~0.15s. zprof attributed 731ms of that, 69.5%
of the total, to a single plugin: oh-my-zsh's gpg-agent, which shells out to
gpg-connect-agent on every start. It was redundant anyway. The old .zshrc
already exported GPG_TTY itself and pinentry-mac is configured in
gpg-agent.conf, so the plugin is dropped and GPG_TTY kept.

The rest of the win comes from loading oh-my-zsh a la carte, and from
wrapping every <tool> init in znap eval, which caches the generated
init to disk instead of forking a subshell per shell start.

Restructure to XDG. ~/.zshenv is now the only file in $HOME; it sets
ZDOTDIR to ~/.config/zsh and everything else moves under there, with
rc.d/.zsh replacing ~/.zsh/.zsh. znap's clones move from ~/.zsh_plugins
to ~/.local/share/znap. History moves to ~/.local/state/zsh/history, with
the existing 10494 lines carried over.

History config lives in .zshrc rather than .zshenv, which is not where it
looks like it belongs. macOS ships an /etc/zshrc that hardcodes
HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history, HISTSIZE=2000 and SAVEHIST=1000,
and zsh sources it after .zshenv for every interactive shell. Setting these
in .zshenv looked correct and was silently overwritten; SAVEHIST had been
pinned at oh-my-zsh's 10000 floor rather than the intended value.

Replace p10k with starship, matching the eos-dotfiles and dungeonbooks
repos. The format string is listed explicitly so starship only evaluates
the named modules instead of probing for every language it supports.
kubectl and aws context are on the prompt since kubectl is by a wide margin
the most-used command in this shell's history.

Replace fnm with mise, which also absorbs the pyenv role removed earlier.
uv stays: mise manages interpreter versions, uv manages packages and venvs,
and mise defers to uv for venv creation when it is installed. Worth noting
that fnm's hook is genuinely faster than mise's (3.9ms vs 9.8ms measured
with hyperfine); the tradeoff is one tool for every runtime, and 6ms is
noise next to what was just removed.

Add atuin on Ctrl-R with the existing history imported (10001 commands).
Up and down arrows stay on prefix-aware history search, hence
--disable-up-arrow. Sync is off by default.

Swap alias-tips for zsh-you-should-use. alias-tips cost ~129ms in preexec,
so it was charged on every command rather than once at startup. YSU's
default three-line message is trimmed to a single "alias tip: gst" line.

Set ZSH_CACHE_DIR explicitly. The oh-my-zsh docker and kubectl plugins
cache generated completions there; the framework normally sets it, and
without it they tried to write to /completions and failed.

chore(brew): add mise, atuin, starship and hyperfine to Brewfile

fnm is left in place as a fallback even though mise now handles node; it
is a small formula and its shell hook is measurably faster if the mise
tradeoff ever stops being worth it.

fix(prompt): use starship defaults, set a Nerd Font in ghostty

The first pass at this hand-built a p10k-style powerline and it was the
wrong approach twice over.

Starship has no segment concept, so every powerline background has to be
maintained by hand, and naming modules explicitly in format means any
module not listed never renders at all. That silently hid the gleam module
in taranat.com. Starship's own default is $all, which cannot lose a
module that way, so the explicit format is gone.

It also cannot reproduce what made p10k's rainbow readable: p10k swapped
the git segment's background from green to yellow on a dirty tree, and
starship only makes foreground colours conditional.

Separately, the arrows were never going to render. ghostty's config set no
font-family at all, so it fell back to a system font with no Nerd Font
glyphs. MesloLGS Nerd Font was already installed (p10k shipped it) and
alacritty was already pointing at it; ghostty just had not been told.
Note that ghostty +list-fonts does not enumerate user-installed fonts,
which made the family look unavailable when it was not. "SF Mono Square"
from the delphinus/sfmono-square tap is installed as an alternative.

Keep Nerd Font glyphs for module symbols, but replace the starship defaults
that are actual emoji rather than glyphs; gleam ships a star emoji and has
no Nerd Font glyph, so it gets a text label.

Disable docker_context: with a single context it printed "orbstack" in
every directory containing a compose file.

Leave the kubernetes module enabled even though the kubeconfig is now
empty, so it comes back on its own if a cluster is added rather than being
a module to remember to re-enable.

revert(prompt): back to powerlevel10k, and track .p10k.zsh

Switching to starship was not justified. The ~730ms that made startup slow
was oh-my-zsh's gpg-agent plugin, not the prompt; with that gone, p10k
starts in 0.17s against starship's 0.15s, so the prompt choice costs
essentially nothing either way. Everything the earlier commits fixed stays
fixed.

p10k also does things starship structurally cannot. It swaps a segment's
background colour on a dirty worktree, which is what makes the rainbow
style readable and which starship cannot express -- only foreground colours
are conditional there. It has transient prompt, already enabled in this
config. And p10k configure is a wizard, rather than hand-editing TOML and
guessing hex values, which is how the starship attempt went wrong.

Upstream is in maintenance mode: no new features and most bugs unfixed, but
the author is explicit that it is stable and not expected to break, which
is a reasonable bet for a zsh theme.

Track .p10k.zsh in the repo for the first time. It was 1734 lines living
only in $HOME, so a fresh machine would have come up with an unconfigured
prompt. It now sits in $ZDOTDIR alongside .zshrc.

The instant prompt block goes at the very top of .zshrc, above everything
including the history setup, since it replays a cached prompt and any
earlier output or input would corrupt the replay. No separate
INSTANT_PROMPT setting is needed in .zshrc because .p10k.zsh already sets
it to quiet, which is what keeps the rc.d banner from being reported as
unexpected console output.

Remove the starship config. The formula stays installed for now.

feat(p10k): rebase config on current template, add a mise segment

The local .p10k.zsh was generated in 2022 and had drifted 68 lines from
stock p10k-rainbow, of which only about ten were deliberate. Rather than
patch the old file, this regenerates from the current template and ports
those ten across, so four years of upstream changes come along too.

That drift was hiding two bugs. The old config listed .tool-version as a
directory anchor where stock has .tool-versions -- a missing "s", so the
anchor never matched. And it predates mise entirely, so .mise.toml was
absent from the same list.

Ported: single-line prompt (no newline element either side,
PROMPT_ADD_NEWLINE=false), the vi_mode segment, blanked visual identifiers
on vcs/command_execution_time/time, Nerd Font battery stages,
TRANSIENT_PROMPT=always, INSTANT_PROMPT=quiet, unconditional ahead/behind
counts, and an empty DIR_CLASSES.

DIR_CLASSES is the subtle one: assigning an empty array is not the same as
leaving it unset. Unset, p10k falls back to default directory styling,
which includes a folder icon. That icon appeared in the prompt until the
empty assignment was restored.

Gained from upstream: newer tool detection in the kubecontext, aws, azure
and google_app_cred show-on-command lists (tofu, cdk, kubent, kubecolor,
cmctl, sparkctl), a wider VPN interface pattern, and the chezmoi_shell
segment, which will be useful shortly.

Add a mise prompt segment. p10k has none built in, as it predates mise and
only ships asdf. It parses the nearest .mise.toml or .tool-versions in pure
zsh rather than shelling out: mise current measures at ~15ms, which is
more than p10k's whole budget for a prompt and would be paid on every one.
Reading the file is effectively free and shows what the project pins, which
is the more useful information. The walk stops at /, and the global config
lives at ~/.config/mise/config.toml rather than ~/.mise.toml, so the
segment stays empty outside mise projects.

Comment out the fifteen version-manager segments mise supersedes (asdf,
pyenv, nodenv, nvm, rbenv and friends).

Track the mise config, which existed only in $HOME. It had no [settings] at
all; now it enables lockfile pinning, uv venv auto-activation (uv is the
most-used tool in this shell's history), idiomatic version files for node
and python, auto_install and env_cache.

Remove starship, which the previous commit stopped using.

fix(p10k): escape '#' in prompt_mise, and read idiomatic version files

prompt_mise used ${line%%#} to strip comments, which fails outright under
EXTENDED_GLOB: p10k enables that option, and with it a bare '#' is a pattern
operator, making '#
' an invalid pattern. Every call raised
"prompt_mise:14: bad pattern: #*", and because the failure happened during
prompt rendering it corrupted p10k's expansion and left raw internals like
${(e)_p9k_t[7]} on screen instead of a prompt. Escaping it as # fixes both.

This was missed because the earlier test stubbed p10k and ran without
EXTENDED_GLOB set, so the pattern parsed fine there and only broke in a real
shell. The fixtures now set the option explicitly and cover .mise.toml,
.tool-versions, .nvmrc alone, both together, and no config at all.

Also read .nvmrc, .node-version and .python-version. mise honours these
because idiomatic_version_file_enable_tools is enabled for node and python,
so ignoring them under-reported what was active: guild pins node in .nvmrc
(where CI reads it) and uses mise.toml only for pnpm, so the segment showed
just pnpm while node was equally managed. Files at the same level are
merged, with the toml winning if it names the same tool twice.

fix(p10k): render mise segment with glyphs, cyan, one segment

Three problems, all found by capturing the real prompt through a pty rather
than trusting a stubbed p10k segment.

Icon names do not resolve. Passing -i NODE_ICON printed the literal string
"NODE_ICON": unlike p10k's built-in segments, which hand an icon name to
_p9k_prompt_segment for lookup, p10k segment -i emits its argument
verbatim. Tool glyphs are now literal characters.

Those glyphs are written as \u escapes rather than pasted in. They live in
the Unicode private use area and an earlier attempt to embed them literally
produced an array with 24 keys and 24 empty values -- the fallback to the
plain tool name masked it, so the segment looked merely verbose rather than
broken. Escapes also keep this file ASCII.

Per-tool colours are not achievable and have been dropped. p10k resolves a
segment's style from its name, so every p10k segment call inside
prompt_mise picked up POWERLEVEL9K_MISE_* whatever -b said; the pty capture
showed both segments rendering with background 7 despite being passed 34.
The redundant calls also left a stray empty segment at the right edge, which
was the unexplained gap there. It is one segment again, and tools without a
glyph keep their name as a label.

Background moves from 7 to 6. Seven was inherited from asdf and reads as a
bland gray; cyan is distinct from the blue directory and green vcs segments
on either side.

Cap the tool list at four so a monorepo pinning many tools cannot run the
prompt off the edge.

refactor(p10k): read mise versions from $path, theme-aware colours

Replace the file-parsing mise segment with PATH scraping, adapted from
2KAbhishek/dots2k. mise activation prepends
.../mise/installs///bin, so the active versions are already
in the environment: no config parsing, and it reports what mise actually
resolved rather than what some file requests.

That also fixes a bug the old approach could not. A stray ~/.nvmrc and
~/.node-version, both left over from fnm and both containing "24", meant the
walk up the tree found them from any directory under $HOME, so every project
appeared to pin node. Global versions are now cached once at startup from
mise ls --offline and hidden when they match, so the segment shows only
what a project actually overrides. ssh-bookshop is correctly empty again
while guild still shows node 24.15.0 and pnpm 11.0.9.

Per-tool colours now work, via -s to set a segment state: p10k resolves
style from segment name plus state, so POWERLEVEL9K_MISE__BACKGROUND
applies. The earlier attempt passed -b, which always loses to the
segment-level setting -- a pty capture showed both segments rendering with
background 7 despite being handed 34.

Those colours are palette indices 0-7 rather than 256-colour values. Indices
are what the terminal theme defines, so they follow srcery; fixed values
like 34 and 208 ignore the theme, which is why they looked foreign next to
the surrounding segments.

Glyphs are written as \u escape sequences. Embedding the literal characters
failed three times: they sit in the Unicode private use area and were
silently stripped to empty strings on write, leaving an array of 24 keys
with 24 empty values. The fallback to the tool name hid it, so the segment
looked merely verbose rather than broken.

Update MesloLGS Nerd Font from the 2022 v2 build to the 2025 v3 build. The
family name is unchanged, so ghostty needs no edit; the old files are
archived in ~/Downloads/meslo-v2-backup.

chore: remove fnm-era leftovers

Reclaims about 1.65GB and removes a hidden global node pin.

~/.nvmrc and ~/.node-version both contained "24" and were left over from
fnm. Because idiomatic_version_file_enable_tools is enabled for node, mise
honoured them, so every directory under $HOME inherited a node pin from
$HOME itself -- which is why ssh-bookshop appeared to pin node 24 despite
having no version file of its own. The mise prompt segment's hide-global
logic already masked the symptom; this removes the cause.

Deleted: ~/.local/share/fnm (1.0G of node builds), ~/.nvm (646M, from
before fnm), ~/.local/state/fnm_multishells, and ~/.pyenv (empty shims and
versions directories left behind when pyenv was uninstalled). Uninstall the
fnm formula and drop it from the Brewfile; it had been kept as a fallback
but mise has fully replaced it.

Also remove ~/.npmrc. Its only content was a scoped registry pointing at a
former employer's internal Nexus, which no longer resolves, so any install
of a package in that scope would have hung.

Small files are backed up to ~/Downloads/fnm-era-backup.

feat(zsh): add mise-prompt-debug as a shell function

Explains what the mise prompt segment is doing: the cached global versions,
each mise PATH entry raw and symlink-resolved, whether each is hidden or
shown and why, and what the segment finally renders.

It has to be a function rather than a script. The state it reports on lives
in shell variables (p9k_mise_global and the POWERLEVEL9K_MISE* settings),
which a separate process cannot see -- an earlier standalone script version
reported an empty global map and so claimed every tool would be SHOWN, when
the segment was in fact correctly hiding them.

refactor(p10k): split config into vendored template plus overrides

The p10k config was a single 1928-line file with about a dozen local edits
scattered through it. That makes upstream updates effectively impossible to
review: regenerating produces a diff where personal settings and template
churn are indistinguishable, and it is easy to silently lose a customisation
-- which already happened once with DIR_CLASSES, whose absence put a folder
icon back in the prompt.

Split into prompt/, following the layout in 2KAbhishek/dots2k:

init.zsh sources the other three in order; later files win
p10k.zsh stock p10k-rainbow vendored verbatim, byte-identical to
upstream and never hand-edited
overrides.zsh every local customisation, 175 lines
mise.zsh the mise segment, which p10k has no equivalent for

Taking a p10k update is now cp over p10k.zsh, with any conflict showing up
as an error rather than a silent loss.

my_git_formatter is redefined in overrides.zsh rather than patched into the
vendored copy. The only difference from stock is that ahead/behind counts
print unconditionally, where stock wraps them in a conditional with an
elif that shows the remote branch name instead.

BATTERY_STAGES needs an explicit unset first. The template declares it as an
array and this config wants a scalar string of glyphs; both are valid to
p10k, but zsh refuses to change a variable's type in place and the
assignment failed with "inconsistent type for assignment".

Verified the rendered prompt is unchanged: guild still shows the branch plus
pnpm 11.0.9 and node 24.15.0, transient prompt, instant prompt, single-line
layout and empty DIR_CLASSES all survive, and startup is ~0.17s.

chore(git): modern defaults, drop dead maintenance repo list

The [maintenance] section listed 30 repositories under ~/.zsh_plugins, a
path that no longer exists: the XDG restructure moved znap's clones to
~/.local/share/znap. Every entry was a dead path. No launchd job was
registered either, so git maintenance had not been running regardless.
znap manages plugin updates itself, so the section is removed rather than
repointed.

Add settings that pay for themselves on any rebase-heavy workflow:

rerere.enabled + autoUpdate record a conflict resolution once and replay
it when the same conflict reappears
rebase.updateRefs move stacked branches along with a rebase
instead of leaving them on pre-rebase commits
rebase.autoStash stash and restore a dirty worktree rather
than refusing to start
fetch.prune + pruneTags drop local refs for deleted remote branches
branch.sort = -committerdate most recent branches first
column.ui = auto multi-column listings
diff.algorithm = histogram clearer diffs than Myers when blocks move
diff.renames = copies detect copies as well as renames

Upgrade merge.conflictstyle from diff3 to zdiff3, which keeps the common
ancestor context but omits lines both sides already agree on, giving smaller
conflict hunks. Requires git >= 2.35; this machine is on 2.55.

Fold the stray second [diff] section into one. Duplicate sections are legal
and git merges them, but having diff.tool separated from the rest was only
going to confuse.

GPG commit signing is left as it is. Switching to SSH signing
(gpg.format = ssh) would avoid the GPG keyring, but the current setup works
and the existing key is already trusted on GitHub.

chore(git): sign commits with SSH instead of GPG

GitHub already has the SSH signing key registered, so this drops the GPG
dependency for signing: no keyring, no gpg-agent, no pinentry prompt, and
the same key that already authenticates to GitHub now signs.

user.signingkey changes meaning with this: under gpg.format = ssh it is a
path to a public key rather than a GPG key id. The old GPG key
CCACDB1344B8B7EC0B4DD439655CBB5895152028 remains in this file's history if
it is ever needed for verifying old commits.

The [gpg] section name and gpgsign = true stay as they are. Those names
are historical -- git kept them when it added other signing formats, so
gpgsign with format = ssh means SSH-signed, not GPG-signed.

Add an allowed_signers file mapping the identity to the key. Without it
local verification fails with "No principal matched" even for signatures git
has just created, because git cannot otherwise know which key may sign for
which identity. It contains only public keys, so it is safe to commit.

Verified end to end: a test commit signs and git verify-commit reports a
good signature.

Note the key is 3072-bit RSA from 2022. It works and GitHub accepts it, but
ed25519 would be the modern choice; switching means generating a key and
registering it as a signing key on GitHub.

gpg itself is left installed, since gpg-agent.conf and pinentry-mac are
still configured and may be used for things other than git.

refactor(zsh): source work config from a directory, not a symlink

The work submodule's dungeonbooks file was reached via a
../../../../work/dungeonbooks/zsh/.zsh/ relative symlink sitting inside
rc.d. That path had to be recomputed by hand when the zsh config moved to
XDG, and broke silently in between. It also meant a public repo contained a
symlink whose target only resolves if a private submodule happens to be
checked out.

.zshrc now globs $ZDOTDIR/work/*.zsh as a second pass after rc.d, so work
config can override the public config and simply does not load on a machine
without access to the private repo. The work repo has been flattened to
match: files live at its root rather than in a stow-shaped
dungeonbooks/zsh/.zsh/ tree.

Also drops rokt from that repo. It defined AWS SSO profiles, EKS kubeconfig
commands and go run wrappers for a job left two years ago, against clusters
already removed from the kubeconfig.

~/.config/zsh/work is currently a hand-made symlink to the submodule. The
chezmoi migration will replace it with a .chezmoiexternal.toml entry that
clones the private repo directly, at which point the submodule goes away.

chore: drop fish, sync alacritty config from disk

fish is not installed and its config dates from 2022; the "fish" match in
the Brewfile was a word inside atuin's description. Also removes a stray
vim/.claude/settings.local.json, which was never tracked and is not config.

alacritty.toml had silently diverged. Unlike every other package it was a
real file in ~/.config rather than a stow symlink, so edits made on disk in
October 2025 never reached this repo. The live copy uses [terminal.shell]
and [terminal], the schema alacritty has wanted since 0.14 (0.17 is
installed); the repo still had the pre-0.14 [shell]. Taking the live version
here, since migrating from the repo copy would have silently reverted the
config to a deprecated schema.

Found by diffing every tracked file against its deployed counterpart before
the chezmoi migration. It was the only genuine divergence.

feat: migrate from GNU stow to chezmoi

Replaces per-package symlink farms with a single source tree that chezmoi
renders into $HOME. The layout maps by filename rather than by position:
dot_config/zsh/dot_zshrc becomes ~/.config/zsh/.zshrc, and executable_
marks files that need +x on disk.

The practical difference is that a fresh machine is now two commands
(chezmoi init --apply, then brew bundle) instead of an undocumented sequence
of stow invocations plus hand-cloning znap.

Both git submodules are retired in favour of .chezmoiexternal.toml entries.
A submodule pins a SHA that must be committed here and needs
--recurse-submodules on clone; externals are fetched on apply and refresh on
their own schedule. znap is now an external too -- it had been cloned by
hand, so a fresh machine would have come up with no plugins and no prompt,
which is exactly the sort of undocumented step this migration removes.

Verified before switching by building the source tree in a scratch
directory and comparing every managed file's rendered content against the
live copy: all 58 were byte-identical. Every path still shows in chezmoi diff because stow deployed symlinks and chezmoi writes regular files, so
the type changes even where content does not.

Post-apply checks: shell starts clean in ~0.17s, ZDOTDIR and HISTFILE
resolve, node comes from mise, the p10k prompt and mise segment load, atuin
holds Ctrl-R, and the private work aliases load from the external.

Add a README covering the bootstrap, the layout, the prompt split, the
externals and the edit-source-not-target workflow.

chore: remove stow

Nothing uses it now that chezmoi manages the tree.

feat(zsh): point config-edit aliases at chezmoi

The zshrc/zshalias/vimrc family opened the deployed file directly. Under
chezmoi that edits a rendered artifact: it works until the next apply
silently overwrites it. They now go through chezmoi edit --apply, which
opens the source and writes the result out in one step.

Adds the usual chezmoi shortcuts (cm, cma, cmd, cme, cmu, cms, cmcd, cmadd)
plus zshprompt, gitconfig and brewfile, which had no alias before.

fix(aerospace): make the alt-enter ghostty check actually match

pgrep -x ghostty could never succeed. macOS truncates a process name to 16
characters, so Ghostty appears as "/Applications/Gh" and the exact-name match
always failed. The binding fell through to open -a, which focuses the
existing window rather than opening a new one -- so alt-enter appeared to
stop working.

Match on the bundle path instead, including Contents/MacOS. The narrower
path matters: a looser pgrep -f Ghostty.app matches the full command line
including environment, and every process launched from a Ghostty terminal
inherits GHOSTTY_RESOURCES_DIR pointing at Contents/Resources. That made an
unrelated npm process match during testing, so Ghostty would have looked
like it was running long after being quit.

The AppleScript half needed no change: System Events still refers to the
process as "ghostty" regardless of the truncated name.

feat(zsh): fzf-tab previews, git workflow functions, aube

Adds rc.d/05-completion.zsh. fzf-tab was already loaded but had no zstyles,
so it replaced the completion menu with fzf and then showed nothing in the
preview pane, which is most of the point. Previews are now wired per command:
directories list through eza, files render through bat, and git dispatches on
the completion group so that tab-completing git checkout previews a branch's
log, a modified file's diff through delta, or a commit's contents, depending
on what is under the cursor. Also covers chezmoi (shows what applying would
change), mise, brew and kill.

Adds rc.d/04-functions.zsh:

review browse everything changed on this branch, diff in the
preview pane, enter opens the file
_git_run_on_files run a command over only the changed files, split by
"diff" (this branch vs its base) and "modified" (dirty
worktree) -- the two questions have different answers
lintjs/lintpy/... thin wrappers over the above per toolchain
search rg to fzf to $EDITOR, opening at the matched line
plz, xin sudo the last command; run something in another dir

The base branch is resolved from origin/HEAD rather than hardcoded to main,
falling back to asking the remote and then to conventional names, so repos on
master or develop work unchanged.

Adds aube to the mise global config, pinned to major version 1 rather than
"latest" so it cannot shift under a project mid-work.

Startup goes from ~0.17s to ~0.20s, which is the completion zstyles being
evaluated at load.

ptaranat added 23 commits July 19, 2026 10:35
The binary is not installed, so znap ran the completion command anyway and
every new shell opened with a `command not found`.
Rename the Claude Code alias cc -> cl. As `cc` it shadowed the C compiler
outright: `command -v cc` resolved to the alias rather than /usr/bin/cc,
which breaks any ./configure, make, or native gem/npm build that shells
out to cc.

Add a Brewfile so a fresh machine can be provisioned with `brew bundle`.
Generated from the installed state after auditing every formula against
actual invocation counts in ~/.zsh_history (with shell aliases resolved,
so `ls`->eza and `k`->kubectl are credited to the right package).

Drop 16 formulae that the audit found were never invoked and that nothing
else depends on: buildifier, cffi, ffmpeg, fontforge, git-filter-repo,
librsvg, meson, mupdf, mysql@8.0, poppler, qemu, yq, docutils, sphinx-doc,
gobject-introspection and pyenv. Removing them let brew autoremove reclaim
their orphaned dependencies too, taking the formula count from 91 to 75.

pyenv is the notable one: zero invocations against 253 for uv, yet
01-environment.zsh still ran both `pyenv init --path` and `pyenv init -`
on every shell start. The zsh side of that is removed separately.

Kept several packages the audit scored at zero, because they are invoked
by other tools rather than typed: git-delta (gitconfig sets it as pager),
tree-sitter-cli (nvim-treesitter compiles parsers with it), gopls,
shellcheck and rust.

Declare the cursor and visual-studio-code casks. Both were installed by
hand, so the 51 vscode extension entries had no editor to install into on
a clean machine.
pyenv was uninstalled in the previous commit but 01-environment.zsh still
ran `pyenv init --path` and `pyenv init -`, so every new shell printed two
"command not found: pyenv" errors. Remove both evals along with PYENV_ROOT
and its $PATH entry.

Drop PYTHONPATH. It was set to /opt/homebrew/bin/python3, which is an
interpreter, not a module search path. Python treats PYTHONPATH entries as
directories to search for imports, so pointing it at a binary contributed
nothing and risked confusing imports.

Drop the ~/Library/Python/3.9/bin path entry: that is the system Python
3.9 user-scripts directory, unused now that uv manages Python.

Remove the commented-out NVM block and NVM_DIR export. nvm is not
installed; fnm has replaced it.

Wrap the pasted-in Graphite (gt) yargs completion block in a
`(( $+commands[gt] ))` guard, and fix its embedded literal newline, which
had been pasted in as a raw line break inside the IFS assignment.
Startup drops from ~0.90s to ~0.15s. zprof attributed 731ms of that, 69.5%
of the total, to a single plugin: oh-my-zsh's gpg-agent, which shells out to
gpg-connect-agent on every start. It was redundant anyway. The old .zshrc
already exported GPG_TTY itself and pinentry-mac is configured in
gpg-agent.conf, so the plugin is dropped and GPG_TTY kept.

The rest of the win comes from loading oh-my-zsh a la carte, and from
wrapping every `<tool> init` in `znap eval`, which caches the generated
init to disk instead of forking a subshell per shell start.

Restructure to XDG. ~/.zshenv is now the only file in $HOME; it sets
ZDOTDIR to ~/.config/zsh and everything else moves under there, with
rc.d/*.zsh replacing ~/.zsh/*.zsh. znap's clones move from ~/.zsh_plugins
to ~/.local/share/znap. History moves to ~/.local/state/zsh/history, with
the existing 10494 lines carried over.

History config lives in .zshrc rather than .zshenv, which is not where it
looks like it belongs. macOS ships an /etc/zshrc that hardcodes
HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history, HISTSIZE=2000 and SAVEHIST=1000,
and zsh sources it after .zshenv for every interactive shell. Setting these
in .zshenv looked correct and was silently overwritten; SAVEHIST had been
pinned at oh-my-zsh's 10000 floor rather than the intended value.

Replace p10k with starship, matching the eos-dotfiles and dungeonbooks
repos. The format string is listed explicitly so starship only evaluates
the named modules instead of probing for every language it supports.
kubectl and aws context are on the prompt since kubectl is by a wide margin
the most-used command in this shell's history.

Replace fnm with mise, which also absorbs the pyenv role removed earlier.
uv stays: mise manages interpreter versions, uv manages packages and venvs,
and mise defers to uv for venv creation when it is installed. Worth noting
that fnm's hook is genuinely faster than mise's (3.9ms vs 9.8ms measured
with hyperfine); the tradeoff is one tool for every runtime, and 6ms is
noise next to what was just removed.

Add atuin on Ctrl-R with the existing history imported (10001 commands).
Up and down arrows stay on prefix-aware history search, hence
--disable-up-arrow. Sync is off by default.

Swap alias-tips for zsh-you-should-use. alias-tips cost ~129ms in preexec,
so it was charged on every command rather than once at startup. YSU's
default three-line message is trimmed to a single "alias tip: gst" line.

Set ZSH_CACHE_DIR explicitly. The oh-my-zsh docker and kubectl plugins
cache generated completions there; the framework normally sets it, and
without it they tried to write to /completions and failed.
fnm is left in place as a fallback even though mise now handles node; it
is a small formula and its shell hook is measurably faster if the mise
tradeoff ever stops being worth it.
The first pass at this hand-built a p10k-style powerline and it was the
wrong approach twice over.

Starship has no segment concept, so every powerline background has to be
maintained by hand, and naming modules explicitly in `format` means any
module not listed never renders at all. That silently hid the gleam module
in taranat.com. Starship's own default is `$all`, which cannot lose a
module that way, so the explicit format is gone.

It also cannot reproduce what made p10k's rainbow readable: p10k swapped
the git segment's background from green to yellow on a dirty tree, and
starship only makes foreground colours conditional.

Separately, the arrows were never going to render. ghostty's config set no
font-family at all, so it fell back to a system font with no Nerd Font
glyphs. MesloLGS Nerd Font was already installed (p10k shipped it) and
alacritty was already pointing at it; ghostty just had not been told.
Note that `ghostty +list-fonts` does not enumerate user-installed fonts,
which made the family look unavailable when it was not. "SF Mono Square"
from the delphinus/sfmono-square tap is installed as an alternative.

Keep Nerd Font glyphs for module symbols, but replace the starship defaults
that are actual emoji rather than glyphs; gleam ships a star emoji and has
no Nerd Font glyph, so it gets a text label.

Disable docker_context: with a single context it printed "orbstack" in
every directory containing a compose file.

Leave the kubernetes module enabled even though the kubeconfig is now
empty, so it comes back on its own if a cluster is added rather than being
a module to remember to re-enable.
Switching to starship was not justified. The ~730ms that made startup slow
was oh-my-zsh's gpg-agent plugin, not the prompt; with that gone, p10k
starts in 0.17s against starship's 0.15s, so the prompt choice costs
essentially nothing either way. Everything the earlier commits fixed stays
fixed.

p10k also does things starship structurally cannot. It swaps a segment's
background colour on a dirty worktree, which is what makes the rainbow
style readable and which starship cannot express -- only foreground colours
are conditional there. It has transient prompt, already enabled in this
config. And `p10k configure` is a wizard, rather than hand-editing TOML and
guessing hex values, which is how the starship attempt went wrong.

Upstream is in maintenance mode: no new features and most bugs unfixed, but
the author is explicit that it is stable and not expected to break, which
is a reasonable bet for a zsh theme.

Track .p10k.zsh in the repo for the first time. It was 1734 lines living
only in $HOME, so a fresh machine would have come up with an unconfigured
prompt. It now sits in $ZDOTDIR alongside .zshrc.

The instant prompt block goes at the very top of .zshrc, above everything
including the history setup, since it replays a cached prompt and any
earlier output or input would corrupt the replay. No separate
INSTANT_PROMPT setting is needed in .zshrc because .p10k.zsh already sets
it to quiet, which is what keeps the rc.d banner from being reported as
unexpected console output.

Remove the starship config. The formula stays installed for now.
The local .p10k.zsh was generated in 2022 and had drifted 68 lines from
stock p10k-rainbow, of which only about ten were deliberate. Rather than
patch the old file, this regenerates from the current template and ports
those ten across, so four years of upstream changes come along too.

That drift was hiding two bugs. The old config listed `.tool-version` as a
directory anchor where stock has `.tool-versions` -- a missing "s", so the
anchor never matched. And it predates mise entirely, so `.mise.toml` was
absent from the same list.

Ported: single-line prompt (no `newline` element either side,
PROMPT_ADD_NEWLINE=false), the vi_mode segment, blanked visual identifiers
on vcs/command_execution_time/time, Nerd Font battery stages,
TRANSIENT_PROMPT=always, INSTANT_PROMPT=quiet, unconditional ahead/behind
counts, and an empty DIR_CLASSES.

DIR_CLASSES is the subtle one: assigning an empty array is not the same as
leaving it unset. Unset, p10k falls back to default directory styling,
which includes a folder icon. That icon appeared in the prompt until the
empty assignment was restored.

Gained from upstream: newer tool detection in the kubecontext, aws, azure
and google_app_cred show-on-command lists (tofu, cdk, kubent, kubecolor,
cmctl, sparkctl), a wider VPN interface pattern, and the chezmoi_shell
segment, which will be useful shortly.

Add a mise prompt segment. p10k has none built in, as it predates mise and
only ships asdf. It parses the nearest .mise.toml or .tool-versions in pure
zsh rather than shelling out: `mise current` measures at ~15ms, which is
more than p10k's whole budget for a prompt and would be paid on every one.
Reading the file is effectively free and shows what the project pins, which
is the more useful information. The walk stops at /, and the global config
lives at ~/.config/mise/config.toml rather than ~/.mise.toml, so the
segment stays empty outside mise projects.

Comment out the fifteen version-manager segments mise supersedes (asdf,
pyenv, nodenv, nvm, rbenv and friends).

Track the mise config, which existed only in $HOME. It had no [settings] at
all; now it enables lockfile pinning, uv venv auto-activation (uv is the
most-used tool in this shell's history), idiomatic version files for node
and python, auto_install and env_cache.

Remove starship, which the previous commit stopped using.
prompt_mise used ${line%%#*} to strip comments, which fails outright under
EXTENDED_GLOB: p10k enables that option, and with it a bare '#' is a pattern
operator, making '#*' an invalid pattern. Every call raised
"prompt_mise:14: bad pattern: #*", and because the failure happened during
prompt rendering it corrupted p10k's expansion and left raw internals like
${(e)_p9k_t[7]} on screen instead of a prompt. Escaping it as \# fixes both.

This was missed because the earlier test stubbed p10k and ran without
EXTENDED_GLOB set, so the pattern parsed fine there and only broke in a real
shell. The fixtures now set the option explicitly and cover .mise.toml,
.tool-versions, .nvmrc alone, both together, and no config at all.

Also read .nvmrc, .node-version and .python-version. mise honours these
because idiomatic_version_file_enable_tools is enabled for node and python,
so ignoring them under-reported what was active: guild pins node in .nvmrc
(where CI reads it) and uses mise.toml only for pnpm, so the segment showed
just pnpm while node was equally managed. Files at the same level are
merged, with the toml winning if it names the same tool twice.
Three problems, all found by capturing the real prompt through a pty rather
than trusting a stubbed `p10k segment`.

Icon names do not resolve. Passing -i NODE_ICON printed the literal string
"NODE_ICON": unlike p10k's built-in segments, which hand an icon name to
_p9k_prompt_segment for lookup, `p10k segment -i` emits its argument
verbatim. Tool glyphs are now literal characters.

Those glyphs are written as \u escapes rather than pasted in. They live in
the Unicode private use area and an earlier attempt to embed them literally
produced an array with 24 keys and 24 empty values -- the fallback to the
plain tool name masked it, so the segment looked merely verbose rather than
broken. Escapes also keep this file ASCII.

Per-tool colours are not achievable and have been dropped. p10k resolves a
segment's style from its name, so every `p10k segment` call inside
prompt_mise picked up POWERLEVEL9K_MISE_* whatever -b said; the pty capture
showed both segments rendering with background 7 despite being passed 34.
The redundant calls also left a stray empty segment at the right edge, which
was the unexplained gap there. It is one segment again, and tools without a
glyph keep their name as a label.

Background moves from 7 to 6. Seven was inherited from asdf and reads as a
bland gray; cyan is distinct from the blue directory and green vcs segments
on either side.

Cap the tool list at four so a monorepo pinning many tools cannot run the
prompt off the edge.
Replace the file-parsing mise segment with PATH scraping, adapted from
2KAbhishek/dots2k. mise activation prepends
.../mise/installs/<tool>/<version>/bin, so the active versions are already
in the environment: no config parsing, and it reports what mise actually
resolved rather than what some file requests.

That also fixes a bug the old approach could not. A stray ~/.nvmrc and
~/.node-version, both left over from fnm and both containing "24", meant the
walk up the tree found them from any directory under $HOME, so every project
appeared to pin node. Global versions are now cached once at startup from
`mise ls --offline` and hidden when they match, so the segment shows only
what a project actually overrides. ssh-bookshop is correctly empty again
while guild still shows node 24.15.0 and pnpm 11.0.9.

Per-tool colours now work, via -s to set a segment state: p10k resolves
style from segment name plus state, so POWERLEVEL9K_MISE_<TOOL>_BACKGROUND
applies. The earlier attempt passed -b, which always loses to the
segment-level setting -- a pty capture showed both segments rendering with
background 7 despite being handed 34.

Those colours are palette indices 0-7 rather than 256-colour values. Indices
are what the terminal theme defines, so they follow srcery; fixed values
like 34 and 208 ignore the theme, which is why they looked foreign next to
the surrounding segments.

Glyphs are written as \u escape sequences. Embedding the literal characters
failed three times: they sit in the Unicode private use area and were
silently stripped to empty strings on write, leaving an array of 24 keys
with 24 empty values. The fallback to the tool name hid it, so the segment
looked merely verbose rather than broken.

Update MesloLGS Nerd Font from the 2022 v2 build to the 2025 v3 build. The
family name is unchanged, so ghostty needs no edit; the old files are
archived in ~/Downloads/meslo-v2-backup.
Reclaims about 1.65GB and removes a hidden global node pin.

~/.nvmrc and ~/.node-version both contained "24" and were left over from
fnm. Because idiomatic_version_file_enable_tools is enabled for node, mise
honoured them, so every directory under $HOME inherited a node pin from
$HOME itself -- which is why ssh-bookshop appeared to pin node 24 despite
having no version file of its own. The mise prompt segment's hide-global
logic already masked the symptom; this removes the cause.

Deleted: ~/.local/share/fnm (1.0G of node builds), ~/.nvm (646M, from
before fnm), ~/.local/state/fnm_multishells, and ~/.pyenv (empty shims and
versions directories left behind when pyenv was uninstalled). Uninstall the
fnm formula and drop it from the Brewfile; it had been kept as a fallback
but mise has fully replaced it.

Also remove ~/.npmrc. Its only content was a scoped registry pointing at a
former employer's internal Nexus, which no longer resolves, so any install
of a package in that scope would have hung.

Small files are backed up to ~/Downloads/fnm-era-backup.
Explains what the mise prompt segment is doing: the cached global versions,
each mise PATH entry raw and symlink-resolved, whether each is hidden or
shown and why, and what the segment finally renders.

It has to be a function rather than a script. The state it reports on lives
in shell variables (_p9k_mise_global and the POWERLEVEL9K_MISE_* settings),
which a separate process cannot see -- an earlier standalone script version
reported an empty global map and so claimed every tool would be SHOWN, when
the segment was in fact correctly hiding them.
The p10k config was a single 1928-line file with about a dozen local edits
scattered through it. That makes upstream updates effectively impossible to
review: regenerating produces a diff where personal settings and template
churn are indistinguishable, and it is easy to silently lose a customisation
-- which already happened once with DIR_CLASSES, whose absence put a folder
icon back in the prompt.

Split into prompt/, following the layout in 2KAbhishek/dots2k:

  init.zsh       sources the other three in order; later files win
  p10k.zsh       stock p10k-rainbow vendored verbatim, byte-identical to
                 upstream and never hand-edited
  overrides.zsh  every local customisation, 175 lines
  mise.zsh       the mise segment, which p10k has no equivalent for

Taking a p10k update is now `cp` over p10k.zsh, with any conflict showing up
as an error rather than a silent loss.

my_git_formatter is redefined in overrides.zsh rather than patched into the
vendored copy. The only difference from stock is that ahead/behind counts
print unconditionally, where stock wraps them in a conditional with an
`elif` that shows the remote branch name instead.

BATTERY_STAGES needs an explicit unset first. The template declares it as an
array and this config wants a scalar string of glyphs; both are valid to
p10k, but zsh refuses to change a variable's type in place and the
assignment failed with "inconsistent type for assignment".

Verified the rendered prompt is unchanged: guild still shows the branch plus
pnpm 11.0.9 and node 24.15.0, transient prompt, instant prompt, single-line
layout and empty DIR_CLASSES all survive, and startup is ~0.17s.
The [maintenance] section listed 30 repositories under ~/.zsh_plugins, a
path that no longer exists: the XDG restructure moved znap's clones to
~/.local/share/znap. Every entry was a dead path. No launchd job was
registered either, so `git maintenance` had not been running regardless.
znap manages plugin updates itself, so the section is removed rather than
repointed.

Add settings that pay for themselves on any rebase-heavy workflow:

  rerere.enabled + autoUpdate  record a conflict resolution once and replay
                               it when the same conflict reappears
  rebase.updateRefs            move stacked branches along with a rebase
                               instead of leaving them on pre-rebase commits
  rebase.autoStash             stash and restore a dirty worktree rather
                               than refusing to start
  fetch.prune + pruneTags      drop local refs for deleted remote branches
  branch.sort = -committerdate most recent branches first
  column.ui = auto             multi-column listings
  diff.algorithm = histogram   clearer diffs than Myers when blocks move
  diff.renames = copies        detect copies as well as renames

Upgrade merge.conflictstyle from diff3 to zdiff3, which keeps the common
ancestor context but omits lines both sides already agree on, giving smaller
conflict hunks. Requires git >= 2.35; this machine is on 2.55.

Fold the stray second [diff] section into one. Duplicate sections are legal
and git merges them, but having diff.tool separated from the rest was only
going to confuse.

GPG commit signing is left as it is. Switching to SSH signing
(gpg.format = ssh) would avoid the GPG keyring, but the current setup works
and the existing key is already trusted on GitHub.
GitHub already has the SSH signing key registered, so this drops the GPG
dependency for signing: no keyring, no gpg-agent, no pinentry prompt, and
the same key that already authenticates to GitHub now signs.

user.signingkey changes meaning with this: under gpg.format = ssh it is a
path to a public key rather than a GPG key id. The old GPG key
CCACDB1344B8B7EC0B4DD439655CBB5895152028 remains in this file's history if
it is ever needed for verifying old commits.

The [gpg] section name and `gpgsign = true` stay as they are. Those names
are historical -- git kept them when it added other signing formats, so
gpgsign with format = ssh means SSH-signed, not GPG-signed.

Add an allowed_signers file mapping the identity to the key. Without it
local verification fails with "No principal matched" even for signatures git
has just created, because git cannot otherwise know which key may sign for
which identity. It contains only public keys, so it is safe to commit.

Verified end to end: a test commit signs and `git verify-commit` reports a
good signature.

Note the key is 3072-bit RSA from 2022. It works and GitHub accepts it, but
ed25519 would be the modern choice; switching means generating a key and
registering it as a signing key on GitHub.

gpg itself is left installed, since gpg-agent.conf and pinentry-mac are
still configured and may be used for things other than git.
The work submodule's dungeonbooks file was reached via a
../../../../work/dungeonbooks/zsh/.zsh/ relative symlink sitting inside
rc.d. That path had to be recomputed by hand when the zsh config moved to
XDG, and broke silently in between. It also meant a public repo contained a
symlink whose target only resolves if a private submodule happens to be
checked out.

.zshrc now globs $ZDOTDIR/work/*.zsh as a second pass after rc.d, so work
config can override the public config and simply does not load on a machine
without access to the private repo. The work repo has been flattened to
match: files live at its root rather than in a stow-shaped
dungeonbooks/zsh/.zsh/ tree.

Also drops rokt from that repo. It defined AWS SSO profiles, EKS kubeconfig
commands and go run wrappers for a job left two years ago, against clusters
already removed from the kubeconfig.

~/.config/zsh/work is currently a hand-made symlink to the submodule. The
chezmoi migration will replace it with a .chezmoiexternal.toml entry that
clones the private repo directly, at which point the submodule goes away.
fish is not installed and its config dates from 2022; the "fish" match in
the Brewfile was a word inside atuin's description. Also removes a stray
vim/.claude/settings.local.json, which was never tracked and is not config.

alacritty.toml had silently diverged. Unlike every other package it was a
real file in ~/.config rather than a stow symlink, so edits made on disk in
October 2025 never reached this repo. The live copy uses [terminal.shell]
and [terminal], the schema alacritty has wanted since 0.14 (0.17 is
installed); the repo still had the pre-0.14 [shell]. Taking the live version
here, since migrating from the repo copy would have silently reverted the
config to a deprecated schema.

Found by diffing every tracked file against its deployed counterpart before
the chezmoi migration. It was the only genuine divergence.
Replaces per-package symlink farms with a single source tree that chezmoi
renders into $HOME. The layout maps by filename rather than by position:
dot_config/zsh/dot_zshrc becomes ~/.config/zsh/.zshrc, and executable_
marks files that need +x on disk.

The practical difference is that a fresh machine is now two commands
(chezmoi init --apply, then brew bundle) instead of an undocumented sequence
of stow invocations plus hand-cloning znap.

Both git submodules are retired in favour of .chezmoiexternal.toml entries.
A submodule pins a SHA that must be committed here and needs
--recurse-submodules on clone; externals are fetched on apply and refresh on
their own schedule. znap is now an external too -- it had been cloned by
hand, so a fresh machine would have come up with no plugins and no prompt,
which is exactly the sort of undocumented step this migration removes.

Verified before switching by building the source tree in a scratch
directory and comparing every managed file's rendered content against the
live copy: all 58 were byte-identical. Every path still shows in `chezmoi
diff` because stow deployed symlinks and chezmoi writes regular files, so
the type changes even where content does not.

Post-apply checks: shell starts clean in ~0.17s, ZDOTDIR and HISTFILE
resolve, node comes from mise, the p10k prompt and mise segment load, atuin
holds Ctrl-R, and the private work aliases load from the external.

Add a README covering the bootstrap, the layout, the prompt split, the
externals and the edit-source-not-target workflow.
Nothing uses it now that chezmoi manages the tree.
The zshrc/zshalias/vimrc family opened the deployed file directly. Under
chezmoi that edits a rendered artifact: it works until the next apply
silently overwrites it. They now go through `chezmoi edit --apply`, which
opens the source and writes the result out in one step.

Adds the usual chezmoi shortcuts (cm, cma, cmd, cme, cmu, cms, cmcd, cmadd)
plus zshprompt, gitconfig and brewfile, which had no alias before.
`pgrep -x ghostty` could never succeed. macOS truncates a process name to 16
characters, so Ghostty appears as "/Applications/Gh" and the exact-name match
always failed. The binding fell through to `open -a`, which focuses the
existing window rather than opening a new one -- so alt-enter appeared to
stop working.

Match on the bundle path instead, including Contents/MacOS. The narrower
path matters: a looser `pgrep -f Ghostty.app` matches the full command line
including environment, and every process launched from a Ghostty terminal
inherits GHOSTTY_RESOURCES_DIR pointing at Contents/Resources. That made an
unrelated npm process match during testing, so Ghostty would have looked
like it was running long after being quit.

The AppleScript half needed no change: System Events still refers to the
process as "ghostty" regardless of the truncated name.
Adds rc.d/05-completion.zsh. fzf-tab was already loaded but had no zstyles,
so it replaced the completion menu with fzf and then showed nothing in the
preview pane, which is most of the point. Previews are now wired per command:
directories list through eza, files render through bat, and git dispatches on
the completion group so that tab-completing `git checkout` previews a branch's
log, a modified file's diff through delta, or a commit's contents, depending
on what is under the cursor. Also covers chezmoi (shows what applying would
change), mise, brew and kill.

Adds rc.d/04-functions.zsh:

  review              browse everything changed on this branch, diff in the
                      preview pane, enter opens the file
  _git_run_on_files   run a command over only the changed files, split by
                      "diff" (this branch vs its base) and "modified" (dirty
                      worktree) -- the two questions have different answers
  lintjs/lintpy/...   thin wrappers over the above per toolchain
  search              rg to fzf to $EDITOR, opening at the matched line
  plz, xin            sudo the last command; run something in another dir

The base branch is resolved from origin/HEAD rather than hardcoded to main,
falling back to asking the remote and then to conventional names, so repos on
master or develop work unchanged.

Adds aube to the mise global config, pinned to major version 1 rather than
"latest" so it cannot shift under a project mid-work.

Startup goes from ~0.17s to ~0.20s, which is the completion zstyles being
evaluated at load.

ptaranat commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

`[url "git@github.com:"] insteadOf = https://github.com/` came from an old
"git: force ssh" commit and rewrote every GitHub HTTPS URL to SSH.

It now costs more than it gives. Every tool that clones from GitHub over
HTTPS -- lazy.nvim plugins, mason servers, go modules, npm git dependencies,
cargo -- became an SSH operation needing an unlocked agent, which is why a
neovim plugin install printed "sign_and_send_pubkey: signing failed for RSA"
for most of its forty plugins. It also breaks the bootstrap this repo now
documents: a fresh machine cannot clone anything from GitHub until keys are
generated and registered, where public HTTPS clones need no auth at all.

It is also largely redundant. credential.helper = gh auth git-credential,
added since, handles HTTPS auth for private repos.

Nothing of ours changes: this repo's origin and the private work external in
.chezmoiexternal.toml are both written as git@github.com: URLs, so they stay
on SSH regardless.
@ptaranat
ptaranat requested a review from Copilot July 19, 2026 18:41
@ptaranat ptaranat changed the title chore: add Brewfile, prune unused packages, rename claude alias refactor: migrate dotfiles from stow to chezmoi, modernize the shell Jul 19, 2026

ptaranat commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Merge activity

  • Jul 19, 7:46 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 19, 7:46 PM UTC: @ptaranat merged this pull request with Graphite.

@ptaranat
ptaranat merged commit d94a478 into main Jul 19, 2026
ptaranat added a commit that referenced this pull request Jul 31, 2026
)

chore: add Brewfile, prune unused packages, rename claude alias

Rename the Claude Code alias cc -> cl. As `cc` it shadowed the C compiler
outright: `command -v cc` resolved to the alias rather than /usr/bin/cc,
which breaks any ./configure, make, or native gem/npm build that shells
out to cc.

Add a Brewfile so a fresh machine can be provisioned with `brew bundle`.
Generated from the installed state after auditing every formula against
actual invocation counts in ~/.zsh_history (with shell aliases resolved,
so `ls`->eza and `k`->kubectl are credited to the right package).

Drop 16 formulae that the audit found were never invoked and that nothing
else depends on: buildifier, cffi, ffmpeg, fontforge, git-filter-repo,
librsvg, meson, mupdf, mysql@8.0, poppler, qemu, yq, docutils, sphinx-doc,
gobject-introspection and pyenv. Removing them let brew autoremove reclaim
their orphaned dependencies too, taking the formula count from 91 to 75.

pyenv is the notable one: zero invocations against 253 for uv, yet
01-environment.zsh still ran both `pyenv init --path` and `pyenv init -`
on every shell start. The zsh side of that is removed separately.

Kept several packages the audit scored at zero, because they are invoked
by other tools rather than typed: git-delta (gitconfig sets it as pager),
tree-sitter-cli (nvim-treesitter compiles parsers with it), gopls,
shellcheck and rust.

Declare the cursor and visual-studio-code casks. Both were installed by
hand, so the 51 vscode extension entries had no editor to install into on
a clean machine.

fix(zsh): drop pyenv, remove dead config, guard gt completions

pyenv was uninstalled in the previous commit but 01-environment.zsh still
ran `pyenv init --path` and `pyenv init -`, so every new shell printed two
"command not found: pyenv" errors. Remove both evals along with PYENV_ROOT
and its $PATH entry.

Drop PYTHONPATH. It was set to /opt/homebrew/bin/python3, which is an
interpreter, not a module search path. Python treats PYTHONPATH entries as
directories to search for imports, so pointing it at a binary contributed
nothing and risked confusing imports.

Drop the ~/Library/Python/3.9/bin path entry: that is the system Python
3.9 user-scripts directory, unused now that uv manages Python.

Remove the commented-out NVM block and NVM_DIR export. nvm is not
installed; fnm has replaced it.

Wrap the pasted-in Graphite (gt) yargs completion block in a
`(( $+commands[gt] ))` guard, and fix its embedded literal newline, which
had been pasted in as a raw line break inside the IFS assignment.

refactor(zsh): XDG layout, starship, mise and atuin; 6x faster startup

Startup drops from ~0.90s to ~0.15s. zprof attributed 731ms of that, 69.5%
of the total, to a single plugin: oh-my-zsh's gpg-agent, which shells out to
gpg-connect-agent on every start. It was redundant anyway. The old .zshrc
already exported GPG_TTY itself and pinentry-mac is configured in
gpg-agent.conf, so the plugin is dropped and GPG_TTY kept.

The rest of the win comes from loading oh-my-zsh a la carte, and from
wrapping every `<tool> init` in `znap eval`, which caches the generated
init to disk instead of forking a subshell per shell start.

Restructure to XDG. ~/.zshenv is now the only file in $HOME; it sets
ZDOTDIR to ~/.config/zsh and everything else moves under there, with
rc.d/*.zsh replacing ~/.zsh/*.zsh. znap's clones move from ~/.zsh_plugins
to ~/.local/share/znap. History moves to ~/.local/state/zsh/history, with
the existing 10494 lines carried over.

History config lives in .zshrc rather than .zshenv, which is not where it
looks like it belongs. macOS ships an /etc/zshrc that hardcodes
HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history, HISTSIZE=2000 and SAVEHIST=1000,
and zsh sources it after .zshenv for every interactive shell. Setting these
in .zshenv looked correct and was silently overwritten; SAVEHIST had been
pinned at oh-my-zsh's 10000 floor rather than the intended value.

Replace p10k with starship, matching the eos-dotfiles and dungeonbooks
repos. The format string is listed explicitly so starship only evaluates
the named modules instead of probing for every language it supports.
kubectl and aws context are on the prompt since kubectl is by a wide margin
the most-used command in this shell's history.

Replace fnm with mise, which also absorbs the pyenv role removed earlier.
uv stays: mise manages interpreter versions, uv manages packages and venvs,
and mise defers to uv for venv creation when it is installed. Worth noting
that fnm's hook is genuinely faster than mise's (3.9ms vs 9.8ms measured
with hyperfine); the tradeoff is one tool for every runtime, and 6ms is
noise next to what was just removed.

Add atuin on Ctrl-R with the existing history imported (10001 commands).
Up and down arrows stay on prefix-aware history search, hence
--disable-up-arrow. Sync is off by default.

Swap alias-tips for zsh-you-should-use. alias-tips cost ~129ms in preexec,
so it was charged on every command rather than once at startup. YSU's
default three-line message is trimmed to a single "alias tip: gst" line.

Set ZSH_CACHE_DIR explicitly. The oh-my-zsh docker and kubectl plugins
cache generated completions there; the framework normally sets it, and
without it they tried to write to /completions and failed.

chore(brew): add mise, atuin, starship and hyperfine to Brewfile

fnm is left in place as a fallback even though mise now handles node; it
is a small formula and its shell hook is measurably faster if the mise
tradeoff ever stops being worth it.

fix(prompt): use starship defaults, set a Nerd Font in ghostty

The first pass at this hand-built a p10k-style powerline and it was the
wrong approach twice over.

Starship has no segment concept, so every powerline background has to be
maintained by hand, and naming modules explicitly in `format` means any
module not listed never renders at all. That silently hid the gleam module
in taranat.com. Starship's own default is `$all`, which cannot lose a
module that way, so the explicit format is gone.

It also cannot reproduce what made p10k's rainbow readable: p10k swapped
the git segment's background from green to yellow on a dirty tree, and
starship only makes foreground colours conditional.

Separately, the arrows were never going to render. ghostty's config set no
font-family at all, so it fell back to a system font with no Nerd Font
glyphs. MesloLGS Nerd Font was already installed (p10k shipped it) and
alacritty was already pointing at it; ghostty just had not been told.
Note that `ghostty +list-fonts` does not enumerate user-installed fonts,
which made the family look unavailable when it was not. "SF Mono Square"
from the delphinus/sfmono-square tap is installed as an alternative.

Keep Nerd Font glyphs for module symbols, but replace the starship defaults
that are actual emoji rather than glyphs; gleam ships a star emoji and has
no Nerd Font glyph, so it gets a text label.

Disable docker_context: with a single context it printed "orbstack" in
every directory containing a compose file.

Leave the kubernetes module enabled even though the kubeconfig is now
empty, so it comes back on its own if a cluster is added rather than being
a module to remember to re-enable.

revert(prompt): back to powerlevel10k, and track .p10k.zsh

Switching to starship was not justified. The ~730ms that made startup slow
was oh-my-zsh's gpg-agent plugin, not the prompt; with that gone, p10k
starts in 0.17s against starship's 0.15s, so the prompt choice costs
essentially nothing either way. Everything the earlier commits fixed stays
fixed.

p10k also does things starship structurally cannot. It swaps a segment's
background colour on a dirty worktree, which is what makes the rainbow
style readable and which starship cannot express -- only foreground colours
are conditional there. It has transient prompt, already enabled in this
config. And `p10k configure` is a wizard, rather than hand-editing TOML and
guessing hex values, which is how the starship attempt went wrong.

Upstream is in maintenance mode: no new features and most bugs unfixed, but
the author is explicit that it is stable and not expected to break, which
is a reasonable bet for a zsh theme.

Track .p10k.zsh in the repo for the first time. It was 1734 lines living
only in $HOME, so a fresh machine would have come up with an unconfigured
prompt. It now sits in $ZDOTDIR alongside .zshrc.

The instant prompt block goes at the very top of .zshrc, above everything
including the history setup, since it replays a cached prompt and any
earlier output or input would corrupt the replay. No separate
INSTANT_PROMPT setting is needed in .zshrc because .p10k.zsh already sets
it to quiet, which is what keeps the rc.d banner from being reported as
unexpected console output.

Remove the starship config. The formula stays installed for now.

feat(p10k): rebase config on current template, add a mise segment

The local .p10k.zsh was generated in 2022 and had drifted 68 lines from
stock p10k-rainbow, of which only about ten were deliberate. Rather than
patch the old file, this regenerates from the current template and ports
those ten across, so four years of upstream changes come along too.

That drift was hiding two bugs. The old config listed `.tool-version` as a
directory anchor where stock has `.tool-versions` -- a missing "s", so the
anchor never matched. And it predates mise entirely, so `.mise.toml` was
absent from the same list.

Ported: single-line prompt (no `newline` element either side,
PROMPT_ADD_NEWLINE=false), the vi_mode segment, blanked visual identifiers
on vcs/command_execution_time/time, Nerd Font battery stages,
TRANSIENT_PROMPT=always, INSTANT_PROMPT=quiet, unconditional ahead/behind
counts, and an empty DIR_CLASSES.

DIR_CLASSES is the subtle one: assigning an empty array is not the same as
leaving it unset. Unset, p10k falls back to default directory styling,
which includes a folder icon. That icon appeared in the prompt until the
empty assignment was restored.

Gained from upstream: newer tool detection in the kubecontext, aws, azure
and google_app_cred show-on-command lists (tofu, cdk, kubent, kubecolor,
cmctl, sparkctl), a wider VPN interface pattern, and the chezmoi_shell
segment, which will be useful shortly.

Add a mise prompt segment. p10k has none built in, as it predates mise and
only ships asdf. It parses the nearest .mise.toml or .tool-versions in pure
zsh rather than shelling out: `mise current` measures at ~15ms, which is
more than p10k's whole budget for a prompt and would be paid on every one.
Reading the file is effectively free and shows what the project pins, which
is the more useful information. The walk stops at /, and the global config
lives at ~/.config/mise/config.toml rather than ~/.mise.toml, so the
segment stays empty outside mise projects.

Comment out the fifteen version-manager segments mise supersedes (asdf,
pyenv, nodenv, nvm, rbenv and friends).

Track the mise config, which existed only in $HOME. It had no [settings] at
all; now it enables lockfile pinning, uv venv auto-activation (uv is the
most-used tool in this shell's history), idiomatic version files for node
and python, auto_install and env_cache.

Remove starship, which the previous commit stopped using.

fix(p10k): escape '#' in prompt_mise, and read idiomatic version files

prompt_mise used ${line%%#*} to strip comments, which fails outright under
EXTENDED_GLOB: p10k enables that option, and with it a bare '#' is a pattern
operator, making '#*' an invalid pattern. Every call raised
"prompt_mise:14: bad pattern: #*", and because the failure happened during
prompt rendering it corrupted p10k's expansion and left raw internals like
${(e)_p9k_t[7]} on screen instead of a prompt. Escaping it as \# fixes both.

This was missed because the earlier test stubbed p10k and ran without
EXTENDED_GLOB set, so the pattern parsed fine there and only broke in a real
shell. The fixtures now set the option explicitly and cover .mise.toml,
.tool-versions, .nvmrc alone, both together, and no config at all.

Also read .nvmrc, .node-version and .python-version. mise honours these
because idiomatic_version_file_enable_tools is enabled for node and python,
so ignoring them under-reported what was active: guild pins node in .nvmrc
(where CI reads it) and uses mise.toml only for pnpm, so the segment showed
just pnpm while node was equally managed. Files at the same level are
merged, with the toml winning if it names the same tool twice.

fix(p10k): render mise segment with glyphs, cyan, one segment

Three problems, all found by capturing the real prompt through a pty rather
than trusting a stubbed `p10k segment`.

Icon names do not resolve. Passing -i NODE_ICON printed the literal string
"NODE_ICON": unlike p10k's built-in segments, which hand an icon name to
_p9k_prompt_segment for lookup, `p10k segment -i` emits its argument
verbatim. Tool glyphs are now literal characters.

Those glyphs are written as \u escapes rather than pasted in. They live in
the Unicode private use area and an earlier attempt to embed them literally
produced an array with 24 keys and 24 empty values -- the fallback to the
plain tool name masked it, so the segment looked merely verbose rather than
broken. Escapes also keep this file ASCII.

Per-tool colours are not achievable and have been dropped. p10k resolves a
segment's style from its name, so every `p10k segment` call inside
prompt_mise picked up POWERLEVEL9K_MISE_* whatever -b said; the pty capture
showed both segments rendering with background 7 despite being passed 34.
The redundant calls also left a stray empty segment at the right edge, which
was the unexplained gap there. It is one segment again, and tools without a
glyph keep their name as a label.

Background moves from 7 to 6. Seven was inherited from asdf and reads as a
bland gray; cyan is distinct from the blue directory and green vcs segments
on either side.

Cap the tool list at four so a monorepo pinning many tools cannot run the
prompt off the edge.

refactor(p10k): read mise versions from $path, theme-aware colours

Replace the file-parsing mise segment with PATH scraping, adapted from
2KAbhishek/dots2k. mise activation prepends
.../mise/installs/<tool>/<version>/bin, so the active versions are already
in the environment: no config parsing, and it reports what mise actually
resolved rather than what some file requests.

That also fixes a bug the old approach could not. A stray ~/.nvmrc and
~/.node-version, both left over from fnm and both containing "24", meant the
walk up the tree found them from any directory under $HOME, so every project
appeared to pin node. Global versions are now cached once at startup from
`mise ls --offline` and hidden when they match, so the segment shows only
what a project actually overrides. ssh-bookshop is correctly empty again
while guild still shows node 24.15.0 and pnpm 11.0.9.

Per-tool colours now work, via -s to set a segment state: p10k resolves
style from segment name plus state, so POWERLEVEL9K_MISE_<TOOL>_BACKGROUND
applies. The earlier attempt passed -b, which always loses to the
segment-level setting -- a pty capture showed both segments rendering with
background 7 despite being handed 34.

Those colours are palette indices 0-7 rather than 256-colour values. Indices
are what the terminal theme defines, so they follow srcery; fixed values
like 34 and 208 ignore the theme, which is why they looked foreign next to
the surrounding segments.

Glyphs are written as \u escape sequences. Embedding the literal characters
failed three times: they sit in the Unicode private use area and were
silently stripped to empty strings on write, leaving an array of 24 keys
with 24 empty values. The fallback to the tool name hid it, so the segment
looked merely verbose rather than broken.

Update MesloLGS Nerd Font from the 2022 v2 build to the 2025 v3 build. The
family name is unchanged, so ghostty needs no edit; the old files are
archived in ~/Downloads/meslo-v2-backup.

chore: remove fnm-era leftovers

Reclaims about 1.65GB and removes a hidden global node pin.

~/.nvmrc and ~/.node-version both contained "24" and were left over from
fnm. Because idiomatic_version_file_enable_tools is enabled for node, mise
honoured them, so every directory under $HOME inherited a node pin from
$HOME itself -- which is why ssh-bookshop appeared to pin node 24 despite
having no version file of its own. The mise prompt segment's hide-global
logic already masked the symptom; this removes the cause.

Deleted: ~/.local/share/fnm (1.0G of node builds), ~/.nvm (646M, from
before fnm), ~/.local/state/fnm_multishells, and ~/.pyenv (empty shims and
versions directories left behind when pyenv was uninstalled). Uninstall the
fnm formula and drop it from the Brewfile; it had been kept as a fallback
but mise has fully replaced it.

Also remove ~/.npmrc. Its only content was a scoped registry pointing at a
former employer's internal Nexus, which no longer resolves, so any install
of a package in that scope would have hung.

Small files are backed up to ~/Downloads/fnm-era-backup.

feat(zsh): add mise-prompt-debug as a shell function

Explains what the mise prompt segment is doing: the cached global versions,
each mise PATH entry raw and symlink-resolved, whether each is hidden or
shown and why, and what the segment finally renders.

It has to be a function rather than a script. The state it reports on lives
in shell variables (_p9k_mise_global and the POWERLEVEL9K_MISE_* settings),
which a separate process cannot see -- an earlier standalone script version
reported an empty global map and so claimed every tool would be SHOWN, when
the segment was in fact correctly hiding them.

refactor(p10k): split config into vendored template plus overrides

The p10k config was a single 1928-line file with about a dozen local edits
scattered through it. That makes upstream updates effectively impossible to
review: regenerating produces a diff where personal settings and template
churn are indistinguishable, and it is easy to silently lose a customisation
-- which already happened once with DIR_CLASSES, whose absence put a folder
icon back in the prompt.

Split into prompt/, following the layout in 2KAbhishek/dots2k:

  init.zsh       sources the other three in order; later files win
  p10k.zsh       stock p10k-rainbow vendored verbatim, byte-identical to
                 upstream and never hand-edited
  overrides.zsh  every local customisation, 175 lines
  mise.zsh       the mise segment, which p10k has no equivalent for

Taking a p10k update is now `cp` over p10k.zsh, with any conflict showing up
as an error rather than a silent loss.

my_git_formatter is redefined in overrides.zsh rather than patched into the
vendored copy. The only difference from stock is that ahead/behind counts
print unconditionally, where stock wraps them in a conditional with an
`elif` that shows the remote branch name instead.

BATTERY_STAGES needs an explicit unset first. The template declares it as an
array and this config wants a scalar string of glyphs; both are valid to
p10k, but zsh refuses to change a variable's type in place and the
assignment failed with "inconsistent type for assignment".

Verified the rendered prompt is unchanged: guild still shows the branch plus
pnpm 11.0.9 and node 24.15.0, transient prompt, instant prompt, single-line
layout and empty DIR_CLASSES all survive, and startup is ~0.17s.

chore(git): modern defaults, drop dead maintenance repo list

The [maintenance] section listed 30 repositories under ~/.zsh_plugins, a
path that no longer exists: the XDG restructure moved znap's clones to
~/.local/share/znap. Every entry was a dead path. No launchd job was
registered either, so `git maintenance` had not been running regardless.
znap manages plugin updates itself, so the section is removed rather than
repointed.

Add settings that pay for themselves on any rebase-heavy workflow:

  rerere.enabled + autoUpdate  record a conflict resolution once and replay
                               it when the same conflict reappears
  rebase.updateRefs            move stacked branches along with a rebase
                               instead of leaving them on pre-rebase commits
  rebase.autoStash             stash and restore a dirty worktree rather
                               than refusing to start
  fetch.prune + pruneTags      drop local refs for deleted remote branches
  branch.sort = -committerdate most recent branches first
  column.ui = auto             multi-column listings
  diff.algorithm = histogram   clearer diffs than Myers when blocks move
  diff.renames = copies        detect copies as well as renames

Upgrade merge.conflictstyle from diff3 to zdiff3, which keeps the common
ancestor context but omits lines both sides already agree on, giving smaller
conflict hunks. Requires git >= 2.35; this machine is on 2.55.

Fold the stray second [diff] section into one. Duplicate sections are legal
and git merges them, but having diff.tool separated from the rest was only
going to confuse.

GPG commit signing is left as it is. Switching to SSH signing
(gpg.format = ssh) would avoid the GPG keyring, but the current setup works
and the existing key is already trusted on GitHub.

chore(git): sign commits with SSH instead of GPG

GitHub already has the SSH signing key registered, so this drops the GPG
dependency for signing: no keyring, no gpg-agent, no pinentry prompt, and
the same key that already authenticates to GitHub now signs.

user.signingkey changes meaning with this: under gpg.format = ssh it is a
path to a public key rather than a GPG key id. The old GPG key
CCACDB1344B8B7EC0B4DD439655CBB5895152028 remains in this file's history if
it is ever needed for verifying old commits.

The [gpg] section name and `gpgsign = true` stay as they are. Those names
are historical -- git kept them when it added other signing formats, so
gpgsign with format = ssh means SSH-signed, not GPG-signed.

Add an allowed_signers file mapping the identity to the key. Without it
local verification fails with "No principal matched" even for signatures git
has just created, because git cannot otherwise know which key may sign for
which identity. It contains only public keys, so it is safe to commit.

Verified end to end: a test commit signs and `git verify-commit` reports a
good signature.

Note the key is 3072-bit RSA from 2022. It works and GitHub accepts it, but
ed25519 would be the modern choice; switching means generating a key and
registering it as a signing key on GitHub.

gpg itself is left installed, since gpg-agent.conf and pinentry-mac are
still configured and may be used for things other than git.

refactor(zsh): source work config from a directory, not a symlink

The work submodule's dungeonbooks file was reached via a
../../../../work/dungeonbooks/zsh/.zsh/ relative symlink sitting inside
rc.d. That path had to be recomputed by hand when the zsh config moved to
XDG, and broke silently in between. It also meant a public repo contained a
symlink whose target only resolves if a private submodule happens to be
checked out.

.zshrc now globs $ZDOTDIR/work/*.zsh as a second pass after rc.d, so work
config can override the public config and simply does not load on a machine
without access to the private repo. The work repo has been flattened to
match: files live at its root rather than in a stow-shaped
dungeonbooks/zsh/.zsh/ tree.

Also drops rokt from that repo. It defined AWS SSO profiles, EKS kubeconfig
commands and go run wrappers for a job left two years ago, against clusters
already removed from the kubeconfig.

~/.config/zsh/work is currently a hand-made symlink to the submodule. The
chezmoi migration will replace it with a .chezmoiexternal.toml entry that
clones the private repo directly, at which point the submodule goes away.

chore: drop fish, sync alacritty config from disk

fish is not installed and its config dates from 2022; the "fish" match in
the Brewfile was a word inside atuin's description. Also removes a stray
vim/.claude/settings.local.json, which was never tracked and is not config.

alacritty.toml had silently diverged. Unlike every other package it was a
real file in ~/.config rather than a stow symlink, so edits made on disk in
October 2025 never reached this repo. The live copy uses [terminal.shell]
and [terminal], the schema alacritty has wanted since 0.14 (0.17 is
installed); the repo still had the pre-0.14 [shell]. Taking the live version
here, since migrating from the repo copy would have silently reverted the
config to a deprecated schema.

Found by diffing every tracked file against its deployed counterpart before
the chezmoi migration. It was the only genuine divergence.

feat: migrate from GNU stow to chezmoi

Replaces per-package symlink farms with a single source tree that chezmoi
renders into $HOME. The layout maps by filename rather than by position:
dot_config/zsh/dot_zshrc becomes ~/.config/zsh/.zshrc, and executable_
marks files that need +x on disk.

The practical difference is that a fresh machine is now two commands
(chezmoi init --apply, then brew bundle) instead of an undocumented sequence
of stow invocations plus hand-cloning znap.

Both git submodules are retired in favour of .chezmoiexternal.toml entries.
A submodule pins a SHA that must be committed here and needs
--recurse-submodules on clone; externals are fetched on apply and refresh on
their own schedule. znap is now an external too -- it had been cloned by
hand, so a fresh machine would have come up with no plugins and no prompt,
which is exactly the sort of undocumented step this migration removes.

Verified before switching by building the source tree in a scratch
directory and comparing every managed file's rendered content against the
live copy: all 58 were byte-identical. Every path still shows in `chezmoi
diff` because stow deployed symlinks and chezmoi writes regular files, so
the type changes even where content does not.

Post-apply checks: shell starts clean in ~0.17s, ZDOTDIR and HISTFILE
resolve, node comes from mise, the p10k prompt and mise segment load, atuin
holds Ctrl-R, and the private work aliases load from the external.

Add a README covering the bootstrap, the layout, the prompt split, the
externals and the edit-source-not-target workflow.

chore: remove stow

Nothing uses it now that chezmoi manages the tree.

feat(zsh): point config-edit aliases at chezmoi

The zshrc/zshalias/vimrc family opened the deployed file directly. Under
chezmoi that edits a rendered artifact: it works until the next apply
silently overwrites it. They now go through `chezmoi edit --apply`, which
opens the source and writes the result out in one step.

Adds the usual chezmoi shortcuts (cm, cma, cmd, cme, cmu, cms, cmcd, cmadd)
plus zshprompt, gitconfig and brewfile, which had no alias before.

fix(aerospace): make the alt-enter ghostty check actually match

`pgrep -x ghostty` could never succeed. macOS truncates a process name to 16
characters, so Ghostty appears as "/Applications/Gh" and the exact-name match
always failed. The binding fell through to `open -a`, which focuses the
existing window rather than opening a new one -- so alt-enter appeared to
stop working.

Match on the bundle path instead, including Contents/MacOS. The narrower
path matters: a looser `pgrep -f Ghostty.app` matches the full command line
including environment, and every process launched from a Ghostty terminal
inherits GHOSTTY_RESOURCES_DIR pointing at Contents/Resources. That made an
unrelated npm process match during testing, so Ghostty would have looked
like it was running long after being quit.

The AppleScript half needed no change: System Events still refers to the
process as "ghostty" regardless of the truncated name.

feat(zsh): fzf-tab previews, git workflow functions, aube

Adds rc.d/05-completion.zsh. fzf-tab was already loaded but had no zstyles,
so it replaced the completion menu with fzf and then showed nothing in the
preview pane, which is most of the point. Previews are now wired per command:
directories list through eza, files render through bat, and git dispatches on
the completion group so that tab-completing `git checkout` previews a branch's
log, a modified file's diff through delta, or a commit's contents, depending
on what is under the cursor. Also covers chezmoi (shows what applying would
change), mise, brew and kill.

Adds rc.d/04-functions.zsh:

  review              browse everything changed on this branch, diff in the
                      preview pane, enter opens the file
  _git_run_on_files   run a command over only the changed files, split by
                      "diff" (this branch vs its base) and "modified" (dirty
                      worktree) -- the two questions have different answers
  lintjs/lintpy/...   thin wrappers over the above per toolchain
  search              rg to fzf to $EDITOR, opening at the matched line
  plz, xin            sudo the last command; run something in another dir

The base branch is resolved from origin/HEAD rather than hardcoded to main,
falling back to asking the remote and then to conventional names, so repos on
master or develop work unchanged.

Adds aube to the mise global config, pinned to major version 1 rather than
"latest" so it cannot shift under a project mid-work.

Startup goes from ~0.17s to ~0.20s, which is the completion zstyles being
evaluated at load.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant