Write an Async Zsh Prompt
Why Not Use P10k
P10k is a fantastic asynchronous Zsh prompt plugin. It is fast, the author is very active, and it provides convenient customization commands, allowing you to easily generate a Zsh prompt you like. However, when you look at the content of the generated file, it is enormous. This is because the author has provided support for many aspects, such as displaying different language environments, language versions, Git information, prompt formatting, styles, custom function hooks, etc. This is the path a plugin should take to meet the needs of most people. However, I usually don’t use so many features. I once generated a minimal pure theme based on P10k. One day, while organizing my dotfiles, I wanted to modify some styles. But even with the minimal pure theme style, P10k still generated hundreds of lines of shell code. So I decided to get rid of it.
Synchronous Zsh Prompt
Writing a simple Zsh theme is easy. First, define some color variables, such
as RED="%F{red}"
. Then, create a simple function to generate the desired prompt
string and pass it to the PROMPT
variable.
# Function to get current directory and Git branch
prompt_info() {
local cwd git_branch
cwd=$(pwd | sed "s|$HOME|~|")
if git rev-parse --is-inside-work-tree &> /dev/null; then
git_branch=$(git rev-parse --abbrev-ref HEAD)
git_commit=$(git rev-parse --short HEAD)
echo "${GREEN}in ${BLUE}$cwd ${YELLOW}$git_branch ${MAGENTA}$git_commit${RESET}"
else
echo "${GREEN}in ${BLUE}$cwd${RESET}"
fi
}
This is simple: when working within a Git project, it gets the branch and commit
information and outputs it via echo
. Then, wrap this in an update_prompt
function and call it in .zshrc
file, finally appending it to precmd_functions
.
Everything is done.
# Function to update the prompt
update_prompt() {
PROMPT="$(prompt_info)
${CYAN}λ ${RESET}"
}
I like to have the input on the second line, so I use a newline in the PROMPT
format string.
Asynchronous Implementation
When working on a PR in Vim, I wanted to see some commit information, so I tried
adding commit info to the prompt_info
function. Then, when I opened a new shell,
it was as slow as a turtle. This is why asynchronous P10k is the best among all
Zsh themes. So I tried to get Git information asynchronously and update the
PROMPT
using Zsh’s asynchronous jobs.
Function to Get Git Status
# Function to get Git status
prompt_git_status() {
git rev-parse --git-dir >&- 2>&- || {
echo -n $'\0'
return
}
local -a parts
local fd line head ahead behind conflicts staged changed untracked commithash
exec {fd}< <(git status --porcelain=v2 --branch)
while read -A -u $fd line; do
case "$line" in
'# branch.oid'*)
if [[ "${line[3]}" != "(initial)" ]]; then
commit_hash="${line[3]:0:7}"
fi
;;
'# branch.head'*) # Current branch
head="$line[3]"
[[ $head == "(detached)" ]] && head="$(echo ":$(git rev-parse --short HEAD)")"
;;
'# branch.ab'*) # Divergence from upstream
ahead="${line[3]/#+}"
behind="${line[4]/#-}"
;;
(1|2)*) # Modified or renamed/copied
[[ "${${line[2]}[1]}" != "." ]] && ((staged++))
[[ "${${line[2]}[2]}" != "." ]] && ((changed++))
;;
'u'*) # Unmerged
((conflicts++))
;;
'?'*) # Untracked
((untracked++))
;;
esac
done
exec {fd}<&-
parts+="%F{8}$head%f"
if [[ -n "$commit_hash" ]]; then
parts+="%F{magenta}$commit_hash%f"
fi
local -a upstream_divergence
[[ $ahead > 0 ]] && upstream_divergence+="%F{blue}↑$ahead%f"
[[ $behind > 0 ]] && upstream_divergence+="%F{blue}↓$behind%f"
if [[ $#upstream_divergence > 0 ]]; then
parts+="${(j::)upstream_divergence}"
fi
local -a working_info
[[ $conflicts > 0 ]] && working_info+="%F{red}×$conflicts%f"
[[ $staged > 0 ]] && working_info+="%F{green}●$staged%f"
[[ $changed > 0 ]] && working_info+="%F{208}✻$changed%f"
[[ $untracked > 0 ]] && working_info+="%F{red}+$untracked%f"
if [[ $#working_info > 0 ]]; then
parts+="${(j::)working_info}"
else
parts+="%F{green}✔%f"
fi
echo -n "${(j: :)parts}"
}
The prompt_git_status function asynchronously fetches the Git status and details
for the current repository. It uses the git status --porcelain=v2 --branch
command to retrieve information about the repository. Here’s what each part of
the function does:
Initial Check: Verifies if the current directory is a Git repository. If not, it returns early with a null character.
Variables and File Descriptors: Initializes necessary variables and opens a file descriptor to read the Git status output.
Parsing Git Status: Iterates over each line of the Git status output, parsing various details such as the branch name, commit hash, upstream divergence, and file changes (staged, modified, untracked, and conflicts).
Output Construction: Constructs the prompt output based on the parsed Git status.
Function to Define the Prompt
# Function to define the prompt
prompt_git_define_prompt() {
setopt localoptions extendedglob
local -a parts=()
# Abbreviated current working directory
parts+="%F{green}in %F{blue}${${PWD/#$HOME/~}}%f"
# Git info (loaded async)
if [[ "$1" != $'\0' ]]; then
if [[ -n "$1" ]]; then
parts+="$1"
else
parts+="..."
fi
fi
# Prompt arrow (red for non-zero status)
parts+="%(?.%F{8}.%F{red})
%F{cyan}λ%f"
PROMPT="${(j: :)parts} "
}
Abbreviated Current Directory: Shortens the current working directory path, replacing the home directory with ~.
Git Information: If available, appends Git status information to the prompt.
Prompt Arrow: Adds the prompt arrow, which changes color based on the previous command’s exit status.
Setting PROMPT: Combines all parts and sets the
PROMPT
variable.
Function to Handle Async Response
# Function to handle async response
prompt_git_response() {
typeset -g _prompt_git_fd
prompt_git_define_prompt "$(<&$1)"
zle reset-prompt
zle -F $1
exec {1}<&-
unset _prompt_git_fd
}
This function handles the asynchronous response from the Git status command:
Reads Response: Reads the Git status from the file descriptor.
Defines Prompt: Calls prompt_git_define_prompt with the Git status information.
Resets Prompt: Uses zle reset-prompt to update the prompt.
Function to Run Before Each Prompt
# Function to run before each prompt
prompt_git_precmd() {
typeset -g _prompt_git_fd
prompt_git_define_prompt
[[ -n $_prompt_git_fd ]] && {
zle -F $_prompt_git_fd
exec {_prompt_git_fd}<&-
}
exec {_prompt_git_fd}< <(prompt_git_status)
zle -F $_prompt_git_fd prompt_git_response
}
in the end need add into zsh hook
# Add hook to run before each prompt
add-zsh-hook precmd prompt_git_precmd
The add-zsh-hook precmd prompt_git_precmd
command adds the
prompt_git_precmd
function to the list of functions that are run
before each prompt. This ensures that the prompt is updated with the
latest Git status information every time a new prompt is displayed.