···11+#!/usr/bin/env zsh
22+33+#
44+# zsh-async
55+#
66+# version: v1.8.6
77+# author: Mathias Fredriksson
88+# url: https://github.com/mafredri/zsh-async
99+#
1010+1111+typeset -g ASYNC_VERSION=1.8.6
1212+# Produce debug output from zsh-async when set to 1.
1313+typeset -g ASYNC_DEBUG=${ASYNC_DEBUG:-0}
1414+1515+# Execute commands that can manipulate the environment inside the async worker. Return output via callback.
1616+_async_eval() {
1717+ local ASYNC_JOB_NAME
1818+ # Rename job to _async_eval and redirect all eval output to cat running
1919+ # in _async_job. Here, stdout and stderr are not separated for
2020+ # simplicity, this could be improved in the future.
2121+ {
2222+ eval "$@"
2323+ } &> >(ASYNC_JOB_NAME=[async/eval] _async_job 'command -p cat')
2424+}
2525+2626+# Wrapper for jobs executed by the async worker, gives output in parseable format with execution time
2727+_async_job() {
2828+ # Disable xtrace as it would mangle the output.
2929+ setopt localoptions noxtrace
3030+3131+ # Store start time for job.
3232+ float -F duration=$EPOCHREALTIME
3333+3434+ # Run the command and capture both stdout (`eval`) and stderr (`cat`) in
3535+ # separate subshells. When the command is complete, we grab write lock
3636+ # (mutex token) and output everything except stderr inside the command
3737+ # block, after the command block has completed, the stdin for `cat` is
3838+ # closed, causing stderr to be appended with a $'\0' at the end to mark the
3939+ # end of output from this job.
4040+ local jobname=${ASYNC_JOB_NAME:-$1} out
4141+ out="$(
4242+ local stdout stderr ret tok
4343+ {
4444+ stdout=$(eval "$@")
4545+ ret=$?
4646+ duration=$(( EPOCHREALTIME - duration )) # Calculate duration.
4747+4848+ print -r -n - $'\0'${(q)jobname} $ret ${(q)stdout} $duration
4949+ } 2> >(stderr=$(command -p cat) && print -r -n - " "${(q)stderr}$'\0')
5050+ )"
5151+ if [[ $out != $'\0'*$'\0' ]]; then
5252+ # Corrupted output (aborted job?), skipping.
5353+ return
5454+ fi
5555+5656+ # Grab mutex lock, stalls until token is available.
5757+ read -r -k 1 -p tok || return 1
5858+5959+ # Return output (<job_name> <return_code> <stdout> <duration> <stderr>).
6060+ print -r -n - "$out"
6161+6262+ # Unlock mutex by inserting a token.
6363+ print -n -p $tok
6464+}
6565+6666+# The background worker manages all tasks and runs them without interfering with other processes
6767+_async_worker() {
6868+ # Reset all options to defaults inside async worker.
6969+ emulate -R zsh
7070+7171+ # Make sure monitor is unset to avoid printing the
7272+ # pids of child processes.
7373+ unsetopt monitor
7474+7575+ # Redirect stderr to `/dev/null` in case unforseen errors produced by the
7676+ # worker. For example: `fork failed: resource temporarily unavailable`.
7777+ # Some older versions of zsh might also print malloc errors (know to happen
7878+ # on at least zsh 5.0.2 and 5.0.8) likely due to kill signals.
7979+ exec 2>/dev/null
8080+8181+ # When a zpty is deleted (using -d) all the zpty instances created before
8282+ # the one being deleted receive a SIGHUP, unless we catch it, the async
8383+ # worker would simply exit (stop working) even though visible in the list
8484+ # of zpty's (zpty -L). This has been fixed around the time of Zsh 5.4
8585+ # (not released).
8686+ if ! is-at-least 5.4.1; then
8787+ TRAPHUP() {
8888+ return 0 # Return 0, indicating signal was handled.
8989+ }
9090+ fi
9191+9292+ local -A storage
9393+ local unique=0
9494+ local notify_parent=0
9595+ local parent_pid=0
9696+ local coproc_pid=0
9797+ local processing=0
9898+9999+ local -a zsh_hooks zsh_hook_functions
100100+ zsh_hooks=(chpwd periodic precmd preexec zshexit zshaddhistory)
101101+ zsh_hook_functions=(${^zsh_hooks}_functions)
102102+ unfunction $zsh_hooks &>/dev/null # Deactivate all zsh hooks inside the worker.
103103+ unset $zsh_hook_functions # And hooks with registered functions.
104104+ unset zsh_hooks zsh_hook_functions # Cleanup.
105105+106106+ close_idle_coproc() {
107107+ local -a pids
108108+ pids=(${${(v)jobstates##*:*:}%\=*})
109109+110110+ # If coproc (cat) is the only child running, we close it to avoid
111111+ # leaving it running indefinitely and cluttering the process tree.
112112+ if (( ! processing )) && [[ $#pids = 1 ]] && [[ $coproc_pid = $pids[1] ]]; then
113113+ coproc :
114114+ coproc_pid=0
115115+ fi
116116+ }
117117+118118+ child_exit() {
119119+ close_idle_coproc
120120+121121+ # On older version of zsh (pre 5.2) we notify the parent through a
122122+ # SIGWINCH signal because `zpty` did not return a file descriptor (fd)
123123+ # prior to that.
124124+ if (( notify_parent )); then
125125+ # We use SIGWINCH for compatibility with older versions of zsh
126126+ # (pre 5.1.1) where other signals (INFO, ALRM, USR1, etc.) could
127127+ # cause a deadlock in the shell under certain circumstances.
128128+ kill -WINCH $parent_pid
129129+ fi
130130+ }
131131+132132+ # Register a SIGCHLD trap to handle the completion of child processes.
133133+ trap child_exit CHLD
134134+135135+ # Process option parameters passed to worker.
136136+ while getopts "np:uz" opt; do
137137+ case $opt in
138138+ n) notify_parent=1;;
139139+ p) parent_pid=$OPTARG;;
140140+ u) unique=1;;
141141+ z) notify_parent=0;; # Uses ZLE watcher instead.
142142+ esac
143143+ done
144144+145145+ # Terminate all running jobs, note that this function does not
146146+ # reinstall the child trap.
147147+ terminate_jobs() {
148148+ trap - CHLD # Ignore child exits during kill.
149149+ coproc : # Quit coproc.
150150+ coproc_pid=0 # Reset pid.
151151+152152+ if is-at-least 5.4.1; then
153153+ trap '' HUP # Catch the HUP sent to this process.
154154+ kill -HUP -$$ # Send to entire process group.
155155+ trap - HUP # Disable HUP trap.
156156+ else
157157+ # We already handle HUP for Zsh < 5.4.1.
158158+ kill -HUP -$$ # Send to entire process group.
159159+ fi
160160+ }
161161+162162+ killjobs() {
163163+ local tok
164164+ local -a pids
165165+ pids=(${${(v)jobstates##*:*:}%\=*})
166166+167167+ # No need to send SIGHUP if no jobs are running.
168168+ (( $#pids == 0 )) && continue
169169+ (( $#pids == 1 )) && [[ $coproc_pid = $pids[1] ]] && continue
170170+171171+ # Grab lock to prevent half-written output in case a child
172172+ # process is in the middle of writing to stdin during kill.
173173+ (( coproc_pid )) && read -r -k 1 -p tok
174174+175175+ terminate_jobs
176176+ trap child_exit CHLD # Reinstall child trap.
177177+ }
178178+179179+ local request do_eval=0
180180+ local -a cmd
181181+ while :; do
182182+ # Wait for jobs sent by async_job.
183183+ read -r -d $'\0' request || {
184184+ # Unknown error occurred while reading from stdin, the zpty
185185+ # worker is likely in a broken state, so we shut down.
186186+ terminate_jobs
187187+188188+ # Stdin is broken and in case this was an unintended
189189+ # crash, we try to report it as a last hurrah.
190190+ print -r -n $'\0'"'[async]'" $(( 127 + 3 )) "''" 0 "'$0:$LINENO: zpty fd died, exiting'"$'\0'
191191+192192+ # We use `return` to abort here because using `exit` may
193193+ # result in an infinite loop that never exits and, as a
194194+ # result, high CPU utilization.
195195+ return $(( 127 + 1 ))
196196+ }
197197+198198+ # We need to clean the input here because sometimes when a zpty
199199+ # has died and been respawned, messages will be prefixed with a
200200+ # carraige return (\r, or \C-M).
201201+ request=${request#$'\C-M'}
202202+203203+ # Check for non-job commands sent to worker
204204+ case $request in
205205+ _killjobs) killjobs; continue;;
206206+ _async_eval*) do_eval=1;;
207207+ esac
208208+209209+ # Parse the request using shell parsing (z) to allow commands
210210+ # to be parsed from single strings and multi-args alike.
211211+ cmd=("${(z)request}")
212212+213213+ # Name of the job (first argument).
214214+ local job=$cmd[1]
215215+216216+ # Check if a worker should perform unique jobs, unless
217217+ # this is an eval since they run synchronously.
218218+ if (( !do_eval )) && (( unique )); then
219219+ # Check if a previous job is still running, if yes,
220220+ # skip this job and let the previous one finish.
221221+ for pid in ${${(v)jobstates##*:*:}%\=*}; do
222222+ if [[ ${storage[$job]} == $pid ]]; then
223223+ continue 2
224224+ fi
225225+ done
226226+ fi
227227+228228+ # Guard against closing coproc from trap before command has started.
229229+ processing=1
230230+231231+ # Because we close the coproc after the last job has completed, we must
232232+ # recreate it when there are no other jobs running.
233233+ if (( ! coproc_pid )); then
234234+ # Use coproc as a mutex for synchronized output between children.
235235+ coproc command -p cat
236236+ coproc_pid="$!"
237237+ # Insert token into coproc
238238+ print -n -p "t"
239239+ fi
240240+241241+ if (( do_eval )); then
242242+ shift cmd # Strip _async_eval from cmd.
243243+ _async_eval $cmd
244244+ else
245245+ # Run job in background, completed jobs are printed to stdout.
246246+ _async_job $cmd &
247247+ # Store pid because zsh job manager is extremely unflexible (show jobname as non-unique '$job')...
248248+ storage[$job]="$!"
249249+ fi
250250+251251+ processing=0 # Disable guard.
252252+253253+ if (( do_eval )); then
254254+ do_eval=0
255255+256256+ # When there are no active jobs we can't rely on the CHLD trap to
257257+ # manage the coproc lifetime.
258258+ close_idle_coproc
259259+ fi
260260+ done
261261+}
262262+263263+#
264264+# Get results from finished jobs and pass it to the to callback function. This is the only way to reliably return the
265265+# job name, return code, output and execution time and with minimal effort.
266266+#
267267+# If the async process buffer becomes corrupt, the callback will be invoked with the first argument being `[async]` (job
268268+# name), non-zero return code and fifth argument describing the error (stderr).
269269+#
270270+# usage:
271271+# async_process_results <worker_name> <callback_function>
272272+#
273273+# callback_function is called with the following parameters:
274274+# $1 = job name, e.g. the function passed to async_job
275275+# $2 = return code
276276+# $3 = resulting stdout from execution
277277+# $4 = execution time, floating point e.g. 2.05 seconds
278278+# $5 = resulting stderr from execution
279279+# $6 = has next result in buffer (0 = buffer empty, 1 = yes)
280280+#
281281+async_process_results() {
282282+ setopt localoptions unset noshwordsplit noksharrays noposixidentifiers noposixstrings
283283+284284+ local worker=$1
285285+ local callback=$2
286286+ local caller=$3
287287+ local -a items
288288+ local null=$'\0' data
289289+ integer -l len pos num_processed has_next
290290+291291+ typeset -gA ASYNC_PROCESS_BUFFER
292292+293293+ # Read output from zpty and parse it if available.
294294+ while zpty -r -t $worker data 2>/dev/null; do
295295+ ASYNC_PROCESS_BUFFER[$worker]+=$data
296296+ len=${#ASYNC_PROCESS_BUFFER[$worker]}
297297+ pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter).
298298+299299+ # Keep going until we find a NULL-character.
300300+ if (( ! len )) || (( pos > len )); then
301301+ continue
302302+ fi
303303+304304+ while (( pos <= len )); do
305305+ # Take the content from the beginning, until the NULL-character and
306306+ # perform shell parsing (z) and unquoting (Q) as an array (@).
307307+ items=("${(@Q)${(z)ASYNC_PROCESS_BUFFER[$worker][1,$pos-1]}}")
308308+309309+ # Remove the extracted items from the buffer.
310310+ ASYNC_PROCESS_BUFFER[$worker]=${ASYNC_PROCESS_BUFFER[$worker][$pos+1,$len]}
311311+312312+ len=${#ASYNC_PROCESS_BUFFER[$worker]}
313313+ if (( len > 1 )); then
314314+ pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter).
315315+ fi
316316+317317+ has_next=$(( len != 0 ))
318318+ if (( $#items == 5 )); then
319319+ items+=($has_next)
320320+ $callback "${(@)items}" # Send all parsed items to the callback.
321321+ (( num_processed++ ))
322322+ elif [[ -z $items ]]; then
323323+ # Empty items occur between results due to double-null ($'\0\0')
324324+ # caused by commands being both pre and suffixed with null.
325325+ else
326326+ # In case of corrupt data, invoke callback with *async* as job
327327+ # name, non-zero exit status and an error message on stderr.
328328+ $callback "[async]" 1 "" 0 "$0:$LINENO: error: bad format, got ${#items} items (${(q)items})" $has_next
329329+ fi
330330+ done
331331+ done
332332+333333+ (( num_processed )) && return 0
334334+335335+ # Avoid printing exit value when `setopt printexitvalue` is active.`
336336+ [[ $caller = trap || $caller = watcher ]] && return 0
337337+338338+ # No results were processed
339339+ return 1
340340+}
341341+342342+# Watch worker for output
343343+_async_zle_watcher() {
344344+ setopt localoptions noshwordsplit
345345+ typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
346346+ local worker=$ASYNC_PTYS[$1]
347347+ local callback=$ASYNC_CALLBACKS[$worker]
348348+349349+ if [[ -n $2 ]]; then
350350+ # from man zshzle(1):
351351+ # `hup' for a disconnect, `nval' for a closed or otherwise
352352+ # invalid descriptor, or `err' for any other condition.
353353+ # Systems that support only the `select' system call always use
354354+ # `err'.
355355+356356+ # this has the side effect to unregister the broken file descriptor
357357+ async_stop_worker $worker
358358+359359+ if [[ -n $callback ]]; then
360360+ $callback '[async]' 2 "" 0 "$0:$LINENO: error: fd for $worker failed: zle -F $1 returned error $2" 0
361361+ fi
362362+ return
363363+ fi;
364364+365365+ if [[ -n $callback ]]; then
366366+ async_process_results $worker $callback watcher
367367+ fi
368368+}
369369+370370+_async_send_job() {
371371+ setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
372372+373373+ local caller=$1
374374+ local worker=$2
375375+ shift 2
376376+377377+ zpty -t $worker &>/dev/null || {
378378+ typeset -gA ASYNC_CALLBACKS
379379+ local callback=$ASYNC_CALLBACKS[$worker]
380380+381381+ if [[ -n $callback ]]; then
382382+ $callback '[async]' 3 "" 0 "$0:$LINENO: error: no such worker: $worker" 0
383383+ else
384384+ print -u2 "$caller: no such async worker: $worker"
385385+ fi
386386+ return 1
387387+ }
388388+389389+ zpty -w $worker "$@"$'\0'
390390+}
391391+392392+#
393393+# Start a new asynchronous job on specified worker, assumes the worker is running.
394394+#
395395+# Note if you are using a function for the job, it must have been defined before the worker was
396396+# started or you will get a `command not found` error.
397397+#
398398+# usage:
399399+# async_job <worker_name> <my_function> [<function_params>]
400400+#
401401+async_job() {
402402+ setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
403403+404404+ local worker=$1; shift
405405+406406+ local -a cmd
407407+ cmd=("$@")
408408+ if (( $#cmd > 1 )); then
409409+ cmd=(${(q)cmd}) # Quote special characters in multi argument commands.
410410+ fi
411411+412412+ _async_send_job $0 $worker "$cmd"
413413+}
414414+415415+#
416416+# Evaluate a command (like async_job) inside the async worker, then worker environment can be manipulated. For example,
417417+# issuing a cd command will change the PWD of the worker which will then be inherited by all future async jobs.
418418+#
419419+# Output will be returned via callback, job name will be [async/eval].
420420+#
421421+# usage:
422422+# async_worker_eval <worker_name> <my_function> [<function_params>]
423423+#
424424+async_worker_eval() {
425425+ setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
426426+427427+ local worker=$1; shift
428428+429429+ local -a cmd
430430+ cmd=("$@")
431431+ if (( $#cmd > 1 )); then
432432+ cmd=(${(q)cmd}) # Quote special characters in multi argument commands.
433433+ fi
434434+435435+ # Quote the cmd in case RC_EXPAND_PARAM is set.
436436+ _async_send_job $0 $worker "_async_eval $cmd"
437437+}
438438+439439+# This function traps notification signals and calls all registered callbacks
440440+_async_notify_trap() {
441441+ setopt localoptions noshwordsplit
442442+443443+ local k
444444+ for k in ${(k)ASYNC_CALLBACKS}; do
445445+ async_process_results $k ${ASYNC_CALLBACKS[$k]} trap
446446+ done
447447+}
448448+449449+#
450450+# Register a callback for completed jobs. As soon as a job is finnished, async_process_results will be called with the
451451+# specified callback function. This requires that a worker is initialized with the -n (notify) option.
452452+#
453453+# usage:
454454+# async_register_callback <worker_name> <callback_function>
455455+#
456456+async_register_callback() {
457457+ setopt localoptions noshwordsplit nolocaltraps
458458+459459+ typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
460460+ local worker=$1; shift
461461+462462+ ASYNC_CALLBACKS[$worker]="$*"
463463+464464+ # Enable trap when the ZLE watcher is unavailable, allows
465465+ # workers to notify (via -n) when a job is done.
466466+ if [[ ! -o interactive ]] || [[ ! -o zle ]]; then
467467+ trap '_async_notify_trap' WINCH
468468+ elif [[ -o interactive ]] && [[ -o zle ]]; then
469469+ local fd w
470470+ for fd w in ${(@kv)ASYNC_PTYS}; do
471471+ if [[ $w == $worker ]]; then
472472+ zle -F $fd _async_zle_watcher # Register the ZLE handler.
473473+ break
474474+ fi
475475+ done
476476+ fi
477477+}
478478+479479+#
480480+# Unregister the callback for a specific worker.
481481+#
482482+# usage:
483483+# async_unregister_callback <worker_name>
484484+#
485485+async_unregister_callback() {
486486+ typeset -gA ASYNC_CALLBACKS
487487+488488+ unset "ASYNC_CALLBACKS[$1]"
489489+}
490490+491491+#
492492+# Flush all current jobs running on a worker. This will terminate any and all running processes under the worker, use
493493+# with caution.
494494+#
495495+# usage:
496496+# async_flush_jobs <worker_name>
497497+#
498498+async_flush_jobs() {
499499+ setopt localoptions noshwordsplit
500500+501501+ local worker=$1; shift
502502+503503+ # Check if the worker exists
504504+ zpty -t $worker &>/dev/null || return 1
505505+506506+ # Send kill command to worker
507507+ async_job $worker "_killjobs"
508508+509509+ # Clear the zpty buffer.
510510+ local junk
511511+ if zpty -r -t $worker junk '*'; then
512512+ (( ASYNC_DEBUG )) && print -n "async_flush_jobs $worker: ${(V)junk}"
513513+ while zpty -r -t $worker junk '*'; do
514514+ (( ASYNC_DEBUG )) && print -n "${(V)junk}"
515515+ done
516516+ (( ASYNC_DEBUG )) && print
517517+ fi
518518+519519+ # Finally, clear the process buffer in case of partially parsed responses.
520520+ typeset -gA ASYNC_PROCESS_BUFFER
521521+ unset "ASYNC_PROCESS_BUFFER[$worker]"
522522+}
523523+524524+#
525525+# Start a new async worker with optional parameters, a worker can be told to only run unique tasks and to notify a
526526+# process when tasks are complete.
527527+#
528528+# usage:
529529+# async_start_worker <worker_name> [-u] [-n] [-p <pid>]
530530+#
531531+# opts:
532532+# -u unique (only unique job names can run)
533533+# -n notify through SIGWINCH signal
534534+# -p pid to notify (defaults to current pid)
535535+#
536536+async_start_worker() {
537537+ setopt localoptions noshwordsplit noclobber
538538+539539+ local worker=$1; shift
540540+ local -a args
541541+ args=("$@")
542542+ zpty -t $worker &>/dev/null && return
543543+544544+ typeset -gA ASYNC_PTYS
545545+ typeset -h REPLY
546546+ typeset has_xtrace=0
547547+548548+ if [[ -o interactive ]] && [[ -o zle ]]; then
549549+ # Inform the worker to ignore the notify flag and that we're
550550+ # using a ZLE watcher instead.
551551+ args+=(-z)
552552+553553+ if (( ! ASYNC_ZPTY_RETURNS_FD )); then
554554+ # When zpty doesn't return a file descriptor (on older versions of zsh)
555555+ # we try to guess it anyway.
556556+ integer -l zptyfd
557557+ exec {zptyfd}>&1 # Open a new file descriptor (above 10).
558558+ exec {zptyfd}>&- # Close it so it's free to be used by zpty.
559559+ fi
560560+ fi
561561+562562+ # Workaround for stderr in the main shell sometimes (incorrectly) being
563563+ # reassigned to /dev/null by the reassignment done inside the async
564564+ # worker.
565565+ # See https://github.com/mafredri/zsh-async/issues/35.
566566+ integer errfd=-1
567567+568568+ # Redirect of errfd is broken on zsh 5.0.2.
569569+ if is-at-least 5.0.8; then
570570+ exec {errfd}>&2
571571+ fi
572572+573573+ # Make sure async worker is started without xtrace
574574+ # (the trace output interferes with the worker).
575575+ [[ -o xtrace ]] && {
576576+ has_xtrace=1
577577+ unsetopt xtrace
578578+ }
579579+580580+ if (( errfd != -1 )); then
581581+ zpty -b $worker _async_worker -p $$ $args 2>&$errfd
582582+ else
583583+ zpty -b $worker _async_worker -p $$ $args
584584+ fi
585585+ local ret=$?
586586+587587+ # Re-enable it if it was enabled, for debugging.
588588+ (( has_xtrace )) && setopt xtrace
589589+ (( errfd != -1 )) && exec {errfd}>& -
590590+591591+ if (( ret )); then
592592+ async_stop_worker $worker
593593+ return 1
594594+ fi
595595+596596+ if ! is-at-least 5.0.8; then
597597+ # For ZSH versions older than 5.0.8 we delay a bit to give
598598+ # time for the worker to start before issuing commands,
599599+ # otherwise it will not be ready to receive them.
600600+ sleep 0.001
601601+ fi
602602+603603+ if [[ -o interactive ]] && [[ -o zle ]]; then
604604+ if (( ! ASYNC_ZPTY_RETURNS_FD )); then
605605+ REPLY=$zptyfd # Use the guessed value for the file desciptor.
606606+ fi
607607+608608+ ASYNC_PTYS[$REPLY]=$worker # Map the file desciptor to the worker.
609609+ fi
610610+}
611611+612612+#
613613+# Stop one or multiple workers that are running, all unfetched and incomplete work will be lost.
614614+#
615615+# usage:
616616+# async_stop_worker <worker_name_1> [<worker_name_2>]
617617+#
618618+async_stop_worker() {
619619+ setopt localoptions noshwordsplit
620620+621621+ local ret=0 worker k v
622622+ for worker in $@; do
623623+ # Find and unregister the zle handler for the worker
624624+ for k v in ${(@kv)ASYNC_PTYS}; do
625625+ if [[ $v == $worker ]]; then
626626+ zle -F $k
627627+ unset "ASYNC_PTYS[$k]"
628628+ fi
629629+ done
630630+ async_unregister_callback $worker
631631+ zpty -d $worker 2>/dev/null || ret=$?
632632+633633+ # Clear any partial buffers.
634634+ typeset -gA ASYNC_PROCESS_BUFFER
635635+ unset "ASYNC_PROCESS_BUFFER[$worker]"
636636+ done
637637+638638+ return $ret
639639+}
640640+641641+#
642642+# Initialize the required modules for zsh-async. To be called before using the zsh-async library.
643643+#
644644+# usage:
645645+# async_init
646646+#
647647+async_init() {
648648+ (( ASYNC_INIT_DONE )) && return
649649+ typeset -g ASYNC_INIT_DONE=1
650650+651651+ zmodload zsh/zpty
652652+ zmodload zsh/datetime
653653+654654+ # Load is-at-least for reliable version check.
655655+ autoload -Uz is-at-least
656656+657657+ # Check if zsh/zpty returns a file descriptor or not,
658658+ # shell must also be interactive with zle enabled.
659659+ typeset -g ASYNC_ZPTY_RETURNS_FD=0
660660+ [[ -o interactive ]] && [[ -o zle ]] && {
661661+ typeset -h REPLY
662662+ zpty _async_test :
663663+ (( REPLY )) && ASYNC_ZPTY_RETURNS_FD=1
664664+ zpty -d _async_test
665665+ }
666666+}
667667+668668+async() {
669669+ async_init
670670+}
671671+672672+async "$@"
+84
zsh/prompt.zsh
···11+source "$HOME/projects/dotfiles/zsh/async.zsh"
22+33+# init zsh-async
44+async_init
55+66+jj_status() {
77+ template="' (' ++ change_id.shortest(8) ++ ')'"
88+ if repo_root=$(jj root --ignore-working-copy 2>/dev/null); then
99+ jj log -r @ -T $template --no-graph --no-pager --color never --ignore-working-copy -R $repo_root
1010+ fi
1111+}
1212+1313+fetch_data() {
1414+ # refetch data
1515+ date_string=$(date +"%Y-%m-%d %H:%M:%S")
1616+ JJ_ST=$(jj_status)
1717+}
1818+reset_prompt() {
1919+ # reset prompt
2020+ PROMPT="%F{green}%n@%m %F{blue}%~%f$JJ_ST %# "
2121+ # re-render prompt
2222+ zle reset-prompt
2323+}
2424+2525+RUNNING=
2626+update_prompt() {
2727+ if [[ -n $RUNNING ]]; then
2828+ return 0
2929+ fi
3030+ RUNNING=1
3131+3232+ fetch_data
3333+3434+ reset_prompt
3535+3636+ # NOTE: don't make recursive loop. use TMOUT & TRAPALRM for 1s throttling
3737+ # # call fetch job again
3838+ # async_job prompt_worker
3939+ RUNNING=
4040+}
4141+4242+# define status_worker to run asynchronously
4343+async_start_worker prompt_worker -n
4444+async_register_callback prompt_worker update_prompt
4545+async_job prompt_worker
4646+4747+# reset_prompt() {
4848+# # reset prompt
4949+# PROMPT="%F{green}%n@%m %F{blue}%~%f$JJ_ST %# "
5050+# # re-render prompt
5151+# zle reset-prompt
5252+#
5353+# async_job prompt_render_worker
5454+# }
5555+# async_start_worker prompt_render_worker -n
5656+# async_register_callback prompt_render_worker reset_prompt
5757+# async_job prompt_render_worker
5858+5959+# call do_update on every seconds
6060+TMOUT=1
6161+TRAPALRM() {
6262+ async_job prompt_worker
6363+}
6464+6565+# NOTE: don't do heavy jobs here. It will make prompt slower
6666+precmd() {
6767+ PROMPT="%F{green}%n@%m %F{blue}%~%f$JJ_ST %# "
6868+ # call fetch job again
6969+ async_job prompt_worker
7070+}
7171+7272+zle_reset_prompt_widget() {
7373+ # async_job prompt_worker
7474+}
7575+# register it under the name “zle_reset_prompt_widget”
7676+zle -N zle_reset_prompt_widget
7777+autoload -Uz add-zle-hook-widget
7878+add-zle-hook-widget line-init zle_reset_prompt_widget
7979+8080+PROMPT="%n@%m %~%f$JJ_ST %# "
8181+8282+# references:
8383+# - https://medium.com/@henrebotha/how-to-write-an-asynchronous-zsh-prompt-b53e81720d32
8484+# - https://void-shana.moe/posts/customize-your-zsh-prompt