Configuring zsh

< Blog frontpage

I've been using the Z shell (zsh) as my daily shell for more than a year now. Recently I decided that I wanted to create my own shell prompt for zsh as I wasn't completely happy with themes and configuration provided by Oh My Zsh (omz). I thought omz is just unnecessary and also it's always nice to get a better grasp of my system.

When creating the prompt, I started to think how and why I actually use Oh My Zsh. Sure, it brings me nice defaults and all these cool powerline prompts everyone seems to use and all, but I really don't think I use most of the features. I don't even know all of the features, for example omz comes with enormous amount of aliases, which I mostly don't use or find hard to learn. I used some of the git aliases and they were fine, but also easy to include in my .aliases file.

I thought that maybe I should give manual configuring a chance.

Previous and new configuration side-by-side

Previous prompt (left) using Oh My Zsh, and current prompt (right) configured manually.

The omz theme used in the left terminal is af-magic. Full configuration before adding new prompt can be seen here.

The new prompt aims to be simple and clean, yet informative. I'll explain below what I did and why, but you can also just check the dotfiles (link to commit, always latest version available here) directly if that's what you fancy.

The prompt

I've split this section into two parts, design and implementation. In design I'll explain what I wanted to take into account when designing the prompt, and in implementation I'll explain the technical details related to getting the look and functionality I wanted.

Designing the prompt

I wanted my prompt to be simple, informative, good-looking and portable. I've had strange rendering issues on some environments even with the af-magic prompt, which doesn't even use powerline fonts, just an arrow », that I think is used for quotations in some languages. Maybe it just isn't common enough for some terminals, or there are some other issues regarding the omz setup. However, simplicity will make the prompt more portable as well. My shell should definetely work in e.g. the plain old Linux virtual console. I could of course have a separate zsh config for those somewhat rare situations, or I could just use bash, but well, if I can have all the good stuff, why break it with something that I really don't need or even want. I admit that there are some cool looking shell themes out there that use custom fonts, emojis and stuff, but I have to say that I currently prefer the simple approach to this matter. Also, in my opinion, simple things are often beautiful.

As the prompt should be informative, I thought that these elements should be included:

I think the af-magic is a fine theme, and my theme is heavily inspired by it. If colors are ignored, the basic prompt in home directory only differs in two parts: the last (non-space) character is % in my prompt and » in af-magic, and my theme does not have the line of dashes on top of the prompt, which enables me to see more lines at a time. This can be seen in the above comparison screenshot, where I've run the same commands using both prompts. I could run an extra neofetch command when using my new configuration, as it uses less lines. There are, however, differences between the two prompts when navigating the file system. My prompt stays relatively short, as it only shows current directory and that directory's parent directory unlike af-magic, which shows the full path from home directory. My git information is also maybe not as fancy as af-magic's.

Implementing the prompt

Next I'll explain how I built the prompt and also link some great resources that I found really useful. This might be handy if you are planning to build a prompt yourself, but if the next section feels too technical, feel free to skip to the next heading.

In zsh, the prompt is formed by defining variables PROMPT and RPROMPT, of which first is the default prompt and RPROMPT is displayed on the right side of the terminal window. I use PROMPT to display current directory, git and virtualenv information as well as showing the prompt character, which is % for regular users and # for root. I use RPROMPT to display the user@host information, as well as the exit code of last program, if it isn't zero. I've defined these variables in ~/.prompt.zsh, which I source in my .zshrc.

First I configured the version control system information. I've tried to explain what the commands do in the comments (lines starting with a #).

# setup git information
autoload -Uz vcs_info
precmd_functions+=( vcs_info )
setopt prompt_subst
# %b: branch
# %u: unstaged changes
zstyle ':vcs_info:git:*' formats '%F{5}(%b%u)%f '
# this makes %u work, but also the prompt is clearly slower in git dirs when this is on
zstyle ':vcs_info:*' check-for-changes true
# what string to use for %u when there are unstaged changes
zstyle ':vcs_info:*' unstagedstr '*'
# vcs_info supports multiple version control systems, but I need just git
zstyle ':vcs_info:*' enable git

In addition to zsh documentation, Arjan van der Gaag's almost a decade old blog post was extremely useful.

Previous setup lines are needed now when I define the actual prompt:

# Explaining prompt:
# %B / %F{n}: begin bold / color
# %b / %f: end bold / color
# %n~: display n latest directories of current directory
# %#: display a '%' (or '#' when root)
# %(..): conditional expression (see docs)
# %?: exit code of last process
# %n@%m: user@host
PROMPT='%B${vcs_info_msg_0_}%F{12}%2~%f %# %b'
# rprompt is located on the right side of the terminal
RPROMPT='%(?..%F{red}(%?%) %f)%n@%m'

Basically I've just combined formatting and prompt fragments into a nice, complete prompt. Again, zsh documentation (this and other chapters) was pretty useful, but also this epic blog post by Armin Briegel and color cheat sheet by Jonas Jacek were great resources.

Plugin management

I tried zplug, but the loading time felt way too slow. I quickly asked the duck about zplug loading times and found this Reddit post that proved my hypothesis right: zplug is slow and there are better options available. I decided to give antigen, which is said to be de facto plugin manager for zsh, a try.

Antigen felt pretty fast and usable, so I added it to my dotfile repository as a git submodule. That way it is updated when I run my update script. I could have used the git submodule approach when adding any zsh plugins and skip using a plugin manager completely, as I've done with vim plugins, but I reasoned that it should be easier with a plugin manager.

At the time of writing this post, I have two plugins in my antigenrc: a syntax highlighter and an autosuggestion plugin. Syntax highlighting can be seen in the comparison screenshot and it quickly indicates when I'm about to mess up with quotation or do something illegal, like using a command that does not exist. Autosuggestions are handy as they can save me some keystrokes, more on that below.


I would say that there are three types of completion in my new zsh setup: tab completion, autocompletion and history search. Tab completion is when I've written something and ask the shell to complete what I've started to written by pressing Tab. Autocompletion suggests complete commands that I've used previously, and history search does the same. More about those below.

Tab completion

I don't always know the exact filenames or remember all command options, but that is something where zsh's built in completion can be really helpful. There are many options and completion strategies to choose from. I used the command compinstall to create a completion configuration file. Some people think it's difficult to use, but I believe it was certainly easier to use that than learn the right configuration file syntax. I think completing partial file paths is a really powerful feature. Below I have two examples of completing partial paths.

Changing directories

If I want to go to the directory test, which is located inside the directory Projects in my home directory, I can exploit the clever completion. I have a demonstration of that below, where represents pressing Tab.

~ % proj/t⭾
~ % Projects/test/

When pressing Enter to execute the line, zsh will take me to ~/Projects/test/ with less typing. This also takes advantage of one nice feature of zsh: one can just type the directory name directly to the shell and omit the cd command completely. This feature needs to be turned on by setting the option autocd.

Editing a file

I don't always remember the exact name of the file that I should for example edit with my favourite text editor (thanks for asking, it's currently neovim). Often I still know, without cheating with ls, some part of the filename. I can just write that part and hit the Tab and have zsh complete the perfect filename for me. Here is a toy example of such situation, where I don't have to remember the numbering that is included in filenames.

~/blog-demo % ls
~/blog-demo % e mid⭾
~/blog-demo % e

So convenient!


As I stated earlier, this is functionality that is coming from a plugin. I haven't ever tried the Fish shell, but apparently it has this nice autocompletion feature that has been implemented in a zsh plugin. The plugin suggests commands that match what the user has already written to the command line, and the suggestion can be accepted by pressing . There are some configuration options which I haven't really sunk my teeth into, as the defaults seem pretty good. However, I did make zsh accept the suggestion by pressing Ctrl + P, a bit like when asking vim for suggestions.

History search

This is a feature I fell in love with when using omz: when writing the beginning of a command to the prompt, I can press and shell completes the command with what I've used previously. I can further explore the history of commands beginning in the same way by pressing the key more times. This is fantastic when I know I've typed some long command earlier and don't want to guess the right parameters once again.

I was surprised that this is functionality that is provided by zsh directly without any third party plugins, just like the powerful completions I've mentioned earlier.


Startup time of a shell is important. I certainly don't want to be waiting for my shell to load after opening a terminal emulator. I was pretty happy with the loading time of my previous configuration with Oh My Zsh, so the new configuration mustn't be any slower. I did some loading time measurements using time zsh -i -c exit (from the previously linked Reddit post) with different configuration. I also included results of sh, bash and zsh with (Ubuntu's) default configurations for comparison.

shell loading time
zsh (omz) 115 ms
zsh (new) 104 ms
zsh (new, no antigen) 95 ms
zsh (no config) 7 ms
bash (default) 38 ms
sh (default) 4 ms

The results are interesting: my new configuration isn't much faster than my previous configuration using omz. The antigen plugin manager really is fast: with two plugins the loading time doesn't increase much. But plain zsh without any configuration is blazing fast, just lake good old sh—the loading time is practically instant. The prompt just appears as one opens up the shell. Bash isn't too bad, either. Sadly, with rapid startup you can't have all the nice features. The 0.1 second starting speed is fast enough for me, and I don't need to make compromises with the features.

Previous and new configuration side-by-side

Same comparison screenshot as above, but with dark colorscheme.

Final thoughts

I now have much greater control and understanding of my shell. I understand which part of the shell is responsible for what, and it is easy to read that part's documentation to learn more. It's always nice to use something that you've made, or at least properly configured yourself. If I feel like something needs improvement, it's probably easier to get started.

Currently I don't think I'll miss Oh My Zsh, but it certainly provides easiness and quality of life features that just work, and can be an option in the future. However, I think I've achieved at least the same level of usability by configuring the shell myself, I feel ownership regarding my shell, and at least it was an interesting and educative journey.

Telegram channel

When I publish a blog post, I'll send a message to this Telegram channel. Feel free to join!

< Blog frontpage