The Fish Shell Terminal Stack: From Slow to Fast

· 9 min read ·
·
Developer Tools Terminal Claude Code Productivity CLI Workflow

I spent years using the default terminal tools. find for searching files. grep for text search. cd for navigation. It worked, but it was slow.

I’d wait 30 seconds for a search to finish. I’d type out full directory paths. I’d scroll through command history looking for that one command I ran yesterday.

Then I discovered the modern terminal stack-tools that replace those defaults with specialized, performant alternatives (written using Rust/ Go). The difference isn’t incremental. It’s transformative.

I’m primarily a Fish shell user, and when you pair these tools with Fish’s 50-100ms startup time and native integrations, you get something special. Also, when you work with Claude Code, you would see significant turnaround.

After switching, I’m saving 20-30 minutes per day. That’s an extra hour of deep work every 2-3 days. Here’s what actually matters.

File System and Navigation: The Core Workflow

fd: Modern find with Gitignore Awareness

fd reimplements Unix find with three critical improvements:

  1. Gitignore Respect: By default, fd excludes files matching .gitignore, .git/, node_modules/, and hidden directories-no complex -not -path excludes needed
  2. Intuitive Syntax: fd pattern directory vs. find directory -name pattern reverses the cognitive load
  3. Parallel Searching: On multi-core systems, fd parallelizes directory traversal

Benchmark: Searching 500K files (typical monorepo) takes 0.3s with fd vs. 3-30s with find depending on exclude rules. See fd benchmarks for detailed performance comparisons.

Fish shell integration:

# Alias (lazy-loaded, zero startup cost)
alias --save find "fd"

# Or as abbreviation (expands in-place)
abbr -a ff "fd --hidden --exclude=.git"

Bash/Zsh integration:

alias find='fd'

zoxide: Frecency-Based Navigation

zoxide learns your most-accessed directories and enables jumps via partial matching:

# Fish shell
z dev        # Jump to ~/projects/dev (if recently accessed)
z project    # Jump to ~/workspace/project (even if not in $PWD)
z -          # Jump to previous directory
zi           # Interactive selection with preview (if fzf installed)

Fish shell setup (~/.config/fish/config.fish):

zoxide init fish | source

Bash/Zsh setup (~/.bashrc or ~/.zshrc):

eval "$(zoxide init bash)"  # or zsh

Internally, zoxide maintains a frecency database (~/.local/share/zoxide/db.zo) that scores directories by frequency × (now - last_accessed_time).

Frequently visited directories can be reached in one command instead of 4-5 cd commands.

Keystroke impact: In a typical 8-hour workday with 50 directory changes, zoxide saves 200-300 keystrokes through intelligent autocomplete and frecency weighting.

ripgrep outperforms grep by orders of magnitude through:

  1. Regex Compilation: Rust’s regex library uses NFA/DFA optimization, not backtracking
  2. Gitignore Respect: Skips excluded directories without system calls
  3. Parallelization: Searches multiple files simultaneously
  4. Memory Efficiency: Memory-mapped file I/O for large files

Benchmark: Searching “useState” across a 200K-file JavaScript monorepo takes 0.05s with rg vs. 8-15s with grep -r-a 200x improvement. See ripgrep benchmarks for more detailed performance comparisons.

Configuration (.ripgreprc):

--max-columns=200      # Prevent runaway output
--smart-case            # Case-sensitive only if uppercase in query
--type-add text:*.mdx   # Support MDX files

Fish shell abbreviation:

# Abbreviation expands in-place, allowing argument modification
abbr -a rg 'rg --max-columns=200 --smart-case'
abbr -a rgr 'rg --hidden'  # Include hidden directories

Bash/Zsh alias:

alias rg='rg --max-columns=200 --smart-case'

Interactive Selection: The fzf Ecosystem

fzf: Core Fuzzy Finding

fzf implements a modal search interface enabling three critical workflows:

BindingFunctionUse Case
CTL-RFuzzy history searchFind previous command without scrolling
CTL-TFuzzy file selectionvim $(fzf) pattern for editor integration
ALT-CFuzzy directory navigationcd to nested directories without knowing exact path

The algorithm: fzf uses a state machine for incremental search. As you type characters, the matching set shrinks in real-time, typically achieving single-keystroke response times even on 100K+ item lists.

Fish shell integration (recommended: modern plugin):

# Install Fisher package manager
curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source && fisher install jorgebucaran/fisher

# Install modern fzf.fish plugin
fisher install PatrickF1/fzf.fish

# Configure in ~/.config/fish/config.fish
fzf_configure_bindings

The modern fzf.fish plugin provides customizable keybindings and integrates seamlessly with Fish’s native key binding system. Fisher is Fish’s package manager for managing plugins.

Bash/Zsh integration:

# Add to ~/.bashrc or ~/.zshrc
eval "$(fzf --bash)"  # or --zsh
. "$(brew --prefix)/opt/fzf/shell/key-bindings.bash"  # or .zsh

Integration with preview (using bat):

# Fish shell: Set environment variables
set -gx FZF_CTRL_T_OPTS "--preview 'bat --color=always --line-range :500 {}' --preview-window=right,50%"
set -gx FZF_DEFAULT_COMMAND 'fd --hidden --exclude=.git'
# Bash/Zsh: Export environment variables
export FZF_CTRL_T_OPTS="--preview 'bat --color=always --line-range :500 {}' --preview-window=right,50%"
export FZF_DEFAULT_COMMAND='fd --hidden --exclude=.git'

This displays a syntax-highlighted preview of the selected file, reducing incorrect selections by ~95% compared to blind selection.

Advanced fzf Patterns

Combining fzf with other tools creates powerful workflows:

Fish shell functions:

# Find and edit a file
function fe --description "Fuzzy find and edit file"
    set -l file (fzf --preview 'bat --color=always {}')
    [ -n "$file" ] && $EDITOR "$file"
end

# Git add with preview
function git_add_preview --description "Interactively add files with preview"
    git diff --name-only | fzf --preview 'git diff {}' | xargs git add
end

Bash/Zsh functions:

# Find and edit a file
fzf_edit() {
  local file=$(fzf --preview 'bat --color=always {}')
  [ -n "$file" ] && $EDITOR "$file"
}

# Git add with preview
fzf_git_add() {
  git ls-files -m -o --exclude-standard | fzf \
    --preview 'git diff --color=always {}' | xargs git add
}

Fish advantage: Native array handling ($argv), direct string interpolation, and cleaner function syntax eliminate the complexity of Bash parameter expansion.

Code Review and Git Workflow

delta: Syntax-Aware Diff Rendering

Git diffs in standard terminals show only line-level changes. delta adds three layers of clarity:

  1. Syntax Highlighting: Language-aware coloring for code blocks
  2. Word-Level Diffs: Highlights character-level changes within lines (shows oldvarnewvar in red/green)
  3. Side-by-Side Layout: Optional parallel view with line wrapping

Impact on code review speed: Research shows side-by-side diffs with syntax highlighting reduce review comprehension time by 30-40% compared to unified diffs. See delta documentation for all available features and themes.

Configuration (.gitconfig):

[core]
  pager = delta

[delta]
  syntax-theme = Dracula
  side-by-side = true
  line-numbers = true

lazygit: Interactive Git UI

lazygit provides a keyboard-driven TUI for git operations, dramatically accelerating complex workflows:

Core capabilities:

  • Interactive Staging: Select individual hunks (5-line chunks) from files to stage, rather than staging entire files
  • Interactive Rebase: Visually reorder, squash, reword, and fixup commits without git rebase -i editor gymnastics
  • Merge Conflict Resolution: Visual diff of conflicts with staged/unstaged pane management
  • Branch Management: Create, delete, merge, rebase branches with keyboard shortcuts

Productivity gain: What requires 10+ CLI commands (e.g., git rebase -i, then editor interactions, then git add --patch) becomes 3-4 keypresses.

Fish shell key binding (~/.config/fish/functions/fish_user_key_bindings.fish):

function fish_user_key_bindings
    # Bind Ctrl-G to lazygit
    bind \cg 'lazygit; commandline -f repaint'
    
    # Bind Ctrl-D to git diff with delta
    bind \cd 'git diff | delta | less -R; commandline -f repaint'
end

Bash/Zsh key binding (~/.bashrc or ~/.zshrc):

# Add to your config file
bind '"\C-g":"lazygit\n"'
bind '"\C-d":"git diff | delta | less -R\n"'

Critical Fish syntax notes:

  • \c prefix = Control key (Ctrl-X → \cx) - see Fish key bindings documentation
  • commandline -f repaint = Redraw prompt after external command execution
  • Fish’s bind command integrates seamlessly with external TUI applications

Example workflow:

  1. Launch lazygit (or press Ctrl-G if bound)
  2. Press I (interactive rebase) on commits pane
  3. Visually drag/drop commits to reorder
  4. Press s to squash into previous commit
  5. Save automatically

System Monitoring

btop: Real-Time Resource Visualization

Traditional system monitors (top, htop) display data in ASCII tables. btop++ adds graphical elements (ASCII-based line graphs) and interactive process management:

Featurehtopbtop++
CPU/Memory/Disk graphsBar indicatorsLine graphs over time
Process tree viewYesYes, with detailed stats
Disk I/O per processNoYes, with speed metrics
GPU monitoringNoPartial (vendor-dependent)
Custom themesLimitedExtensive (themes in config)
Performance overhead~2-3% CPU~1-2% CPU (C++ vs. Python)

Use case: During a long-running deployment, btop reveals whether slowness comes from disk I/O (saturated NVMe), network (bandwidth exhaustion), or CPU contention-actionable diagnostics in seconds.

Supporting Utilities

jq: JSON Processing Pipeline

jq treats JSON as a first-class stream type, not unstructured text:

# Fish shell: Direct pipe composition without escaping
curl https://api.github.com/users/octocat | jq '.repos[] | {name, stars: .stargazers_count}'

# Variable interpolation without $()
set repos (curl https://api.github.com/users/octocat | jq '.public_repos')
echo "Total repos: $repos"
# Bash/Zsh: Same syntax works identically
curl https://api.github.com/users/octocat | jq '.repos[] | {name, stars: .stargazers_count}'

jq’s filter language (similar to functional programming) enables complex data transformations without writing loops, reducing JSON-to-CSV/summary operations from 20 lines of code to single-line expressions.

tldr: Practical Command Examples

While man pages provide authoritative documentation, they bury practical examples in 100+ lines of prose. tldr provides community-curated examples for 5,000+ commands in 10-15 lines:

$ tldr tar
tar
Pack or unpack files to/from an archive

$ tldr -l | fzf --preview 'tldr {1}'  # Interactive browsing

Combined with fzf, you can fuzzy-search for commands and preview examples before running them.

trash-cli: Safe Deletion

rm -rf permanently deletes files-a disaster waiting to happen in scripts. trash-cli moves files to the system trash instead:

# Fish shell: Alias replaces rm
alias --save rm "trash"
# Bash/Zsh: Alias replaces rm
alias rm='trash'

Usage:

trash file.txt      # Moves to ~/.local/share/Trash
trash 'project/**'  # Pattern-based deletion

Recovery is straightforward: ls ~/.local/share/Trash/files/ to verify, then restore via file manager or mv from trash.

macOS-Specific Tools

Raycast: Application Launcher

Raycast replaces macOS Spotlight with a searchable launcher featuring 300+ extensions:

Built-in commands:

  • Clipboard history (searchable, timestamped)
  • Window management (snap to halves, thirds, grid)
  • Focus sessions (blocks distracting apps)
  • Calendar integration (one-key meeting join with camera check)
  • Quick links (custom shortcuts to frequently used URLs/tools)

Extension examples: GitHub repo search, Spotify control, language translation, system information.

Keyboard shortcuts: You can assign single hotkeys to frequently-used commands (e.g., Cmd-Shift-C for clipboard history), eliminating the need to open Raycast’s search bar.

Rectangle: Keyboard-Driven Window Management

Rectangle removes the mouse from window resizing on macOS:

Cmd-Option-Left      → Snap to left half
Cmd-Option-Right     → Snap to right half
Cmd-Option-Up        → Maximize (not fullscreen)
Cmd-Option-1,2,3,4   → Snap to quadrants

Without Rectangle, macOS’s native window snapping leaves a gap under the menu bar and requires dragging. Keyboard shortcuts enable power users to manage 4 windows in <1 second.

Integration Patterns: The Full-Stack Development Loop

A developer using this toolkit completes: find file → edit → test → stage → commit → review in a single terminal session without touching the mouse:

Fish shell workflow:

# 1. Find changed files (using git + fzf)
git diff --name-only | fzf --preview 'bat {}'

# 2. Edit selected file (Fish uses parentheses for command substitution)
$EDITOR (git diff --name-only | fzf)

# 3. View diffs with syntax highlighting
delta                    # Automatically integrated into git

# 4. Interactive staging (Ctrl-G if bound)
lazygit                  # Ctrl-A to stage all, 's' to squash commits

# 5. Monitoring (Ctrl-S if bound)
btop                     # Check if tests are CPU-bound or I/O-bound

Bash/Zsh workflow:

# 1. Find changed files (using git + fzf)
git diff --name-only | fzf --preview 'bat {}'

# 2. Edit selected file
vim $(git diff --name-only | fzf)

# 3. View diffs with syntax highlighting
delta                    # Automatically integrated into git

# 4. Interactive staging
lazygit                  # Ctrl-A to stage all, 's' to squash commits

# 5. Monitoring
btop                     # Check if tests are CPU-bound or I/O-bound

The entire workflow is keyboard-driven, requires zero mouse interaction, and provides visual feedback at each step.

Cross-Tool Composition

These tools follow Unix philosophy-each solves one problem well, and they compose via stdin/stdout:

Fish shell:

# Find all *.ts files, preview with bat, select with fzf, open in editor
fd '\.ts$' | fzf --preview 'bat {}' | xargs $EDITOR

# Interactively add git changes with visual diff preview
git diff | delta | fzf --preview-window=right,50%

# Search code, preview matches, fuzzy select, navigate
rg 'useState' --type javascript | fzf --preview 'bat {1} --highlight-line {2}'

Bash/Zsh:

# Find all *.ts files, preview with bat, select with fzf, open in editor
fd '\.ts$' | fzf --preview 'bat {}' | xargs vim

# Interactively add git changes with visual diff preview
git diff | delta | fzf --preview-window=right,50%

# Search code, preview matches, fuzzy select, navigate
rg 'useState' --type javascript | fzf --preview 'bat {1} --highlight-line {2}'

Performance Benchmarks

OperationTraditionalModern StackImprovement
Search 500K files for pattern15-30s (find + grep)0.15s (fd + rg)100-200x
Navigate to nested directory5-8 keystrokes (cd + cd + cd)1-2 keystrokes (zoxide)75-80% reduction
Review code diff45-60s (read unified diff)15-20s (delta side-by-side)60% faster
Stage changes for commit30-45s (git add -p, editor loops)5-10s (lazygit visual UI)70% faster
Terminal prompt render100-500ms (bash/Oh My Zsh)<5ms (Starship)20-100x faster
Shell startup time300-600ms (Zsh/Oh My Zsh)50-100ms (Fish)3-6x faster
System monitoring insight1-2 minutes (reading htop)10-15s (btop graphs)80% faster

Daily impact: A developer performing 100 file searches, 20 directory navigations, 10 diffs, and 8 commits saves 20-30 minutes per day-equivalent to an extra hour of deep work every 2-3 days.

Fish Shell: Aliases, Abbreviations, and Functions

Fish has three distinct constructs for command shortcuts. Choosing correctly improves performance and usability:

PatternUse CaseStartup ImpactExample
aliasSimple command wrappingNone (lazy-loaded)alias --save gst "git status"
abbrFrequently-typed commands1-2ms each (loads at init)abbr -a gco "git checkout"
functionMulti-line logic, conditional behaviorNone (lazy-loaded)Complex git workflows

Best practices:

Aliases - Use for simple replacements (zero startup cost):

alias --save ll "ls -lah"
alias --save cat "bat"
alias --save find "fd"
alias --save grep "rg"

Abbreviations - Use for frequently-typed commands (expands in-place):

abbr -a gpl "git pull"
abbr -a gps "git push"
abbr -a gac "git add .; git commit -m"

Functions - Use for complex logic or multi-command operations:

function git_clean_branches --description "Remove deleted remote branches"
    git branch -vv | grep '\[.*gone\]' | awk '{print $1}' | xargs git branch -d
end

function git_add_preview --description "Preview files before staging"
    git diff --name-only | fzf --preview 'git diff {}' | xargs git add
end

Performance notes:

Configuration and Maintenance

Most tools require minimal configuration. Common patterns:

  1. Install via Homebrew (macOS) or distro package manager (Linux)
  2. Create config files in ~/.config/toolname/ (follows XDG standard)
  3. Add shell integrations to your shell config file
  4. Create aliases/abbreviations/functions for frequently-combined commands

Fish Shell Configuration

Complete ~/.config/fish/config.fish template:

# Initialize core tools
zoxide init fish | source

# Aliases (lazy-loaded, zero startup cost)
alias --save ll "ls -lah"
alias --save cat "bat"
alias --save find "fd"
alias --save grep "rg"
alias --save diff "delta"
alias --save rm "trash"
alias --save gst "git status"

# Abbreviations (in-place expansion, use sparingly)
abbr -a -- gpl "git pull"
abbr -a -- gps "git push"
abbr -a -- gac "git add .; git commit -m"

# Environment variables
set -gx EDITOR nvim
set -gx FZF_DEFAULT_COMMAND 'fd --hidden --exclude=.git'
set -gx FZF_CTRL_T_OPTS "--preview 'bat --color=always --line-range :500 {}' --preview-window=right,50%"

# Functions (lazy-loaded)
function git_add_preview --description "Interactively add files with preview"
    git diff --name-only | fzf --preview 'git diff {}' | xargs git add
end

function fe --description "Fuzzy find and edit file"
    set -l file (fzf --preview 'bat --color=always {}')
    [ -n "$file" ] && $EDITOR "$file"
end

Key bindings (~/.config/fish/functions/fish_user_key_bindings.fish):

function fish_user_key_bindings
    bind \cg 'lazygit; commandline -f repaint'
    bind \cd 'git diff | delta | less -R; commandline -f repaint'
    bind \cs 'btop; commandline -f repaint'
end

Fish performance advantages:

  • Aliases: Lazy-loaded functions; zero startup overhead
  • Abbreviations: Load as universal variables; 1-2ms each (limit to 15-20 total)
  • Functions: Stored in ~/.config/fish/functions/; lazy-loaded
  • Startup time: 50-100ms vs. 300-600ms for Zsh with Oh My Zsh

Bash/Zsh Configuration

Example .bashrc or .zshrc additions:

# Initialize core tools
eval "$(zoxide init bash)"    # or zsh
eval "$(fzf --bash)"          # or --zsh
. "$(brew --prefix)/opt/fzf/shell/key-bindings.bash"  # or .zsh

# Aliases
alias ll='ls -lah'
alias cat='bat'
alias find='fd'
alias grep='rg'
alias diff='delta'
alias rm='trash'

# Functions
fzf_edit() {
  local file=$(fzf --preview 'bat --color=always {}')
  [ -n "$file" ] && $EDITOR "$file"
}

# Environment variables
export EDITOR=nvim
export FZF_DEFAULT_COMMAND='fd --hidden --exclude=.git'
export FZF_CTRL_T_OPTS="--preview 'bat --color=always --line-range :500 {}' --preview-window=right,50%"

The Bottom Line

The modern terminal stack replaces historical Unix utilities with specialized, performant tools optimized for developer ergonomics.

Rather than a single “best” terminal, this ecosystem emphasizes composability-each tool solves one problem exceptionally well, enabling powerful workflows through piping and integration.

Why Fish shell fits this stack:

  • Lazy-loaded aliases: Zero shell startup overhead
  • Superior key binding UX: bind syntax is cleaner and more discoverable (see Fish key bindings)
  • Universal variables: Persistent configuration without sourcing scripts (see Fish universal variables)
  • Performance: 50-100ms startup vs. 300-600ms for Zsh with Oh My Zsh
  • Modern syntax: end keyword, 1-based array indexing, direct string interpolation (see Fish syntax documentation)

For developers using this setup daily, the cumulative time savings (20-30 minutes per day) and ergonomic improvements (zero mouse usage, keyboard-driven workflows) represent a 10-15% productivity boost, equivalent to 4-6 weeks of additional focus time per year.

The tools profiled here aren’t experimental-they’re actively maintained, widely adopted across the developer community, and available across macOS, Linux, and Windows platforms. The learning curve is front-loaded (1-2 weeks to internalize keybindings), but the payoff compounds daily.

Quick reference links:

  • fd - Modern find replacement
  • zoxide - Smart directory navigation
  • ripgrep - Fast text search
  • fzf - Fuzzy finder
  • bat - Syntax-highlighted cat
  • delta - Git diff viewer
  • lazygit - Interactive git UI
  • btop - System monitor
  • jq - JSON processor
  • tldr - Command examples
  • trash-cli - Safe deletion
  • Fish shell - Modern shell
  • Fisher - Fish package manager

Rebuilding your terminal workflow? I’d love to hear what tools you’re using. Reach out on LinkedIn.