Skip to content

Terminal & Shell Setup

Ghostty with zsh and acecat theme Ghostty with zsh and acecat theme, remote and root

Ghostty

I've started using Mitchell Hashimoto's Ghostty, it's fast, clean and runs natively on both macOS and Linux.

Ghostty is a fast, feature-rich, and cross-platform terminal emulator that uses platform-native UI and GPU acceleration.

Configuration

theme = catppuccin-mocha
font-family = "MesloLGM Nerd Font"
keybind = global:cmd+grave_accent=toggle_quick_terminal

Ghostty on macOS has a Quick Terminal, I've set a global keybinding of Cmd+` so I can access it from anywhere.

Ghossty quick terminal

Optional: VS Code Settings

VS Code users will need to add the following to their settings.json:

"terminal.external.osxExec": "Ghostty.app",
"terminal.integrated.fontFamily": "MesloLGM Nerd Font",

Z shell (zsh)

Required software:

brew install bat eza fzf jandedobbeleer/oh-my-posh/oh-my-posh

For bat, eza, and fzf most distros have recent enough versions, use your package manager.

You'll need to install oh-my-posh using their script:

curl -s https://ohmyposh.dev/install.sh | bash -s

Note

Ghostty has Nerd Fonts built-in, so if you use another terminal emulator you will need to install the Meslo font yourself.

Automatic Install

curl -s https://andrew.lecody.com/guides/terminal-and-shell-setup/install.sh | bash -s

Files

Here are the files, if you just want to look at them instead of running my install script.

install.sh
#!/bin/bash

set -e

REQUIRED_PROGRAMS=(
  wget
  curl
  git
  bat
  fzf
  eza
  oh-my-posh
)

[[ "$OSTYPE" == darwin* ]] && REQUIRED_PROGRAMS+=(brew)

MISSING_PROGRAMS=()

# Check each command
for cmd in "${REQUIRED_PROGRAMS[@]}"; do
  if ! command -v "$cmd" &> /dev/null; then
    MISSING_PROGRAMS+=("$cmd")
  fi
done

# If any commands are missing, list them and exit
if [ ${#MISSING_PROGRAMS[@]} -ne 0 ]; then
  echo "Error: Missing required programs:"
  printf '%s\n' "${MISSING_PROGRAMS[@]}"
  exit 1
fi

[ -f /etc/zsh/zshrc ] && grep -q "skip_global_compinit=1" /etc/zsh/zshrc && \
  (grep -q "^skip_global_compinit=1$" ~/.zshenv 2>/dev/null || \
  (echo "skip_global_compinit=1" >> ~/.zshenv && echo "Updated ~/.zshenv"))


echo "Installing plugins to ~/.local/share"
ZSH_PLUGINS=(
  mattmc3/ez-compinit
  lukechilds/zsh-nvm
)
if [[ "$OSTYPE" == darwin* ]]; then
  # On macOS I prefer installing these using brew
  brew install zsh-autosuggestions zsh-syntax-highlighting
else
  ZSH_PLUGINS+=(zsh-users/zsh-syntax-highlighting zsh-users/zsh-autosuggestions)
fi
for REPO in "${ZSH_PLUGINS[@]}"; do
  echo "$REPO"
  git clone --depth 1 "https://github.com/$REPO" "$HOME/.local/share/$(basename "$REPO")" >/dev/null
done

echo "Installing themes to ~/.local/share/themes"
THEME_URLS=(
  https://andrew.lecody.com/guides/terminal-and-shell-setup/acecat.omp.toml
  https://github.com/catppuccin/zsh-syntax-highlighting/raw/main/themes/catppuccin_mocha-zsh-syntax-highlighting.zsh
  https://github.com/catppuccin/delta/raw/main/catppuccin.gitconfig
)
mkdir -p ~/.local/share/themes
for THEME_URL in "${THEME_URLS[@]}"; do
  THEME_FILENAME=$(basename "$THEME_URL")
  echo "$THEME_FILENAME"
  wget -q -c "$THEME_URL" -O ~/.local/share/themes/"$THEME_FILENAME"
done

BAT_THEMES_DIR="$(bat --config-dir)/themes"
echo "Installing Catppuccin Mocha theme for bat"
mkdir -p "$BAT_THEMES_DIR"
wget -q -c https://github.com/catppuccin/bat/raw/main/themes/Catppuccin%20Mocha.tmTheme -O "$BAT_THEMES_DIR/Catppuccin Mocha.tmTheme"
bat cache --build

[ -f ~/.zshrc ] && mv ~/.zshrc ~/.zshrc-"$(date +%s)"
curl -s https://andrew.lecody.com/guides/terminal-and-shell-setup/zshrc > ~/.zshrc
zshrc
# Set ZSH_DEBUGRC to profile startup times, e.g.
# time ZSH_DEBUGRC=1 zsh -i -c exit
# https://www.dotruby.com/articles/profiling-zsh-setup-with-zprof
if [[ -n "$ZSH_DEBUGRC" ]]; then
  zmodload zsh/zprof
fi

# better command history
HISTSIZE=10000
SAVEHIST=$HISTSIZE
HISTFILE=~/.zsh_history
setopt histignoredups extendedhistory

export PATH="$HOME/.local/bin:$PATH"

# faster way to initialize completion system, and some nice styles
# https://github.com/mattmc3/ez-compinit
# Available completion styles: gremlin, ohmy, prez, zshzoo
# You can add your own too. To see all available completion styles
# run 'compstyle -l'
zstyle ':plugin:ez-compinit' 'compstyle' 'zshzoo'
source ~/.local/share/ez-compinit/ez-compinit.plugin.zsh

# automatically escapes characters in unquoted URLs
autoload -Uz url-quote-magic
zle -N self-insert url-quote-magic
# bracketed-paste-magic is required for url-quote-magic
autoload -Uz bracketed-paste-magic
zle -N bracketed-paste bracketed-paste-magic

# This speeds up pasting w/ autosuggest
# https://github.com/zsh-users/zsh-autosuggestions/issues/238
pasteinit() {
  OLD_SELF_INSERT=${${(s.:.)widgets[self-insert]}[2,3]}
  zle -N self-insert url-quote-magic
}
pastefinish() {
  zle -N self-insert $OLD_SELF_INSERT
}
zstyle :bracketed-paste-magic paste-init pasteinit
zstyle :bracketed-paste-magic paste-finish pastefinish

# bat config
export BAT_THEME="Catppuccin Mocha"
export MANPAGER="sh -c 'col -bx | bat -l man -p'"
alias bathelp="bat --language=help --style=plain"
alias -g -- --help="--help 2>&1 | bathelp"
help() {
  "$@" --help 2>&1 | bathelp
}

# fzf keybindings and fuzzy search
source <(fzf --zsh)
# catppuccin mocha theme for fzf
export FZF_DEFAULT_OPTS=" \
--color=bg+:#313244,bg:#1e1e2e,spinner:#f5e0dc,hl:#f38ba8 \
--color=fg:#cdd6f4,header:#f38ba8,info:#cba6f7,pointer:#f5e0dc \
--color=marker:#b4befe,fg+:#cdd6f4,prompt:#cba6f7,hl+:#f38ba8 \
--color=selected-bg:#45475a \
--multi"

ZSH_AUTOSUGGEST_STRATEGY=(history completion)
ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=20
ZSH_AUTOSUGGEST_CLEAR_WIDGETS+=(bracketed-paste)
source ~/.local/share/themes/catppuccin_mocha-zsh-syntax-highlighting.zsh

# Brew should be setting HOMEBREW_PREFIX automatically via ~/.zprofile
case "$OSTYPE" in
  darwin*)  SHARE_DIR="$HOMEBREW_PREFIX/share";;
  *)        SHARE_DIR="$HOME/.local/share";;
esac
source "$SHARE_DIR/zsh-autosuggestions/zsh-autosuggestions.zsh"
source "$SHARE_DIR/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh"

# NodeJS (nvm)
# Using zsh-nvm for lazy loading nvm
# https://github.com/lukechilds/zsh-nvm
export NVM_LAZY_LOAD=true
export NVM_COMPLETION=true
[ -f ~/.local/share/zsh-nvm/zsh-nvm.plugin.zsh ] && source ~/.local/share/zsh-nvm/zsh-nvm.plugin.zsh

# uv autocompletion
if command -v uv > /dev/null; then
  eval "$(uv generate-shell-completion zsh)"
  eval "$(uvx --generate-shell-completion zsh)"
fi

# aliases & functions
alias zls="zfs list -o type,name,available,used,logicalused,usedbysnapshots,compressratio,mountpoint"
alias zsl="zfs list -t snapshot"
alias s="ssh -l root"

# use eza instead of ls
alias ls="eza"
alias l="eza --long --binary --classify=auto" # list, binary size, type indicator (e.g. `/` for dirs)
alias ll="l --all" # list, all
alias llm="ll --sort=modified" # list, all, sort by modification date
alias lls="ll --sort=size" # list, all, sort by size
alias la="eza -lbhHigUmuSa" # list, all, verbose
alias lx="eza -lbhHigUmuSa@" # list, all, verbose, and xattrs
alias tree="eza --tree" # tree
alias lS="eza -1" # display one entry per line

# get all kubernetes resources for a namespace
kubectlgetall() {
  if [ -z "$1" ]; then
    echo "Usage: $0 <namespace>"
    return 1
  fi
  for i in $(kubectl api-resources --verbs=list --namespaced -o name | grep -v "events.events.k8s.io" | grep -v "events" | sort | uniq); do
    echo "Resource:" $i
    kubectl -n ${1} get --ignore-not-found ${i}
  done
}

# base64 decode that also works with base64url variant (no padding)
b64d() {
  local data=""
  while IFS= read -r line || [ -n "$line" ]; do
    data+="$line"
  done
  echo "${data}==" | base64 --decode
}

ytd() {
  yt-dlp --cookies-from-browser=firefox \
    --replace-in-metadata "uploader_id" "^@" "" \
    --output "$HOME/Downloads/yt-dlp/%(uploader_id)s/%(extractor)s_%(display_id)s.%(ext)s" \
    --embed-metadata \
    --sponsorblock-mark all \
    "$@"
}

jitter() {
  local secs=$(( ( RANDOM % 60 ) + 1 ))
  echo "sleep for $secs seconds..."
  sleep "$secs"
}

zshspeed() {
  for i in $(seq 1 10); do
    time zsh -i -c exit
  done
}

# THESE LINES MUST ALWAYS BE AT THE BOTTOM OF THIS FILE!
eval "$(oh-my-posh init zsh --config ~/.local/share/themes/acecat.omp.toml)"
if [[ -n "$ZSH_DEBUGRC" ]]; then
  zprof
fi
acecat Oh-My-Posh Theme
version = 3

[upgrade]
  source = "cdn"
  interval = "168h"
  auto = false
  notice = false

[palette]
  base = "#1e1e2e"
  text = "#cdd6f4"
  pink = "#f5c2e7"
  red = "#f38ba8"
  yellow = "#f9e2af"
  green = "#a6e3a1"
  teal = "#94e2d5"
  blue = "#89b4fa"

[[blocks]]
  type = "prompt"
  alignment = "left"

  [[blocks.segments]]
    template = "{{ if .WSL }}WSL at {{ end }}{{.Icon}}"
    foreground = "p:text"
    type = "os"
    style = "plain"

  [[blocks.segments]]
    template = " \uf0e7"
    foreground = "p:red"
    type = "root"
    style = "plain"

  [[blocks.segments]]
    template = " {{ if eq .PWD \"~\" }}\uf015{{ else }}\uf07c{{ end }} {{ .Path }}"
    foreground = "p:blue"
    type = "path"
    style = "plain"

    [blocks.segments.properties]
      style = "full"
      folder_format = "<d>%s</d>"
      edge_format = "<b>%s</b>"

  [[blocks.segments]]
    template = " {{ .UpstreamIcon }} {{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} \uf044 {{ .Working.String }}{{ end }}{{ if and (.Staging.Changed) (.Working.Changed) }} |{{ end }}{{ if .Staging.Changed }} \uf046 {{ .Staging.String }}{{ end }}"
    foreground = "p:yellow"
    type = "git"
    style = "plain"

    [blocks.segments.properties]
      fetch_upstream_icon = true
      fetch_status = true

[[blocks]]
  type = "prompt"
  alignment = "right"

  [[blocks.segments]]
    template = " <p:red>\uf071 {{ reason .Code }}({{ .Code }})</>"
    type = "status"
    style = "plain"

  [[blocks.segments]]
    template = " \uf252 {{ .FormattedMs }}"
    type = "executiontime"
    style = "plain"

    [blocks.segments.properties]
      style = "round"
      threshold = 3000

  [[blocks.segments]]
    template = "{{ if .SSHSession }} \ueba9 {{ .UserName }}@{{ .HostName }}{{ end }}"
    foreground = "p:yellow"
    type = "session"
    style = "plain"

  [[blocks.segments]]
    template = " \uf017 {{ .CurrentDate | date .Format }}"
    foreground = "p:blue"
    type = "time"
    style = "plain"

    [blocks.segments.properties]
      time_format = "15:04:05"

[[blocks]]
  type = "prompt"
  alignment = "left"
  newline = true

  [[blocks.segments]]
    template = "❯ "
    foreground = "p:green"
    type = "status"
    style = "plain"
    foreground_templates = ["{{ if gt .Code 0 }}p:red{{ end }}"]

    [blocks.segments.properties]
      always_enabled = true

[[tooltips]]
  template = "\ue81d {{.Context}}{{if .Namespace}} :: {{.Namespace}}{{end}}"
  type = "kubectl"
  style = "plain"
  tips = [ "kubectl" ]

Finishing Touches

To use delta with git, update your ~/.gitconfig with the following:

[interactive]
    diffFilter = delta --color-only
[include]
    path = ~/.local/share/themes/catppuccin.gitconfig
[delta]
    features = catppuccin-mocha
    side-by-side = true
    navigate = true    # use n and N to move between diff sections
    dark = true
[merge]
    conflictstyle = zdiff3
[diff]
    colorMoved = default