Show background jobs number in your ZSH prompt

My daily workflow is based around vim (neovim, actually) in an iTerm 2 terminal. As a consequence, I spend most my day navigating iTerm 2 tabs and switching between editing/viewing code in vim and doing stuff in my terminal: running tests, interacting with my file system or with git for instance. Which makes me use Ctrl-Z pretty often.

Ctrl-Z and background jobs

What is Ctrl-Z you may ask? It allows you to suspend your current process, and move it to the background. You can list background jobs by using the jobs command in your shell:

1
2
3
$ jobs
[1]  - suspended  nvim
[2]  + suspended  bundle

And you can then make these processes active again by using the fg command. Running fg without any argument would make the last suspended process active again (here, it would be bundle). Or you can specify which process to bring back to the foreground, by using fg $1 for instance (which would bring nvim back to the foreground here).

Prefix my ZSH prompt with the number of background jobs

But I had one problem with this: I often forgot I had some process running, and ended up re-opening a brand new editor, or simply closing my terminal with these process still running, realizing afterwards that I lost my vim buffers and tabs and all that. Not a super big deal but annoying 😖

Having in my prompt an indicator of background jobs would do the job (ah!) for me, so I started looking into this. Like in Bash, you'll want to look into how to customize your PS1 environment variable.

Prompt sequences to the rescue!

Lucky for me, ZSH provides prompt sequences. They are sets of characters, prefixed with a %, that are interpreted by ZSH when present in your PS1 environment variable, and are replaced with some specific information. For instance, the following prompt:

1
2
# .zshrc
PS1='%n @ %m ▶ '

would show the current username (%n) and the current host (%m), and then a "triangle" character that I like to use for my prompt. Among all the available prompt sequences, one of them is %j and is "the number of jobs". Exactly what I'm looking for.

So let's try something simple like this:

1
2
# .zshrc
PS1='[%j] %n @ %m ▶ '

And re-source my zsh configuration file or open a new shell. Here's what I'm getting:

1
[0] myself @ myhost ▶

That's a good start already! 💪

Show the number of background jobs conditionally

But most of the time, I have no process in the background and I don't care about this information. What I'd love is to display this information only when I actually have background processes. Once again, ZSH has this built-in. It's called "conditional substrings in prompts" and uses this format:

1
%(expression.true-text.false-text)

Simply put, it's a ternary expression. The trick is in the expression part: it accepts a few values, and I'll be honest, most of them seem of little use for me. BUT one of them is, again, exactly what I'm looking for:

  • j returns true if the number of jobs is at least n.

So, writing something like this:

1
2
# .zshrc
PS1='%(1j.[%j] .)%n @ %m ▶ '

would check if the number of background jobs is at least 1. If so, it will display the number of jobs in square brackets, and otherwise, will display nothing.

1
2
3
myself @ myhost ▶ sleep 5
^Z
[1] myself @ myhost ▶

It works! The first line shows my prompt with no background process, then I start a long running process that I Ctrl-Z, and then my prompts shows this [1] prefix, indicating that I have one process in background!

Adding colors…

Now, I "only" need to customize the color for my prompt to make this a bit more appealing. I won't dive into the details as this topic could justify a whole other blog post, but here's what I finally got:

1
2
3
4
5
6
7
8
9
10
# .zshrc

# %(x.true-text.false-text)
# x = 1j <=> evaluates to true if the number of jobs is at least 1, false otherwise
# %j = number of background jobs
BACKGROUND_JOBS='%(1j.%F{red}[%j] %f.)'

# %F{color} starts the color, %f stops it
# %B starts bold, %b stops it
PS1='%B$BACKGROUND_JOBS%F{cyan}%n%f@%F{blue}%m%f%b %F{yellow}▶%f '

Hard to decipher, but let's break this down into smaller chunks:

  • %B: starts bold text,
  • $BACKGROUND_JOBS: insert my $BACKGROUND_JOBS variable that we broke down before,
  • %F{cyan}: starts writing in cyan,
  • %n: insert the username,
  • %f: stops the current color (cyan),
  • @: just insert an arobase character,
  • %F{blue}: starts writing in blue,
  • %m: insert my hostname,
  • %f: stops writing in blue,
  • %b: stops writing in bold,
  • %F{yellow}▶ %f: my prompt character, in yellow.

Which eventually looks like this, exactly what I was looking for:

Successive ZSH prompts with no background jobs, one background job, two background jobs
Exactly what I was looking for!

Hope this will also help you to tune your ZSH prompt!


References