My new ZSH prompt

One topic I have not yet wrote about is my custom shell. If you're comfortable with unix at all, and use a unix based system (pretty much anything but Windows), you've come to know that the shell is possibly one of the most important parts of your computer. Yet, so many people neglect their shells, using the system default. Some people are unaware that you can even customize it. Then there is the opposite end of the spectrum, those who customize their shells so much to the point that they don't really know how to use non-customized shells.

I've recently reinstalled and updated my zsh on my shell, and thought I might share the process. People on Google+ seemed supportive of the idea, so if you want to know more, please, keep reading.

Old and Busted

Most people use Bash as their primary shell, and there is nothing wrong with this, it just lacks a lot of customization options. A little under a year ago, I decided to move over to a shell called ZSH, or Z Shell. You may have heard of it, you may not have. Basically, it aims to be a bash replacement. It is compatible with all bash scripts and whatnot, so you don't lose anything, but adds so much more. Things like Autocomplete, right-hand prompts, arrays, better variable, file globbing, etc. Lets just agree that it is, for the sake of discussion, better.

But, with all these features, come customization. You can use it the way it is, but thats boring.

Anyway, about a year ago, I transitioned over to Zsh, specifically, a project called oh-my-zsh. While this is a good starting point, it has a lot of bloat, and is kind of slow. Back in Feb I spent some time customizing my theme/prompt to my liking, based off of an even older bash prompt I had.

That was fairly useful, and displayed important information. It had git information, a timestamp, and a timer that would show the command runtime if a command took over 10 seconds to execute.

Unfortunately, like everything else, I grew tired of it. So i began exploring my options

Updating

My OMZ installation was horribly outdated, as I had been a bad computer user and hadn't maintained the updates. It was to the point that updating it would have virtually been a completely new install, so I decided to do exactly that, a completely new install.

This time, instead of using OMZ, I decided to use a different but similar project called Prezto. Prezto was originally a fork of OMZ, but it has been modified and updated, and is in a lot of ways “better.” Its lighter, faster, and has some unique features. Your plugin base is smaller, but the more plugins you load, the slower your shell load time, which isnt good.

Installing is fairly simple, I just git cloned it into my home directory, and then ran the various little scriptlings the install instructions (on the github page) provided. Within a couple of minutes I was up and running.

New theme

Being a tinkerer, I was not happy with the default theme, so I set out to find/make my own. I did some exploring into themes, and found a very pretty one called agnoster theme. The theme, unfortunately, was written for OMZ, and had some compatibility problems with prezto, but the general idea was good.

Agnoster theme used a patched font file, the powerline font patch, to add some custom characters, such as git icons and whatnot. In the gist for the theme, links to these fonts are provided. I chose the Menlo patched font, because Menlo is a gorgeous font, and I use it as much as I can.

Since the theme did not work with prezto, I set out to make my own. I started by cloning the default theme, and then removing a lot of the crap from it, features that are useful, but I would not be using.

I then proceeded to copy over the two key functions from agnoster's theme. These functions enable the easy rendering of the “bars” or “chunks” or whatever you want to call the colored segments:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
CURRENT_BG='NONE'
SEGMENT_SEPARATOR='⮀'


# Begin a segment
# Takes two arguments, background and foreground. Both can be omitted,
# rendering default background/foreground.
prompt_segment() {
  local bg fg
  [[ -n $1 ]] && bg="%K{$1}" || bg="%k"
  [[ -n $2 ]] && fg="%F{$2}" || fg="%f"
  if [[ $CURRENT_BG != 'NONE' && $1 != $CURRENT_BG ]]; then
    echo -n " %{$bg%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR%{$fg%} "
  else
    echo -n "%{$bg%}%{$fg%} "
  fi
  CURRENT_BG=$1
  [[ -n $3 ]] && print -Pn $3
}

# End the prompt, closing any open segments
prompt_end() {
  if [[ -n $CURRENT_BG ]]; then
    echo -n " %{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR"
  else
    echo -n "%{%k%}"
  fi
  echo -n "%{%f%}"
  CURRENT_BG=''
}

With this, I began constructing a basic prompt helper function, that would render the various segments of the prompt. The prompt line itself (PROMPT=) would call this function, which would allow me a terser, easier to manage prompt, as opposed to a single line with a bunch of string interpolations. The build_prompt function:

1
2
3
4
5
6
7
8
function build_prompt {
  prompt_segment black default '%(1?;%{%F{red}%}✘ ;)%(!;%{%F{yellow}%}⚡ ;)%(1j;%{%F{cyan}%}%j⚙ ;)%{%F{blue}%}%n%{%F{red}%}@%{%F{green}%}%M'
  prompt_segment blue black '%2~'
  if $git_status; then
    prompt_segment green black '${(e)git_info[prompt]}${git_info[status]}'
  fi
  prompt_end
}

I'll go over it line-by-line. Line 2 is the first line, which consists of an exit status indicator, which shows if the last command executed successfully or returned with a non-zero exit code, a permissions indicator, which indicates if my current shell is running with elevated permissions, and a background jobs indicator, which shows if, and how many, background jobs I have running. Finally, it ends with my user@host.

The next line is straightforward, it shows the 2 most recent entries in my present working directory. There are various tricks to get a truncated output, but I really only felt the last two directories were relevant, so I went with this. If you want your entire PWD, remove the 2 before the ~.

The next line is a tricky part, and requires some more code, which I'll go over later. It is designed to show the current git status of the directory I'm in, and makes use of a prezto plugin. Basically, what this segment of code does is check if the git status exists, and if it does, prints a prompt-segment with the git info.

With all that finished, its time to end the prompt. But this is not enough to make the actual prompt. I still need the git information, as well as some other features I like, such as command timers.

The prompt setup function

Having a function that builds a prompt works fine and dandy, but I still need to build the actual prompt. Thats where this function comes in. It sets some options, loads some utilities, and configures some things, and then sets the actual prompt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
tion prompt_paradox_setup {
  setopt LOCAL_OPTIONS
  unsetopt XTRACE KSH_ARRAYS
  prompt_opts=(cr percent subst)

  # Load required functions.
  autoload -Uz add-zsh-hook

  # Add hook for calling git-info before each command.
  add-zsh-hook preexec prompt_paradox_preexec
  add-zsh-hook precmd prompt_paradox_precmd

  zstyle ':prezto:module:editor:info:completing' format '%B%F{red}...%f%b'
  zstyle ':prezto:module:editor:info:keymap:primary' format '%B%F{blue}❯%f%b'
  zstyle ':prezto:module:editor:info:keymap:primary:overwrite' format '%F{red}♺%f'
  zstyle ':prezto:module:editor:info:keymap:alternate' format '%B%F{red}❮%f%b'
  zstyle ':prezto:module:git:info:action' format '! %s'
  zstyle ':prezto:module:git:info:added' format '✚'
  zstyle ':prezto:module:git:info:ahead' format '⬆'
  zstyle ':prezto:module:git:info:behind' format '⬇'
  zstyle ':prezto:module:git:info:branch' format '⭠ %b'
  zstyle ':prezto:module:git:info:commit' format '➦ %.7c'
  zstyle ':prezto:module:git:info:deleted' format '✖'
  zstyle ':prezto:module:git:info:modified' format '✱'
  zstyle ':prezto:module:git:info:position' format '%p'
  zstyle ':prezto:module:git:info:renamed' format '➙'
  zstyle ':prezto:module:git:info:stashed' format 's'
  zstyle ':prezto:module:git:info:unmerged' format '═'
  zstyle ':prezto:module:git:info:untracked' format '?'
  zstyle ':prezto:module:git:info:keys' format \
    'prompt' '$(coalesce "%b" "%p" "%c")%s' \
    'status' ' %A%B%S%a%d%m%r%U%u'

  # Define prompts.
  PROMPT='
%{%f%b%k%}$(build_prompt)
 ${editor_info[keymap]} '
  RPROMPT='[%D{%L:%M:%S %p}]'
  SPROMPT='zsh: correct %F{red}%R%f to %F{green}%r%f [nyae]? '
}

Thats quite a script, no? Its pretty straightforward, though.

The first part sets various Zsh and environment options, which ensures the prompt is nice and updated with the current environmental settings. This enables things such as terminal resizing and various ZSH escapes.

I then load the zsh-hook module, and add two hooks, which run at various times of the prompt execution. I will use this to get git-info, as well as to run the command timer.

Next up, I have a large block of zstyle entities. These configure various aspects about portions of the prompt. The first few, for the editor module, handle things such as the completion indicator, in my case, a red ellipsis, the keymap indicator (useful with zsh's vim-style line editor), and a few other configs.

The next major section of the zstyle entries are the git formats. These configure how the various statuses git-info will return are rendered. They are fairly straightforward, and mostly consist of a single character. A few of them, such as prezto:module:git:info:branch contain substitution characters, such as %b. These characters are replaced with dynamic bits of info, in this case, the current working branch name.

I set the actual info keys, which I called earlier, with the prezto:module:git:info:keys line. This basically gives us 2 separate variables I can call, one that displays an icon for the git status, and one that displays richer information, such as the current working branch or the command I am in (useful in rehashes).

And, last but not least, I define the actual prompt. I call the build_prompt command, then the editor info I defined earlier. For the right-hand prompt, I decided to use a simple clock, using the zsh date prompt expansion with a bit of customized string. This will render something like [11:33 AM]. The sprompt is an interesting beast, its only shown when the shell attempts to correct a mistyped command to something else. I left it at the defaults I got from the sorin theme, as they seem sensible.

The hooks

I'm nearly done with the prompt, but I still have to set a few hooks, that I defined earlier in the prompt_paradox_setup function.

Preexec

1
2
3
4
start_time=$SECONDS
function prompt_paradox_preexec {
  start_time=$SECONDS
}

The preexec hook is run right after a command is processed, but right before it is called. This is useful for many things, in this case, I use it to log the $SECONDS variable to a local variable. $SECONDS is a useful little variable, it stores the total time the current shell has been opened for. While not the most accurate measurement, its fine for what I'm using it for.

I define the start_time var just before the function as well, so if the function isn't called, say, on the creation of a new terminal, I don't get nasty errors. You can set this to 0 if you like, but if you set it to seconds, it can show how long your terminal took to open.

Precmd

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function prompt_paradox_precmd {
  setopt LOCAL_OPTIONS
  unsetopt XTRACE KSH_ARRAYS

  # Get Git repository information.
  if (( $+functions[git-info] )); then
    git_status=git-info
  fi
  timer_result=$(($SECONDS-$start_time))
  if [[ $timer_result -gt 10 ]]; then
    calc_elapsed_time
  fi
  start_time=$SECONDS
}

This command does a whole lot more than preexec. Since I'm using git-info, which is a prezto plugin, I need to set up the environment a bit. Thats what the first 2 lines of the function do. The next is fairly straightforward, if the git module is loaded, I run the git-info command, and store its output in the git_status var. This var is used in the build_prompt command to determine if I want to render the git segment or not.

After the git command, I subtract the $start-time I established in the preexec, and store the value in a timer result variable. If this value is over 10, I call a function calc_elapsed_time, and then reset the start time to $seconds again, just to minimize the risk of absurdly high timers.

The calc_elapsed_time function is a useful bit of basic math, and takes a seconds value and splits it into a string of human-readable time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function calc_elapsed_time {
  if [[ $timer_result -ge 3600 ]]; then
    let "timer_hours = $timer_result / 3600"
    let "remainder = $timer_result % 3600"
    let "timer_minutes = $remainder / 60"
    let "timer_seconds = $remainder % 60"
    print -P "%B%F{red}>>> elapsed time ${timer_hours}h${timer_minutes}m${timer_seconds}s%b"
  elif [[ $timer_result -ge 60 ]]; then
    let "timer_minutes = $timer_result / 60"
    let "timer_seconds = $timer_result % 60"
    print -P "%B%F{yellow}>>> elapsed time ${timer_minutes}m${timer_seconds}s%b"
  elif [[ $timer_result -gt 10 ]]; then
    print -P "%B%F{green}>>> elapsed time ${timer_result}s%b"
  fi
}

Basically, I use some basic math to take a seconds integer, say 300 seconds, and turn it into a human readable string, say “5m0s”

Wrapping up the prompt

Nearly there. At the bottom of the prompt file, I called the prompt_paradox_setup function, via prompt_paradox_setup "$@". This sets up the prompt vars, and ensures everything is ready to go.

Now I just need to set the prompt, which can be done in the .zpreztorc file.

And here's the end result Final terminal

You can customize a lot more about prezto, and I only scratched the surface

References

These guides were particularly useful when working on my theme:

You can see my current repo on my github at https://github.com/paradox460/prezto. The latest version of the theme is located here