My dotfiles for macOS — zsh, tmux, and dev tool configurations.
| Component | Details |
|---|---|
| Shell | Bare zsh (no Oh My Zsh) with autosuggestions, syntax highlighting, history substring search |
| Prompt | Custom theme: git branch, dirty marker, ahead/behind, stash count, status icons — one git call per prompt |
| Tmux | C-f prefix, mouse support with a toggle, powerline bar (session, host, IPs / load, cwd, branch), auto-start |
| Git | Shorthand aliases (gcam, gck, gst...), helper functions (gbclean, gplb) |
| Tools | bat, ripgrep, fd, htop, jq, tmux, emacs |
A screenshot of the prompt and tmux bar is in docs/screenshot-mac.png.
manifest.tsv → the one link list: repo file -> dotfile in $HOME
Brewfile.core → Core CLI tools (git, tmux, ripgrep, fd, bat...)
Brewfile.zsh → Zsh plugins (autosuggestions, syntax-highlighting...)
Brewfile.editors → Editors (emacs-plus@31)
Brewfile.dev → Dev toolchains (ansible, pyenv)
Brewfile.apps → GUI apps (iterm2, rectangle, github...)
bin/
├── setup.sh Interactive installer; --links-only relinks dotfiles
├── check.sh Health check
├── iterm2-export.sh Exports iTerm2 prefs as reviewable XML
└── pi-dev-install.sh Auto-discovered by setup.sh (any *-install.sh)
zsh/
├── zshenv zshrc -> ~/.zshenv ~/.zshrc
├── init.sh Loads the layer below, then private.sh
├── git-prompt.zsh Git prompt engine (one git call per prompt)
├── theme-powerline.zsh-theme Colors and glyphs for the prompt
├── paths.sh EDITOR and personal PATH additions
├── private.sh Your local overrides — git-ignored, sourced last
└── env/
├── terminal.sh Shell navigation, editors, ls variants
├── git.sh Git aliases and helper functions
├── apps.sh Brew, tmux, rails helpers
├── languages.sh Node, Maven, Go helpers
└── system.sh System utilities (ports, kill, ps)
git/ignore -> ~/.gitignore (global excludes template)
tmux/tmux.conf -> ~/.tmux.conf
tmux/powerline/ → submodule: erikw/tmux-powerline pinned at v4.0.0.
It draws the status bar; run git submodule update --init
tmux/powerline-config/ → THE status bar config, linked as ~/.config/tmux-powerline
├── config.sh Theme name, overlay directories, segment options
├── themes/rlazoti.sh Segments and colors: session host lan_ip wan_ip / load pwd branch
└── segments/ Drop-in custom segments (empty until you add one)
ruby/irbrc -> ~/.irbrc
iterm2/ → iTerm2 settings snapshot, see its README
fonts/ → Font files that bin/setup.sh installs into ~/Library/Fonts
docs/ → Screenshots
- macOS
- iTerm2 (or Terminal.app)
# 1. Clone this repo
git clone https://github.com/rlazoti/dotfiles ~/.terminal
cd ~/.terminal
# 2. Install Homebrew (if not already installed)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 3. Run setup and choose what to install
bash bin/setup.sh
# 4. Verify, then restart your terminal
bash bin/check.shbin/setup.sh prompts for each category. Answer y or n for each one. It links every
pair in manifest.tsv (~/.zshrc, ~/.zshenv, ~/.tmux.conf, ~/.gitignore,
~/.irbrc) and copies fonts/ into ~/Library/Fonts.
After a file move or a rename, relink without touching packages:
bash bin/setup.sh --links-onlysetup.sh uses the Homebrew 7 form brew bundle install --file=… --no-upgrade.
On Homebrew 6 or older, use brew bundle --file=… --no-lock.
Create ~/.terminal/zsh/private.sh for machine-specific settings — extra paths,
tokens, host names. It is git-ignored and sourced after every other file in zsh/env/,
so it can override anything.
Edit the relevant Brewfile:
Brewfile.core— core CLI toolsBrewfile.zsh— zsh pluginsBrewfile.editors— editorsBrewfile.dev— dev toolchainsBrewfile.apps— GUI apps
brew "my-tool" # Add a formula
cask "my-app" # Add a caskThen apply it: brew bundle install --file=Brewfile.core --no-upgrade
Create a script in bin/ whose name ends in -install.sh:
#!/bin/bash
# Install My Tool
# Usage: bash bin/my-tool-install.sh
echo "==> Installing My Tool..."
curl -fsSL https://example.com/install.sh | sh
echo "==> Done!"bin/setup.sh prompts for every bin/*-install.sh. The first line that starts with
# becomes the prompt text.
Each concern is one file. Put aliases and functions for the same topic in the same file:
zsh/env/terminal.sh— shell navigation, editors, ls variantszsh/env/git.sh— git aliases and helper functionszsh/env/apps.sh— brew, tmux, railszsh/env/languages.sh— node, maven, gozsh/env/system.sh— system utilities
Edit zsh/theme-powerline.zsh-theme. It only sets colors and glyphs, and it loads the
colors module and zsh/terminfo itself. The git data comes from zsh/git-prompt.zsh,
which runs git status --porcelain=v2 --branch --show-stash once per prompt in precmd
and caches it in $_git_prompt. The prompt segments read that cache, so drawing the
prompt starts no extra processes.
Supported settings:
| Variable | Meaning |
|---|---|
ZSH_THEME_GIT_PROMPT_PREFIX / SUFFIX |
Text around the branch name |
ZSH_THEME_GIT_PROMPT_DIRTY |
Marker when the working tree has changes |
ZSH_THEME_GIT_PROMPT_AHEAD / BEHIND |
Counts against the upstream branch |
ZSH_THEME_GIT_PROMPT_STASH |
Prefix for the stash count |
ZSH_THEME_GIT_PROMPT_ADDED, _MODIFIED, _RENAMED, _DELETED, _UNMERGED, _UNTRACKED |
Right-side status icons |
In a very large repository, one git status per prompt can still be slow. Skip it with:
export GIT_PROMPT_HIDE_STATUS=1The bar is tmux-powerline drawing this repo's theme. Three files, all tracked:
| File | Controls |
|---|---|
tmux/powerline-config/config.sh |
Which theme, where themes/segments are found, per-segment options |
tmux/powerline-config/themes/rlazoti.sh |
The segments themselves, their order and colors |
tmux/powerline-config/segments/ |
New segments, if you write one |
They reach the tool through one link, ~/.config/tmux-powerline → tmux/powerline-config,
which bin/setup.sh creates from manifest.tsv. tmux-powerline reads exactly that directory,
so an edit takes effect on the next redraw — no restart, no reload.
Both sides are emitted in array order, left to right, so on the right side the first entry is
nearest the window list and the last one sits on the right screen edge. Current layout: left
session host lan_ip wan_ip, right load pwd vcs_branch.
Never edit tmux/powerline/themes/ — that directory belongs to the submodule, and
git submodule update owns it. Both ways a theme breaks are silent, so check after editing:
~/.terminal/tmux/powerline/powerline.sh right # what the right side draws now
bash tmux/powerline/doctor.sh | sed -n '/active theme/p' # which theme file won
bash bin/check.sh # both, plus errors inside segmentsA new segment means a new shell per redraw, and maybe more width: status-left-length and
status-right-length in tmux/tmux.conf truncate without warning.
The zsh prompt uses private-use glyphs. Non Ascii Font in the iTerm2 profile must name
a font that exists. Add new font files to fonts/ so bin/setup.sh installs them and
bin/check.sh can verify them. See iterm2/README.md.
iterm2/README.md explains how the plist snapshot relates to the live settings, how to
export it without pulling in runtime noise (bin/iterm2-export.sh), how to check what
keys your terminal sends, and how to move a profile to a Dynamic Profile JSON file.
| Command | Action |
|---|---|
.. / ... / .... / ..... |
cd .. / cd ../.. / cd ../../.. / cd ../../../.. |
gcam "msg" |
git commit -m "msg" |
gck |
git checkout |
gckb |
git checkout -b |
gst |
git status -sb |
gpl |
git pull --rebase --tags |
gpso |
git push origin |
gbclean |
Delete merged branches, confirm unmerged ones |
b |
brew |
t |
tmux |
E |
sudo $EDITOR |
vii |
emacs -nw |
Word movement with Option+Arrow is bound in zsh/zshrc and does not depend on the
iTerm2 key map. iterm2/README.md shows how to check the sequence your terminal sends.
| Keybinding | Action |
|---|---|
C-f |
Prefix key |
C-f c |
New window in the current path |
C-f " |
Split window |
C-f % |
Split window horizontally |
C-f m |
Toggle mouse mode |
C-f y |
Copy the tmux buffer to the macOS clipboard |
C-f : |
Command prompt with emacs-style editing |
Y in copy mode |
Copy the selection and run /feedback-capture in pi |
Recorded here so they do not have to be argued again.
Oh My Zsh was removed on 2026-10-01. It supplied four things, each now owned here:
| Oh My Zsh provided | Now provided by |
|---|---|
| git prompt info | zsh/git-prompt.zsh |
ZSH_THEME_* variables |
zsh/theme-powerline.zsh-theme |
| tmux autostart | zsh/zshrc |
| plugin loading | zsh/zshrc loop over $HOMEBREW_PREFIX/share |
Anything that still references an :omz: zstyle or an oh-my-zsh.* git config key is
dead code. Remove it instead of keeping the probe.
zsh/git-prompt.zsh runs one git status --porcelain=v2 --branch --show-stash in
precmd and caches the result in $_git_prompt. The theme only reads that cache. Five
separate git calls cost about 146 ms per prompt in a small repository; one call costs
about 37 ms, and large repositories scale far worse.
- No async prompt framework: it needs a persistent worker per shell, and one status call is already cheap enough.
- The engine calls git with
GIT_OPTIONAL_LOCKS=0, so the prompt never takes an index lock while an editor or build holds one. GIT_PROMPT_HIDE_STATUS=1skips the call completely. That is the answer for a repository that is too large, not a second code path.--show-stashneeds git 2.22 or newer. Older git prints no stash line, so the stash segment disappears; nothing else breaks.
~/.zshenv locale, HOMEBREW_PREFIX + PATH, tool PATHs, cargo env (every zsh)
~/.zshrc setopt -> git engine -> tmux autostart -> zsh/init.sh ->
lazy pyenv/neovim -> theme -> plugins -> bindkey (interactive)
~/.terminal/zsh/init.sh
zsh/env/{terminal,git,apps,languages,system}.sh (one file per concern)
then zsh/paths.sh
then zsh/private.sh (last, wins)
Consequences to respect:
- Anything that must be visible to non-interactive shells (scripts, git helpers, Emacs
shells) belongs in
.zshenv, not.zshrc. zsh/private.shis sourced last, so it overrides every alias and PATH entry. It is git-ignored, so secrets stay off GitHub. It is plain text; this repo does not encrypt it.- Homebrew plugins load after the theme, because zsh-syntax-highlighting must be sourced last.
- Key bindings for terminal keys live in
.zshrc, never in the iTerm2Keyboard Map. That keeps them working across terminals and through tmuxextended-keys.
.zshenv puts $HOMEBREW_PREFIX/bin and sbin in front of PATH, and sets
HOMEBREW_PREFIX without starting a brew subshell. Homebrew git therefore wins over
/usr/bin/git. paths.sh only appends personal directories, and only when they exist.
Deliberate limit: keg-only formulae are not added to PATH. Homebrew's curl is
keg-only, so brew "curl" keeps the formula updated but does not change which curl
runs. Add the keg path yourself if you ever need brew's curl.
Five category Brewfiles, applied with brew bundle install --file=Brewfile.<cat> --no-upgrade. install upgrades by default in Homebrew 7, so --no-upgrade keeps a
plain install from silently updating formulae. The old --no-lock form is gone in
Homebrew 7 and fails.
reattach-to-user-namespace is not in Brewfile.core. tmux 3.6 and newer reach the
macOS pasteboard on their own, and that helper has had no upstream release since 2017.
Clipboard support is bind y run 'tmux save-buffer - | pbcopy'.
Directories are named after the tool that reads them, and every dotfile sits next to the
files that support it. Before 2026-10-15 the root held 13 loose files: environment (a
loader with no clue in its name), my-custom-theme.zsh-theme, gitignore sitting next
to .gitignore, install-scripts/ plus check.sh plus setup.sh in three different
naming styles, and two screenshots.
Rules that keep it tidy:
bin/holds executables.bin/setup.shauto-discovers installers by the name suffixbin/*-install.sh, so adding a tool adds no wiring.manifest.tsvis the only link list.bin/setup.shreads it,bin/check.shverifies it. A pair appears once, assource<TAB>target.- One file per concern under
zsh/env/. Aliases and functions for the same topic live in the same file, sogcamandgbcleanare together. - Brewfiles stay at the root: five files, and
--file=Brewfile.corestays short. - After any move, run
bash bin/setup.sh --links-only, thenbash bin/check.sh. The links in$HOMEpoint at repo paths, so a move breaks the shell until you relink. tmux/powerlineis a submodule. A fresh clone needsgit submodule update --init, whichbin/setup.shruns for you. An empty submodule directory draws a blank status bar and tmux reports nothing, sobin/check.shchecks that the bar actually renders.
- Prefix is
C-f, notC-b. extended-keys onwithextended-keys-format csi-u: pi needs to tellShift+EnterfromEnter. Requires tmux 3.5+.- The status bar is drawn by
tmux-powerline, a submodule pinned atv4.0.0(tmux/powerline), but with this repo's theme: left is session, host, LAN IP, WAN IP; right is load, directory, branch. Theme and config live intmux/powerline-config/and reach the tool through one manifest link,~/.config/tmux-powerline, because that directory is the path tmux-powerline reads. Nothing is ever edited inside the submodule. - The config is a symlink, not
TMUX_POWERLINE_CONFIG_FILEset fromtmux.conf, so that every caller resolves the same theme: tmux's#(),bin/check.sh,doctor.sh, and you runningpowerline.sh leftby hand while debugging. An environment variable reaches only processes that inherit it, and a hand-run that quietly used the bundled theme would have you debugging the wrong file. tmux 3.8 also changed that command's syntax —set-environmentnow takesvariable value, and theNAME=valueform fails with "variable name contains =". - Both theme failure modes are silent, so
bin/check.shasksdoctor.shwhich theme actually resolved and compares resolved paths: a missing symlink falls back to the bundled theme without a word, and a theme file that cannot be found draws a blank bar with no error. - It used to be an ignored clone in the repo root, so no version was recorded, and on
2026-10-15 it turned out to be gutted: 59 files were gone,
themes/default.shincluded, so the bar drew blank and nothing reported it. Pinning it is what fixes that class of breakage;bin/check.shnow renders the bar and fails if a segment is missing. - Segments cost a shell each, per
status-interval, and that is the price of the powerline bar. It is why the theme carries seven segments instead of the stock twelve: four on the left, three on the right.status-interval 5is the other half of that trade — status segments start shells, so a 2 second interval means many processes per hour for no visible gain. - Window names use the
b:format modifier instead of a#()shell call. - Mouse is on, and
prefix mtoggles the boolean option. The oldcut -c3-trick and its pre-tmux-3.0mode-mousecalls are gone.
iterm2/com.googlecode.iterm2.plist is a full preference snapshot: 1097 keys, 2360
lines, 62 KB. One GUI change can rewrite hundreds of lines, and importing iTerm2's own
file would add 47 runtime keys (NoSync*, NS*, SU*, Apple*) that change by
themselves. That cost is accepted, not solved: the file stays tracked so a new machine
can be configured from it. bin/iterm2-export.sh keeps the noise out when you re-export,
and bin/check.sh runs plutil -lint on the file. The way out, if the churn ever hurts
more than the convenience, is a Dynamic Profile JSON file — described in
iterm2/README.md. Never reuse a GUID, and never point "Load preferences from a custom
folder" at that directory.
Font rule: Non Ascii Font in the default profile must name a font that bin/setup.sh
can install from fonts/. Today that is FontAwesome 11 and fonts/FontAwesome.otf.
bin/check.sh verifies it.
- No dotfile manager (GNU Stow, chezmoi):
manifest.tsvplusbin/setup.shis about 20 lines of shell, and it adds no dependency. - No Linux support.
Brewfile.*and the macOS clipboard paths assume macOS. - No symlink for
.zlogin: the file was empty, so it was removed on 2026-10-15.
Homebrew 7 changed the syntax: it is brew bundle install now, and --no-lock and
the install-side --cask flag are gone. install upgrades by default, so --no-upgrade
is what you want for a plain install.
brew bundle install --file=Brewfile.core --upgrade
brew bundle install --file=Brewfile.zsh --upgrade
brew bundle install --file=Brewfile.editors --upgrade
brew bundle install --file=Brewfile.dev --upgrade
brew bundle install --file=Brewfile.apps --upgradeTo see whether a Brewfile is satisfied without installing anything:
brew bundle check --file=Brewfile.corebash bin/check.shIt verifies every pair in manifest.tsv (files and directories), each Brewfile package, the
zsh plugin path for this Mac, installed fonts against the iTerm2 profile's font, that the
plist parses, the zsh/ file set including zsh/env/, that the zsh config parses and starts
without errors and still loads the colors module, that tmux/tmux.conf parses on an isolated
tmux server, that tmux-powerline renders both sides of the bar and that the theme it resolved
is the one tracked in this repo, and that git is new enough for the prompt engine.
Errors exit 1; warnings exit 0.
- If it is a Homebrew formula or cask: add it to the relevant Brewfile
- If it is a non-Brew installer: create
bin/<name>-install.sh - Run
bash bin/check.sh
Prompt shows raw escape sequences?
setopt PROMPT_SUBSTmust stay on (it is set inzsh/zshrc)- Check that
fonts/FontAwesome.otfis installed:ls ~/Library/Fonts
Prompt shows boxes instead of icons?
- The iTerm2 profile's
Non Ascii Fontdoes not match an installed font. Seeiterm2/README.md.
Prompt is one flat color?
- The
colorsmodule is not loaded.zsh/theme-powerline.zsh-themeloads it; do not move that load into the prompt engine.bin/check.shtests it.
Prompt is slow in one repository?
- One
git statusruns per prompt. In a huge repository, setexport GIT_PROMPT_HIDE_STATUS=1in that project's local overrides.
tmux doesn't auto-start?
zsh/zshrcstartstmux new-session -A -D -s mainfor a shell outside tmux.- To skip tmux once:
TMUX_ENV_SET=1 zsh
tmux status bar is empty?
tmux source-file ~/.tmux.conf. Thenbash bin/check.sh: it renders both sides and reports an uninitialized submodule or a theme file that cannot be found, both of which draw blank without any error.
tmux bar shows segments you did not choose?
- The theme is selected through
~/.config/tmux-powerline→tmux/powerline-config. If that link is missing, tmux-powerline silently uses its own bundleddefaulttheme. Fix:bash bin/setup.sh --links-only, thenbash bin/check.sh.
Changed the tmux theme and nothing moved?
bash tmux/powerline/doctor.sh | sed -n '/active theme/p'prints the theme file that won. If it points insidetmux/powerline/themes/, thenTMUX_POWERLINE_DIR_USER_THEMESis not reachingtmux/powerline-config/themes— that variable is what keeps the lookup out of the submodule.
Aliases stopped working after a pull?
- Reload the config:
source ~/.zshrc - After a file move in this repo:
bash bin/setup.sh --links-only - Confirm the package is installed:
brew list <tool>
Something on this page is wrong?
- Run
bash bin/check.shfirst — it tests the machine, not the docs.
Rodrigo Lazoti