Creating an Asynchronous Zsh Theme

Pure bliss

I converted to Zsh a while back, when I first saw the Oh-My-Zsh framework. OMZ is really just a long chain of source calls with some logic to turn groups of features on or off. I’ve since changed to Zplugin (not to be confused with a similar framework, zplug), a wonderful framework which supports asynchronous plugin loading. This has better than halved the load time for Zsh on my Pi and phone (Termux).

However, my theme had not changed, it still has to wait until (relatively) expensive git calls have completed before redrawing the prompt. Now is the time to change this.

The theme I will be replacing is one I had modified from OMZ’s dieter theme. Over the years (!) I have occasionally tweaked it a bit, removing the time, adding, then removing Vi-mode status, adding, then removing the classic λ prefix… What I’m left with is this:

# Two directories, colored by permissions
function color_pwd() {
	if (( $(stat -c "%u" . ) == UID )); then
		# owner
		print "%{\033[38;5;4m%}%2c"
	elif [[ -w . ]]; then
		# not owner, but have write permissions
		print "%{\033[38;5;3m%}%2c"
	else
		# no write permissions
		print "%{\033[38;5;5m%}%2c"
	fi
}

Edit 2019-02-01: We can actually remove the fork to stat by abusing zsh globbing qualifiers. The test [[ -n .(#qNu$UID) ]] replaces the stat call and comparison:

  • .( ): Match . (current working directory), with the following options:
  • #q: enables glob qualifiers
  • N: sets NULL_GLOB: if the pattern doesn’t match, it returns no string
  • u$UID: Matches if user id of each file is owned by $UID

So, if the cwd is owned by $UID, this resolves to [[ -n '.' ]] (true). Otherwise, it resolves to [[ -n ]] (false).

ZSH_THEME_GIT_PROMPT_PREFIX=" %{\033[38;5;3m%} "
ZSH_THEME_GIT_PROMPT_DIRTY=""
ZSH_THEME_GIT_PROMPT_UNTRACKED="?"
ZSH_THEME_GIT_PROMPT_ADDED="+"
ZSH_THEME_GIT_PROMPT_DELETED="-"
ZSH_THEME_GIT_PROMPT_MODIFIED="*"

function git_info() {
	local info="$(git_prompt_info)"
	(( ${+info} )) && print -n "$info$(git_prompt_status)%{\033[0m%}" \
		|| print -n "$info%{\033[0m%}"
}

PROMPT='$PS1_HEADER$(color_pwd)$(git_info) '

# exitcode on the right when >0
return_code_enabled="%(?..%{$fg[red]%}%? ↵%{$reset_color%})"
return_code_disabled=
return_code=$return_code_enabled

RPS1="${return_code}"

function accept-line-or-clear-warning () {
	if [[ -z $BUFFER ]]; then
		time=$time_disabled
		return_code=$return_code_disabled
	else
		time=$time_enabled
		return_code=$return_code_enabled
	fi
	zle accept-line
}
zle -N accept-line-or-clear-warning
zle -N zle-keymap-select
bindkey '^M' accept-line-or-clear-warning

I am still using OMZ’s libs to get git status. What is the most unusual is my conditionally coloring the CWD depending on my permissions. If I own the directory, it is printed in blue, if I can write to it (like /tmp), it shows yellow, and if I don’t have write permissions, I will see magenta. Also, I show the current directory only to a depth of 2, but still replace $HOME with ~.

Enter pure, an asynchronous Zsh prompt. This will be our starting point to recreate and extend my current theme.

Pure asynchronously fetches your git remote if it detects it is in a repository. It then will show arrows up and/or down if you are ahead and/or behind your default remote.

I like seeing my status, but I want to emulate git status -sb, which concisely shows your status with the commit count. So let’s nix the arrows, and instead use [ahead_count:behind_count], which takes up only a few characters and can indicate exactly how much I’ve forgotten to push. Let’s rename and rewrite Pure’s prompt_pure_check_git_arrows:

prompt_pure_check_git_status() {
	setopt localoptions noshwordsplit
	local ret left=${1:-0} right=${2:-0}

	(( left > 0 )) && ret+="%F{green}$left"
	(( left * right > 0 )) && ret+="%F{242}:"
	(( right > 0 )) && ret+="%F{red}$right"

	[[ -n $ret ]] || return
	typeset -g REPLY="%F{242}[$ret%F{242}]"
}

Using (( math )) is pretty quick, and by multiplying left and right together, we can detect whether we need a : to separate the two.

Really, there aren’t many other changes to Pure. I still use two directories, colored by permissions; I still use RPS1 for non-zero exit codes; I still keep the prompt compact on one line; I still have no prompt character.

And here’s the end result.

zsh  code