1
0
mirror of https://github.com/ohmyzsh/ohmyzsh.git synced 2026-03-28 18:53:47 +08:00
This commit is contained in:
Josh Allen 2026-03-26 13:38:53 -04:00 committed by GitHub
commit cbcc39bcbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 280 additions and 29 deletions

View File

@ -46,7 +46,7 @@ function _omz_register_handler {
function _omz_async_request {
setopt localoptions noksharrays unset
local -i ret=$?
typeset -gA _OMZ_ASYNC_FDS _OMZ_ASYNC_PIDS _OMZ_ASYNC_OUTPUT
typeset -gA _OMZ_ASYNC_FDS _OMZ_ASYNC_PIDS _OMZ_ASYNC_OUTPUT _OMZ_ASYNC_PENDING
# executor runs a subshell for all async requests based on key
local handler
@ -79,6 +79,7 @@ function _omz_async_request {
# Define global variables to store the file descriptor, PID and output
_OMZ_ASYNC_FDS[$handler]=-1
_OMZ_ASYNC_PIDS[$handler]=-1
_OMZ_ASYNC_PENDING[$handler]=1
# Fork a process to fetch the git status and open a pipe to read from it
exec {fd}< <(
@ -117,14 +118,23 @@ function _omz_async_callback() {
# Get handler name from fd
local handler="${(k)_OMZ_ASYNC_FDS[(r)$fd]}"
# Store old output which is supposed to be already printed
# Store old output and pending state before updating
local old_output="${_OMZ_ASYNC_OUTPUT[$handler]}"
local was_pending="${_OMZ_ASYNC_PENDING[$handler]}"
# Mark handler as no longer pending
_OMZ_ASYNC_PENDING[$handler]=0
# Read output from fd
IFS= read -r -u $fd -d '' "_OMZ_ASYNC_OUTPUT[$handler]"
# Repaint prompt if output has changed
if [[ "$old_output" != "${_OMZ_ASYNC_OUTPUT[$handler]}" ]]; then
# Repaint prompt if output has changed, or if the git prompt handler was
# pending — even when the output is identical, the prompt needs redrawing
# to clear stale/unbolded styling applied while the handler was in-flight.
# Only the git handler uses pending-state styling, so other handlers skip
# the extra repaint to avoid unnecessary redraws.
if [[ "$old_output" != "${_OMZ_ASYNC_OUTPUT[$handler]}" ]] \
|| { (( was_pending )) && [[ "$handler" == _omz_git_prompt_info ]]; }; then
zle .reset-prompt
zle -R
fi

View File

@ -36,7 +36,19 @@ function _omz_git_prompt_info() {
&& upstream=" -> ${upstream}"
fi
echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref:gs/%/%%}${upstream:gs/%/%%}$(parse_git_dirty)${ZSH_THEME_GIT_PROMPT_SUFFIX}"
local escaped_ref="${ref:gs/%/%%}${upstream:gs/%/%%}"
local dirty="$(parse_git_dirty)"
# In async context, output ref and dirty indicator separated by Unit Separator
# (U+001F) so the renderer can assemble the prompt and apply pending-state
# styling without needing to decompose a pre-formatted string.
# Check for the specific handler key to avoid false positives when the
# associative array exists but this handler hasn't been registered.
if (( ${+_OMZ_ASYNC_PENDING[_omz_git_prompt_info]} )); then
printf '%s\x1f%s' "$escaped_ref" "$dirty"
else
echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${escaped_ref}${dirty}${ZSH_THEME_GIT_PROMPT_SUFFIX}"
fi
}
function _omz_git_prompt_status() {
@ -143,21 +155,95 @@ function _omz_git_prompt_status() {
# - https://github.com/ohmyzsh/ohmyzsh/issues/12331
# - https://github.com/ohmyzsh/ohmyzsh/issues/12360
# TODO(2024-06-12): @mcornella remove workaround when CentOS 7 reaches EOL
# Helper functions for async pending-state rendering (used by _omz_render_git_prompt_info).
# Detect whether bold is the active terminal state at the end of a prompt string.
# Expands zsh prompt escapes (%B, %b) then parses ANSI SGR sequences in order.
# NOTE: Only standard SGR sequences (\e[...m) are parsed. Non-SGR CSI sequences
# (e.g. cursor movement) in the prefix may cause incorrect results.
function _omz_is_bold_at_end() {
local expanded=$(print -Pn -- "$1")
local is_bold=0 remaining="$expanded"
local params p
while [[ "$remaining" == *$'\e['*'m'* ]]; do
remaining="${remaining#*$'\e['}"
params="${remaining%%m*}"
remaining="${remaining#*m}"
# Bare \e[m (empty params) is equivalent to \e[0m (full reset)
if [[ -z "$params" ]]; then
is_bold=0
else
for p in ${(s/;/)params}; do
case "$p" in
0) is_bold=0 ;;
1|01) is_bold=1 ;;
22) is_bold=0 ;;
esac
done
fi
done
return $(( ! is_bold ))
}
function _omz_render_git_prompt_info() {
local raw="${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]}"
[[ -z "$raw" ]] && return
# Backward compat: if output has no Unit Separator, it's the old single-line format
if [[ "$raw" != *$'\x1f'* ]]; then
echo -n "$raw"
return
fi
# Async output is two fields separated by Unit Separator (U+001F): ref and dirty indicator.
# Assemble the prompt from these parts and the current theme variables.
local ref="${raw%%$'\x1f'*}"
local dirty="${raw#*$'\x1f'}"
if (( _OMZ_ASYNC_PENDING[_omz_git_prompt_info] )); then
local stale_prefix="${ZSH_THEME_GIT_PROMPT_STALE_PREFIX-}"
local stale_suffix="${ZSH_THEME_GIT_PROMPT_STALE_SUFFIX-}"
# If user hasn't set custom stale vars, auto-detect from the theme prefix:
# only apply unbold/rebold if the ref text would actually be rendered bold.
# Cache the result keyed on the prefix value to avoid re-parsing SGR on every render.
if (( ! ${+ZSH_THEME_GIT_PROMPT_STALE_PREFIX} && ! ${+ZSH_THEME_GIT_PROMPT_STALE_SUFFIX} )); then
if [[ "$ZSH_THEME_GIT_PROMPT_PREFIX" != "$_OMZ_CACHED_BOLD_PREFIX" ]]; then
typeset -g _OMZ_CACHED_BOLD_PREFIX="$ZSH_THEME_GIT_PROMPT_PREFIX"
if _omz_is_bold_at_end "$ZSH_THEME_GIT_PROMPT_PREFIX"; then
typeset -g _OMZ_CACHED_BOLD_RESULT=1
else
typeset -g _OMZ_CACHED_BOLD_RESULT=0
fi
fi
if (( _OMZ_CACHED_BOLD_RESULT )); then
stale_prefix=$'%{\e[22m%}'
stale_suffix=$'%{\e[1m%}'
fi
fi
echo -n "${ZSH_THEME_GIT_PROMPT_PREFIX}${stale_prefix}${ref}${dirty}${stale_suffix}${ZSH_THEME_GIT_PROMPT_SUFFIX}"
else
echo -n "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref}${dirty}${ZSH_THEME_GIT_PROMPT_SUFFIX}"
fi
}
# Async prompt functions — used by both the auto-detect and "force" branches below.
# Overridden with synchronous versions if async is disabled.
function git_prompt_info() {
_omz_render_git_prompt_info
}
function git_prompt_status() {
if [[ -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]}" ]]; then
echo -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]}"
fi
}
local _style
if zstyle -t ':omz:alpha:lib:git' async-prompt \
|| { is-at-least 5.0.6 && zstyle -T ':omz:alpha:lib:git' async-prompt }; then
function git_prompt_info() {
if [[ -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]}" ]]; then
echo -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]}"
fi
}
function git_prompt_status() {
if [[ -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]}" ]]; then
echo -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]}"
fi
}
# Conditionally register the async handler, only if it's needed in $PROMPT
# or any of the other prompt variables
function _defer_async_git_register() {
@ -182,18 +268,6 @@ if zstyle -t ':omz:alpha:lib:git' async-prompt \
# the async request prompt is run
precmd_functions=(_defer_async_git_register $precmd_functions)
elif zstyle -s ':omz:alpha:lib:git' async-prompt _style && [[ $_style == "force" ]]; then
function git_prompt_info() {
if [[ -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]}" ]]; then
echo -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]}"
fi
}
function git_prompt_status() {
if [[ -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]}" ]]; then
echo -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]}"
fi
}
_omz_register_handler _omz_git_prompt_info
_omz_register_handler _omz_git_prompt_status
else

View File

@ -0,0 +1,167 @@
#!/usr/bin/zsh -df
# Tests for the git pending branch name (stale/unbolded) feature
local -i failures=0
local ZSH=${0:A:h:h:h}
run_test() {
local description="$1"
local actual="$2"
local expected="$3"
print -u2 "Test: $description"
if [[ "$actual" == "$expected" ]]; then
print -u2 "\e[32mSuccess\e[0m"
else
print -u2 "\e[31mError\e[0m: output does not match expected"
print -u2 " expected: ${(q+)expected}"
print -u2 " actual: ${(q+)actual}"
(( failures++ ))
fi
print -u2 ""
}
# Reset theme variables to a known state between tests
reset_theme_vars() {
unset ZSH_THEME_GIT_PROMPT_PREFIX ZSH_THEME_GIT_PROMPT_SUFFIX
unset ZSH_THEME_GIT_PROMPT_DIRTY ZSH_THEME_GIT_PROMPT_CLEAN
unset ZSH_THEME_GIT_PROMPT_STALE_PREFIX ZSH_THEME_GIT_PROMPT_STALE_SUFFIX
unset ZSH_THEME_GIT_SHOW_UPSTREAM
unset _OMZ_ASYNC_OUTPUT _OMZ_ASYNC_PENDING
# Force async-prompt mode so we test the _omz_render_git_prompt_info path
zstyle ':omz:alpha:lib:git' async-prompt yes
}
() {
local description="git_prompt_info - when pending=1 with bold prefix - then ref is wrapped with stale styling"
reset_theme_vars
autoload -Uz is-at-least
ZSH_THEME_GIT_PROMPT_PREFIX="%B("
ZSH_THEME_GIT_PROMPT_SUFFIX=")%b"
unset ZSH_THEME_GIT_PROMPT_STALE_PREFIX
unset ZSH_THEME_GIT_PROMPT_STALE_SUFFIX
source "$ZSH/lib/git.zsh"
typeset -gA _OMZ_ASYNC_OUTPUT _OMZ_ASYNC_PENDING
# Async output format: ref + Unit Separator (U+001F) + dirty
_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]="main"$'\x1f'" *"
_OMZ_ASYNC_PENDING[_omz_git_prompt_info]=1
local actual
actual=$(git_prompt_info)
local stale_prefix=$'%{\e[22m%}'
local stale_suffix=$'%{\e[1m%}'
local expected="%B(${stale_prefix}main *${stale_suffix})%b"
run_test "$description" "$actual" "$expected"
}
() {
local description="git_prompt_info - when pending=1 with non-bold prefix - then no stale styling is applied"
reset_theme_vars
autoload -Uz is-at-least
ZSH_THEME_GIT_PROMPT_PREFIX="("
ZSH_THEME_GIT_PROMPT_SUFFIX=")"
unset ZSH_THEME_GIT_PROMPT_STALE_PREFIX
unset ZSH_THEME_GIT_PROMPT_STALE_SUFFIX
source "$ZSH/lib/git.zsh"
typeset -gA _OMZ_ASYNC_OUTPUT _OMZ_ASYNC_PENDING
# Async output format: ref + Unit Separator (U+001F) + dirty
_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]="main"$'\x1f'" *"
_OMZ_ASYNC_PENDING[_omz_git_prompt_info]=1
local actual
actual=$(git_prompt_info)
run_test "$description" "$actual" "(main *)"
}
() {
local description="git_prompt_info - when pending=0 - then formatted output is returned as-is"
reset_theme_vars
autoload -Uz is-at-least
ZSH_THEME_GIT_PROMPT_PREFIX="%B("
ZSH_THEME_GIT_PROMPT_SUFFIX=")%b"
unset ZSH_THEME_GIT_PROMPT_STALE_PREFIX
unset ZSH_THEME_GIT_PROMPT_STALE_SUFFIX
source "$ZSH/lib/git.zsh"
typeset -gA _OMZ_ASYNC_OUTPUT _OMZ_ASYNC_PENDING
# Async output format: ref + Unit Separator (U+001F) + dirty
_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]="main"$'\x1f'" *"
_OMZ_ASYNC_PENDING[_omz_git_prompt_info]=0
local actual
actual=$(git_prompt_info)
run_test "$description" "$actual" "%B(main *)%b"
}
()
local description="git_prompt_info - when pending=1 with custom stale vars - then ref uses custom stale prefix/suffix"{
reset_theme_vars
autoload -Uz is-at-least
ZSH_THEME_GIT_PROMPT_PREFIX="("
ZSH_THEME_GIT_PROMPT_SUFFIX=")"
ZSH_THEME_GIT_PROMPT_STALE_PREFIX="%{dim%}"
ZSH_THEME_GIT_PROMPT_STALE_SUFFIX="%{/dim%}"
source "$ZSH/lib/git.zsh"
typeset -gA _OMZ_ASYNC_OUTPUT _OMZ_ASYNC_PENDING
# Async output format: ref + Unit Separator (U+001F) + dirty (empty dirty here)
_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]="develop"$'\x1f'
_OMZ_ASYNC_PENDING[_omz_git_prompt_info]=1
local actual
actual=$(git_prompt_info)
local expected="(%{dim%}develop%{/dim%})"
run_test "$description" "$actual" "$expected"
}
() {
local description="git_prompt_info - when old single-line format (no ref line) - then output is passed through as-is"
reset_theme_vars
autoload -Uz is-at-least
ZSH_THEME_GIT_PROMPT_PREFIX="%B("
ZSH_THEME_GIT_PROMPT_SUFFIX=")%b"
source "$ZSH/lib/git.zsh"
typeset -gA _OMZ_ASYNC_OUTPUT _OMZ_ASYNC_PENDING
# Simulate old-format output with no newline (single line, no ref header)
_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]="%B(main *)%b"
_OMZ_ASYNC_PENDING[_omz_git_prompt_info]=0
local actual
actual=$(git_prompt_info)
run_test "$description" "$actual" "%B(main *)%b"
}
# Summary
if (( failures > 0 )); then
print -u2 "\e[31m${failures} test(s) failed\e[0m"
return 1
else
print -u2 "\e[32mAll tests passed\e[0m"
return 0
fi