The other day I noticed a post on BlueSky:

hailey (@hailey.at)
in other news i replaced ohmyzsh today with starship. its quite nice out of the box, though god knows why both the AWS and gcloud plugins are enabled by default lol

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, npx without 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

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+C requires iTerm2's Option key set to "Esc+". Run ./iterm2/setup-iterm.sh set-preferences to 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