The other day I noticed a post on BlueSky:
I have used ohmyzsh for a long time, but it made me realize just how bloated and slow my shell had become. I spend most of my time in the terminal and realized I was missing some useful tools and had far too many that are unnecessary. This post documents building a replacement terminal environment from scratch with three goals: speed, aesthetics, and usability. The result is a shell that starts in under 100ms, looks clean, can be set up on any new machine in minutes, and includes many handy features.
A Modular Dotfiles Architecture
Instead of one monolithic .zshrc, we created a modular structure that's easy to understand, maintain, and extend:
~/dotfiles/
├── .zshrc # Minimal - just sources modules
├── Brewfile # All Homebrew dependencies
├── install.sh # One-command setup script
├── config/
│ ├── zsh/
│ │ ├── 01-paths.zsh # PATH configuration
│ │ ├── 02-nvm-lazy.zsh # Lazy NVM loading
│ │ ├── 03-tools.zsh # Atuin, zoxide, fzf
│ │ ├── 04-aliases.zsh # Modern CLI aliases
│ │ └── 05-completions.zsh # Shell completions
│ ├── starship.toml # Prompt configuration
│ └── atuin/
│ └── config.toml # History sync settings
└── iterm2/
├── setup-iterm.sh # iTerm2 configuration script
└── com.googlecode.iterm2.plist # Exported preferences
Note: The code examples below show the essential configuration. The full dotfiles repository includes additional tools, language support, and customizations.
Part 1: The Shell Foundation
Minimal .zshrc
Keep .zshrc minimal. It sets up the config directory and sources modular files:
# ~/.zshrc - Minimal, fast shell configuration
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
# Source modular configs in order
for config in "$XDG_CONFIG_HOME/zsh"/*.zsh(N); do
[[ -f "$config" ]] && source "$config"
done
# Starship prompt (must be last, only in interactive shells)
[[ $- == *i* ]] && eval "$(starship init zsh)"
This approach provides:
- Maintainability: Each concern is in its own file
- Ordering: Files are sourced alphabetically (hence the number prefixes)
- Debugging: Easy to disable a module by renaming it
PATH Configuration (01-paths.zsh)
We centralize all PATH modifications in one file:
# Homebrew
eval "$(/opt/homebrew/bin/brew shellenv)"
# pnpm
export PNPM_HOME="$HOME/Library/pnpm"
[[ ":$PATH:" != *":$PNPM_HOME:"* ]] && export PATH="$PNPM_HOME:$PATH"
# pyenv
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
if command -v pyenv &> /dev/null; then
eval "$(pyenv init -)"
fi
# Local binaries
export PATH="$HOME/.local/bin:$PATH"
# VS Code
[[ -d "/Applications/Visual Studio Code.app/Contents/Resources/app/bin" ]] && \
export PATH="$PATH:/Applications/Visual Studio Code.app/Contents/Resources/app/bin"
Part 2: Solving the NVM Speed Problem
NVM (Node Version Manager) is notoriously slow, adding ~300ms to shell startup. The solution is lazy loading - we add Node to PATH directly but only load NVM when you actually need to switch versions.
Smart Lazy Loading (02-nvm-lazy.zsh)
export NVM_DIR="$HOME/.nvm"
if [[ -d "$NVM_DIR" ]]; then
# Add default node to PATH for immediate use (fast)
NODE_VERSIONS_PATH="$NVM_DIR/versions/node"
if [[ -d "$NODE_VERSIONS_PATH" ]]; then
if [[ -f "$NVM_DIR/alias/default" ]]; then
DEFAULT_ALIAS=$(cat "$NVM_DIR/alias/default")
# Handle aliases like "20" -> "v20.14.0"
DEFAULT_VERSION=$(ls -1 "$NODE_VERSIONS_PATH" 2>/dev/null | grep "^v${DEFAULT_ALIAS}" | sort -V | tail -1)
fi
if [[ -z "$DEFAULT_VERSION" ]]; then
DEFAULT_VERSION=$(ls -1 "$NODE_VERSIONS_PATH" 2>/dev/null | sort -V | tail -1)
fi
if [[ -n "$DEFAULT_VERSION" && -d "$NODE_VERSIONS_PATH/$DEFAULT_VERSION/bin" ]]; then
export PATH="$NODE_VERSIONS_PATH/$DEFAULT_VERSION/bin:$PATH"
fi
fi
# Lazy load NVM itself only when needed
nvm() {
unset -f nvm
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm "$@"
}
fi
This gives us:
- Instant access to
node,npm,npxwithout loading NVM - Starship compatibility - it can detect the Node version for the prompt
- Full NVM functionality when you need to switch versions
Part 3: Modern CLI Tools
We replace outdated Unix tools with modern alternatives that are faster and more user-friendly.
The Brewfile (Essential Packages)
# Shell & Prompt
brew "starship" # Cross-shell prompt
brew "atuin" # Shell history with sync
# Modern CLI Replacements
brew "eza" # Better ls (icons, git status)
brew "bat" # Better cat (syntax highlighting)
brew "zoxide" # Smarter cd (learns your habits)
brew "fzf" # Fuzzy finder
brew "fd" # Better find
brew "ripgrep" # Better grep
brew "git-delta" # Better git diff
brew "lazygit" # Git TUI
brew "tlrc" # Simplified man pages
# Data Processing
brew "jq" # JSON processor
brew "yq" # YAML processor
# Development Tools
brew "gh" # GitHub CLI
brew "git" # Latest git
brew "awscli" # AWS CLI
# Security & Auth
cask "1password-cli" # 1Password CLI
# Fonts (Nerd Font patched)
cask "font-fira-code-nerd-font"
cask "font-meslo-lg-nerd-font"
Tool Initialization (03-tools.zsh)
# Atuin - replaces shell history with fuzzy search + sync
if command -v atuin &> /dev/null && [[ $- == *i* ]]; then
eval "$(atuin init zsh)"
fi
# Zoxide - smart cd that learns your habits
if command -v zoxide &> /dev/null && [[ $- == *i* ]]; then
eval "$(zoxide init zsh)"
fi
# FZF - fuzzy finder
if command -v fzf &> /dev/null && [[ $- == *i* ]]; then
source <(fzf --zsh)
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
fi
Aliases (04-aliases.zsh)
# Modern replacements
alias ls='eza --icons --group-directories-first'
alias ll='eza -la --icons --group-directories-first --git'
alias lt='eza --tree --level=2 --icons'
alias cat='bat --paging=never'
alias grep='rg'
alias find='fd'
alias diff='delta'
# Git shortcuts
alias g='git'
alias gs='git status'
alias gd='git diff'
alias lg='lazygit'
# Navigation
alias ..='cd ..'
alias ...='cd ../..'
Shell Completions (05-completions.zsh)
The final module configures tab completion with arrow-key menu navigation:
# Docker completions
[[ -d "$HOME/.docker/completions" ]] && fpath=($HOME/.docker/completions $fpath)
# Homebrew completions
if type brew &>/dev/null; then
fpath=($(brew --prefix)/share/zsh/site-functions $fpath)
fi
# Load completion list module (required for menu selection)
zmodload zsh/complist
# Completion styling (must be before compinit)
zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}"
# Initialize completion system
autoload -Uz compinit
compinit -C # -C for faster startup (uses cache)
# AWS completions (bash-style - doesn't support menu select)
if [[ -f /usr/local/bin/aws_completer ]]; then
autoload -Uz bashcompinit && bashcompinit
complete -C '/usr/local/bin/aws_completer' aws
fi
Key points:
zmodload zsh/complist- Required for arrow key navigation in menus- zstyles before compinit - Styling must be configured before initialization
menu select- Enables interactive menu selection with arrow keys- Docker/Homebrew/AWS - Adds completion for container names, packages, and AWS services
Part 4: The Starship Prompt
Starship is a fast, customizable prompt written in Rust. We configured a two-line prompt inspired by Powerlevel10k with Catppuccin colors.
Key Starship Configuration
# Performance settings
scan_timeout = 10
command_timeout = 500
# Two-line prompt format
format = """
[╭─](overlay1)\
[](fg:surface0)$os$username[](bg:blue fg:surface0)\
$directory[](fg:blue bg:yellow)\
$git_branch$git_status[](fg:yellow bg:green)\
$nodejs$python$rust$golang[](fg:green bg:sapphire)\
$docker_context[](fg:sapphire)\
$fill\
[](fg:lavender)$cmd_duration[](bg:lavender fg:surface0)\
$time[](fg:surface0)[─╮](overlay1)
[╰─](overlay1)$character"""
palette = 'catppuccin_mocha'
Nerd Font Symbol Setup
Getting Nerd Font symbols to render correctly requires the actual Unicode characters in the TOML file.
We used Python to write the correct bytes:
# Fix nodejs symbol (U+E718 = Node.js icon)
content = re.sub(
r'\[nodejs\]\nsymbol = ".*?"',
'[nodejs]\nsymbol = "\ue718 "',
content
)
Part 5: Font Configuration
The Nerd Font Solution
The terminal prompt uses Nerd Font icons for visual indicators. If you have MonoLisa (a premium coding font), the setup uses it as the main font with FiraCode Nerd Font as a fallback for icons. If MonoLisa isn't installed, FiraCode Nerd Font handles everything.
The set-fonts command detects what's available:
# Check if MonoLisa is installed, fall back to FiraCode Nerd Font
if system_profiler SPFontsDataType 2>/dev/null | grep -q "MonoLisa"; then
MAIN_FONT="MonoLisa-Regular 13"
else
MAIN_FONT="FiraCodeNerdFont-Regular 13"
fi
iTerm2 Font Settings
Preferences → Profiles → Text
├── Font: FiraCode Nerd Font (or MonoLisa if available)
├── ☑ Use a different font for non-ASCII text
└── Non-ASCII Font: FiraCode Nerd Font Mono 13
This ensures Nerd Font icons render correctly regardless of your main font choice.
Part 6: Shell History with Atuin
Atuin replaces the default shell history with a SQLite database that:
- Syncs across machines (encrypted, end-to-end)
- Provides fuzzy search (Ctrl+R)
- Records context (directory, exit code, duration)
Configuration (config/atuin/config.toml)
auto_sync = true
sync_frequency = "5m"
sync_address = "https://api.atuin.sh"
search_mode = "fuzzy"
filter_mode = "global"
style = "compact"
# Privacy
secrets_filter = true
cwd_filter = ["/tmp"]
Part 7: Git with Delta
Delta provides syntax-highlighted git diffs with side-by-side view.
Git Configuration
[core]
pager = delta
[interactive]
diffFilter = delta --color-only
[delta]
navigate = true
side-by-side = true
line-numbers = true
syntax-theme = Catppuccin-mocha
[merge]
conflictstyle = diff3
Part 8: iTerm2 Configuration Script
A script manages iTerm2 preferences:
# Export current settings (on configured machine)
~/dotfiles/iterm2/setup-iterm.sh export
# Import settings (on new machine)
~/dotfiles/iterm2/setup-iterm.sh import
# Configure iTerm2 to auto-sync with dotfiles
~/dotfiles/iterm2/setup-iterm.sh configure
# Set up fonts (with MonoLisa fallback to FiraCode)
~/dotfiles/iterm2/setup-iterm.sh set-fonts
# Apply recommended preferences
~/dotfiles/iterm2/setup-iterm.sh set-preferences
Recommended iTerm2 Settings
The set-preferences command configures:
- Anti-aliasing: Smooth text rendering
- Option key as Meta: Set left Option to Esc+ for proper terminal keybindings
- Scrollback buffer: 100,000 lines
- Silent bell: Disable audible bell
# What set-preferences does:
defaults write com.googlecode.iterm2 "Anti Aliased" -bool true
defaults write com.googlecode.iterm2 "LeftOptionKey" -int 2
defaults write com.googlecode.iterm2 "Scrollback Lines" -int 100000
defaults write com.googlecode.iterm2 "Silence Bell" -bool true
Part 9: The Install Script
The install.sh script automates the entire setup:
#!/bin/bash
set -e
DOTFILES_DIR="$HOME/dotfiles"
CONFIG_DIR="$HOME/.config"
# Install Homebrew if missing
if ! command -v brew &> /dev/null; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
# Install all packages
brew bundle --file="$DOTFILES_DIR/Brewfile"
# Create symlinks
ln -sf "$DOTFILES_DIR/.zshrc" "$HOME/.zshrc"
ln -sf "$DOTFILES_DIR/config/zsh" "$CONFIG_DIR/zsh"
ln -sf "$DOTFILES_DIR/config/starship.toml" "$CONFIG_DIR/starship.toml"
ln -sf "$DOTFILES_DIR/config/atuin" "$CONFIG_DIR/atuin"
# Import shell history into Atuin
atuin import auto 2>/dev/null || true
# Configure iTerm2
"$DOTFILES_DIR/iterm2/setup-iterm.sh" import 2>/dev/null || true
Results
| Metric | Result |
|---|---|
| Shell startup | ~98ms |
| History search | Fuzzy search with sync |
| New machine setup | Minutes |
Final Setup
The complete environment includes:
- Prompt: Starship with Catppuccin Mocha theme, two-line layout
- History: Atuin with cross-machine sync
- Navigation: Zoxide (smart cd) + FZF (fuzzy finder)
- File listing: Eza with icons and git integration
- File viewing: Bat with syntax highlighting
- Git: Delta for diffs, Lazygit for TUI
- Search: Ripgrep + fd
Getting Started
New Machine Setup
# 1. Clone dotfiles
git clone https://github.com/schuettc/dotfiles.git ~/dotfiles
# 2. Run install script
cd ~/dotfiles && ./install.sh
# 3. Configure iTerm2
./iterm2/setup-iterm.sh import
./iterm2/setup-iterm.sh set-fonts
./iterm2/setup-iterm.sh set-preferences
# 4. Set up Atuin sync
atuin login # or: atuin register
# 5. Restart terminal
Repository
The complete dotfiles are available at: github.com/schuettc/dotfiles
Keyboard Shortcuts & Commands
Quick reference for the installed tools:
Keyboard Shortcuts
| Shortcut | Tool | Description |
|---|---|---|
Ctrl+R |
Atuin | Fuzzy search command history |
Ctrl+T |
FZF | Fuzzy find files, insert path at cursor |
Alt+C |
FZF | Fuzzy cd into directory |
Tab |
zsh | Menu completion with arrow key navigation |
Note:
Alt+Crequires iTerm2's Option key set to "Esc+". Run./iterm2/setup-iterm.sh set-preferencesto configure this.
FZF Triggers
Type ** then press Tab to trigger fuzzy completion:
cd **<Tab> # Fuzzy find directories
vim **<Tab> # Fuzzy find files to edit
kill **<Tab> # Fuzzy find processes
ssh **<Tab> # Fuzzy find SSH hosts
Commands
| Command | Tool | Description |
|---|---|---|
z <partial> |
Zoxide | Smart jump to frequently used directory |
zi |
Zoxide | Interactive directory picker with FZF |
lg |
Lazygit | Terminal UI for git operations |
ll |
eza | Long list with icons and git status |
lt |
eza | Tree view (2 levels deep) |
cat |
bat | Syntax-highlighted file viewer |
catp |
bat | Plain output without line numbers (for copying) |
Examples
# Jump to a project (learns from your cd history)
z blog # Jumps to ~/GitHub/schuettc/blog
# Interactive directory selection
zi # Opens FZF to pick any directory
# Find and edit a file
vim **<Tab> # Type partial name, select with arrows
# Search history for a command
Ctrl+R # Type partial command, select from matches
Tools mentioned: Starship, Atuin, Eza, Bat, Zoxide, FZF, Ripgrep, fd, Delta, Lazygit, MonoLisa, Nerd Fonts, Catppuccin
