Useful zsh Init File Things

/Errata: in the section on adding a bottom bar to the ZSH prompt, the previously given $PROMPT variable contained bugs. Use the current one instead./


/This post's intended audience is those with Unix command line experience. More in-depth terminal and shell experience is required to understand how the given snippets work, but they're designed such that anyone could copy and paste them into their init file without understanding how they work./

I use zsh quite a lot, and in the process of using it I've created several pieces of code one can put in their shell init files (~/.zshenv, ~/.zshrc, ~/.zlogin, etc.). These chiefly include a system to store the current directory of each shell session, allowing one to move between them; and an extension of the shell prompt at the bottom of the screen.

One may ask: why aren't these published on Github or something? These aren't fully-fledged programs in and of themselves, and they warrant some explanation. My dotfiles are in a Github repository, but that repository isn't particularly suitable for public release.

Examples

Click the below examples to enlarge them.

An XTerm window with a zsh prompt at top left,
          containing `[000]~% `
          and a bottom bar containing
          `[LOCAL]tucker@Control-C-5D ~`.
An XTerm window with a lot of command output,
          a zsh prompt at bottom right,
          containing `[000]~% `
          and a bottom bar containing
          `[LOCAL]tucker@Control-C-5D ~`.
A wezterm window with a zsh prompt at top left,
          containing `[000]~% `
          and a bottom bar containing
          `[SSH  ]tucker@maildrop ~`.
A wezterm window with fzf running inside.
          A list is at the bottom of the window;
          each line contains a path to a TTY (like `/dev/pts/6`) and then a
          directory (like `~/docs/website/context/posts/useful-zsh-init-things`)
          The bottom line has a currently-empty search prompt.

Current directory indexing

 1# ~/.zshrc
 2local curr_dir_file=~/.curr_dirs
 3upd_curr_dirs()
 4{
 5  rm_self_curr_dirs
 6  tty >/dev/null && printf '%-12s %s\n' $(tty) "$(print -P '%~')" \
 7    >>$curr_dir_file
 8}
 9add-zsh-hook chpwd upd_curr_dirs
10
11rm_self_curr_dirs()
12{
13  if tty >/dev/null
14  then
15    local newdirs=$(cat $curr_dir_file | grep -v "^$(tty)")
16    echo $newdirs >$curr_dir_file
17  fi
18}
19add-zsh-hook zshexit rm_self_curr_dirs
20
21select_saved_directory()
22{
23  cat $curr_dir_file | fzf | sed -E 's!^(.*) (/|~)!\2!g'
24}
25
26d()
27{
28  dir=$(select_saved_directory)
29  [[ -n $dir ]] && eval cd "$dir"
30}

I often find myself in the situation of having to open a new shell session (in a new terminal window or tmux pane) with the same current working directory as an existing session. This script makes jumping to the current working directory of an existing shell session easy. One need only type d, and a menu of other shell sessions and their working directories will be displayed. Selecting a directory and pressing Enter will change to that directory. When exiting a shell, the entry associated with that shell session will be cleared.

Typically, the ~/.curr_dirs file looks like this:

1/dev/pts/5   ~/docs/website
2/dev/pts/1   ~/code/projects/trt-dotfiles/desktops
3/dev/pts/9   ~/docs/website
4/dev/pts/3   ~/docs/website

Each line represents a shell session. The first component of each line represents the pseudoterminal associated with that session, while the second represents the current working directory of the session. It's theoretically possible to use the ~/.curr_dirs file to store directory bookmarks by making the first component a value that doesn't start with the name of a terminal and doesn't contain words starting with ~ or /. However, the system was not designed for this, nor have I used it this way.

A quick note on design: the use of an intermediate variable in rm_self_curr_dirs is necessary to prevent clearing the current directories file before reading it, as would happen if a pipeline read the file, processed it, and wrote back to it all at once. Ensuring that directories with spaces are handled properly is perilous when the file format used to store them separates records with spaces. Nonetheless, the code as written handles it correctly unless the terminals associated with each shell session (or whatever other "keys" there are) contain a space followed by a slash or a space followed by a tilde. This won't happen in normal operation.

Bottom bar

 1clearprompt()
 2{
 3  # notes:  aaaaabbbbccccccccccdddddeeee
 4  print -n "\e[2K\e[s\e[1000;1H\e[2K\e[u" # Bottom bar
 5  # Alerts certain terminals that a command has been typed and is about to be
 6  # run.
 7  print -n "\e]133;C\e\\"            # Shell int
 8}
 9
10if [[ ! (($TERM == dumb && $INSIDE_EMACS) || -n $VIM) &&
11  $TERM =~ "screen.*" || $TERM =~ "xterm.*" ]]
12then
13  # Assumes that terminal is no greater than 999 lines high
14  # see notes:      fffffffffgggggggghhiiiiiiiiiiijjkk
15  local bbcontents="%F{white}%K{blue}%Ehello world%k%f"
16  # see notes:     ll    bbbbmmccccccccccnnoooooooooooooeeee    ll
17  PROMPT+=$(print "%{\e[B\e[s\n\e[1000;1H%E${bbcontents}\e[u\e[A%}")
18fi
19add-zsh-hook preexec clearprompt

What all of that means

This involves a fair amount of terminal wizardry. A useful reference on the escape codes supported by all modern terminals is the XTerm project's ctlseqs(ms) manpage. A useful reference for zsh prompt expansion escape codes (the %x codes) is the relevant chapter in the zsh manual. I've put together a table below explaining what each part of the character strings on lines 4, 15, and 17 do; match each entry with the relevant part of the string using the letters in the # Notes: … line above the part in question of the string.

Note Who interprets it What it does Why
a Terminal Clears the current line. When clearprompt is run, the Enter key has just been pressed and the cursor is at the beginning of a new line. This ensures that that line, which might be at the bottom of the screen, is cleared. This is necessary to fix certain bugs.
b Terminal Saves the current cursor position This saves the current cursor position, because we're about to explicitly move to the bottom of the screen and we need to be able to get back to where we were.
c Terminal Goes to character cell (1,1000); (1,1) is top left of screen Since the relevant standards define no "go to bottom of screen" sequence, we just assume that the terminal has fewer than 1000 lines. If that assumption holds, this escape sequence will put the cursor at the bottom left corner of the screen.
d Terminal Clears the current line. Erases the bottom bar.
e Terminal Returns the cursor to the previously saved position. Returns the cursor to where it was.
f zsh Sets the foreground color to white.
g zsh Sets the background color to blue.
h zsh Clears to the end of the line. This "paints" the entire bottom bar with the selected background color, no matter how many characters of space are actually used on the bottom. This is useful for setting the bottom bar apart from everything else.
i A human This is the actual text displayed on the bottom bar.
j zsh Resets the foreground color.
k zsh Resets the background color.
l zsh Begins/ends an "escape sequence" block. zsh needs to properly calculate the length of the prompt so it knows where the cursor should be. This tells zsh that everything we're sending will not be displayed with the prompt.
m Terminal Creates a new line, moving the cursor down and scrolling if necessary. If the cursor was at the bottom of the screen, this ensures that space is made for the bottom bar.
n zsh Clears to the end of the line. This erases whatever might be at the bottom.
o zsh Substitutes in the contents of the bottom bar.

How I have it configured

 1if [[ $SSH_CONNECTION ]]
 2then
 3  ssh="%f[SSH  ]%K{red}"
 4else
 5  ssh="[LOCAL]"
 6fi
 7
 8clearprompt()
 9{
10  print -n "\e[2K\e[s\e[1000;1H\e[2K\e[u" # Bottom bar
11  print -n "\e]133;C\e\\"            # Shell int
12}
13
14local fancy_prompt=1
15if [[ ($TERM == dumb && $INSIDE_EMACS) || -n $VIM ]]
16then
17  promptinit
18  prompt adam2
19elif [[ $TERM =~ "screen.*" || $TERM =~ "xterm.*" ]]
20then
21  PROMPT=''
22  if [[ $fancy_prompt == 1 ]]
23  then
24    # Assumes that terminal is no greater than 999 lines high
25    local bbcontents=$(print "%F{white}%K{blue}%E\
26%B$ssh%n%k%K{blue}@%m %~    $jobctl%k%f")
27    PROMPT+=$(print "%{\e[s\n\e[1000;1H%E${bbcontents}\e[u%}")
28  fi
29  # Set xterm title.
30  PROMPT+=$'%{\e]0;%~\e\\%}'
31
32  PROMPT+=$'%{\e]133;D;%?\e\\\e]133;P\e\\%}'
33  PROMPT+=$'%B[\$(printf "%03u" \$?)]%F{blue}%2~%f%b%# '
34  PROMPT+=$'%{\e]133;B\e\\%}'
35fi
36add-zsh-hook preexec clearprompt
37
38export PROMPT RPROMPT

This implements a few useful things. First, the bottom bar has an explicit marker of whether this shell is being accessed over an SSH connection or locally (and, in the latter case, marks the hostname in red); this is to prevent, say, shutting down or disconnecting the network on a server located 30 miles away. (I have done this.) Lines 15-18 display a simple prompt for the remaining terminals in common use that don't support modern escape sequences: the emacs and nvim integrated terminals. (As of right now, the nvim terminal detection does not always work.) Lines 25-26 define the contents of the bottom bar, which consists of the previously-mentioned local/SSH indicator, the username, the hostname, and the current directory. The actual prompt is defined on line 33, and contains, in order, the exit status of the previous command, the last two components of the current directory, and the % or # prompt. Because I frequently use zsh job control, I'm looking into putting a count of suspended processes somewhere.