Moving to zsh, part 6 – Customizing the zsh Prompt

Apple has announced that in macOS 10.15 Catalina the default shell will be zsh.

In this series, I will document my experiences moving bash settings, configurations, and scripts over to zsh.

This series has grown into a book: reworked and expanded with more detail and topics. Like my other books, I plan to update and add to it after release as well, keeping it relevant and useful. You can order it on the Apple Books Store now.

As I have mentioned in the earlier posts, I am aware that there are many solutions out there that give you a pre-configured ‘shortcut’ into lots of zsh goodness. But I am interested in learning this the ‘hard way’ without shortcuts. Call me old-fashioned. (“Uphill! In the snow! Both ways!”)

The default bash prompt on macOS is quite elaborate. It shows the username, the hostname, and the current directory.

Calypso:~ armin$

On the other hand, the default bash prompt doesn’t show the previous command’s exit code, a piece of information I find very useful. I have written before how I re-configured my bash prompt to have the information I want:

Of course, I wanted to recreate the same experience in zsh.

Minimal Zsh Prompt

The only (visual) difference to my bash prompt is the % instead of the $.

Note: creating a file ~/.hushlogin will suppress the status message at the start of each Terminal session in zsh as well as in bash (or any other shell).

Basic Prompt Configuration

The basic zsh prompt configuration works similar to bash, even though it uses a different syntax. The different placeholders are described in detail in the zsh manual.

zsh uses the same shell variable PS1 to store the default prompt. However, the variable names PROMPT and prompt are synonyms for PS1 and you will see either of those three being used in various examples. I am going to use PROMPT.

The default prompt in zsh is %m%#. The %m shows the first element of the hostname, the %# shows a # when the current prompt has super-user privileges (e.g. after a sudo -s) and otherwise the % symbol (the default zsh prompt symbol).

The zsh default prompt is far shorter than the bash default, but even less useful. Since I work on the local system most of the time, the hostname bears no useful information, and repeating it every line is superfluous.

Note: you can argue that the hostname in the prompt is useful when you frequently have multiple terminal windows open to different hosts. This is true, but then the prompt is defined by the remote shell and its configuration files on the remote host. In your configuration file, you can test if the SSH_CLIENT variable is set and show a different prompt for remote sessions. There are more ways of showing the host in remote shell sessions, for example in the Terminal window title bar or with different window background colors.

In our first iteration, I want to show the current working directory instead of the hostname. When you look through the list of prompt placeholders in the zsh documentation, you find %d, %/, and %~. The first two do exactly the same. The last substitution will display a path that starts with the user’s home directory with the ~, so it will shorten /Users/armin/Projects/ to ~/Projects.

Note: in the end you want to set your PROMPT variable in the .zshrc file, so it will take effect in all your zsh sessions. For testing, however, you can just change the PROMPT variable in the interactive shell. This will give you immediate feedback, how your current setup works.

% PROMPT='%/ %# '
/Users/armin/Projects/dotfiles/zshfunctions % 

% PROMPT='%~ %# '
~/Projects/dotfiles/zshfunctions % 

Note the trailing space in the prompt string, to separate the final % or # from the command entry.

I prefer the shorter output of the %~ option, but it can still be quite long, depending on your working directory. zsh has a trick for this: when you insert a number n between the % and the ~, then only the last n elements of the path will be shown:

% PROMPT='%2~ %# '
dotfiles/zshfunctions %                       

When you do %1~ it will show only the name of the working directory or ~ if it is the home directory. (This also works with %/, e.g. %2/.)

Adding Color

Adding a bit of color or shades of gray to the prompt can make it more readable. In bash you need cryptic escape codes to switch the colors. zsh provides an easier way. To turn the directory in the path blue, you can use:

PROMPT='%F{blue}%1~%f %# '

The F stands for ‘Foreground color.’ zsh understands the colors black, red, green, yellow, blue, magenta, cyan and white. %F or %f resets to the default text color. Furthermore, Terminal.app represents itself as a 256-color terminal to the shell. You can verify this with

% echo $TERM
xterm-256color

You can access the 256 color pallet with %F{0} through %F{255}. There are tables showing which number maps to which color:

So, since I want a dark gray for my current working dir in my prompt, I chose 240, I also set it to bold with the %B code:

PROMPT='%B%F{240}%1~%f%b %# '

You can find a detailed list of the codes for visual effects in the documentation.

Dynamic Prompt

I wrote an entire post on how to get bash to show the color-coded exit code of the last command. As it turns out, this is much easier in zsh.

One of the prompt codes provides a ‘ternary conditional,’ which means it will show one of two expressions, depending on a condition. There are several conditions you can use. Once again the details can be found in the documentation.

There is one condition for the previous commands exit code:

%(?.<success expression>.<failure expression>)

This expression will use the <success expression> when the previous command exited successfully (exit code zero) and <failure expression> when the previous command failed (non-zero exit code). So it is quite easy to build an conditional prompt:

% PROMPT='%(?.√.?%?) %1~ %# ' 
√ ~ % false
?1 ~ % 

You can get the character with option-V on the US or international macOS keyboard layout. The last part of the ternary ?%? looks confusing. The first ? will print a literal question mark, and the second part %? will be replaced with previous command’s exit code.

You can add colors in the ternary expression as well:

PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# ' 

Another interesting conditional code is ! which returns whether the shell is privileged (i.e. running as root) or not. This allows us to change the default prompt symbol from % to something else, while maintaining the warning functionality when running as root:

% PROMPT='%1~ %(!.#.>) ' 
~ > sudo -s
~ # exit
~ > 

Complete Prompt

Here is the complete prompt we assembled, with all the parts explained:

PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# '
%(?.√.?%?) if return code ? is 0, show , else show ?%?
%? exit code of previous command
%1~ current working dir, shortening home to ~, show only last 1 element
%# # with root privileges, % otherwise
%B %b start/stop bold
%F{...} text (foreground) color, see table
%f reset to default textcolor

Right Sided Prompt

zsh also offers a right sided prompt. It uses the same placeholders as the ‘normal’ prompt. Use the RPROMPT variable to set the right side prompt:

% RPROMPT='%*'
√ zshfunctions %                    11:02:55

zsh will automatically hide the right prompt when the cursor reaches it when typing a long command. You can use all the other substitutions from the left side prompt, including colors and other visual markers in the right side prompt.

Git Integration

zsh includes some basic integration for version control systems. Once again there is a voluminous, but hard to understand description of it in the documentation.

I found a better, more specific example in the ‘Pro git’ documentation. This example will show the current branch on the right side prompt.

I have changed the example to include the repo name and the branch, and to change the color.

autoload -Uz vcs_info
precmd_vcs_info() { vcs_info }
precmd_functions+=( precmd_vcs_info )
setopt prompt_subst
RPROMPT=\$vcs_info_msg_0_
zstyle ':vcs_info:git:*' formats '%F{240}(%b)%r%f'
zstyle ':vcs_info:*' enable git

In this case %b and %r are placeholders for the VCS (version control system) system for the branch and the repository name.

There are git prompt solutions other than the built-in module, which deliver more information. There is a script in the git repository, and many of the larger zsh theme projects, such as ‘oh-my-zsh’ and ‘prezto’ have all kinds of git status widgets or modules or themes or what ever they call them.

Summary

You can spend (or waste) a lot of time on fine-tuning your prompt. Whether these modifications really improve your productivity is a matter of opinion.

In the next post, we will cover some miscellaneous odds and ends that haven’t yet really fit into any of preceding posts.

Published by

ab

Mac Admin, Consultant, and Author

21 thoughts on “Moving to zsh, part 6 – Customizing the zsh Prompt”

  1. Very useful! I’ve switched to zsh after 25 years of bash. I did this entirely using the tips and tricks from your blog series on zsh. Excellent work, and many thanks.

  2. Thank you, this was very useful.
    I had no luck editing ~/.profile (maybe because I had not yet set ‘zsh’ as my default shell).
    Could you please also mention the use of ~/.zprofile (run at login) and ~/.zshrc (run for new Terminal session).
    More information is available from:
    https://support.apple.com/en-au/HT208050

      1. Hmm, Google sent me directly to this (zsh Prompt) page, and it’s not clear from this page that modifying the variables in .profile will not work as one might expect. Perhaps you could add a line in the conclusion section that mentions this and refers back to the configuration file section, but these comments are probably enough now anyway.

  3. thank you so much this was great! with a bit of extra research wrote a prompt that looks like agnoster/powerlevel9k!

    here it is :
    PROMPT=”%F{yellow} %n@%m %F{blue}%S”$’\ue0b0′”%s%K{blue}%F{black} %~ %k%F{blue}”$’\ue0b0′”%f “

  4. can i put this in .zshenv? i mean the prompt? i added it but didn’t work

    1. macOS Catalina has a file `/etc/zshrc`, which sets the macOS default prompt to `PS1=”%n@%m %1~ %# “`. Since `/etc/zshrc` is executed _after_ your `~/.zshenv` this will overwrite the your setting. (See Part 2: Configuration Files) Since `~/.zshenv` is executed for _all_ instances of zsh, including scripts, it is recommended to use this minimally, if at all.

      The recommended location for your personal settings is `~/.zshrc`.

  5. Nice article and well explained. However if the goal is changing just path info, leaving everything everything else intact (colors, git info and potentially other customisations before this one), you could just use following:
    PROMPT=${PROMPT/\%c/\%~}

    See my StackOverflow answer: https://stackoverflow.com/a/62203156/4973640

  6. Hi there. Is this a change we have to make every time we want to use the terminal? I applied the changes and it worked fine but when I relaunched terminal, it was back to the default prompt settings. Sorry if this is an obvious questions, I’m a beginner who just started coding for fun.

  7. Great post (and great book) — thanks!

    I’m having a very strange problem — if I put the command that sets the prompt into .zshrc the prompt is set to an empty string.

    I made a .zshrc file with a single line:

    prompt=’xx ‘

    But after the shell reads the initialization file the prompt is empty.

    This is happening on two different systems (iMac and MacBook Pro), both running macOS 10.15.5, and both using Terminal.app.

    Any ideas?

    1. As we discussed in email, you were able to fix this by changing the line endings for the zshrc file! Glad you worked it out!

  8. Pure gold my friend. This article saved me a bunch of time and also illustrated nicely what can be done. Thanks a ton!

  9. Lovely prompt customisation hints, thanks!

    Since I prefer having my prompt on a new line (initial cursor always in the same position), I’d like to add my 2 cents to /etc/zshrc:

    NEWLINE=$’\n’
    PROMPT=”${NEWLINE}[%n@%F{green}%m%f :: %~]${NEWLINE}# ”

    which leads to:

    [User@Hostname :: ~/Desktop/stuff/and/gear]
    #

    1. OMG, I was looking for that! I would never imagined that it was too simple

      Thank you

  10. Unfortunately, after setting up the git integration (show name of branch in brackets on right side) the prompt takes much longer to come back after entering any given command.
    Response times improve when moving out of a directory under version control…

    Can this be improved somehow?

  11. Great set of articles as an intro to zsh. Having been a csh (and its later variants / improvements) since SunOS 4, I have finally made the switch to zsh with Catalina as I am using the command line much more now than in recent years.

    Anyway, just want to add my 2¢, and that using emojis can also enhance the prompt.

    Here is my first iteration using your example but w/ emojis:

    PROMPT=’%(?.%F✅%?.%F{red}❗️%?)%f %B%F{252}%2~%f%b %# ‘

  12. Thanks for this series. Super helpful for someone given a Mac that has no idea why there are so many shell files and which one to edit for what!

Comments are closed.