summaryrefslogtreecommitdiff
path: root/lib/async_prompt.zsh
blob: db48446e70351d34f535996c0c4e32afc69796d6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# 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
autoload -Uz is-at-least

# For now, async prompt function handlers are set up like so:
# First, define the async function handler and register the handler
# with _omz_register_handler:
#
#  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_async]
#  }
#
#  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 {
  local -i ret=$?
  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]}
      # Set exit code for the handler if used
      () { return $ret }
      # 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
    # and https://github.com/zsh-users/zsh-autosuggestions/pull/612
    is-at-least 5.8 || command true

    # Save the PID from the handler child process
    read -u $fd "_OMZ_ASYNC_PIDS[$handler]"

    # 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 fd
    local handler="${(k)_OMZ_ASYNC_FDS[(r)$fd]}"

    # Store old output which is supposed to be already printed
    local old_output="${_OMZ_ASYNC_OUTPUT[$handler]}"

    # 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
      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