Pretty, minimal and fast ZSH prompt; nix and jj ified
0

Configure Feed

Select the types of activity you want to include in your feed.

impure / async.zsh
20 kB 695 lines
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 "$@"