Completing the Terminal Stack: Bash, Starship, and History Tools


The Portability Question

Every shell choice involves a tradeoff: features vs ubiquity.

Fish has amazing defaults—syntax highlighting, autosuggestions, sane scripting. But it’s not POSIX-compatible, so scripts break. And it’s not installed by default anywhere.

Zsh has plugins for everything. But plugin managers add complexity, and “everything” includes a lot you don’t need.

Bash is everywhere. Every server, every container, every CI runner. No installation, no compatibility issues. The skills transfer.

My choice: Stay on Bash, but layer modern tools on top. I get portability and power.

Starship: One Prompt Everywhere

The problem: Shell prompts are shell-specific. A Zsh prompt config doesn’t work in Bash. If you switch shells, you reconfigure everything.

The solution: Starship is a standalone binary. It works with Bash, Zsh, Fish, PowerShell, even Nushell. One config, every shell.

# In .bashrc, .zshrc, or config.fish
eval "$(starship init bash)"  # or zsh, fish, etc.

The mental model: The prompt is a separate concern from the shell.

What Starship Shows

Starship auto-detects context:

~/projects/myapp on  main via  v20.10.0 via 🐍 v3.11.0
  • Directory path
  • Git branch and status (only in git repos)
  • Node version (only in Node projects)
  • Python version (only in Python projects)

Enter a different directory, the prompt adapts. No configuration per project.

My starship.toml customizations
# ~/.config/starship.toml

# Compact - no extra newline
add_newline = false

# Only show slow command duration
[cmd_duration]
min_time = 2000  # 2 seconds

# Clear success/failure indicator
[character]
success_symbol = "[➜](bold green)"
error_symbol = "[✗](bold red)"

# Only detect languages I actually use
[nodejs]
detect_files = ["package.json"]

[python]
detect_files = ["requirements.txt", "pyproject.toml"]

[rust]
detect_files = ["Cargo.toml"]

The defaults are sensible. I only customize what matters to me.

History: Three Tools, One Interface

The problem: Bash’s built-in history is primitive. Ctrl+R does substring search. No fuzzy matching, no context awareness, no sync across machines.

The insight: History search is a separate concern. Replace it without replacing the shell.

I use three tools interchangeably, all bound to Ctrl+R:

Atuin (Default)

Stores history in SQLite. Fuzzy search. Filters by directory, exit code, time. Optional cloud sync.

Key insight: Directory-aware search. In project A, Ctrl+R prioritizes commands from project A. Your global history doesn’t pollute project context.

McFly

Neural network predicts which command you want based on:

  • Current directory
  • Recent commands
  • Exit codes (successful commands rank higher)
  • Frequency

Tradeoff: Smarter but heavier. Better for long sessions where context matters.

fzf

General-purpose fuzzy finder. Less smart than the others, but starts instantly and works everywhere.

Tradeoff: No context awareness, but zero startup overhead.

Switching Between Them

# In .bashrc
case "${HISTORY_TOOL:-atuin}" in
    atuin) eval "$(atuin init bash)" ;;
    mcfly) eval "$(mcfly init bash)" ;;
    fzf)   eval "$(fzf --bash)" ;;
esac

Start a shell with a specific tool:

HISTORY_TOOL=mcfly bash

The mental model: History backend is pluggable. The interface (Ctrl+R) stays consistent.

Installing each tool
# Atuin
curl --proto '=https' --tlsv1.2 -LsSf https://setup.atuin.sh | sh

# McFly (Arch)
paru -S mcfly

# fzf (most package managers)
sudo pacman -S fzf  # Arch
brew install fzf    # macOS

Shell Integration: The Glue

For Kitty to open new tabs in your current directory, it needs to know where you are. Terminals and shells don’t share this information by default.

The mechanism: OSC 7. An escape sequence that reports the current directory.

# In .bashrc
if [[ -n "$KITTY_WINDOW_ID" ]]; then
    _kitty_report_pwd() {
        printf '\e]7;file://%s%s\e\\' "$HOSTNAME" "$PWD"
    }
    PROMPT_COMMAND="_kitty_report_pwd${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
fi

This runs after every command and tells Kitty where you are. Now Ctrl+Shift+T opens a tab here, not in $HOME.

The pattern: Terminal and shell cooperate via escape sequences. Same mechanism powers Starship’s timing, Atuin’s history capture, and more.

Aliases: Consistent Behavior Across Machines

Aliases ensure commands behave consistently regardless of which machine I’m on.

Philosophy: Make dangerous things safer, make common things shorter.

# Safer defaults (confirm before overwriting)
alias cp='cp -i'
alias mv='mv -i'
alias rm='rm -I'  # -I prompts once for >3 files

# Human-readable output
alias df='df -h'
alias free='free -m'
alias du='du -h'

# Quick access
alias '?'='claude -p'  # Quick Claude queries

The ? alias is my favorite. Quick questions without a full session:

? "how do I revert the last git commit"
Universal archive extraction function

Different archives need different commands. I never remember which.

ex() {
    if [ -f "$1" ]; then
        case "$1" in
            *.tar.bz2) tar xjf "$1" ;;
            *.tar.gz)  tar xzf "$1" ;;
            *.tar.xz)  tar xJf "$1" ;;
            *.tar)     tar xf "$1" ;;
            *.tbz2)    tar xjf "$1" ;;
            *.tgz)     tar xzf "$1" ;;
            *.zip)     unzip "$1" ;;
            *.rar)     unrar x "$1" ;;
            *.7z)      7z x "$1" ;;
            *.gz)      gunzip "$1" ;;
            *.bz2)     bunzip2 "$1" ;;
            *.xz)      unxz "$1" ;;
            *)         echo "'$1' cannot be extracted" ;;
        esac
    else
        echo "'$1' is not a valid file"
    fi
}

Now: ex whatever.tar.gz — no thinking about syntax.

The Complete Stack

block-beta
  columns 1
  block:layer1["Terminal Layer"]:1
    kitty["Kitty (GPU rendering)"]
  end
  block:layer2["Shell Layer"]:1
    bash["Bash (portable, everywhere)"]
  end
  block:layer3["Enhancements"]:1
    columns 3
    starship["Starship"]
    history["Atuin/McFly/fzf"]
    aliases["Aliases"]
  end

Each layer is independently replaceable:

  • Don’t like Bash? Starship works with Zsh
  • Don’t like Atuin? Swap in fzf
  • Don’t like Kitty? Everything else still works

The mental model: Unix philosophy applied to shell setup. Small tools, clear interfaces, compose freely.

Design Decisions Summary

DecisionTradeoffWhy I Chose It
Bash over Fish/ZshFewer featuresPortability to any server
StarshipExtra binaryOne config across shells
Atuin defaultHeavier than fzfDirectory-aware search
Switchable historyComplexityDifferent tools for different contexts
OSC 7 integrationShell modificationDirectory inheritance in tabs

Next in this series: From Terminal to Development Environment: Putting It All Together