My Zsh Setup

11 min read

Few years ago, I wrote Supercharge Your Terminal with Zsh. Since then, I've made a few changes to my Zsh setup. So it's time for an update.

I won't go through the installation process, as it depends on your operating system. So make sure to install it first using your favorite package manager. If you're on MacOS, an older version of zsh is pre-installed, so you still may want to install a newer version with Homebrew or another package manager.

To make Zsh your default shell, run:

chsh -s $(which zsh)

Now we're ready to configure Zsh by editing the ~/.zshrc file.

If you want to see my full Zsh configuration, jump to the Wrapping Up section.

Why not Oh My Zsh?

One of the most popular Zsh frameworks is Oh My Zsh. It's an easy way to have a great Zsh setup with a lot of plugins and themes. However, it adds a lot of things that I don't need. All these additional code also slows down the shell startup time. So I set up my Zsh manually with the plugins and configurations that I need.

Plugin manager

When configuring Zsh, we often need to install "plugins". They are often installed using a plugin manager. By now I have switched the plugin manager many times since the ones I used before got abandoned. And now there are so many of them that it's hard to choose.

You can see a list of plugin managers and their benchmarks at rossmacarthur/zsh-plugin-manager-benchmark.

Personally I decided to not use a plugin manager and use a simple function that clones and sources the plugins:

# Enable zsh recompilation
autoload -Uz zrecompile
 
plugins=(
  # List of plugins
)
 
PLUGIN_DIR=$HOME/.zsh_plugins
 
for plugin in $plugins; do
  if [[ ! -d $PLUGIN_DIR/${plugin:t} ]]; then
    git clone --depth 1 https://github.com/${plugin} $PLUGIN_DIR/${plugin:t}
 
    for f in $PLUGIN_DIR/${plugin:t}/**/*.zsh; do
      echo "Recompiling $f"
      zrecompile -pq "$f"
    done
  fi
 
  if [[ -f $PLUGIN_DIR/${plugin:t}/${plugin:t}.plugin.zsh ]]; then
    source $PLUGIN_DIR/${plugin:t}/${plugin:t}.plugin.zsh
  fi
done

Make sure to place it at the top of your ~/.zshrc file so that the plugins are loaded before they are used. For a more advanced setup, see mattmc3/zsh_unplugged.

You can also choose to use a plugin manager if you prefer. You'll need to install it first, and adapt the code in this article to match the plugin manager you choose.

Autocompletions

To setup autocompletion, we need to load compinit. However, compinit is the slowest part of my shell's startup. So with the below snippet, we use a cache for compinit if it was last updated a day ago:

autoload -Uz compinit
 
typeset -i updated_at=$(date +'%j' -r $HOME/.zcompdump 2>/dev/null || stat -f '%Sm' -t '%j' $HOME/.zcompdump 2>/dev/null)
typeset -i today=$(date +'%j')
 
if [[ $updated_at -eq $today ]]; then
  compinit -C -i
else
  compinit -i
fi

The first part in updated_at in the above snippet is for Linux, and the second part is for MacOS.

Then we need to load the complist module that provides a list of completions to select from:

zmodload -i zsh/complist

Then we can configure the style of the completions:

zstyle ':completion:*' menu select # select completions with arrow keys
zstyle ':completion:*' group-name '' # group results by category
zstyle ':completion:::::' completer _expand _complete _ignored _approximate # enable approximate matches for completion

We can tweak few more things to improve the autocomplete menu:

setopt auto_list # automatically list choices on ambiguous completion
setopt auto_menu # automatically use menu completion
setopt always_to_end # move cursor to end if word had one match

Now when we type a command and press Tab, we can cycle through the completions with the arrow keys and select one with Enter, instead of cycling through them with Tab.

In addition, we can also add the zsh-users/zsh-completions plugin to get additional completions. To use it, add it to the plugins array:

plugins=(
  zsh-users/zsh-completions
)

Syntax Highlighting

The zdharma-continuum/fast-syntax-highlighting plugin can add syntax highlighting the commands as you type them. In addition, when typing a command, it’ll be highlighted in red if it’s invalid and in green if it’s valid.

To use it, add it to the plugins array:

plugins=(
  zdharma/fast-syntax-highlighting
)

One thing I disable is the paste highlighting. When I paste a command, the plugin adds a background to the pasted text to add a highlight. It makes it hard for me to see the cursor to edit the pasted text. So I disable it with the following:

zle_highlight+=(paste:none)

Autosuggestions

The zsh-users/zsh-autosuggestions plugin can suggest completions based on your command history. which you can select with the right arrow key (➡).

To set it up, first we need to configure Zsh to store the history in a file since it's not enabled by default:

HISTFILE=$HOME/.zsh_history # path to the history file
HISTSIZE=100000 # number of history items to store in memory
HISTDUP=erase # remove older duplicate entries from history
SAVEHIST=$HISTSIZE # number of history items to save to the history file

We can also configure the history items to ignore duplicates and other improvements:

setopt hist_expire_dups_first # expire duplicate entries first when trimming history
setopt hist_find_no_dups # don't display duplicate entries in history
setopt hist_ignore_space # ignore commands starting with space
setopt hist_ignore_all_dups # remove older duplicate entries from history
setopt hist_reduce_blanks # remove superfluous blanks from history items
setopt hist_save_no_dups # don't save duplicate entries in history
setopt hist_verify # don't execute immediately upon history expansion
setopt inc_append_history # save history entries as soon as they are entered
setopt share_history # share history between different instances

Then we can add the plugin to the plugins array:

plugins=(
  zdharma-continuum/fast-syntax-highlighting
  zsh-users/zsh-autosuggestions
)

The plugin depends on the zdharma-continuum/fast-syntax-highlighting plugin, so make sure to add it to the plugins array as well.

The zsh-users/zsh-history-substring-search lets type part of a command which exists in the history, and then select the matching command with a keybinding.

To use it, add it to the plugins array:

plugins=(
  zsh-users/zsh-history-substring-search
)

Then we need to configure the keybindings. For example, to select with up and down arrow keys, we need to add the following configuration:

bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down

One of the coolest tools I use is fzf. It's a command-line fuzzy finder that can be used to search through history, files, and more. I also like to integrate it with Zsh so my autocomplete menu is replaced with fzf.

To use it, first make sure to install it using your package manager. Then add the following to your ~/.zshrc file:

if [[ -x $(command -v fzf) ]]; then eval "$(fzf --zsh)"; fi

Now when you typ Ctrl + R, it'll bring up a fuzzy search menu to search through the history, and when you press Ctrl + T, it'll bring up a fuzzy search menu to search through the files in the current directory.

I also have a color scheme for fzf to match Palenight since it doesn't use the shell colors. You can set it using the FZF_DEFAULT_OPTS environment variable:

export FZF_DEFAULT_OPTS=" \
--color=bg+:#424762,spinner:#b0bec5,hl:#f78c6c \
--color=fg:#bfc7d5,header:#ff9e80,info:#82aaff,pointer:#a5adce \
--color=marker:#89ddff,fg+:#eeffff,prompt:#c792ea,hl+:#ff9e80 \
--color=selected-bg:#424762"

Next step is to integrate it with the autocompletions. We can use the Aloxaf/fzf-tab plugin to achieve this. To use it, add it to the plugins array:

plugins=(
  Aloxaf/fzf-tab
)

Then we can configure the style of the completions:

zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' # case-insensitive completion
zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" # colorize filenames
zstyle ':completion:*' menu no # disable menu completion for fzf-tab
zstyle ':fzf-tab:complete:cd:*' fzf-preview 'ls --color $realpath' # preview directory contents with cd
zstyle ':fzf-tab:complete:__zoxide_z:*' fzf-preview 'ls --color $realpath' # preview directory contents with zoxide
zstyle ':fzf-tab:*' use-fzf-default-opts yes # use FZF_DEFAULT_OPTS for fzf-tab

Make sure to remove the previous completion configuration (starting with zstyle ':completion) as it's not needed anymore.

Now when you type a command and press Tab, you'll see a fuzzy search menu instead of the regular completion menu.

Custom Prompt

For my setup, I use the spaceship prompt. It shows various information like the current directory, git status, and more.

Spaceship Prompt

This is my configuration:

# Theme
SPACESHIP_PROMPT_ORDER=(
  user          # Username section
  dir           # Current directory section
  host          # Hostname section
  git           # Git section (git_branch + git_status)
  hg            # Mercurial section (hg_branch  + hg_status)
  node          # Node.js section
  exec_time     # Execution time
  async         # Async jobs indicator
  jobs          # Background jobs indicator
  exit_code     # Exit code section
  sudo          # Sudo indicator
  line_sep      # Line break
  char          # Prompt character
)
 
SPACESHIP_PROMPT_ADD_NEWLINE=false
SPACESHIP_CHAR_SYMBOL=""
SPACESHIP_CHAR_SUFFIX=" "

To use it, add it to the plugins array:

plugins=(
  spaceship-prompt/spaceship-prompt
)

And then source the prompt (after the code that clones the plugins):

source $PLUGIN_DIR/spaceship-prompt/spaceship.zsh

It's also necessary to install Nerd Fonts font so that icons in the prompt are displayed correctly. I use the FiraCode Nerd Font.

Few other popular prompts you may want to check out are:

Miscellaneous

A cool feature in Zsh is the ability to navigate to a directory by typing the directory name without cd, or going up a directory with .. for 1 level, ../.. for 2 levels, and so on. To enable it, we can do the following:

setopt auto_cd # cd by typing directory name if it's not a command

I make typos all the time. Zsh can autocorrect those typos and ask us to run the correct command when we try to run a wrong command. To enable it, we can do the following:

# Stop zsh autocorrect from suggesting undesired completions
CORRECT_IGNORE_FILE=".*"
CORRECT_IGNORE="_*"
 
setopt correct_all # autocorrect commands

Sometimes I copy/paste content of a file to the terminal which may contain comments. By default it will result in a syntax error. To allow comments in interactive shells, we can do the following:

setopt interactive_comments # allow comments in interactive shells

In some environments, the delete key doesn’t work as expected and inputs ~ instead. To workaround this, we need to add the following keybinding:

bindkey '^[[3~' delete-char # delete key

Credits: https://blog.pilif.me/2004/10/21/delete-key-in-zsh.

On Terminal.app on Mac OS, opening a new tab doesn’t preserve the current working directory when using Zsh. To make it work, we need to add the following:

if [[ "$TERM_PROGRAM" == "Apple_Terminal" ]]; then
  function chpwd {
    printf '\e]7;%s\a' "file://$HOSTNAME${PWD// /%20}"
  }
  chpwd
fi

Though now I use Ghostty which doesn't have this issue.

Wrapping Up

To make it easier to copy/paste, here is the complete configuration with all above tweaks:

# Theme
SPACESHIP_PROMPT_ORDER=(
  user          # Username section
  dir           # Current directory section
  host          # Hostname section
  git           # Git section (git_branch + git_status)
  hg            # Mercurial section (hg_branch  + hg_status)
  node          # Node.js section
  exec_time     # Execution time
  async         # Async jobs indicator
  jobs          # Background jobs indicator
  exit_code     # Exit code section
  sudo          # Sudo indicator
  line_sep      # Line break
  char          # Prompt character
)
 
SPACESHIP_PROMPT_ADD_NEWLINE=false
SPACESHIP_CHAR_SYMBOL=""
SPACESHIP_CHAR_SUFFIX=" "
 
# Enable zsh recompilation
autoload -Uz zrecompile
 
# Install and load plugins
plugins=(
  Aloxaf/fzf-tab
  zdharma-continuum/fast-syntax-highlighting
  zsh-users/zsh-autosuggestions
  zsh-users/zsh-history-substring-search
  zsh-users/zsh-completions
  spaceship-prompt/spaceship-prompt
)
 
PLUGIN_DIR=$HOME/.zsh_plugins
 
for plugin in $plugins; do
  if [[ ! -d $PLUGIN_DIR/${plugin:t} ]]; then
    git clone --depth 1 https://github.com/${plugin} $PLUGIN_DIR/${plugin:t}
 
    for f in $PLUGIN_DIR/${plugin:t}/**/*.zsh; do
      echo "Recompiling $f"
      zrecompile -pq "$f"
    done
  fi
 
  if [[ -f $PLUGIN_DIR/${plugin:t}/${plugin:t}.plugin.zsh ]]; then
    source $PLUGIN_DIR/${plugin:t}/${plugin:t}.plugin.zsh
  fi
done
 
# Load spaceship prompt
source $PLUGIN_DIR/spaceship-prompt/spaceship.zsh
 
# Enable autocompletions
autoload -Uz compinit
 
typeset -i updated_at=$(date +'%j' -r $HOME/.zcompdump 2>/dev/null || stat -f '%Sm' -t '%j' $HOME/.zcompdump 2>/dev/null)
typeset -i today=$(date +'%j')
 
if [[ $updated_at -eq $today ]]; then
  compinit -C -i
else
  compinit -i
fi
 
zmodload -i zsh/complist
 
# Save history so we get auto suggestions
HISTFILE=$HOME/.zsh_history # path to the history file
HISTSIZE=100000 # number of history items to store in memory
HISTDUP=erase # remove older duplicate entries from history
SAVEHIST=$HISTSIZE # number of history items to save to the history file
 
# Stop zsh autocorrect from suggesting undesired completions
CORRECT_IGNORE_FILE=".*"
CORRECT_IGNORE="_*"
 
# Options
setopt auto_cd # cd by typing directory name if it's not a command
setopt auto_list # automatically list choices on ambiguous completion
setopt auto_menu # automatically use menu completion
setopt always_to_end # move cursor to end if word had one match
setopt hist_expire_dups_first # expire duplicate entries first when trimming history
setopt hist_find_no_dups # don't display duplicate entries in history
setopt hist_ignore_space # ignore commands starting with space
setopt hist_ignore_all_dups # remove older duplicate entries from history
setopt hist_reduce_blanks # remove superfluous blanks from history items
setopt hist_save_no_dups # don't save duplicate entries in history
setopt hist_verify # don't execute immediately upon history expansion
setopt inc_append_history # save history entries as soon as they are entered
setopt share_history # share history between different instances
setopt correct_all # autocorrect commands
setopt interactive_comments # allow comments in interactive shells
 
# Improve autocompletion style
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' # case-insensitive completion
zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" # colorize filenames
zstyle ':completion:*' menu no # disable menu completion for fzf-tab
zstyle ':fzf-tab:complete:cd:*' fzf-preview 'ls --color $realpath' # preview directory contents with cd
zstyle ':fzf-tab:complete:__zoxide_z:*' fzf-preview 'ls --color $realpath' # preview directory contents with zoxide
zstyle ':fzf-tab:*' use-fzf-default-opts yes # use FZF_DEFAULT_OPTS for fzf-tab
 
# Keybindings
bindkey '^[[A' history-substring-search-up # up arrow
bindkey '^[[B' history-substring-search-down # down arrow
bindkey '^[[3~' delete-char # delete key
 
# Disable paste highlighting for syntax-highlighting plugin
zle_highlight+=(paste:none)
 
# Open new tabs in same directory
if [[ "$TERM_PROGRAM" == "Apple_Terminal" ]]; then
  function chpwd {
    printf '\e]7;%s\a' "file://$HOSTNAME${PWD// /%20}"
  }
 
  chpwd
fi
 
# Setup fuzzy finder
export FZF_DEFAULT_OPTS=" \
--color=bg+:#424762,spinner:#b0bec5,hl:#f78c6c \
--color=fg:#bfc7d5,header:#ff9e80,info:#82aaff,pointer:#a5adce \
--color=marker:#89ddff,fg+:#eeffff,prompt:#c792ea,hl+:#ff9e80 \
--color=selected-bg:#424762"
 
if [[ -x $(command -v fzf) ]]; then eval "$(fzf --zsh)"; fi

You can copy the whole config and paste it to your ~/.zshrc file. The only pre-requisites are that you need to have git and fzf installed. It will automatically install the required plugins on the first run.

My current Zsh configuration is in my dotfiles repo. It also includes a few more tweaks that I didn't cover here.

If you are looking for more Zsh plugins, check out awesome-zsh-plugins. However, be mindful of how many plugins you install as they can slow down your shell startup time. Happy Zsh-ing! 🚀