SJ cartoon avatar

Development Zsh Prompts... Anything is Better than "username@hostname"

I think we can all admit that the default zsh prompt is kinda meh. username@hostname statically there, all the time, staring at you... unblinking, unchanging, uninteresting...

Not only is that information boring, but it's also not very useful. I mean, I know my username and hostname, I don't need to see them every time I open a new terminal window. I also don't need to see them every time I run a command. I created both of those names, I already know what they are.

Time to fix that, or break my terminal trying.

Default prompt

Before we start fiddling, let’s take a look at the default MacOS prompt:

username@hostname ~ % echo $PROMPT
%n@%m %1~ %#

You can get a full list of the available Zsh prompt variables by checking the Zsh prompt expansion page, but briefly:

  • %n is the username
  • %m is the hostname
  • %1~ is the current directory
  • %# is the prompt character

If things ever go too far off the rails, you can always reset your prompt to the default by running PROMPT='%n@%m %1~ %# '. But, for most of the changes below, we'll only be editing the current terminal, so to reset, just close and re-open the terminal.

Empty prompt

# Note the use of single quotes, this is semi-important.
# If you use double quotes, the shell will try to expand
# variables automatically and you may not want that. A
# lot of the time you won't notice the difference though,
# either way.

username@hostname ~ % PROMPT=''

echo "Hello world"
Hello world

There we go, valuable terminal real-estate is back in play.

Buuuut, I think we can do better. This is a little too minimal.

PROMPT='> '
>
> echo "Hello world"
Hello world
>

Where was I again?

One thing I do like about the default Mac prompt is that the current working directory is always shown. I’ve used terminal prompts that hide that piece of information - and I end up continually typing pwd to figure out where I am.

...Bad memory I guess...?

So, let’s bring our current working directory back to the party.

> PROMPT='%1~ > '
~ >
~ > echo "Hello world"
Hello world
~ > cd ~/Downloads
Downloads >

Okay, that looks good - but I think we can do better. An extra shot of context won’t hurt anyone. Let’s add the current working directory’s parent directory. It doesn’t take up too much of the terminal, but it’s just that nice little perk that helps when you have tons of src, build, or bin directories everywhere.

So, let's change that 1 to a 2.

Downloads > PROMPT='%2~ > '
~/Downloads >
~/Downloads > cd ~/Developer/oss/pants/src
pants/src >

Note that I'm using the tilde (~), instead of %d or %/ for the directory so that the HOME directory is always abbreviated to ~. I find that it makes the prompt a little more readable instead of always having my username or even /Users/username on the prompt.

Let’s rice this terminal

Actually, let’s not. This isn’t /r/unixporn (safe for work, just nerdy Linux UI tweaking). Much like how I dress, it’s function over form all day.

Command status indication

Taking some inspiration from VS Code, I like how "success vs fail" commands give a coloured circle indication on the far left. It’s a small thing, but just a nice touch.

First, let's put a unicode placeholder on the far left of the prompt, just to see what it looks like:

pants/src > PROMPT='⏺ %2~ > '
⏺ pants/src >

The circle can be any unicode character like a checkmark (✓) or a cross (×), or an emoji (💩 🤖 👍).

Now, let's try to show a different icon depending on the success or failure of the last command. To show a different icon for success/failure, we can use the ternary operator in the form of %(x.true-text.false-text).

To get the last command’s exit code, we can use the $? variable. If the last command succeeded, it will be 0, and if it failed, it will be non-zero. So, we can use that to show a different icon depending on the exit code.

⏺ pants/src > PROMPT='%(?.✓.💩) %2~ > '
✓ pants/src > echo "Hello world"
Hello world
✓ pants/src > blahblahblah
zsh: command not found: blahblahblah
💩 pants/src >

While the checkmark and poop is fun, my actual prompt uses the humble circle.

To add colour to the prompt, we can use the %F{colour} and %f escape sequences. The %F{colour} will set the foreground colour, and %f will reset it back to the default. You can find a list of colours here, or you can use certain words.

Let's create a prompt that will show a red circle if the last command failed, and a blue circle if it succeeded.

💩 pants/src > PROMPT='%(?.%F{blue}⏺.%F{red}⏺)%f %2~ > '
⏺ pants/src >

Zsh command status indication with blue/red dots

I don't actually use "blue" and "red", I use 14 and 9 as I think they pop a little more.

⏺ pants/src > PROMPT='%(?.%F{14}⏺.%F{9}⏺)%f %2~ > '
⏺ pants/src >

Zsh command status indication with cyan/red dots

Root user indication

I almost never use sudo on my Mac, just never really need it. Since I removed my username - I can’t really identify if I elevate to a super user... Or so I thought... In reality, the prompt I'm using is only for my user, not for all users. So, as soon as I switch to an administrator or root, I can see the username again. That's a nice perk, since the PROMPT is starting to get a little long and complicated. Otherwise, we'd have to add %(!.#.>) to the prompt to show the # if we're root, and > otherwise.

What I will do, though, is revert back to the % instead of the > prompt character. I think it looks a little cleaner (or maybe I'm just used to it). Let's look back at the default prompt for a sec and grab the %# at the end. Oh hey, look at that - we get that root ternary check for free!

⏺ pants/src > PROMPT='%(?.%F{14}⏺.%F{9}⏺)%f %2~ %# '
⏺ pants/src %

West Coast vs East Coast

The left side of the terminal is very important, as that’s where our focus will be most of the time. But, that doesn’t mean we can’t use the right side of the screen for some secondary information. Using Powerlevel9k, Powerlevel10K, and Oh-My-Zsh themes as inspiration - the right side of the terminal might be a good place to put performance-centric commands and/or information which is directory dependent. That way, changing directories doesn't come with a large visual shift (e.g. inside vs outside a git repo).

When did I do that?

Let's start off with the time using %*.

This clock timestamp functionality kinda replicates history -E, so it’s not providing any novel information. But if you’re like me and leave all your terminal windows open for hours/days, it’s occasionally nice to see at a glance when you last ran certain commands.

⏺ pants/src % RPROMPT='%F{8}⏱ %*%f'
⏺ pants/src %                           ⏱ 20:52:52

Putting information on the right hand side may seem like it strays away from the original goal of keeping more available terminal space, but right prompts magically disappear when you reach them on the command line. So, this is a rare case of getting something for nothing.

⏺ pants/src %                     ⏱ 20:52:53
⏺ pants/src % blahblahblah blahblahblah
⏺ pants/src %                     ⏱ 20:52:55

As good as git gets

The last prompt edit I want to make is to add some git information to the right side of the prompt. I like to see the current branch, and whether or not there are any uncommitted changes.

Fortunately, Zsh has a built-in git plugin, so we can use that to get the current branch and whether or not there are any uncommitted changes.

Unlike the other prompt tweaks, we'll need to edit our .zshrc file, since we'll need to load the git plugin and run it in precmd.

Below is the snippet of interest with some comments:

# ~/.zshrc

# Autoload zsh's `add-zsh-hook` and `vcs_info` functions
# (-U autoload w/o substition, -z use zsh style)
autoload -Uz add-zsh-hook vcs_info

# Set prompt substitution so we can use the vcs_info_message variable
setopt prompt_subst

# Run the `vcs_info` hook to grab git info before displaying the prompt
add-zsh-hook precmd vcs_info

# Style the vcs_info message
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:git*' formats '%b%u%c'
# Format when the repo is in an action (merge, rebase, etc)
zstyle ':vcs_info:git*' actionformats '%F{14}⏱ %*%f'
zstyle ':vcs_info:git*' unstagedstr '*'
zstyle ':vcs_info:git*' stagedstr '+'
# This enables %u and %c (unstaged/staged changes) to work,
# but can be slow on large repos
zstyle ':vcs_info:*:*' check-for-changes true

# Set the right prompt to the vcs_info message
RPROMPT='⎇ ${vcs_info_msg_0_}'

Either close and re-open your terminal, or run source ~/.zshrc to reload the config.

⏺ pants/src % source ~/.zshrc
⏺ pants/src %                     ⎇ cc-first-party-compilation

At this point, I would style the right prompt to be a little more subtle. Something like this:

RPROMPT='%F{8}⎇ $vcs_info_msg_0_%f'

Zsh terminal including git branch/changes

Next time

I think this is a good place to stop for now. The current prompt is much more useful than the default MacOS Zsh prompt, but I'm not done optimizing my terminal.

Next time around, I'll be covering more Zsh plugins and colourizing ls to be more Linux'y (e.g. green for directories, blue for executables, etc).

References

Big shout outs to the following resources:

  1. Customizing the zsh Prompt
  2. A Guide to Customizing the Zsh Shell Prompt
  3. Customize your ZSH prompt with vcs_info