Customizing zsh prompt from the ground up
This was initially inspired by the look of powerlevel10k and oh-my-zsh, which I used for a long time. Then migrated to starship for a while. Finally, in a bid to learn more of what was going on under the hood (and reduce unused code/configs), I started looking at zsh prompts work. But first, the final product.
The prompt above is simple, but provides a whole bunch of useful information. Specifically, it covers the user, host, current directory, git information, success/failure of last command, (python) virtual environment (if any), length of history file, tmux session info (if any), and ssh info (from remote, if any)
This is composed of only a few basic pieces of functionality, as outlined below.
Basic Prompt Creation
Lets begin with the easiest - changing prompt text, colors and placements. ZSH allows you to change
the environment variables such as PS1
(or PROMPT
) and RPROMPT
. First off, let’s set setopt PROMPT_SUBST
to enable prompt substitutions[^1]. This allows us to use a host of substitutions that
allow us to query various info (such as username, hostname, etc.) as well as set formatting options
(such as colors, bold, etc.).
Now we can set our desird text/info with colors in the PROMPT
environment variable, and zsh will
render it as our prompt. Similarly, setting RPROMPT
will set the right side prompt. We will be
using both for this - lets start with something simple.
Copy the following to ~/.zshrc
PROMPT='%F{blue}%n@%M%f $ '
RPROMPT=' %(?.%F{green}✓.%F{red}x)%f'
This should make the prompt look like this
Let us break down what this is doing
%F{blue}
lets zsh know that we will be changing the format for all items (to blue text, in this
case). This will affect everything until a corresponding %f
is encountered. %n
expands to user,
%M
to host machine, @
and $
are rendered as is.
The right hand side is a little more involved. We will use a condition to color the output based on
the outcome of our previous command (using %?
). The format is %(condition.if_true.if_false)
. We
query the outcome of the previous command, and set green tick if the command was successful, and red
cross otherwise. This can be used
A full listing of available substitions is provided in zsh documentation.
Adding on specific functionality / information
Information not directly provided by the prompt expansions can be added to the prompt via the
precmd
hook. A precmd
runs before the prompt is displayed everytime, and functions can be
registered to be called, which can in turn modify the commands or set local variables that can
incorporated in the prompt.
For example, the snippet below will check for existance of a TMUX sessiona and use that to
populate TMUX_INFO
based on current environment variables.
## TMUX info
precmd_tmux_info () {
if [[ -n $TMUX ]]; then
# $TMUX = socket_path,pid,session_id
local tmux_string=$TMUX
t=(${(s/,/)tmux_string})
TMUX_INFO=" $(basename $t[1])($t[3]) #${TMUX_PANE:1} "
else
TMUX_INFO=""
fi
}
precmd_functions+=( precmd_tmux_info )
This runs everytime the prompt is displayed. Now we can incorporate TMUX_INFO
in our PROMPT
with
the required formatting to display it. Similar approach can be used to query and populate any other
information one may need in the prompt, such as python venv, SSH info, conda info, or git info.
On the topic of git info, zsh has built in support for this, which makes querying and displaying this very straightforward. This can be achieved as below
## VCS info
autoload -Uz vcs_info # enable vcs_info
precmd_vcs_info() { vcs_info }
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:*' check-for-changes true
zstyle ':vcs_info:*' get-revision true
zstyle ':vcs_info:*' stagedstr ' '
zstyle ':vcs_info:*' unstagedstr ' '
zstyle ':vcs_info:git:*' formats '(%b[%8.8i]:%r) %c%u '
zstyle ':vcs_info:git:*' actionformats '(%b:%r) (%a) [%6.6i] %c%u '
zstyle ':vcs_info:git:*' branchformats '(%b:%r) %c%u '
precmd_functions+=( precmd_vcs_info )
Much like other aspects of zsh, this has a ton of options that can be found in the zsh docs
Multiline prompt
The last aspect of this configuration is achiveing the multiline prompt. By default if you use only
PROMPT
and RPROMPT
, the righ prompt will start where the left on ends, so setting both to
multiline (with a literal linebreak`) will cause the prompts to look staggered. If your intention is
to only show the right prompt in a single line aligned with the prompt user input, this is ok! If
not, the following approach can be used.
We first create a precmd
to calculate the correct distance between our left prompt end and the
right prompt beginning.
get_distance() {
LEFT='%n@%M'
RIGHT='[%h]'
FILL=$((COLUMNS-${#${(%):-$LEFT}}-${#RIGHT}-1))
}
precmd_functions+=( get_distance )
Lets break this down. Since we only need the information of calculating the size of the prompts, we
recreate LEFT
and RIGHT
. We use COLUMN
to get the width of our terminal, and subtract the
lengths of the left and right prompt text from them.
Finally, we set PROMPT
to include all the content (left + fill + right) upto the last line.
RPOMPT
is set to only the trailing right side prompt text.
PROMPT='%F{blue}%n@%M%f ${(l:$FILL::─:)} [%h]
$ '
RPROMPT=' %(?.%F{green}✓.%F{red}x)%f'
which gives us the following prompt
Outro
With these tools, you can craft prompts with the exact formatting and content you want. However,
note that all these run every time the prompt is rendered, so be careful to not have long running or
very heavy information seeking functions set in precmd
.
Happy customization!
[^1] PROMPT_SUBST allows a lot more - from zsh docs “If the PROMPT_SUBST option is set, the prompt string is first subjected to parameter expansion, command substitution and arithmetic expansion.”