1
0
Fork 0
mirror of https://github.com/ohmyzsh/ohmyzsh.git synced 2024-10-16 11:40:46 +00:00

feat(async)!: implement async prompt API and apply to git prompt

BREAKING CHANGE: the `git_prompt_info` prompt function has been
reworked by default to use the new async prompt feature. If you're
experiencing issues see #12257.
This commit is contained in:
Marc Cornellà 2024-02-26 20:51:54 +01:00
parent fd01fd66ce
commit 7efb6963ca
No known key found for this signature in database
GPG key ID: 0314585E776A9C1B
2 changed files with 190 additions and 20 deletions

127
lib/async_prompt.zsh Normal file
View file

@ -0,0 +1,127 @@
# The async code is taken from
# https://github.com/zsh-users/zsh-autosuggestions/blob/master/src/async.zsh
# https://github.com/woefe/git-prompt.zsh/blob/master/git-prompt.zsh
zmodload zsh/system
# For now, async prompt function handlers are set up like so:
# First, define the async function handler and add the function name
# to the _omz_async_functions array:
#
# function _git_prompt_status_async {
# # Do some expensive operation that outputs to stdout
# }
# _omz_async_functions+=(_git_prompt_status_async)
#
# Then add a stub prompt function in `$PROMPT` or similar prompt variables,
# which will show the output of "$_OMZ_ASYNC_OUTPUT[handler_name]":
#
# function git_prompt_status {
# echo $_OMZ_ASYNC_OUTPUT[_git_prompt_status]
# }
#
# RPROMPT='$(git_prompt_status)'
#
# This API is subject to change and optimization. Rely on it at your own risk.
typeset -ga _omz_async_functions
# Set up async handlers and callbacks
function _omz_async_request {
typeset -gA _OMZ_ASYNC_FDS _OMZ_ASYNC_PIDS _OMZ_ASYNC_OUTPUT
# executor runs a subshell for all async requests based on key
local handler
for handler in ${_omz_async_functions}; do
(( ${+functions[$handler]} )) || continue
local fd=${_OMZ_ASYNC_FDS[$handler]:--1}
local pid=${_OMZ_ASYNC_PIDS[$handler]:--1}
# If we've got a pending request, cancel it
if (( fd != -1 && pid != -1 )) && { true <&$fd } 2>/dev/null; then
# Close the file descriptor and remove the handler
exec {fd}<&-
zle -F $fd
# Zsh will make a new process group for the child process only if job
# control is enabled (MONITOR option)
if [[ -o MONITOR ]]; then
# Send the signal to the process group to kill any processes that may
# have been forked by the async function handler
kill -TERM -$pid 2>/dev/null
else
# Kill just the child process since it wasn't placed in a new process
# group. If the async function handler forked any child processes they may
# be orphaned and left behind.
kill -TERM $pid 2>/dev/null
fi
fi
# Define global variables to store the file descriptor, PID and output
_OMZ_ASYNC_FDS[$handler]=-1
_OMZ_ASYNC_PIDS[$handler]=-1
# Fork a process to fetch the git status and open a pipe to read from it
exec {fd}< <(
# Tell parent process our PID
builtin echo ${sysparams[pid]}
# Store handler name for callback
builtin echo $handler
# Run the async function handler
$handler
)
# Save FD for handler
_OMZ_ASYNC_FDS[$handler]=$fd
# There's a weird bug here where ^C stops working unless we force a fork
# See https://github.com/zsh-users/zsh-autosuggestions/issues/364
command true
# Save the PID from the handler child process
read pid <&$fd
_OMZ_ASYNC_PIDS[$handler]=$pid
# When the fd is readable, call the response handler
zle -F "$fd" _omz_async_callback
done
}
# Called when new data is ready to be read from the pipe
function _omz_async_callback() {
emulate -L zsh
local fd=$1 # First arg will be fd ready for reading
local err=$2 # Second arg will be passed in case of error
if [[ -z "$err" || "$err" == "hup" ]]; then
# Get handler name from first line
local handler
read handler <&$fd
# Store old output which is supposed to be already printed
local old_output="${_OMZ_ASYNC_OUTPUT[$handler]}"
# Read output from fd
_OMZ_ASYNC_OUTPUT[$handler]="$(cat <&$fd)"
# Repaint prompt if output has changed
if [[ "$old_output" != "${_OMZ_ASYNC_OUTPUT[$handler]}" ]]; then
zle reset-prompt
zle -R
fi
# Close the fd
exec {fd}<&-
fi
# Always remove the handler
zle -F "$fd"
# Unset global FD variable to prevent closing user created FDs in the precmd hook
_OMZ_ASYNC_FDS[$handler]=-1
_OMZ_ASYNC_PIDS[$handler]=-1
}
autoload -Uz add-zsh-hook
add-zsh-hook precmd _omz_async_request

View file

@ -9,29 +9,72 @@ function __git_prompt_git() {
GIT_OPTIONAL_LOCKS=0 command git "$@"
}
function git_prompt_info() {
# If we are on a folder not tracked by git, get out.
# Otherwise, check for hide-info at global and local repository level
if ! __git_prompt_git rev-parse --git-dir &> /dev/null \
|| [[ "$(__git_prompt_git config --get oh-my-zsh.hide-info 2>/dev/null)" == 1 ]]; then
return 0
fi
# Enable async prompt by default unless the setting is at false / no
if zstyle -T ':omz:alpha:lib:git' async-prompt; then
_omz_async_functions+=(_omz_git_prompt_status)
local ref
ref=$(__git_prompt_git symbolic-ref --short HEAD 2> /dev/null) \
|| ref=$(__git_prompt_git describe --tags --exact-match HEAD 2> /dev/null) \
|| ref=$(__git_prompt_git rev-parse --short HEAD 2> /dev/null) \
|| return 0
function git_prompt_info() {
if [[ -n "$_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]" ]]; then
echo -n "$_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]"
fi
}
# Use global ZSH_THEME_GIT_SHOW_UPSTREAM=1 for including upstream remote info
local upstream
if (( ${+ZSH_THEME_GIT_SHOW_UPSTREAM} )); then
upstream=$(__git_prompt_git rev-parse --abbrev-ref --symbolic-full-name "@{upstream}" 2>/dev/null) \
&& upstream=" -> ${upstream}"
fi
function _omz_git_prompt_status() {
# If we are on a folder not tracked by git, get out.
# Otherwise, check for hide-info at global and local repository level
if ! __git_prompt_git rev-parse --git-dir &> /dev/null \
|| [[ "$(__git_prompt_git config --get oh-my-zsh.hide-info 2>/dev/null)" == 1 ]]; then
return 0
fi
echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref:gs/%/%%}${upstream:gs/%/%%}$(parse_git_dirty)${ZSH_THEME_GIT_PROMPT_SUFFIX}"
}
# Get either:
# - the current branch name
# - the tag name if we are on a tag
# - the short SHA of the current commit
local ref
ref=$(__git_prompt_git symbolic-ref --short HEAD 2> /dev/null) \
|| ref=$(__git_prompt_git describe --tags --exact-match HEAD 2> /dev/null) \
|| ref=$(__git_prompt_git rev-parse --short HEAD 2> /dev/null) \
|| return 0
# Use global ZSH_THEME_GIT_SHOW_UPSTREAM=1 for including upstream remote info
local upstream
if (( ${+ZSH_THEME_GIT_SHOW_UPSTREAM} )); then
upstream=$(__git_prompt_git rev-parse --abbrev-ref --symbolic-full-name "@{upstream}" 2>/dev/null) \
&& upstream=" -> ${upstream}"
fi
echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref:gs/%/%%}${upstream:gs/%/%%}$(parse_git_dirty)${ZSH_THEME_GIT_PROMPT_SUFFIX}"
}
else
function git_prompt_info() {
# If we are on a folder not tracked by git, get out.
# Otherwise, check for hide-info at global and local repository level
if ! __git_prompt_git rev-parse --git-dir &> /dev/null \
|| [[ "$(__git_prompt_git config --get oh-my-zsh.hide-info 2>/dev/null)" == 1 ]]; then
return 0
fi
# Get either:
# - the current branch name
# - the tag name if we are on a tag
# - the short SHA of the current commit
local ref
ref=$(__git_prompt_git symbolic-ref --short HEAD 2> /dev/null) \
|| ref=$(__git_prompt_git describe --tags --exact-match HEAD 2> /dev/null) \
|| ref=$(__git_prompt_git rev-parse --short HEAD 2> /dev/null) \
|| return 0
# Use global ZSH_THEME_GIT_SHOW_UPSTREAM=1 for including upstream remote info
local upstream
if (( ${+ZSH_THEME_GIT_SHOW_UPSTREAM} )); then
upstream=$(__git_prompt_git rev-parse --abbrev-ref --symbolic-full-name "@{upstream}" 2>/dev/null) \
&& upstream=" -> ${upstream}"
fi
echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref:gs/%/%%}${upstream:gs/%/%%}$(parse_git_dirty)${ZSH_THEME_GIT_PROMPT_SUFFIX}"
}
fi
# Checks if working tree is dirty
function parse_git_dirty() {