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.

zsh custom prompt_screenshot

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 prompt_basic.png

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

basic prompt in ubuntu

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.”