Pretty, minimal and fast ZSH prompt; nix and jj ified
1#!/usr/bin/env zsh
2
3#
4# zsh-async
5#
6# version: v1.8.6
7# author: Mathias Fredriksson
8# url: https://github.com/mafredri/zsh-async
9#
10
11typeset -g ASYNC_VERSION=1.8.6
12# Produce debug output from zsh-async when set to 1.
13typeset -g ASYNC_DEBUG=${ASYNC_DEBUG:-0}
14
15# Execute commands that can manipulate the environment inside the async worker. Return output via callback.
16_async_eval() {
17 local ASYNC_JOB_NAME
18 local ret tmp
19
20 if ! zmodload -F zsh/files b:zf_rm 2>/dev/null; then
21 ASYNC_JOB_NAME=[async/eval] _async_job "return 1"
22 return
23 fi
24
25 if ! tmp=$(command mktemp "${TMPDIR:-/tmp}/zsh-async-eval.XXXXXXXXXX" 2>/dev/null); then
26 ASYNC_JOB_NAME=[async/eval] _async_job "return 1"
27 return
28 fi
29
30 eval "$@" >| "$tmp" 2>&1
31 ret=$?
32
33 ASYNC_JOB_NAME=[async/eval] _async_job "command -p cat ${(q)tmp}; return $ret"
34 zf_rm -f -- "$tmp"
35}
36
37# Wrapper for jobs executed by the async worker, gives output in parseable format with execution time
38_async_job() {
39 # Disable xtrace as it would mangle the output.
40 setopt localoptions noxtrace
41
42 # Store start time for job.
43 float -F duration=$EPOCHREALTIME
44
45 # Run the command and capture both stdout (`eval`) and stderr (`cat`) in
46 # separate subshells. When the command is complete, we grab write lock
47 # (mutex token) and output everything except stderr inside the command
48 # block, after the command block has completed, the stdin for `cat` is
49 # closed, causing stderr to be appended with a $'\0' at the end to mark the
50 # end of output from this job.
51 local jobname=${ASYNC_JOB_NAME:-$1} out
52 out="$(
53 local stdout stderr ret tok
54 {
55 stdout=$(eval "$@")
56 ret=$?
57 duration=$(( EPOCHREALTIME - duration )) # Calculate duration.
58
59 print -r -n - $'\0'${(q)jobname} $ret ${(q)stdout} $duration
60 } 2> >(stderr=$(command -p cat) && print -r -n - " "${(q)stderr}$'\0')
61 )"
62 if [[ $out != $'\0'*$'\0' ]]; then
63 # Corrupted output (aborted job?), skipping.
64 return
65 fi
66
67 # Grab mutex lock, stalls until token is available.
68 read -r -k 1 -p tok || return 1
69
70 # Return output (<job_name> <return_code> <stdout> <duration> <stderr>).
71 print -r -n - "$out"
72
73 # Unlock mutex by inserting a token.
74 print -n -p $tok
75}
76
77# The background worker manages all tasks and runs them without interfering with other processes
78_async_worker() {
79 # Reset all options to defaults inside async worker.
80 emulate -R zsh
81
82 # Make sure monitor is unset to avoid printing the
83 # pids of child processes.
84 unsetopt monitor
85
86 # Redirect stderr to `/dev/null` in case unforseen errors produced by the
87 # worker. For example: `fork failed: resource temporarily unavailable`.
88 # Some older versions of zsh might also print malloc errors (know to happen
89 # on at least zsh 5.0.2 and 5.0.8) likely due to kill signals.
90 exec 2>/dev/null
91
92 # When a zpty is deleted (using -d) all the zpty instances created before
93 # the one being deleted receive a SIGHUP, unless we catch it, the async
94 # worker would simply exit (stop working) even though visible in the list
95 # of zpty's (zpty -L). This has been fixed around the time of Zsh 5.4
96 # (not released).
97 if ! is-at-least 5.4.1; then
98 TRAPHUP() {
99 return 0 # Return 0, indicating signal was handled.
100 }
101 fi
102
103 local -A storage
104 local unique=0
105 local notify_parent=0
106 local parent_pid=0
107 local coproc_pid=0
108 local processing=0
109
110 local -a zsh_hooks zsh_hook_functions
111 zsh_hooks=(chpwd periodic precmd preexec zshexit zshaddhistory)
112 zsh_hook_functions=(${^zsh_hooks}_functions)
113 unfunction $zsh_hooks &>/dev/null # Deactivate all zsh hooks inside the worker.
114 unset $zsh_hook_functions # And hooks with registered functions.
115 unset zsh_hooks zsh_hook_functions # Cleanup.
116
117 close_idle_coproc() {
118 local -a pids
119 pids=(${${(v)jobstates##*:*:}%\=*})
120
121 # If coproc (cat) is the only child running, we close it to avoid
122 # leaving it running indefinitely and cluttering the process tree.
123 if (( ! processing )) && [[ $#pids = 1 ]] && [[ $coproc_pid = $pids[1] ]]; then
124 coproc :
125 coproc_pid=0
126 fi
127 }
128
129 child_exit() {
130 close_idle_coproc
131
132 # On older version of zsh (pre 5.2) we notify the parent through a
133 # SIGWINCH signal because `zpty` did not return a file descriptor (fd)
134 # prior to that.
135 if (( notify_parent )); then
136 # We use SIGWINCH for compatibility with older versions of zsh
137 # (pre 5.1.1) where other signals (INFO, ALRM, USR1, etc.) could
138 # cause a deadlock in the shell under certain circumstances.
139 kill -WINCH $parent_pid
140 fi
141 }
142
143 # Register a SIGCHLD trap to handle the completion of child processes.
144 trap child_exit CHLD
145
146 # Process option parameters passed to worker.
147 while getopts "np:uz" opt; do
148 case $opt in
149 n) notify_parent=1;;
150 p) parent_pid=$OPTARG;;
151 u) unique=1;;
152 z) notify_parent=0;; # Uses ZLE watcher instead.
153 esac
154 done
155
156 # Terminate all running jobs, note that this function does not
157 # reinstall the child trap.
158 terminate_jobs() {
159 trap - CHLD # Ignore child exits during kill.
160 coproc : # Quit coproc.
161 coproc_pid=0 # Reset pid.
162
163 if is-at-least 5.4.1; then
164 trap '' HUP # Catch the HUP sent to this process.
165 kill -HUP -$$ # Send to entire process group.
166 trap - HUP # Disable HUP trap.
167 else
168 # We already handle HUP for Zsh < 5.4.1.
169 kill -HUP -$$ # Send to entire process group.
170 fi
171 }
172
173 killjobs() {
174 local tok
175 local -a pids
176 pids=(${${(v)jobstates##*:*:}%\=*})
177
178 # No need to send SIGHUP if no jobs are running.
179 (( $#pids == 0 )) && continue
180 (( $#pids == 1 )) && [[ $coproc_pid = $pids[1] ]] && continue
181
182 # Grab lock to prevent half-written output in case a child
183 # process is in the middle of writing to stdin during kill.
184 (( coproc_pid )) && read -r -k 1 -p tok
185
186 terminate_jobs
187 trap child_exit CHLD # Reinstall child trap.
188 }
189
190 local request do_eval=0
191 local -a cmd
192 while :; do
193 # Wait for jobs sent by async_job.
194 read -r -d $'\0' request || {
195 # Unknown error occurred while reading from stdin, the zpty
196 # worker is likely in a broken state, so we shut down.
197 terminate_jobs
198
199 # Stdin is broken and in case this was an unintended
200 # crash, we try to report it as a last hurrah.
201 print -r -n $'\0'"'[async]'" $(( 127 + 3 )) "''" 0 "'$0:$LINENO: zpty fd died, exiting'"$'\0'
202
203 # We use `return` to abort here because using `exit` may
204 # result in an infinite loop that never exits and, as a
205 # result, high CPU utilization.
206 return $(( 127 + 1 ))
207 }
208
209 # We need to clean the input here because sometimes when a zpty
210 # has died and been respawned, messages will be prefixed with a
211 # carraige return (\r, or \C-M).
212 request=${request#$'\C-M'}
213
214 # Check for non-job commands sent to worker
215 case $request in
216 _killjobs) killjobs; continue;;
217 _async_eval*) do_eval=1;;
218 esac
219
220 # Parse the request using shell parsing (z) to allow commands
221 # to be parsed from single strings and multi-args alike.
222 cmd=("${(z)request}")
223
224 # Name of the job (first argument).
225 local job=$cmd[1]
226
227 # Check if a worker should perform unique jobs, unless
228 # this is an eval since they run synchronously.
229 if (( !do_eval )) && (( unique )); then
230 # Check if a previous job is still running, if yes,
231 # skip this job and let the previous one finish.
232 for pid in ${${(v)jobstates##*:*:}%\=*}; do
233 if [[ ${storage[$job]} == $pid ]]; then
234 continue 2
235 fi
236 done
237 fi
238
239 # Guard against closing coproc from trap before command has started.
240 processing=1
241
242 # Because we close the coproc after the last job has completed, we must
243 # recreate it when there are no other jobs running.
244 if (( ! coproc_pid )); then
245 # Use coproc as a mutex for synchronized output between children.
246 coproc command -p cat
247 coproc_pid="$!"
248 # Insert token into coproc
249 print -n -p "t"
250 fi
251
252 if (( do_eval )); then
253 shift cmd # Strip _async_eval from cmd.
254 _async_eval $cmd
255 else
256 # Run job in background, completed jobs are printed to stdout.
257 _async_job $cmd &
258 # Store pid because zsh job manager is extremely unflexible (show jobname as non-unique '$job')...
259 storage[$job]="$!"
260 fi
261
262 processing=0 # Disable guard.
263
264 if (( do_eval )); then
265 do_eval=0
266
267 # When there are no active jobs we can't rely on the CHLD trap to
268 # manage the coproc lifetime.
269 close_idle_coproc
270 fi
271 done
272}
273
274#
275# Get results from finished jobs and pass it to the to callback function. This is the only way to reliably return the
276# job name, return code, output and execution time and with minimal effort.
277#
278# If the async process buffer becomes corrupt, the callback will be invoked with the first argument being `[async]` (job
279# name), non-zero return code and fifth argument describing the error (stderr).
280#
281# usage:
282# async_process_results <worker_name> <callback_function>
283#
284# callback_function is called with the following parameters:
285# $1 = job name, e.g. the function passed to async_job
286# $2 = return code
287# $3 = resulting stdout from execution
288# $4 = execution time, floating point e.g. 2.05 seconds
289# $5 = resulting stderr from execution
290# $6 = has next result in buffer (0 = buffer empty, 1 = yes)
291#
292async_process_results() {
293 setopt localoptions unset noshwordsplit noksharrays noposixidentifiers noposixstrings
294
295 local worker=$1
296 local callback=$2
297 local caller=$3
298 local -a items
299 local null=$'\0' data
300 integer -l len pos num_processed has_next
301
302 typeset -gA ASYNC_PROCESS_BUFFER
303
304 # Read output from zpty and parse it if available.
305 while zpty -r -t $worker data 2>/dev/null; do
306 if [[ -z $data ]]; then
307 # Empty read indicates a broken zpty fd (EOF without proper close).
308 # This prevents a busy-loop that causes 100% CPU usage.
309 if [[ -n $callback ]]; then
310 $callback '[async]' 2 "" 0 "$0:$LINENO: error: empty read from worker $worker, fd is broken" 0
311 fi
312 # Trap and watcher callers must not leak the recovery status to the shell.
313 if [[ $caller = trap || $caller = watcher ]]; then
314 return 0
315 fi
316 return 1
317 fi
318 ASYNC_PROCESS_BUFFER[$worker]+=$data
319 len=${#ASYNC_PROCESS_BUFFER[$worker]}
320 pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter).
321
322 # Keep going until we find a NULL-character.
323 if (( ! len )) || (( pos > len )); then
324 continue
325 fi
326
327 while (( pos <= len )); do
328 # Take the content from the beginning, until the NULL-character and
329 # perform shell parsing (z) and unquoting (Q) as an array (@).
330 items=("${(@Q)${(z)ASYNC_PROCESS_BUFFER[$worker][1,$pos-1]}}")
331
332 # Remove the extracted items from the buffer.
333 ASYNC_PROCESS_BUFFER[$worker]=${ASYNC_PROCESS_BUFFER[$worker][$pos+1,$len]}
334
335 len=${#ASYNC_PROCESS_BUFFER[$worker]}
336 if (( len > 1 )); then
337 pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter).
338 fi
339
340 has_next=$(( len != 0 ))
341 if (( $#items == 5 )); then
342 items+=($has_next)
343 $callback "${(@)items}" # Send all parsed items to the callback.
344 (( num_processed++ ))
345 elif [[ -z $items ]]; then
346 # Empty items occur between results due to double-null ($'\0\0')
347 # caused by commands being both pre and suffixed with null.
348 else
349 # In case of corrupt data, invoke callback with *async* as job
350 # name, non-zero exit status and an error message on stderr.
351 $callback "[async]" 1 "" 0 "$0:$LINENO: error: bad format, got ${#items} items (${(q)items})" $has_next
352 fi
353 done
354 done
355
356 (( num_processed )) && return 0
357
358 # Avoid printing exit value when `setopt printexitvalue` is active.`
359 [[ $caller = trap || $caller = watcher ]] && return 0
360
361 # No results were processed
362 return 1
363}
364
365# Watch worker for output
366_async_zle_watcher() {
367 setopt localoptions noshwordsplit
368 typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
369 local worker=$ASYNC_PTYS[$1]
370 local callback=$ASYNC_CALLBACKS[$worker]
371
372 if [[ -n $2 ]]; then
373 # from man zshzle(1):
374 # `hup' for a disconnect, `nval' for a closed or otherwise
375 # invalid descriptor, or `err' for any other condition.
376 # Systems that support only the `select' system call always use
377 # `err'.
378
379 # this has the side effect to unregister the broken file descriptor
380 async_stop_worker $worker
381
382 if [[ -n $callback ]]; then
383 $callback '[async]' 2 "" 0 "$0:$LINENO: error: fd for $worker failed: zle -F $1 returned error $2" 0
384 fi
385 return
386 fi;
387
388 if [[ -n $callback ]]; then
389 async_process_results $worker $callback watcher
390 fi
391}
392
393_async_send_job() {
394 setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
395
396 local caller=$1
397 local worker=$2
398 shift 2
399
400 zpty -t $worker &>/dev/null || {
401 typeset -gA ASYNC_CALLBACKS
402 local callback=$ASYNC_CALLBACKS[$worker]
403
404 if [[ -n $callback ]]; then
405 $callback '[async]' 3 "" 0 "$0:$LINENO: error: no such worker: $worker" 0
406 else
407 print -u2 "$caller: no such async worker: $worker"
408 fi
409 return 1
410 }
411
412 zpty -w $worker "$@"$'\0'
413}
414
415#
416# Start a new asynchronous job on specified worker, assumes the worker is running.
417#
418# Note if you are using a function for the job, it must have been defined before the worker was
419# started or you will get a `command not found` error.
420#
421# usage:
422# async_job <worker_name> <my_function> [<function_params>]
423#
424async_job() {
425 setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
426
427 local worker=$1; shift
428
429 local -a cmd
430 cmd=("$@")
431 if (( $#cmd > 1 )); then
432 cmd=(${(q)cmd}) # Quote special characters in multi argument commands.
433 fi
434
435 _async_send_job $0 $worker "$cmd"
436}
437
438#
439# Evaluate a command (like async_job) inside the async worker, then worker environment can be manipulated. For example,
440# issuing a cd command will change the PWD of the worker which will then be inherited by all future async jobs.
441#
442# Output will be returned via callback, job name will be [async/eval].
443#
444# usage:
445# async_worker_eval <worker_name> <my_function> [<function_params>]
446#
447async_worker_eval() {
448 setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
449
450 local worker=$1; shift
451
452 local -a cmd
453 cmd=("$@")
454 if (( $#cmd > 1 )); then
455 cmd=(${(q)cmd}) # Quote special characters in multi argument commands.
456 fi
457
458 # Quote the cmd in case RC_EXPAND_PARAM is set.
459 _async_send_job $0 $worker "_async_eval $cmd"
460}
461
462# This function traps notification signals and calls all registered callbacks
463_async_notify_trap() {
464 setopt localoptions noshwordsplit
465
466 local k
467 for k in ${(k)ASYNC_CALLBACKS}; do
468 async_process_results $k ${ASYNC_CALLBACKS[$k]} trap
469 done
470}
471
472#
473# Register a callback for completed jobs. As soon as a job is finnished, async_process_results will be called with the
474# specified callback function. This requires that a worker is initialized with the -n (notify) option.
475#
476# usage:
477# async_register_callback <worker_name> <callback_function>
478#
479async_register_callback() {
480 setopt localoptions noshwordsplit nolocaltraps
481
482 typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
483 local worker=$1; shift
484
485 ASYNC_CALLBACKS[$worker]="$*"
486
487 # Enable trap when the ZLE watcher is unavailable, allows
488 # workers to notify (via -n) when a job is done.
489 if [[ ! -o interactive ]] || [[ ! -o zle ]]; then
490 trap '_async_notify_trap' WINCH
491 elif [[ -o interactive ]] && [[ -o zle ]]; then
492 local fd w
493 for fd w in ${(@kv)ASYNC_PTYS}; do
494 if [[ $w == $worker ]]; then
495 zle -F $fd _async_zle_watcher # Register the ZLE handler.
496 break
497 fi
498 done
499 fi
500}
501
502#
503# Unregister the callback for a specific worker.
504#
505# usage:
506# async_unregister_callback <worker_name>
507#
508async_unregister_callback() {
509 typeset -gA ASYNC_CALLBACKS
510
511 unset "ASYNC_CALLBACKS[$1]"
512}
513
514#
515# Flush all current jobs running on a worker. This will terminate any and all running processes under the worker, use
516# with caution.
517#
518# usage:
519# async_flush_jobs <worker_name>
520#
521async_flush_jobs() {
522 setopt localoptions noshwordsplit
523
524 local worker=$1; shift
525
526 # Check if the worker exists
527 zpty -t $worker &>/dev/null || return 1
528
529 # Send kill command to worker
530 async_job $worker "_killjobs"
531
532 # Clear the zpty buffer.
533 local junk
534 if zpty -r -t $worker junk '*'; then
535 (( ASYNC_DEBUG )) && print -n "async_flush_jobs $worker: ${(V)junk}"
536 while zpty -r -t $worker junk '*'; do
537 (( ASYNC_DEBUG )) && print -n "${(V)junk}"
538 done
539 (( ASYNC_DEBUG )) && print
540 fi
541
542 # Finally, clear the process buffer in case of partially parsed responses.
543 typeset -gA ASYNC_PROCESS_BUFFER
544 unset "ASYNC_PROCESS_BUFFER[$worker]"
545}
546
547#
548# Start a new async worker with optional parameters, a worker can be told to only run unique tasks and to notify a
549# process when tasks are complete.
550#
551# usage:
552# async_start_worker <worker_name> [-u] [-n] [-p <pid>]
553#
554# opts:
555# -u unique (only unique job names can run)
556# -n notify through SIGWINCH signal
557# -p pid to notify (defaults to current pid)
558#
559async_start_worker() {
560 setopt localoptions noshwordsplit noclobber
561
562 local worker=$1; shift
563 local -a args
564 args=("$@")
565 zpty -t $worker &>/dev/null && return
566
567 typeset -gA ASYNC_PTYS
568 typeset -h REPLY
569 typeset has_xtrace=0
570
571 if [[ -o interactive ]] && [[ -o zle ]]; then
572 # Inform the worker to ignore the notify flag and that we're
573 # using a ZLE watcher instead.
574 args+=(-z)
575
576 if (( ! ASYNC_ZPTY_RETURNS_FD )); then
577 # When zpty doesn't return a file descriptor (on older versions of zsh)
578 # we try to guess it anyway.
579 integer -l zptyfd
580 exec {zptyfd}>&1 # Open a new file descriptor (above 10).
581 exec {zptyfd}>&- # Close it so it's free to be used by zpty.
582 fi
583 fi
584
585 # Workaround for stderr in the main shell sometimes (incorrectly) being
586 # reassigned to /dev/null by the reassignment done inside the async
587 # worker.
588 # See https://github.com/mafredri/zsh-async/issues/35.
589 integer errfd=-1
590
591 # Redirect of errfd is broken on zsh 5.0.2.
592 if is-at-least 5.0.8; then
593 exec {errfd}>&2
594 fi
595
596 # Make sure async worker is started without xtrace
597 # (the trace output interferes with the worker).
598 [[ -o xtrace ]] && {
599 has_xtrace=1
600 unsetopt xtrace
601 }
602
603 if (( errfd != -1 )); then
604 zpty -b $worker _async_worker -p $$ $args 2>&$errfd
605 else
606 zpty -b $worker _async_worker -p $$ $args
607 fi
608 local ret=$?
609
610 # Re-enable it if it was enabled, for debugging.
611 (( has_xtrace )) && setopt xtrace
612 (( errfd != -1 )) && exec {errfd}>& -
613
614 if (( ret )); then
615 async_stop_worker $worker
616 return 1
617 fi
618
619 if ! is-at-least 5.0.8; then
620 # For ZSH versions older than 5.0.8 we delay a bit to give
621 # time for the worker to start before issuing commands,
622 # otherwise it will not be ready to receive them.
623 sleep 0.001
624 fi
625
626 if [[ -o interactive ]] && [[ -o zle ]]; then
627 if (( ! ASYNC_ZPTY_RETURNS_FD )); then
628 REPLY=$zptyfd # Use the guessed value for the file desciptor.
629 fi
630
631 ASYNC_PTYS[$REPLY]=$worker # Map the file desciptor to the worker.
632 fi
633}
634
635#
636# Stop one or multiple workers that are running, all unfetched and incomplete work will be lost.
637#
638# usage:
639# async_stop_worker <worker_name_1> [<worker_name_2>]
640#
641async_stop_worker() {
642 setopt localoptions noshwordsplit
643
644 local ret=0 worker k v
645 for worker in $@; do
646 # Find and unregister the zle handler for the worker
647 for k v in ${(@kv)ASYNC_PTYS}; do
648 if [[ $v == $worker ]]; then
649 zle -F $k
650 unset "ASYNC_PTYS[$k]"
651 fi
652 done
653 async_unregister_callback $worker
654 zpty -d $worker 2>/dev/null || ret=$?
655
656 # Clear any partial buffers.
657 typeset -gA ASYNC_PROCESS_BUFFER
658 unset "ASYNC_PROCESS_BUFFER[$worker]"
659 done
660
661 return $ret
662}
663
664#
665# Initialize the required modules for zsh-async. To be called before using the zsh-async library.
666#
667# usage:
668# async_init
669#
670async_init() {
671 (( ASYNC_INIT_DONE )) && return
672 typeset -g ASYNC_INIT_DONE=1
673
674 zmodload zsh/zpty
675 zmodload zsh/datetime
676
677 # Load is-at-least for reliable version check.
678 autoload -Uz is-at-least
679
680 # Check if zsh/zpty returns a file descriptor or not,
681 # shell must also be interactive with zle enabled.
682 typeset -g ASYNC_ZPTY_RETURNS_FD=0
683 [[ -o interactive ]] && [[ -o zle ]] && {
684 typeset -h REPLY
685 zpty _async_test :
686 (( REPLY )) && ASYNC_ZPTY_RETURNS_FD=1
687 zpty -d _async_test
688 }
689}
690
691async() {
692 async_init
693}
694
695async "$@"