Optimising the Terminal Prompt

If you spend a lot of time in the terminal, it's important that the UI makes it easy to get things done.

Having spent a fair amount of time thinking about how I wanted mine to look, I wanted to note down the considerations and tradeoffs, not least for myself.

If you just want to see what my current prompt looks like, scroll to the bottom.

Where to put Information

How much is too much?

The number one consideration with a prompt is how much information to provide.

Tradeoff:

Prompt Character

I use , piggybacking off Sindre Sorhus's research:

Comes with the perfect prompt character. Author went through the whole Unicode range to find it.

It stands out pretty well.

Left Prompt

First question is what should go on the left prompt. For me it's simple, left prompt should be as minimal as possible, i.e. just the prompt character.

Ability to scan through prompts

When scrolling or visually scanning back through terminal history, it's important to be able to easily see where each prompt line is.

When every prompt starts on a different line, it's harder to see where each line starts, as you can't scan up the first column to find the prompt characters.

Here's an example of just the prompt character.

Nothing on the left prompt:
 l
total 32K
-rw-r--r--  1 gib staff  295 Mar  9 07:57 .editorconfig
drwxr-xr-x 17 gib staff  544 Mar 28 19:07 .git
-rw-r--r--  1 gib staff   33 Mar  9 07:57 .gitignore
-rw-r--r--  1 gib staff  972 Mar  9 07:57 .gitlab-ci.yml
-rw-r--r--  1 gib staff 1.1K Mar  9 07:57 LICENSE
-rw-r--r--  1 gib staff  367 Mar  9 07:57 README.md
-rw-r--r--  1 gib staff  582 Mar  9 07:57 TODO.md
-rw-r--r--  1 gib staff  246 Mar  9 07:57 _cobalt.yml
drwxr-xr-x  3 gib staff   96 Mar  9 07:57 docker
drwxr-xr-x  7 gib staff  224 Mar  9 07:57 logo-gen
drwxr-xr-x 16 gib staff  512 Mar 28 12:52 public
drwxr-xr-x 17 gib staff  544 Mar 28 12:50 src
-rwxr-xr-x  1 gib staff 1.7K Mar  9 07:57 update_certs
 g s
On branch master
Your branch is ahead of 'up/master' by 2 commits.
  (use "git push" to publish your local commits)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	new file:   src/posts/optimising-the-terminal-prompt.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	src/img/optimising-the-terminal-prompt/

no changes added to commit (use "git add" and/or "git commit -a")
Your branch is 2 commit(s) ahead and 0 commit(s) behind push branch up/master.
 g log -2
  commit 862468fd53640565f21791889212588d5349a34a (HEAD -> master)
  Author:     Gibson Fahnestock <gibfahn@gmail.com>
  AuthorDate: Sat Mar 28 12:56:50 2020 +0000
  Commit:     Gibson Fahnestock <gibfahn@gmail.com>
  CommitDate: Sat Mar 28 12:56:50 2020 +0000

      fix(posts)!: remove date from last post

  commit 29dbf973e2343a18feb62c8b635ee774a1a1fc46
  Author:     Gibson Fahnestock <gibfahn@gmail.com>
  AuthorDate: Sat Mar 28 12:51:23 2020 +0000
  Commit:     Gibson Fahnestock <gibfahn@gmail.com>
  CommitDate: Sat Mar 28 12:51:23 2020 +0000

      fix(templates): use default.liquid for blog posts

      This means that you can generate new pages with:

      ```shell
      cobalt new -f src/posts/ "Post title"
      ```

      and they will work (as they get the default.liquid template by default).

Compare this with a varying-width left prompt.

Varying-width left prompt:
~/path/to/current directory ❯ l
total 32K
-rw-r--r--  1 gib staff  295 Mar  9 07:57 .editorconfig
drwxr-xr-x 17 gib staff  544 Mar 28 19:07 .git
-rw-r--r--  1 gib staff   33 Mar  9 07:57 .gitignore
-rw-r--r--  1 gib staff  972 Mar  9 07:57 .gitlab-ci.yml
-rw-r--r--  1 gib staff 1.1K Mar  9 07:57 LICENSE
-rw-r--r--  1 gib staff  367 Mar  9 07:57 README.md
-rw-r--r--  1 gib staff  582 Mar  9 07:57 TODO.md
-rw-r--r--  1 gib staff  246 Mar  9 07:57 _cobalt.yml
drwxr-xr-x  3 gib staff   96 Mar  9 07:57 docker
drwxr-xr-x  7 gib staff  224 Mar  9 07:57 logo-gen
drwxr-xr-x 16 gib staff  512 Mar 28 12:52 public
drwxr-xr-x 17 gib staff  544 Mar 28 12:50 src
-rwxr-xr-x  1 gib staff 1.7K Mar  9 07:57 update_certs
~ ❯ g s
On branch master
Your branch is ahead of 'up/master' by 2 commits.
  (use "git push" to publish your local commits)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	new file:   src/posts/optimising-the-terminal-prompt.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	src/img/optimising-the-terminal-prompt/

no changes added to commit (use "git add" and/or "git commit -a")
Your branch is 2 commit(s) ahead and 0 commit(s) behind push branch up/master.
~/path/to/some/other/directory ❯ g log -2
  commit 862468fd53640565f21791889212588d5349a34a (HEAD -> master)
  Author:     Gibson Fahnestock <gibfahn@gmail.com>
  AuthorDate: Sat Mar 28 12:56:50 2020 +0000
  Commit:     Gibson Fahnestock <gibfahn@gmail.com>
  CommitDate: Sat Mar 28 12:56:50 2020 +0000

      fix(posts)!: remove date from last post

  commit 29dbf973e2343a18feb62c8b635ee774a1a1fc46
  Author:     Gibson Fahnestock <gibfahn@gmail.com>
  AuthorDate: Sat Mar 28 12:51:23 2020 +0000
  Commit:     Gibson Fahnestock <gibfahn@gmail.com>
  CommitDate: Sat Mar 28 12:51:23 2020 +0000

      fix(templates): use default.liquid for blog posts

      This means that you can generate new pages with:

      ```shell
      cobalt new -f src/posts/ "Post title"
      ```

      and they will work (as they get the default.liquid template by default).

It's also easier to focus when you just see the commands you typed and their output as you read down the left hand side, without having a lot of other information mixed in.

Multiline code snippets

If you use a shell like zsh that allows you to use multiline code snippets, it's quite nice to be able to type things easily on multiple lines.

Having a long bunch of stuff in your prompt before the prompt character makes it hard to read the indentation of the code, and means you tend to lose the first line, e.g.

~/some/long/path/to/current/directory ❯ for i in *; do
    if [[ -d $i ]]; then
        echo ">>> $i"
        mv $i $i.bak
    fi
done

v.s.

 for i in *; do
    if [[ -d $i ]]; then
        echo ">>> $i"
        mv $i $i.bak
    fi
done

With the second example the first line is still indented 2 chars too many, but it's still visually obvious how the code is structured, rather than seeming like the first line isn't part of the code block.

Line Space

When you type a long single-line command, it's annoying when it wraps because too much of the line is taken by the prompt. Thus shorter is better than longer on the left.

Right Prompt

Luckily, if you use a shell like zsh, you can define a right prompt as well as a left one. This helps solve a lot of the issues that putting information in the left prompt causes.

Auto-Hiding

The prompt is automatically hidden when the typed line becomes long enough to overlap with it. This allows you to have the information, but drop it when you need the space for something else.

Invisible Prompt Sections

For information you don't usually need, but want to be able to go back and find occasionally, you can have black-on-black text on the far left of the right prompt.

This won't show up unless you highlight it with the mouse cursor or double-click on it.

Hidden by Default

Most prompt information is an alert, useful when something unusual has happened. This kind of information only adds clutter if it is always-on, and makes it harder to find the important info.

So most information should be hidden by default.

Multiline Prompts

An alternative to putting information on the right is putting it above the main prompt line on the left.

This helps avoid some of the issues that putting it before the prompt character provides, but it also takes up an extra line.

Once you start adding extra lines, you start to lose vertical information density quite rapidly. Being able to easily see the commands you typed is very important, especially as your muscle memory builds and you start typing three different commands in sequence from memory faster than you can actually read the output. A prompt usually has to work on a laptop screen as well as a larger working desktop screen.

Blank Space / Ruler

One thing that can be useful is using a blank line to separate prompts. This helps make each command and output seem like a paragraph when scanning through, so your eye naturally sees the blocks of text in the terminal history.

 ls
LICENSE  README.md  TODO.md  _cobalt.yml  docker  logo-gen  public  src  update_certs

 echo foo
foo

❯

You can also have a ruler between each prompt to make the separation even more obvious. The tradeoff here is that when not paired with a blank line things can get very cluttered, and when paired with a blank line you lose too much vertical density. The best solution I've found is to use (or a different unicode underscore char) as the ruler character, this pushes the ruled line down, so you get the equivalent of the blank line + ruler without having to lose two lines. It's also a nice thick line, which adds to the separation between commands it provides.

Without a blank line:

─────────────────────────────────────────────────────────────────────────────────────
❯ ls
LICENSE  README.md  TODO.md  _cobalt.yml  docker  logo-gen  public  src  update_certs
─────────────────────────────────────────────────────────────────────────────────────
❯ echo foo
foo
─────────────────────────────────────────────────────────────────────────────────────
❯

With a blank line:


─────────────────────────────────────────────────────────────────────────────────────
❯ ls
LICENSE  README.md  TODO.md  _cobalt.yml  docker  logo-gen  public  src  update_certs

─────────────────────────────────────────────────────────────────────────────────────
❯ echo foo
foo

─────────────────────────────────────────────────────────────────────────────────────
❯

With an underscore:

▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
❯ ls
LICENSE  README.md  TODO.md  _cobalt.yml  docker  logo-gen  public  src  update_certs
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
❯ echo foo
foo
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
❯

Terminal Title / Tab Bar Info

Most terminal emulators have a tab bar, and some have a title bar too.

iTerm 2 also has a Status Bar that can show extra information.

This is usually a good place for something minimal (that in the case of the tab bar disambiguates your open tabs).

The problem with these areas is that they:

  1. Use screen space
  2. Are further from your focus area (current prompt line)
  3. Don't show the history per prompt command.

The last one is key, for most information, being able to scroll up to see what the status of something was when each command is run is extremely useful.

What Information to Show

Once you know where you can put information, you need to decide what information is worth showing.

Current Working Directory

The current working directory is key, knowing where you are anchors your train of thought. And when you have multiple tabs and windows open this keeps you clear.

However the full path can be extremely long, and just having the directory name is frequently ambiguous.

I have the directory name as the tab title, and the shortest unambiguous abbreviated path on the right of the right prompt.

To quote the documentation for the path:

If directory is too long, shorten some of its segments to the shortest possible unique prefix. The shortened directory can be tab-completed to the original.

This has so far done a good job of being short while unambiguous. It also highlights the most important part (the current directory name).

Return Code

The Return Code tells you whether the previous command passed or failed. This is often vitally important, and having to echo $? afterwards is impossible if you already ran a command since then.

The simplest way to do it is to make the prompt character green by default, and red if the previous command returned a non-zero exit code. This takes no extra characters, and makes the information very obvious, as once you hit enter you're looking at the next character.

It's also useful to know the exit code though, and I do this by having the number show up in red in the Right Prompt if the exit code was non-zero.

Git Information

Having git information (including just that you're in a git repo) is very useful. However there are lots of things you might want to note.

For me the branch name is important enough to always show in the Right Prompt, it fulfils the same function as having the Current Working Directory there.

Everything else only shows up in abnormal situations:

Execution Time

Knowing when the prompt showed up for a command is often really useful. Sometimes you come back to a session the next day, and have to work out when exactly you ran a command.

When you want to know this information, it's too late, so you need it in the prompt. However you rarely need it, so having it cluttering up your terminal isn't useful.

This is where the Invisible Prompt is useful, by colouring the text the same as the background colour, you make it invisible until you want it.

Command Duration

You often want to know how long a command ran, both for normal commands ("how long did that git fetch take?") or interactive ones ("how long did I have that Vim window open for?"). When you want this info, if it's not already in the prompt then you have to rerun it, which can take time or be impossible to reproduce.

However you don't need accurate timing info (use a better tool like time or hyperfine for that). You also don't care if it was instant. So only logging when the command took longer than 1s, and only logging to second precision, strikes a good balance between usefulness and brevity.

Background Jobs

If you have jobs running in the background, it's useful to have a little status indicator for that.

Tool Versions

You can show your git/rust/npm/go/ruby/python/bazel (etc.) version in the prompt. I've don't currently use these, as the languages I work with mostly allow you to have a version file committed that ensures the right version is used. If I was working with e.g. node a lot I might have different requirements.

Active Environments

In contrast to the above point, sometimes you've enabled something that makes your environment non-standard, at which point you want to have a reminder of that because it's not on by default.

Examples of this include:

My current Right Prompt can look like this when everything happens to be turned on:

Right Prompt

Even while being as minimal as possible, there's still a lot going on here.

This is also where Nerd Fonts are very useful, they allow you to condense a bunch of information into a single easily-recognisable character.

Zsh Vi Mode

If you use Zsh Vi Mode, it's very useful to have an easy way to see which mode you're in.

For my purposes putting the mode in the Right Prompt is too far away to be useful when actually writing text.

I currently have it change the cursor from an I-bar (I = insert mode) to a block ( = normal mode). When in visual mode the visually selected text is highlighted, so that's visually obvious. This is the same cursor setup I use in neovim, so it's intuitive for me.

My Current Prompt

My current prompt looks like this:

Current Terminal Prompt

I want it to feel minimal yet feature-rich. I use the excellent Powerlevel10k theme.

To see it in action with slightly wrong colouring (the date and time aren't hidden as they are in my terminal):