For the last few weeks I’ve been setting up a new laptop from scratch for the first time in a… very long time, taking the time especially to re-evaluate my terminal setup and integrating some new tools, and re-integrating some old tools in a new way.

Note: Almost all of the tools and scripts mentioned below can be easily installed using homebrew. If you want to use my .zshrc config as-is, you’ll want to use homebrew.


If you want to skip ahead and see the final outcome, checkout the screencast at the end of this post.

Building a Better Prompt

Everything in the terminal starts with your prompt. Before I moved to zsh, I used bash-it with bash to create a fancy prompt that gave me things like current directory, git status and branch, and tool versions. Moving to zsh, I installed oh-my-zsh and my prompt took a giant leap forward with display of command exit status, command timing, icons, and more tool versions. Unfortunately, my prompt got so bloated that I started to dread opening a new terminal.

Thankfully, I found Starship, a super fast, super configurable prompt written in Rust. It works with most shells, on most operating systems.

Even with additional prompt information like Kubernetes context and namespace, Google Cloud CLI version, and a prettier (to me!) more usable prompt, it’s blazing fast.

Here you can see that I’m in the bag directory, which is on the main branch with some untracked files, using NodeJS v22.2.0, and PHP 8.3.7. You can also see that the previous command took five seconds to run, and the current (24 hours) time, and you can see how quickly it renders using starship explain:

As you can see, the total rendering time is less than 150ms. And my configuration (which is minimally updated from the default) includes support for dozens of other languages and useful information.

After the Prompt

Before we add any other applications, let’s add some zsh scripts to make using the parts after the prompt nicer.

First, we’re going to configure how zsh stores your history. The history is a log of every command you’ve entered into the shell. We are going to set the following options:

# Save the history in your home directory as .zsh_history
export HISTFILE=$HOME/.zsh_history 

# Set the history size to 2000 commands
export HISTSIZE=2000               

# Store the same number to disk
export SAVEHIST=$HISTSIZE          

# Share history between sessions
setopt share_history               

# Remove duplicates first when HISTSIZE is met
setopt hist_expire_dups_first      

# If a command is issued multiple times in a row, ignore dupes
setopt hist_ignore_dups  

# Allow editing the command before executing again
setopt hist_verify                 

# Do not add commands prefixed with a space to the history
setopt hist_ignore_space           

Next, let’s change how we interact with the history. By default, you use Ctrl+r or Ctrl+Shift+R which will bring up the bck-i-search UI, that will match history commands as you type, to navigate forward or backwards from the current match you need to then press Ctrl+r or Ctrl+Shift+R again multiple times until you find the item you want.

This is not intuitive, or efficient, so our first task is fix that using zsh-autosuggestions. zsh-autosuggestions will provide autocomplete suggestions from your history (and other locations) as you type, simply press the right arrow key, or use the end-of-line keyboard shortcut (e.g. ctrl+e) to accept the suggestion. If you only want part of a suggestion, you can use the forward-word keyboard shortcut, which is option+right arrow or alt+right arrow which will complete only up to the end of the next word and continue providing suggestions from there.

This works great if you can remember the exact start of the previous command you want to use, but what if you only remember part of it? Enter zsh-history-substring-search. This script allows you to navigate your history using substring matches, and you can bind it to your up and down arrow keys so that you can simply type the substring and then hit the up key to start finding matches.

Lastly, let’s make what commands we’re running easier to read, using zsh-syntax-highlighting. This script (which must be loaded before the zsh-history-substring-search above) will provide syntax highlighting of commands as you write them. This will help you avoid typos for incorrect commands, and make complex commands easier to read.

Changing Directories For The Better

One of the most — if not the most — common commands that I use is cd. A very simple commands, it changes your current working directory to the one specified.

One simple trick to improve your use of cd is knowing that cd - will take you back to whatever directory you were previously in. This alone had a massive impact on my ability to navigate around my directory structure.

There is, however, an even better solution: zoxide, a cd alternate, and can be used as a drop-in replacement.

zoxide uses fuzzy matching and a frequency of usage ranking to intelligently let you change directories based on substrings. For example, to reach the bag repo that lives at ~/src/bag, I can use cd bag from anywhere, if however I want to enter the bag-benchmarks repo, I can use cd bag- and it will take me there instead.

Under the hood, zoxide uses fzf for completions and interactive selection, but fzf‘s utility doesn’t end there.

Finding Clarity Through Fuzziness

fzf is really the hero of this story, as it can integrate into many different aspects of your terminal to bring improvements.

For example, it can be used as part of tab suggestions. When you hit <tab> to get suggestions, it will display a UI with a keyboard navigable, searchable, list of possibilities:

This also works for arguments:

But fzf can do so much more. To expand on it’s capabilities, let’s add two helpers: fd and eza.

Find Everything

fd is a simpler, faster alternative to find, which is valuable in and of itself, but paired with fzf, it can enable faster/better suggestions. Adding the following will use fd for both file and folder completion:

_fzf_compgen_path() {
    fd --hidden --exclude .git . "$1"
}

_fzf_compgen_dir() {
    fd --type=d --hidden --exclude .git . "$1"
}

With this in place, fzf will suggest both directories and files (except .git) when trying to auto-complete paths (see above screenshot with ls), but will only show directories (except .git) when using commands like cd that only accept a directory:

Show Everything

eza is a drop-in replacement for ls, and provides a much richer file list experience, from adding rich colors, to showing icons and git status.

I alias eza to ls with the following options:

alias ls="eza --icons=always --color=always --git"

This allows me to continue to use ls the way I always have (compact format by default, adding -al to show all files in list format) but will add eza colors, icons, and show git information is list view.

For example, calling ls -al will show the following:

Here you can see the standard ls -al style output but with excellent highlighting, as well as the introduction of git status (between the date time and the icon) and the icons.

WARNING: eza expects you to be using a nerdfont, which you will install as part of installing Starship, but if you chose not to do so, you will want to do that separately. I use SauceCodePro.

See Everything

Now that you have eza installed, you can setup fzf previews, which can be context aware, similar to the file and directory lists shown when tab completing those.

For example, with the integration of eza and fzf, that list of directories for cd can include a tree-based preview of the selected directory. To invoke fzf you use the placeholder ** and then hit <tab>, e.g. cd ** could show the following:

You can then make your selection using the up/down arrow keys and hit <enter> to replace the ** with the selected directory.

With some additional configuration, and a simple script, we can show either a directory tree preview, or use bat to show a syntax highlighted preview of the file contents:

 _fzf_comprun() {
   local command=$1
   shift

   case "$command" in
     cd) fzf --preview 'eza --tree --color=always {} | head -200' "$@" ;;
     export|unset) fzf --preview "eval 'echo \$'{}"         "$@" ;;
     ssh)          fzf --preview 'dig {}'                   "$@" ;;
     cat|bat)          fzf --preview 'bat -n --color=always {}' "$@" ;;
     *)            fzf --preview '$HOME/bin/fzf-preview.sh {}' "$@" ;;
   esac
 }
#!/bin/bash
if test -d "$@"; then
    eza --tree --color=always --icons=always --git "$@" | head -200
else
    bat -n --color=always "$@"
fi

Lastly, we can override the default <tab> completion behavior to use fzf with preview by adding the following configuration (and using the same fzf-preview.sh script above) by adding the following to your .zshrc:

 # Enable using fzf preview with eza when using tab completion with `cd`
 zstyle ':completion:*' menu no
 zstyle ':fzf-tab:complete:*' fzf-preview '$HOME/bin/fzf-preview.sh $realpath'
 zstyle ':fzf-tab:complete:cd:*' fzf-preview 'eza --tree --color=always --icons=always --git $realpath | head -200'
 zstyle ':fzf-tab:*' switch-group '<' '>'

Reaching Nerdvana

Now that you know all the pieces, you need to put them together, which is why I have made my entire .zshrc available here.

With this .zshrc you will have the following features:

  • Starship prompt
  • command syntax highlighting
  • automatic suggestions for completions as you type
  • substring search using the up/down keys
  • zoxide for a better cd experience (aliased)
  • eza for a better ls experience (aliased)
  • fd for better file search
  • fzf for fuzzy finding and previews with <tab> activation for directories and files

You can see a demo of all of these features below:

While you don’t need to adopt all of these tools and features, together they create a powerful terminal experience.

Is there something missing? Let me know in the comments below!

If you want to learn more about bat, used to show syntax highlighted file previews, and other developer-focused tools, check out my earlier post: