An Asynchronous Shell Prompt

A blocking shell prompt that is slow can lead to a pretty terrible user experience.

Prompt Demo

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.