My Terminal, Step by Step: Ghostty, zsh, Oh My Zsh and Powerlevel10k
October 12, 202616 min read
The terminal I work in every day, set up one step at a time: Ghostty with MesloLGS Nerd Font Mono, Homebrew's zsh, Oh My Zsh with three plugins, my Powerlevel10k prompt, zoxide, fzf and my aliases, then all of it in one run of Post-Mac-Setup.
My terminal is Ghostty running zsh, with Oh My Zsh, the Powerlevel10k prompt, three plugins, zoxide, fzf and a handful of aliases. Each section below adds one piece, with the commands to type and the exact lines that go into your shell's settings files, so you can stop at any point and keep what you have. The last section does all of it in one run of my setup script, Post-Mac-Setup.
Four layers stack up, and the post builds them from the outside in. The terminal is the window: a program that draws a grid of characters and passes the keys you type to whatever runs inside it. The shell is the program inside that window, which reads each command you type and runs it. A framework organises the shell's plugins, and a theme draws the prompt, the text the shell prints before each command. Mine are Ghostty, zsh, Oh My Zsh and Powerlevel10k.
It is written for macOS, and I test it nowhere else. You need Homebrew, the package manager that installs programs from the command line with brew install. If you do not have it yet, the Post-Mac-Setup post starts from a Mac that has nothing.
01.Ghostty and the Font
Install Ghostty and the Meslo Nerd Font before anything else. A Nerd Font is a programming font with a large set of small icons added to it: folders, git branches, file types. My prompt and my ls draw those icons, and without the font every one of them shows as an empty box.
| brew install --cask ghostty font-meslo-lg-nerd-font |
A cask is Homebrew's word for an app or a font, as opposed to a command-line program. Ghostty reads its settings from one plain text file, ~/.config/ghostty/config.ghostty, a list of name = value lines. Mine lives in the repository of my Neovim configuration, ACH-NEOVIM, at ghostty/config.ghostty, because the terminal's background and colours are the editor's, and the two change together.
To use my settings, clone that repository to the folder where Neovim looks for its configuration, and link Ghostty's settings file to the copy inside it. If you already have a ~/.config/nvim, move it aside before you clone. I mean it: the clone goes exactly where your current configuration lives.
| git clone https://github.com/anirbanchakraborty-dev/ACH-NEOVIM ~/.config/nvim | |
| mkdir -p ~/.config/ghostty | |
| ln -s ~/.config/nvim/ghostty/config.ghostty ~/.config/ghostty/config.ghostty |
ln -s makes a link. Ghostty opens the file in ~/.config/ghostty, and what it reads is the copy in the repository, so a git pull there updates the terminal too.
If you want only the terminal and not the editor, skip the clone and write these lines into ~/.config/ghostty/config.ghostty yourself. They are the three that matter for everything below:
| font-family = MesloLGS Nerd Font Mono | |
| font-size = 14 | |
| macos-option-as-alt = left |
The first two set the font at 14 points. The Mono in its name is the version of Meslo whose icons are exactly one character wide, so they line up with the text around them. The third makes the left Option key send Alt, which terminal programs expect, and fzf's Alt-C below needs it. The right Option key still types special characters, such as é from Option-e and then e.
Quit Ghostty and open it again. If the font has not changed, look in ~/Library/Application Support/com.mitchellh.ghostty/ for a file named config or config.ghostty. Ghostty reads it after yours, so a setting there wins. Move what you want from it into your config.ghostty, then delete it.
02.zsh from Homebrew
macOS comes with zsh at /bin/zsh. I use Homebrew's copy instead, so it updates with the rest of my tools through brew upgrade rather than waiting for a macOS update.
| brew install zsh | |
| BREW_ZSH="$(brew --prefix)/bin/zsh" | |
| grep -qxF "$BREW_ZSH" /etc/shells || echo "$BREW_ZSH" | sudo tee -a /etc/shells | |
| chsh -s "$BREW_ZSH" |
/etc/shells is macOS's list of shells a user may log in with, and chsh, which changes your login shell, refuses one that is not on it. The grep adds Homebrew's zsh to the list only if it is missing. sudo asks for your password because the file belongs to the system, and chsh asks for it again.
Quit Ghostty and open it again, then check:
| echo $SHELL |
On a Mac with Apple silicon it prints /opt/homebrew/bin/zsh. If it still prints /bin/zsh, log out of macOS and back in.
03.Five Files and the Marker Lines
zsh reads its settings from files in your home folder whose names start with a dot, which is why Finder hides them. I keep mine in five, because zsh reads different files for different kinds of shell.
~/.zshenvis read by every zsh, including the ones that run scripts. It loads~/.zsh_paths, so a script finds the same programs your terminal does.~/.zprofileis read by login shells, after macOS's own/etc/zprofile, which reordersPATH. It loads~/.zsh_pathsagain to put Homebrew's programs back in front.~/.zshrcis read by interactive shells, the ones you type into. It holds the prompt, the plugins and the history settings, and it loads~/.zsh_pathsand~/.zsh_aliases.~/.zsh_pathsholdsPATH, the list of folders the shell searches for a program, and the other environment variables.~/.zsh_aliasesholds the aliases, short names that stand for longer commands.
My setup script writes each piece of these files as a block between two marker lines. This one, near the top of ~/.zshrc, loads ~/.zsh_paths:
| ### BEGIN SOURCE-PATHS (managed by mac-setup.sh - do not edit) | |
| # Load PATH exports and environment variables | |
| [[ -f ~/.zsh_paths ]] && source ~/.zsh_paths | |
| ### END SOURCE-PATHS |
Each run of the script finds its marker lines and rewrites what sits between them, and leaves everything else in the file alone. So don't hand-edit what is between the markers, because the next run puts it back. Put your own lines outside the markers, where they survive. If you set things up by hand from this post, paste each block with its marker lines, and a later run of the script will update the block in place instead of adding a second copy.
The two PATH files are long and tied to the programs Post-Mac-Setup installs, so I leave them to the script, and the post on the script describes them. If you are setting up by hand, the line Homebrew's installer told you to add to ~/.zprofile is all the PATH this post needs. In ~/.zshrc the order of the blocks matters, and each section below says where its block goes.
04.Oh My Zsh and Its Plugins
Oh My Zsh is a framework for zsh. It lives in ~/.oh-my-zsh, loads the plugins and the theme you name in ~/.zshrc, and comes with many plugins of its own. Its installer is a script from GitHub, and as with my own setup script, download it and read it before you run it:
| touch ~/.zshrc | |
| curl -fsSL -o install-omz.sh https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh | |
| less install-omz.sh | |
| RUNZSH=no KEEP_ZSHRC=yes sh install-omz.sh --unattended | |
| rm install-omz.sh |
KEEP_ZSHRC=yes keeps your ~/.zshrc instead of replacing it with Oh My Zsh's own template, and the touch first makes sure there is a ~/.zshrc to keep. RUNZSH=no stops the installer from starting a new shell when it finishes, and --unattended makes it ask nothing.
Three of my plugins come from their authors' repositories, cloned into Oh My Zsh's folder for your own additions. --depth=1 fetches only the latest version of each, without its history.
| ZSH_CUSTOM="$HOME/.oh-my-zsh/custom" | |
| git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions.git "$ZSH_CUSTOM/plugins/zsh-autosuggestions" | |
| git clone --depth=1 https://github.com/zsh-users/zsh-syntax-highlighting.git "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" | |
| git clone --depth=1 https://github.com/zsh-users/zsh-completions.git "$ZSH_CUSTOM/plugins/zsh-completions" |
This block goes in ~/.zshrc, after the one that loads ~/.zsh_paths:
| ### BEGIN OH-MY-ZSH-CONFIG (managed by mac-setup.sh - do not edit) | |
| # Oh My Zsh | |
| export ZSH="$HOME/.oh-my-zsh" | |
| ZSH_THEME="powerlevel10k/powerlevel10k" | |
| # Plugins | |
| plugins=( | |
| git | |
| zsh-autosuggestions | |
| zsh-syntax-highlighting | |
| zsh-completions | |
| fzf | |
| ) | |
| # zsh-completions: add to fpath BEFORE compinit (sourced by oh-my-zsh) | |
| fpath+=${ZSH_CUSTOM:-${ZSH:-~/.oh-my-zsh}/custom}/plugins/zsh-completions/src | |
| source "$ZSH/oh-my-zsh.sh" | |
| ### END OH-MY-ZSH-CONFIG |
ZSH_THEME names the prompt, which the next section installs. The plugins list loads five plugins. git adds short aliases for git commands, and fzf connects the fuzzy finder to the shell, which the section on history uses. Both come with Oh My Zsh. The other three are the ones you cloned. zsh-autosuggestions offers the rest of a command you typed before, in grey, as you type, and the Right arrow key takes it. zsh-syntax-highlighting colours the command line as you type, and turns a command that does not exist red before you press Return. zsh-completions adds Tab completion for programs zsh does not know by itself, and the fpath+= line puts its definitions where zsh's completion system looks, before Oh My Zsh starts that system.
Don't pile on Oh My Zsh plugins. Each one adds to the time every new shell takes to start, and I use only these five.
Next in ~/.zshrc comes the block that loads your aliases. It comes after Oh My Zsh, so that an alias of yours wins over one a plugin defines.
| ### BEGIN SOURCE-ALIASES (managed by mac-setup.sh - do not edit) | |
| # Load aliases and shell shortcuts | |
| [[ -f ~/.zsh_aliases ]] && source ~/.zsh_aliases | |
| ### END SOURCE-ALIASES |
Wait to open a new window until you reach the end of the section on zoxide and fzf. Before then, Oh My Zsh warns that the theme and fzf are missing, because the next two sections install them.
05.Powerlevel10k with My Prompt
Powerlevel10k is a theme for zsh, which Oh My Zsh loads, and it draws the prompt: the folder you are in, the git branch and whether it has changes, and more, each part shown only when it has something to say. Install it, then my prompt's settings:
| git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "$HOME/.oh-my-zsh/custom/themes/powerlevel10k" | |
| test -e ~/.p10k.zsh || curl -fsSL -o ~/.p10k.zsh https://raw.githubusercontent.com/anirbanchakraborty-dev/Post-Mac-Setup/main/p10k.zsh |
The second line downloads my settings file to ~/.p10k.zsh, and only when you have none, so a prompt of your own is never overwritten. Mine is two lines in the lean style, with icons and an empty line above each prompt. It is set for version 3 of the Nerd Fonts, which is what the font from the first step is.
Don't run the p10k configure wizard if you want my prompt. It asks a series of questions and writes a new ~/.p10k.zsh from your answers, replacing mine. Use my file and change only what you dislike, since the file explains its settings in comments. The wizard is for the day you want a prompt of your own.
Powerlevel10k needs two places in ~/.zshrc. This block goes first, above everything else:
| ### BEGIN P10K-INSTANT-PROMPT (managed by mac-setup.sh - do not edit) | |
| # Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. | |
| if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then | |
| source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" | |
| fi | |
| ### END P10K-INSTANT-PROMPT |
This is instant prompt. It draws the prompt from a saved copy the moment a window opens, while the rest of ~/.zshrc is still loading, so you can start typing at once. And this line goes last, at the end of the file, where it loads your prompt's settings:
| [[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh |
It is the one line in this post without marker lines around it. My setup script writes the same line inside its own markers at the end of the file, so if you run the script later, delete this copy first.
06.zoxide and fzf
Two small programs change how I move around. zoxide remembers every folder you visit, and z followed by part of a folder's name jumps to the matching folder you go to most, counting recent visits higher. fzf is a fuzzy finder. It takes a list, and as you type a few letters, it narrows the list to the lines that contain those letters in that order, with anything in between.
| brew install zoxide fzf |
zoxide hooks itself into the shell with this block, which goes in ~/.zshrc after the one that loads your aliases:
| ### BEGIN ZOXIDE (managed by mac-setup.sh - do not edit) | |
| # zoxide - smarter cd command (use "z" to jump, "zi" for interactive) | |
| if command -v zoxide &>/dev/null; then | |
| eval "$(zoxide init zsh)" | |
| fi | |
| ### END ZOXIDE |
Once you have been to ~/Downloads, z down takes you there from anywhere, and zi lets you pick in fzf when several folders match.
fzf needs nothing more, because the fzf entry in the plugin list already connects it to the shell. That gives you three keys. Ctrl-R searches your command history: type a few letters of a command you ran last week, press Return, and it lands on the command line, for you to run or edit. Ctrl-T picks a file and pastes its path where your cursor is. Alt-C picks a folder and goes into it, and Alt is the left Option key, thanks to the Ghostty setting above.
Ctrl-R is only as good as the history behind it, so I keep a long one. This block goes in ~/.zshrc after zoxide's:
| ### BEGIN SHELL-OPTIONS (managed by mac-setup.sh - do not edit) | |
| # History | |
| HISTSIZE=50000 | |
| SAVEHIST=50000 | |
| setopt HIST_IGNORE_ALL_DUPS | |
| setopt HIST_SAVE_NO_DUPS | |
| setopt SHARE_HISTORY | |
| ### END SHELL-OPTIONS |
It keeps 50,000 commands in memory and on disk, removes the older copy of a command when you run it again, and shares one history between every open window, so a command you ran in one window can be found from another.
07.The Aliases
An alias is a short name that stands for a longer command: type ll, and the shell runs the whole eza line below. Mine replace a few of the oldest Unix commands with newer ones. Install the programs first:
| brew install eza bat fd dust bottom difftastic |
All the blocks in this section go in ~/.zsh_aliases. The first three check with command -v that their program is installed, and define their aliases only if it is.
| ### BEGIN EZA-ALIASES (managed by mac-setup.sh - do not edit) | |
| # eza - modern ls replacement with icons and git status | |
| # --icons=auto and --color=auto: icons and colour in a terminal window only, so | |
| # a pipe or a file gets plain names with no escape codes in them. | |
| if command -v eza &>/dev/null; then | |
| alias ls='eza --icons=auto --color=auto --group-directories-first' | |
| alias ll='eza -l --icons=auto --color=auto --group-directories-first --git --time-style=long-iso' | |
| alias la='eza -la --icons=auto --color=auto --group-directories-first --git --time-style=long-iso' | |
| alias lt='eza -l --sort=modified --icons=auto --color=auto --group-directories-first --git --time-style=long-iso' | |
| alias tree='eza --tree --icons=auto --color=auto --group-directories-first' | |
| fi | |
| ### END EZA-ALIASES |
ls is eza, which lists a folder with an icon beside each name, colours by file type, and folders first. --icons=auto and --color=auto show the icons and colours only when the output goes to your screen, so a pipe such as ls | wc -l gets plain names. ll is the long listing, with each file's git status and its date written year first. la adds hidden files, lt sorts by when each file last changed, and tree draws the folder as a tree.
| ### BEGIN BAT-ALIASES (managed by mac-setup.sh - do not edit) | |
| # bat - cat with syntax highlighting | |
| if command -v bat &>/dev/null; then | |
| alias cat='bat --paging=never' | |
| alias catp='bat' | |
| fi | |
| ### END BAT-ALIASES |
cat is bat, which prints a file with syntax colouring and line numbers. --paging=never makes it print straight through, as cat does, and catp is bat with its pager, for a long file. When its output goes into a pipe, bat prints the file plain, exactly as cat would.
| ### BEGIN FD-ALIASES (managed by mac-setup.sh - do not edit) | |
| # fd - fast find replacement | |
| if command -v fd &>/dev/null; then | |
| alias find='fd' | |
| fi | |
| ### END FD-ALIASES |
find is fd, which searches by name and skips hidden files and whatever .gitignore excludes. Its arguments differ from find's: fd notes finds every file and folder below this one with notes in its name. Aliases work only at the prompt, so a script that calls find still gets the original, and command find reaches it from the prompt.
| ### BEGIN GENERAL-ALIASES (managed by mac-setup.sh - do not edit) | |
| # General shortcuts | |
| alias ..='cd ..' | |
| alias ...='cd ../..' | |
| alias ....='cd ../../..' | |
| alias mkdir='mkdir -p' | |
| alias df='df -h' | |
| alias du='dust' | |
| alias top='btm' | |
| alias diff='difft' | |
| alias ports='lsof -iTCP -sTCP:LISTEN -n -P' | |
| alias myip='curl -s ifconfig.me' | |
| alias reload='exec zsh' | |
| ### END GENERAL-ALIASES |
This block does not check for its programs, so install dust, bottom and difftastic with the rest, or its du, top and diff will report that the command is not found. .., ... and .... go up one, two and three folders. mkdir also makes any missing parent folders, and df prints sizes in units you can read. du is dust, which draws what takes up the space, top is bottom, a process monitor, and diff is difftastic, which compares code by its structure rather than line by line. ports lists the programs listening on a network port, myip asks the ifconfig.me service for your public address, and reload starts a fresh shell that reads your edited files.
| ### BEGIN GIT-ALIASES (managed by mac-setup.sh - do not edit) | |
| # Git shortcuts (supplement oh-my-zsh git plugin) | |
| alias gs='git status' | |
| alias gd='git diff' | |
| alias gds='git diff --staged' | |
| alias glog='git log --oneline --graph --decorate -20' | |
| ### END GIT-ALIASES |
And four git shortcuts beside the ones Oh My Zsh's git plugin adds: gs for the status, gd for changes not yet staged, gds for staged ones, and glog for the last twenty commits drawn as a graph.
08.All of It in One Run
Every step above is something my setup script, Post-Mac-Setup, does for you. Download it, read it, and run it:
| curl -fsSL -o mac-setup.sh \ | |
| https://raw.githubusercontent.com/anirbanchakraborty-dev/Post-Mac-Setup/main/mac-setup.sh | |
| less mac-setup.sh | |
| chmod +x mac-setup.sh | |
| ./mac-setup.sh |
For this post's part, it installs Ghostty, the font, zsh and every program above with Homebrew, and makes Homebrew's zsh your login shell. It installs Oh My Zsh, the three plugins and Powerlevel10k, and puts my .p10k.zsh in place when you have none. It clones ACH-NEOVIM into ~/.config/nvim when that folder does not exist, and links Ghostty's settings file when you have no ~/.config/ghostty/config.ghostty of your own. Then it backs up each of the five files you already have, and writes every block in this post into them, between its marker lines. It is safe to run again: whatever is installed already is skipped, and each block is rewritten in place.
Everything else the script does, from Apple's command-line tools to the apps it installs, is in the Post-Mac-Setup post. The editor I run inside this terminal has a post of its own.
— Anirban
Hardware–software at the seam where compilers meet microcode.