Skip to content

Commit

Permalink
Enable partial preprompt updates when possible
Browse files Browse the repository at this point in the history
This commit adds a third mode to the preprompt update: lines between
previous and new preprompt match. We then compare the preprompts,
character-by-character to find the position where it differs. From this
we can calculate the new position for the update.

Since "%~" is the only prompt substitution we use in the preprompt, we
now expand it in-place so that we get comparable preprompts from the
get-go.
  • Loading branch information
mafredri committed Feb 11, 2017
1 parent 1c312b1 commit 26f2266
Showing 1 changed file with 68 additions and 30 deletions.
98 changes: 68 additions & 30 deletions pure.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ prompt_pure_preprompt_render() {
local prompt_subst_status=$options[prompt_subst]

# make sure prompt_subst is unset to prevent parameter expansion in preprompt
setopt local_options no_prompt_subst
setopt local_options no_prompt_subst no_sh_word_split

# check that no command is currently running, the preprompt will otherwise be rendered in the wrong place
[[ -n ${prompt_pure_cmd_timestamp+x} && "$1" != "precmd" ]] && return
Expand All @@ -113,8 +113,12 @@ prompt_pure_preprompt_render() {
local git_color=242
[[ -n ${prompt_pure_git_last_dirty_check_timestamp+x} ]] && git_color=red

# construct preprompt, beginning with path
local preprompt="%F{blue}%~%f"
# Expand the working directory for later comparison.
local expanded_pwd="%~"
expanded_pwd=${(%)expanded_pwd}

# Construct preprompt, beginning with path.
local preprompt="%F{blue}$expanded_pwd%f"

# git info
typeset -gA prompt_pure_vcs_info
Expand All @@ -131,65 +135,99 @@ prompt_pure_preprompt_render() {
# execution time
preprompt+="%F{yellow}${prompt_pure_cmd_exec_time}%f"

# make sure prompt_pure_last_preprompt is a global array
typeset -g -a prompt_pure_last_preprompt

# if executing through precmd, do not perform fancy terminal editing
if [[ "$1" == "precmd" ]]; then
print -P "\n${preprompt}"
else
# only redraw if the expanded preprompt has changed
[[ "${prompt_pure_last_preprompt[2]}" != "${(S%%)preprompt}" ]] || return
local prev_preprompt=$prompt_pure_last_preprompt
if [[ $prev_preprompt == $preprompt ]]; then
# Avoid redrawing when there are no changes.
return
fi

# calculate length of preprompt and store it locally in preprompt_length
# We need the length/lines of the preprompt to
# figure out what update method we should use.
integer preprompt_length lines
prompt_pure_string_length_to_var "${preprompt}" "preprompt_length"
prompt_pure_string_length_to_var "$preprompt" preprompt_length
lines=$(( ((preprompt_length - 1) / COLUMNS) + 1 ))

# calculate number of preprompt lines for redraw purposes
(( lines = ( preprompt_length - 1 ) / COLUMNS + 1 ))

# calculate previous preprompt lines to figure out how the new preprompt should behave
# We use the length/lines of the previous preprompt to
# figure out the best update method for the new one.
integer last_preprompt_length last_lines
prompt_pure_string_length_to_var "${prompt_pure_last_preprompt[1]}" "last_preprompt_length"
(( last_lines = ( last_preprompt_length - 1 ) / COLUMNS + 1 ))
prompt_pure_string_length_to_var "$prev_preprompt" last_preprompt_length
last_lines=$(( ((last_preprompt_length - 1) / COLUMNS) + 1 ))

# The patch position can tell us if we can perform a partial redraw to
# update the preprompt. A partial update is faster and causes less
# visual flicker.
integer patch_pos=1

# clr_prev_preprompt erases visual artifacts from previous preprompt
# With clr_prev_preprompt we can erase visual artifacts,
# if any, from the previous preprompt.
local clr_prev_preprompt

if (( last_lines > lines )); then
# move cursor up by last_lines, clear the line and move it down by one line
# Move the cursor to the top (of previous preprompt) and erase
# line-by-line, moving downwards, until the number of lines matches
# the new preprompt.
clr_prev_preprompt="\e[${last_lines}A\e[2K\e[1B"
while (( last_lines - lines > 1 )); do
# clear the line and move cursor down by one
# Add a line clear and move cursor down one line.
clr_prev_preprompt+='\e[2K\e[1B'
(( last_lines-- ))
done

# move cursor into correct position for preprompt update
# Move cursor all the way back down just so we can move it up again
# during the update, this simplifies the algorithm.
clr_prev_preprompt+="\e[${lines}B"
# create more space for preprompt if new preprompt has more lines than last
elif (( last_lines < lines )); then
# move cursor using newlines because ansi cursor movement can't push the cursor beyond the last line
# When the preprompt takes up more lines than the previous, we must
# make more vertical space using newlines. Ansi escape sequences
# alone cannot move the cursor beyond the terminal edge.
printf $'\n'%.0s {1..$(( lines - last_lines ))}
else
# When lines equal, we can try to perform a partial prompt update.
integer pos unexpanded_length=$#preprompt

# TODO: Can we use zsh matching to find where the strings differ?
for (( pos = 1; pos < unexpanded_length; pos++ )); do
patch_pos=$pos
if [[ $preprompt[$pos] != $last_preprompt[$pos] ]]; then
break # We found where the preprompts differ.
fi
done
fi

# disable clearing of line if last char of preprompt is last column of terminal
local clr='\e[K'
(( COLUMNS * lines == preprompt_length )) && clr=
if (( (COLUMNS * lines) == preprompt_length )) || (( preprompt_length > last_preprompt_length )); then
# When the preprompt ends at the last column of the terminal, or is
# longer than the previous, clearing the rest makes no sense.
clr=
fi

local move_to_patch_pos
if (( patch_pos > 1 )); then
# Consider the line where our patch starts.
lines=(( lines - (patch_pos / COLUMNS) ))

# Move the cursor N columns right, into the correct patch position.
move_to_patch_pos="\e[$(( (patch_pos % COLUMNS) - 1))C"
fi

# modify previous preprompt
print -Pn "${clr_prev_preprompt}\e[${lines}A\e[${COLUMNS}D${preprompt}${clr}\n"
# Redraw preprompt, either by patching or fully redrawing.
print -Pn "${clr_prev_preprompt}\e[${lines}A\e[${COLUMNS}D${move_to_patch_pos}${preprompt[$patch_pos,-1]}${clr}\n"

if [[ $prompt_subst_status = 'on' ]]; then
# re-eanble prompt_subst for expansion on PS1
# Re-enable prompt_subst for expansion on PS1.
setopt prompt_subst
fi

# redraw prompt (also resets cursor position)
# Redraw prompt to reset cursor position.
zle && zle .reset-prompt
fi

# store both unexpanded and expanded preprompt for comparison
prompt_pure_last_preprompt=("$preprompt" "${(S%%)preprompt}")
# Store the preprompt for later comparision.
prompt_pure_last_preprompt=$preprompt
}

prompt_pure_precmd() {
Expand Down

0 comments on commit 26f2266

Please sign in to comment.