An Asynchronous Shell Prompt
A blocking shell prompt that is slow can lead to a pretty terrible user experience.
Above is a demo of a stock oh-my-zsh install (left) compared to an asynchronously updated prompt (right). Everything is running on one machine, in parallel, using tmux pane synchronization to send the same exact input at the same exact time to both shells. Observe that the shell on the left blocks the user from executing commands, while the shell on the right asynchronously updates the prompt, allowing the user to run commands without delay.
Causes
Shell prompts are usually configured to display contextual information such as the current directory, username, hostname, and so on. It is quite common for users to include the version control system status in the prompt as well. Some of the information that appears in the prompt can be computed almost instantaneously. For example, computing the name of the current working directory does not take all that much time.
On the other hand, some information can take much longer to compute, sometimes on the order of several seconds, as shown above. For example, determining the version control system status can take quite a long time, because it usually requires a traversal of the entire directory tree from the root of the repository. If the metadata is not already in the buffer cache, it can take a ton of time, and even when the information is in the cache, the traversal is still noticeably time-consuming.
Programmed in the straightforward way, complex prompts can take up to a couple seconds to render, degrading the experience of using a terminal. Until the prompt is computed, the shell blocks and prevents the user from running commands.
A Non-blocking Prompt
This problem can be solved using an asynchronously updated shell prompt, using
a technique that is fairly straightforward to implement in zsh. The shell
supports displaying a prompt on both the left and right sides of the screen by
setting PROMPT
and RPROMPT
. Separating information between the two parts,
information that is slow to update can be kept in the right side prompt. The
shell can be configured to update the left prompt synchronously and update the
right prompt asynchronously, providing a smooth user experience.
The general method for updating the prompt asynchronously is to fork off processes to compute the information in the background and send a signal to the shell once the information is ready. Then, the shell can read in and display this information, updating the prompt.
In zsh, the precmd
function is executed before displaying each prompt, so
this can be used to fork off a background process.
The following is example code that can be used in ~/.zshrc
to implement an
asynchronous prompt. In the example, prompt_cmd
is the command that generates
the content to be displayed in PROMPT
, and rprompt_cmd
is the command that
generates the content to be displayed in RPROMPT
.
setopt prompt_subst # enable command substition in prompt
PROMPT='$(prompt_cmd)' # single quotes to prevent immediate execution
RPROMPT='' # no initial prompt, set dynamically
ASYNC_PROC=0
function precmd() {
function async() {
# save to temp file
printf "%s" "$(rprompt_cmd)" > "/tmp/zsh_prompt_$$"
# signal parent
kill -s USR1 $$
}
# do not clear RPROMPT, let it persist
# kill child if necessary
if [[ "${ASYNC_PROC}" != 0 ]]; then
kill -s HUP $ASYNC_PROC >/dev/null 2>&1 || :
fi
# start background computation
async &!
ASYNC_PROC=$!
}
function TRAPUSR1() {
# read from temp file
RPROMPT="$(cat /tmp/zsh_prompt_$$)"
# reset proc number
ASYNC_PROC=0
# redisplay
zle && zle reset-prompt
}
The full code for my shell prompt is available online.