ohmyzsh/lib/async_prompt.zsh
Marc Cornellà 083cc2c8e8
feat(async)!: implement async prompt API and apply to git prompt (#12257)
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.

Co-authored-by: Carlo Sala <carlosalag@protonmail.com>
2024-03-07 14:39:05 +01:00

141 lines
4.4 KiB
Bash

# 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_register_handler _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 -n $_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.
function _omz_register_handler {
setopt localoptions noksharrays
typeset -ga _omz_async_functions
# we want to do nothing if there's no $1 function or we already set it up
if [[ -z "$1" ]] || (( ! ${+functions[$1]} )) \
|| (( ${_omz_async_functions[(Ie)$1]} )); then
return
fi
_omz_async_functions+=("$1")
# let's add the hook to async_request if it's not there yet
if (( ! ${precmd_functions[(Ie)_omz_async_request]} )) \
&& (( ${+functions[_omz_async_request]})); then
autoload -Uz add-zsh-hook
add-zsh-hook precmd _omz_async_request
fi
}
# 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
}