├── .github ├── issue_template.md └── pull_request_template.md ├── INSTALL.md ├── LICENSE ├── README.md ├── dracula.zsh-theme ├── lib └── async.zsh └── screenshot.png /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | > If you're reporting an UI issue, make sure you take a screenshot that shows the actual bug. -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | > If you're fixing a UI issue, make sure you take two screenshots. One that shows the actual bug and another that shows how you fixed it. -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | ### [Zsh](http://zsh.org/) 2 | 3 | #### Install using Git 4 | 5 | If you are a git user, you can install the theme and keep up to date by cloning the repo: 6 | 7 | git clone https://github.com/dracula/zsh.git 8 | 9 | And creating a symbolic link to [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh/)'s theme folder: 10 | 11 | ln -s $DRACULA_THEME/dracula.zsh-theme $OH_MY_ZSH/themes/dracula.zsh-theme 12 | 13 | _P.S.: Remember that you should replace `$DRACULA_THEME` and `$OH_MY_ZSH` with the actual directories for this command to work._ 14 | 15 | #### Install manually 16 | 17 | 1. Download using the [GitHub .zip download](https://github.com/dracula/zsh/archive/master.zip) option and unzip them. 18 | 2. Move `dracula.zsh-theme` file to [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh/)'s theme folder: `oh-my-zsh/themes/dracula.zsh-theme`. 19 | 3. Move `/lib` to [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh/)'s theme folder: `oh-my-zsh/themes/lib`. 20 | 21 | #### Activating theme 22 | 23 | Go to your `~/.zshrc` file and set `ZSH_THEME="dracula"`. 24 | 25 | #### Install using [zplug](https://github.com/zplug/zplug) 26 | 27 | Just add `zplug "dracula/zsh", as:theme` to your `~/.zshrc` file. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dracula Theme 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dracula for [ZSH](http://zsh.org) 2 | 3 | > A dark theme for [ZSH](http://zsh.org). 4 | 5 | ![Screenshot](./screenshot.png) 6 | 7 | ## Install 8 | 9 | All instructions can be found at [draculatheme.com/zsh](https://draculatheme.com/zsh). 10 | 11 | ## Configuration 12 | 13 | ### Disabling Git Segment 14 | 15 | The git segment can be disabled by setting the following in the config: 16 | 17 | ``` 18 | DRACULA_DISPLAY_GIT=0 19 | ``` 20 | 21 | ### Time Segment 22 | 23 | The time segment shows a clock in either a 12 or 24 hour format 24 | based on your locale. To enable it, use the following in your config file: 25 | 26 | ``` 27 | DRACULA_DISPLAY_TIME=1 28 | ``` 29 | 30 | If you want to set the time format, you can set this using the 31 | [strftime(3)](https://manpage.me/?q=strftime) format. For example to use a 24 32 | hour format: 33 | 34 | ``` 35 | DRACULA_TIME_FORMAT="%-H:%M" 36 | ``` 37 | 38 | ### Context Segment 39 | 40 | The context segment shows the username, and, if the user is root or logged in via 41 | SSH, the hostname of the system. To enable this segment, use the following in your config file: 42 | 43 | ``` 44 | DRACULA_DISPLAY_CONTEXT=1 45 | ``` 46 | 47 | ### Directory Segment 48 | 49 | The current working directory is displayed as base name only. To display full 50 | path, use the following in your config file: 51 | 52 | ``` 53 | DRACULA_DISPLAY_FULL_CWD=1 54 | ``` 55 | 56 | ### Status Segment Indicator 57 | 58 | The status segment indicator (the arrow at the beginning), can be changed by setting the `DRACULA_ARROW_ICON` variable. For example, to use an ASCII '->': 59 | 60 | ```sh 61 | DRACULA_ARROW_ICON="-> " 62 | 63 | ``` 64 | 65 | ### Custom Segment 66 | 67 | The custom segment can be changed by setting the `DRACULA_CUSTOM_VARIABLE` environmental variable. 68 | 69 | ```sh 70 | export DRACULA_CUSTOM_VARIABLE=AWS:PROD:EU-WEST-1 71 | ``` 72 | 73 | ### New Line for commands 74 | 75 | You can display a new line for your commands. So you are able to split the terminal infos and the following command in seperate lines. 76 | 77 | ```sh 78 | DRACULA_DISPLAY_NEW_LINE=1 79 | ``` 80 | 81 | ### Git Locking 82 | 83 | This program automatically makes use of git's `--no-optional-locks` option, 84 | and it should automatically detect if your version supports the option. However, 85 | if, for some reason, the automatically detected values are incorrect, you can 86 | forcefully disable or enable the functionality by setting the variable 87 | `DRACULA_GIT_NOLOCK` to 0 or 1, respectively. 88 | 89 | ## Team 90 | 91 | This theme is maintained by the following person(s) and a bunch of [awesome contributors](https://github.com/dracula/zsh/graphs/contributors). 92 | 93 | | [![Avalon Williams](https://avatars0.githubusercontent.com/u/30708886?s=70)](https://github.com/avalonwilliams) | 94 | | --------------------------------------------------------------------------------------------------------------- | 95 | | [Avalon Williams](https://github.com/avalonwilliams) | 96 | 97 | ## Community 98 | 99 | - [Twitter](https://twitter.com/draculatheme) - Best for getting updates about themes and new stuff. 100 | - [GitHub](https://github.com/dracula/dracula-theme/discussions) - Best for asking questions and discussing issues. 101 | - [Discord](https://draculatheme.com/discord-invite) - Best for hanging out with the community. 102 | 103 | ## License 104 | 105 | [MIT License](./LICENSE) 106 | -------------------------------------------------------------------------------- /dracula.zsh-theme: -------------------------------------------------------------------------------- 1 | # -*- mode: sh; -*- 2 | # vim: set ft=sh : 3 | # Dracula Theme v1.2.5 4 | # 5 | # https://github.com/dracula/dracula-theme 6 | # 7 | # Copyright 2019, All rights reserved 8 | # 9 | # Code licensed under the MIT license 10 | # http://zenorocha.mit-license.org 11 | # 12 | # @author Zeno Rocha 13 | # @maintainer Avalon Williams 14 | 15 | # Initialization {{{ 16 | source ${0:A:h}/lib/async.zsh 17 | autoload -Uz add-zsh-hook 18 | setopt PROMPT_SUBST 19 | async_init 20 | PROMPT='' 21 | # }}} 22 | 23 | # Options {{{ 24 | # Set to 0 to disable the git status 25 | DRACULA_DISPLAY_GIT=${DRACULA_DISPLAY_GIT:-1} 26 | 27 | # Set to 1 to show the date 28 | DRACULA_DISPLAY_TIME=${DRACULA_DISPLAY_TIME:-0} 29 | 30 | # Set to 1 to show the 'context' segment 31 | DRACULA_DISPLAY_CONTEXT=${DRACULA_DISPLAY_CONTEXT:-0} 32 | 33 | # Changes the arrow icon 34 | DRACULA_ARROW_ICON=${DRACULA_ARROW_ICON:-➜ } 35 | 36 | # Set to 1 to use a new line for commands 37 | DRACULA_DISPLAY_NEW_LINE=${DRACULA_DISPLAY_NEW_LINE:-0} 38 | 39 | # Set to 1 to show full path of current working directory 40 | DRACULA_DISPLAY_FULL_CWD=${DRACULA_DISPLAY_FULL_CWD:-0} 41 | 42 | # function to detect if git has support for --no-optional-locks 43 | dracula_test_git_optional_lock() { 44 | local git_version=${DEBUG_OVERRIDE_V:-"$(git version | cut -d' ' -f3)"} 45 | local git_version="$(git version | cut -d' ' -f3)" 46 | # test for git versions < 2.14.0 47 | case "$git_version" in 48 | [0-1].*) 49 | echo 0 50 | return 1 51 | ;; 52 | 2.[0-9].*) 53 | echo 0 54 | return 1 55 | ;; 56 | 2.1[0-3].*) 57 | echo 0 58 | return 1 59 | ;; 60 | esac 61 | 62 | # if version > 2.14.0 return true 63 | echo 1 64 | } 65 | 66 | # use --no-optional-locks flag on git 67 | DRACULA_GIT_NOLOCK=${DRACULA_GIT_NOLOCK:-$(dracula_test_git_optional_lock)} 68 | 69 | # time format string 70 | if [[ -z "$DRACULA_TIME_FORMAT" ]]; then 71 | DRACULA_TIME_FORMAT="%-H:%M" 72 | # check if locale uses AM and PM 73 | if locale -ck LC_TIME 2>/dev/null | grep -q '^t_fmt="%r"$'; then 74 | DRACULA_TIME_FORMAT="%-I:%M%p" 75 | fi 76 | fi 77 | # }}} 78 | 79 | # Status segment {{{ 80 | dracula_arrow() { 81 | if [[ "$1" = "start" ]] && (( ! DRACULA_DISPLAY_NEW_LINE )); then 82 | print -P "$DRACULA_ARROW_ICON" 83 | elif [[ "$1" = "end" ]] && (( DRACULA_DISPLAY_NEW_LINE )); then 84 | print -P "\n$DRACULA_ARROW_ICON" 85 | fi 86 | } 87 | 88 | # arrow is green if last command was successful, red if not, 89 | # turns yellow in vi command mode 90 | PROMPT+='%(1V:%F{yellow}:%(?:%F{green}:%F{red}))%B$(dracula_arrow start)' 91 | # }}} 92 | 93 | # Time segment {{{ 94 | dracula_time_segment() { 95 | if (( DRACULA_DISPLAY_TIME )); then 96 | print -P "%D{$DRACULA_TIME_FORMAT} " 97 | fi 98 | } 99 | 100 | PROMPT+='%F{green}%B$(dracula_time_segment)' 101 | # }}} 102 | 103 | # User context segment {{{ 104 | dracula_context() { 105 | if (( DRACULA_DISPLAY_CONTEXT )); then 106 | if [[ -n "${SSH_CONNECTION-}${SSH_CLIENT-}${SSH_TTY-}" ]] || (( EUID == 0 )); then 107 | echo '%n@%m ' 108 | else 109 | echo '%n ' 110 | fi 111 | fi 112 | } 113 | 114 | PROMPT+='%F{magenta}%B$(dracula_context)' 115 | # }}} 116 | 117 | # Directory segment {{{ 118 | dracula_directory() { 119 | if (( DRACULA_DISPLAY_FULL_CWD )); then 120 | print -P '%~ ' 121 | else 122 | print -P '%c ' 123 | fi 124 | } 125 | 126 | PROMPT+='%F{blue}%B$(dracula_directory)' 127 | # }}} 128 | 129 | # Custom variable {{{ 130 | custom_variable_prompt() { 131 | [[ -z "$DRACULA_CUSTOM_VARIABLE" ]] && return 132 | echo "%F{yellow}$DRACULA_CUSTOM_VARIABLE " 133 | } 134 | 135 | PROMPT+='$(custom_variable_prompt)' 136 | # }}} 137 | 138 | # Async git segment {{{ 139 | 140 | dracula_git_status() { 141 | (( ! DRACULA_DISPLAY_GIT )) && return 142 | cd "$1" 143 | 144 | local ref branch lockflag 145 | 146 | (( DRACULA_GIT_NOLOCK )) && lockflag="--no-optional-locks" 147 | 148 | ref=$(=git $lockflag symbolic-ref --quiet HEAD 2>/dev/null) 149 | 150 | case $? in 151 | 0) ;; 152 | 128) return ;; 153 | *) ref=$(=git $lockflag rev-parse --short HEAD 2>/dev/null) || return ;; 154 | esac 155 | 156 | branch=${ref#refs/heads/} 157 | 158 | if [[ -n $branch ]]; then 159 | echo -n "${ZSH_THEME_GIT_PROMPT_PREFIX}${branch}" 160 | 161 | local git_status icon 162 | git_status="$(LC_ALL=C =git $lockflag status 2>&1)" 163 | 164 | if [[ "$git_status" =~ 'new file:|deleted:|modified:|renamed:|Untracked files:' ]]; then 165 | echo -n "$ZSH_THEME_GIT_PROMPT_DIRTY" 166 | else 167 | echo -n "$ZSH_THEME_GIT_PROMPT_CLEAN" 168 | fi 169 | 170 | echo -n "$ZSH_THEME_GIT_PROMPT_SUFFIX" 171 | fi 172 | } 173 | 174 | dracula_git_callback() { 175 | DRACULA_GIT_STATUS="$3" 176 | zle && zle reset-prompt 177 | async_stop_worker dracula_git_worker dracula_git_status "$(pwd)" 178 | } 179 | 180 | dracula_git_async() { 181 | async_start_worker dracula_git_worker -n 182 | async_register_callback dracula_git_worker dracula_git_callback 183 | async_job dracula_git_worker dracula_git_status "$(pwd)" 184 | } 185 | 186 | add-zsh-hook precmd dracula_git_async 187 | 188 | PROMPT+='$DRACULA_GIT_STATUS' 189 | 190 | ZSH_THEME_GIT_PROMPT_CLEAN=") %F{green}%B✔ " 191 | ZSH_THEME_GIT_PROMPT_DIRTY=") %F{yellow}%B✗ " 192 | ZSH_THEME_GIT_PROMPT_PREFIX="%F{cyan}%B(" 193 | ZSH_THEME_GIT_PROMPT_SUFFIX="%f%b" 194 | # }}} 195 | 196 | # Linebreak {{{ 197 | PROMPT+='%(1V:%F{yellow}:%(?:%F{green}:%F{red}))%B$(dracula_arrow end)' 198 | # }}} 199 | 200 | # define widget without clobbering old definitions 201 | dracula_defwidget() { 202 | local fname=dracula-wrap-$1 203 | local prev=($(zle -l -L "$1")) 204 | local oldfn=${prev[4]:-$1} 205 | 206 | # if no existing zle functions, just define it normally 207 | if [[ -z "$prev" ]]; then 208 | zle -N $1 $2 209 | return 210 | fi 211 | 212 | # if already defined, return 213 | [[ "${prev[4]}" = $fname ]] && return 214 | 215 | oldfn=${prev[4]:-$1} 216 | 217 | zle -N dracula-old-$oldfn $oldfn 218 | 219 | eval "$fname() { $2 \"\$@\"; zle dracula-old-$oldfn -- \"\$@\"; }" 220 | 221 | zle -N $1 $fname 222 | } 223 | 224 | # ensure vi mode is handled by prompt 225 | dracula_zle_update() { 226 | if [[ $KEYMAP = vicmd ]]; then 227 | psvar[1]=vicmd 228 | else 229 | psvar[1]='' 230 | fi 231 | 232 | zle reset-prompt 233 | zle -R 234 | } 235 | 236 | dracula_defwidget zle-line-init dracula_zle_update 237 | dracula_defwidget zle-keymap-select dracula_zle_update 238 | 239 | # Ensure effects are reset 240 | PROMPT+='%f%b' 241 | -------------------------------------------------------------------------------- /lib/async.zsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # 4 | # zsh-async 5 | # 6 | # version: v1.8.5 7 | # author: Mathias Fredriksson 8 | # url: https://github.com/mafredri/zsh-async 9 | # 10 | 11 | typeset -g ASYNC_VERSION=1.8.5 12 | # Produce debug output from zsh-async when set to 1. 13 | typeset -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 | # Rename job to _async_eval and redirect all eval output to cat running 19 | # in _async_job. Here, stdout and stderr are not separated for 20 | # simplicity, this could be improved in the future. 21 | { 22 | eval "$@" 23 | } &> >(ASYNC_JOB_NAME=[async/eval] _async_job 'command -p cat') 24 | } 25 | 26 | # Wrapper for jobs executed by the async worker, gives output in parseable format with execution time 27 | _async_job() { 28 | # Disable xtrace as it would mangle the output. 29 | setopt localoptions noxtrace 30 | 31 | # Store start time for job. 32 | float -F duration=$EPOCHREALTIME 33 | 34 | # Run the command and capture both stdout (`eval`) and stderr (`cat`) in 35 | # separate subshells. When the command is complete, we grab write lock 36 | # (mutex token) and output everything except stderr inside the command 37 | # block, after the command block has completed, the stdin for `cat` is 38 | # closed, causing stderr to be appended with a $'\0' at the end to mark the 39 | # end of output from this job. 40 | local jobname=${ASYNC_JOB_NAME:-$1} out 41 | out="$( 42 | local stdout stderr ret tok 43 | { 44 | stdout=$(eval "$@") 45 | ret=$? 46 | duration=$(( EPOCHREALTIME - duration )) # Calculate duration. 47 | 48 | print -r -n - $'\0'${(q)jobname} $ret ${(q)stdout} $duration 49 | } 2> >(stderr=$(command -p cat) && print -r -n - " "${(q)stderr}$'\0') 50 | )" 51 | if [[ $out != $'\0'*$'\0' ]]; then 52 | # Corrupted output (aborted job?), skipping. 53 | return 54 | fi 55 | 56 | # Grab mutex lock, stalls until token is available. 57 | read -r -k 1 -p tok || return 1 58 | 59 | # Return output ( ). 60 | print -r -n - "$out" 61 | 62 | # Unlock mutex by inserting a token. 63 | print -n -p $tok 64 | } 65 | 66 | # The background worker manages all tasks and runs them without interfering with other processes 67 | _async_worker() { 68 | # Reset all options to defaults inside async worker. 69 | emulate -R zsh 70 | 71 | # Make sure monitor is unset to avoid printing the 72 | # pids of child processes. 73 | unsetopt monitor 74 | 75 | # Redirect stderr to `/dev/null` in case unforseen errors produced by the 76 | # worker. For example: `fork failed: resource temporarily unavailable`. 77 | # Some older versions of zsh might also print malloc errors (know to happen 78 | # on at least zsh 5.0.2 and 5.0.8) likely due to kill signals. 79 | exec 2>/dev/null 80 | 81 | # When a zpty is deleted (using -d) all the zpty instances created before 82 | # the one being deleted receive a SIGHUP, unless we catch it, the async 83 | # worker would simply exit (stop working) even though visible in the list 84 | # of zpty's (zpty -L). This has been fixed around the time of Zsh 5.4 85 | # (not released). 86 | if ! is-at-least 5.4.1; then 87 | TRAPHUP() { 88 | return 0 # Return 0, indicating signal was handled. 89 | } 90 | fi 91 | 92 | local -A storage 93 | local unique=0 94 | local notify_parent=0 95 | local parent_pid=0 96 | local coproc_pid=0 97 | local processing=0 98 | 99 | local -a zsh_hooks zsh_hook_functions 100 | zsh_hooks=(chpwd periodic precmd preexec zshexit zshaddhistory) 101 | zsh_hook_functions=(${^zsh_hooks}_functions) 102 | unfunction $zsh_hooks &>/dev/null # Deactivate all zsh hooks inside the worker. 103 | unset $zsh_hook_functions # And hooks with registered functions. 104 | unset zsh_hooks zsh_hook_functions # Cleanup. 105 | 106 | close_idle_coproc() { 107 | local -a pids 108 | pids=(${${(v)jobstates##*:*:}%\=*}) 109 | 110 | # If coproc (cat) is the only child running, we close it to avoid 111 | # leaving it running indefinitely and cluttering the process tree. 112 | if (( ! processing )) && [[ $#pids = 1 ]] && [[ $coproc_pid = $pids[1] ]]; then 113 | coproc : 114 | coproc_pid=0 115 | fi 116 | } 117 | 118 | child_exit() { 119 | close_idle_coproc 120 | 121 | # On older version of zsh (pre 5.2) we notify the parent through a 122 | # SIGWINCH signal because `zpty` did not return a file descriptor (fd) 123 | # prior to that. 124 | if (( notify_parent )); then 125 | # We use SIGWINCH for compatibility with older versions of zsh 126 | # (pre 5.1.1) where other signals (INFO, ALRM, USR1, etc.) could 127 | # cause a deadlock in the shell under certain circumstances. 128 | kill -WINCH $parent_pid 129 | fi 130 | } 131 | 132 | # Register a SIGCHLD trap to handle the completion of child processes. 133 | trap child_exit CHLD 134 | 135 | # Process option parameters passed to worker. 136 | while getopts "np:uz" opt; do 137 | case $opt in 138 | n) notify_parent=1;; 139 | p) parent_pid=$OPTARG;; 140 | u) unique=1;; 141 | z) notify_parent=0;; # Uses ZLE watcher instead. 142 | esac 143 | done 144 | 145 | # Terminate all running jobs, note that this function does not 146 | # reinstall the child trap. 147 | terminate_jobs() { 148 | trap - CHLD # Ignore child exits during kill. 149 | coproc : # Quit coproc. 150 | coproc_pid=0 # Reset pid. 151 | 152 | if is-at-least 5.4.1; then 153 | trap '' HUP # Catch the HUP sent to this process. 154 | kill -HUP -$$ # Send to entire process group. 155 | trap - HUP # Disable HUP trap. 156 | else 157 | # We already handle HUP for Zsh < 5.4.1. 158 | kill -HUP -$$ # Send to entire process group. 159 | fi 160 | } 161 | 162 | killjobs() { 163 | local tok 164 | local -a pids 165 | pids=(${${(v)jobstates##*:*:}%\=*}) 166 | 167 | # No need to send SIGHUP if no jobs are running. 168 | (( $#pids == 0 )) && continue 169 | (( $#pids == 1 )) && [[ $coproc_pid = $pids[1] ]] && continue 170 | 171 | # Grab lock to prevent half-written output in case a child 172 | # process is in the middle of writing to stdin during kill. 173 | (( coproc_pid )) && read -r -k 1 -p tok 174 | 175 | terminate_jobs 176 | trap child_exit CHLD # Reinstall child trap. 177 | } 178 | 179 | local request do_eval=0 180 | local -a cmd 181 | while :; do 182 | # Wait for jobs sent by async_job. 183 | read -r -d $'\0' request || { 184 | # Unknown error occurred while reading from stdin, the zpty 185 | # worker is likely in a broken state, so we shut down. 186 | terminate_jobs 187 | 188 | # Stdin is broken and in case this was an unintended 189 | # crash, we try to report it as a last hurrah. 190 | print -r -n $'\0'"'[async]'" $(( 127 + 3 )) "''" 0 "'$0:$LINENO: zpty fd died, exiting'"$'\0' 191 | 192 | # We use `return` to abort here because using `exit` may 193 | # result in an infinite loop that never exits and, as a 194 | # result, high CPU utilization. 195 | return $(( 127 + 1 )) 196 | } 197 | 198 | # We need to clean the input here because sometimes when a zpty 199 | # has died and been respawned, messages will be prefixed with a 200 | # carraige return (\r, or \C-M). 201 | request=${request#$'\C-M'} 202 | 203 | # Check for non-job commands sent to worker 204 | case $request in 205 | _killjobs) killjobs; continue;; 206 | _async_eval*) do_eval=1;; 207 | esac 208 | 209 | # Parse the request using shell parsing (z) to allow commands 210 | # to be parsed from single strings and multi-args alike. 211 | cmd=("${(z)request}") 212 | 213 | # Name of the job (first argument). 214 | local job=$cmd[1] 215 | 216 | # Check if a worker should perform unique jobs, unless 217 | # this is an eval since they run synchronously. 218 | if (( !do_eval )) && (( unique )); then 219 | # Check if a previous job is still running, if yes, 220 | # skip this job and let the previous one finish. 221 | for pid in ${${(v)jobstates##*:*:}%\=*}; do 222 | if [[ ${storage[$job]} == $pid ]]; then 223 | continue 2 224 | fi 225 | done 226 | fi 227 | 228 | # Guard against closing coproc from trap before command has started. 229 | processing=1 230 | 231 | # Because we close the coproc after the last job has completed, we must 232 | # recreate it when there are no other jobs running. 233 | if (( ! coproc_pid )); then 234 | # Use coproc as a mutex for synchronized output between children. 235 | coproc command -p cat 236 | coproc_pid="$!" 237 | # Insert token into coproc 238 | print -n -p "t" 239 | fi 240 | 241 | if (( do_eval )); then 242 | shift cmd # Strip _async_eval from cmd. 243 | _async_eval $cmd 244 | else 245 | # Run job in background, completed jobs are printed to stdout. 246 | _async_job $cmd & 247 | # Store pid because zsh job manager is extremely unflexible (show jobname as non-unique '$job')... 248 | storage[$job]="$!" 249 | fi 250 | 251 | processing=0 # Disable guard. 252 | 253 | if (( do_eval )); then 254 | do_eval=0 255 | 256 | # When there are no active jobs we can't rely on the CHLD trap to 257 | # manage the coproc lifetime. 258 | close_idle_coproc 259 | fi 260 | done 261 | } 262 | 263 | # 264 | # Get results from finished jobs and pass it to the to callback function. This is the only way to reliably return the 265 | # job name, return code, output and execution time and with minimal effort. 266 | # 267 | # If the async process buffer becomes corrupt, the callback will be invoked with the first argument being `[async]` (job 268 | # name), non-zero return code and fifth argument describing the error (stderr). 269 | # 270 | # usage: 271 | # async_process_results 272 | # 273 | # callback_function is called with the following parameters: 274 | # $1 = job name, e.g. the function passed to async_job 275 | # $2 = return code 276 | # $3 = resulting stdout from execution 277 | # $4 = execution time, floating point e.g. 2.05 seconds 278 | # $5 = resulting stderr from execution 279 | # $6 = has next result in buffer (0 = buffer empty, 1 = yes) 280 | # 281 | async_process_results() { 282 | setopt localoptions unset noshwordsplit noksharrays noposixidentifiers noposixstrings 283 | 284 | local worker=$1 285 | local callback=$2 286 | local caller=$3 287 | local -a items 288 | local null=$'\0' data 289 | integer -l len pos num_processed has_next 290 | 291 | typeset -gA ASYNC_PROCESS_BUFFER 292 | 293 | # Read output from zpty and parse it if available. 294 | while zpty -r -t $worker data 2>/dev/null; do 295 | ASYNC_PROCESS_BUFFER[$worker]+=$data 296 | len=${#ASYNC_PROCESS_BUFFER[$worker]} 297 | pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter). 298 | 299 | # Keep going until we find a NULL-character. 300 | if (( ! len )) || (( pos > len )); then 301 | continue 302 | fi 303 | 304 | while (( pos <= len )); do 305 | # Take the content from the beginning, until the NULL-character and 306 | # perform shell parsing (z) and unquoting (Q) as an array (@). 307 | items=("${(@Q)${(z)ASYNC_PROCESS_BUFFER[$worker][1,$pos-1]}}") 308 | 309 | # Remove the extracted items from the buffer. 310 | ASYNC_PROCESS_BUFFER[$worker]=${ASYNC_PROCESS_BUFFER[$worker][$pos+1,$len]} 311 | 312 | len=${#ASYNC_PROCESS_BUFFER[$worker]} 313 | if (( len > 1 )); then 314 | pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter). 315 | fi 316 | 317 | has_next=$(( len != 0 )) 318 | if (( $#items == 5 )); then 319 | items+=($has_next) 320 | $callback "${(@)items}" # Send all parsed items to the callback. 321 | (( num_processed++ )) 322 | elif [[ -z $items ]]; then 323 | # Empty items occur between results due to double-null ($'\0\0') 324 | # caused by commands being both pre and suffixed with null. 325 | else 326 | # In case of corrupt data, invoke callback with *async* as job 327 | # name, non-zero exit status and an error message on stderr. 328 | $callback "[async]" 1 "" 0 "$0:$LINENO: error: bad format, got ${#items} items (${(q)items})" $has_next 329 | fi 330 | done 331 | done 332 | 333 | (( num_processed )) && return 0 334 | 335 | # Avoid printing exit value when `setopt printexitvalue` is active.` 336 | [[ $caller = trap || $caller = watcher ]] && return 0 337 | 338 | # No results were processed 339 | return 1 340 | } 341 | 342 | # Watch worker for output 343 | _async_zle_watcher() { 344 | setopt localoptions noshwordsplit 345 | typeset -gA ASYNC_PTYS ASYNC_CALLBACKS 346 | local worker=$ASYNC_PTYS[$1] 347 | local callback=$ASYNC_CALLBACKS[$worker] 348 | 349 | if [[ -n $2 ]]; then 350 | # from man zshzle(1): 351 | # `hup' for a disconnect, `nval' for a closed or otherwise 352 | # invalid descriptor, or `err' for any other condition. 353 | # Systems that support only the `select' system call always use 354 | # `err'. 355 | 356 | # this has the side effect to unregister the broken file descriptor 357 | async_stop_worker $worker 358 | 359 | if [[ -n $callback ]]; then 360 | $callback '[async]' 2 "" 0 "$0:$LINENO: error: fd for $worker failed: zle -F $1 returned error $2" 0 361 | fi 362 | return 363 | fi; 364 | 365 | if [[ -n $callback ]]; then 366 | async_process_results $worker $callback watcher 367 | fi 368 | } 369 | 370 | _async_send_job() { 371 | setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings 372 | 373 | local caller=$1 374 | local worker=$2 375 | shift 2 376 | 377 | zpty -t $worker &>/dev/null || { 378 | typeset -gA ASYNC_CALLBACKS 379 | local callback=$ASYNC_CALLBACKS[$worker] 380 | 381 | if [[ -n $callback ]]; then 382 | $callback '[async]' 3 "" 0 "$0:$LINENO: error: no such worker: $worker" 0 383 | else 384 | print -u2 "$caller: no such async worker: $worker" 385 | fi 386 | return 1 387 | } 388 | 389 | zpty -w $worker "$@"$'\0' 390 | } 391 | 392 | # 393 | # Start a new asynchronous job on specified worker, assumes the worker is running. 394 | # 395 | # Note if you are using a function for the job, it must have been defined before the worker was 396 | # started or you will get a `command not found` error. 397 | # 398 | # usage: 399 | # async_job [] 400 | # 401 | async_job() { 402 | setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings 403 | 404 | local worker=$1; shift 405 | 406 | local -a cmd 407 | cmd=("$@") 408 | if (( $#cmd > 1 )); then 409 | cmd=(${(q)cmd}) # Quote special characters in multi argument commands. 410 | fi 411 | 412 | _async_send_job $0 $worker "$cmd" 413 | } 414 | 415 | # 416 | # Evaluate a command (like async_job) inside the async worker, then worker environment can be manipulated. For example, 417 | # issuing a cd command will change the PWD of the worker which will then be inherited by all future async jobs. 418 | # 419 | # Output will be returned via callback, job name will be [async/eval]. 420 | # 421 | # usage: 422 | # async_worker_eval [] 423 | # 424 | async_worker_eval() { 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 | # Quote the cmd in case RC_EXPAND_PARAM is set. 436 | _async_send_job $0 $worker "_async_eval $cmd" 437 | } 438 | 439 | # This function traps notification signals and calls all registered callbacks 440 | _async_notify_trap() { 441 | setopt localoptions noshwordsplit 442 | 443 | local k 444 | for k in ${(k)ASYNC_CALLBACKS}; do 445 | async_process_results $k ${ASYNC_CALLBACKS[$k]} trap 446 | done 447 | } 448 | 449 | # 450 | # Register a callback for completed jobs. As soon as a job is finnished, async_process_results will be called with the 451 | # specified callback function. This requires that a worker is initialized with the -n (notify) option. 452 | # 453 | # usage: 454 | # async_register_callback 455 | # 456 | async_register_callback() { 457 | setopt localoptions noshwordsplit nolocaltraps 458 | 459 | typeset -gA ASYNC_PTYS ASYNC_CALLBACKS 460 | local worker=$1; shift 461 | 462 | ASYNC_CALLBACKS[$worker]="$*" 463 | 464 | # Enable trap when the ZLE watcher is unavailable, allows 465 | # workers to notify (via -n) when a job is done. 466 | if [[ ! -o interactive ]] || [[ ! -o zle ]]; then 467 | trap '_async_notify_trap' WINCH 468 | elif [[ -o interactive ]] && [[ -o zle ]]; then 469 | local fd w 470 | for fd w in ${(@kv)ASYNC_PTYS}; do 471 | if [[ $w == $worker ]]; then 472 | zle -F $fd _async_zle_watcher # Register the ZLE handler. 473 | break 474 | fi 475 | done 476 | fi 477 | } 478 | 479 | # 480 | # Unregister the callback for a specific worker. 481 | # 482 | # usage: 483 | # async_unregister_callback 484 | # 485 | async_unregister_callback() { 486 | typeset -gA ASYNC_CALLBACKS 487 | 488 | unset "ASYNC_CALLBACKS[$1]" 489 | } 490 | 491 | # 492 | # Flush all current jobs running on a worker. This will terminate any and all running processes under the worker, use 493 | # with caution. 494 | # 495 | # usage: 496 | # async_flush_jobs 497 | # 498 | async_flush_jobs() { 499 | setopt localoptions noshwordsplit 500 | 501 | local worker=$1; shift 502 | 503 | # Check if the worker exists 504 | zpty -t $worker &>/dev/null || return 1 505 | 506 | # Send kill command to worker 507 | async_job $worker "_killjobs" 508 | 509 | # Clear the zpty buffer. 510 | local junk 511 | if zpty -r -t $worker junk '*'; then 512 | (( ASYNC_DEBUG )) && print -n "async_flush_jobs $worker: ${(V)junk}" 513 | while zpty -r -t $worker junk '*'; do 514 | (( ASYNC_DEBUG )) && print -n "${(V)junk}" 515 | done 516 | (( ASYNC_DEBUG )) && print 517 | fi 518 | 519 | # Finally, clear the process buffer in case of partially parsed responses. 520 | typeset -gA ASYNC_PROCESS_BUFFER 521 | unset "ASYNC_PROCESS_BUFFER[$worker]" 522 | } 523 | 524 | # 525 | # Start a new async worker with optional parameters, a worker can be told to only run unique tasks and to notify a 526 | # process when tasks are complete. 527 | # 528 | # usage: 529 | # async_start_worker [-u] [-n] [-p ] 530 | # 531 | # opts: 532 | # -u unique (only unique job names can run) 533 | # -n notify through SIGWINCH signal 534 | # -p pid to notify (defaults to current pid) 535 | # 536 | async_start_worker() { 537 | setopt localoptions noshwordsplit noclobber 538 | 539 | local worker=$1; shift 540 | local -a args 541 | args=("$@") 542 | zpty -t $worker &>/dev/null && return 543 | 544 | typeset -gA ASYNC_PTYS 545 | typeset -h REPLY 546 | typeset has_xtrace=0 547 | 548 | if [[ -o interactive ]] && [[ -o zle ]]; then 549 | # Inform the worker to ignore the notify flag and that we're 550 | # using a ZLE watcher instead. 551 | args+=(-z) 552 | 553 | if (( ! ASYNC_ZPTY_RETURNS_FD )); then 554 | # When zpty doesn't return a file descriptor (on older versions of zsh) 555 | # we try to guess it anyway. 556 | integer -l zptyfd 557 | exec {zptyfd}>&1 # Open a new file descriptor (above 10). 558 | exec {zptyfd}>&- # Close it so it's free to be used by zpty. 559 | fi 560 | fi 561 | 562 | # Workaround for stderr in the main shell sometimes (incorrectly) being 563 | # reassigned to /dev/null by the reassignment done inside the async 564 | # worker. 565 | # See https://github.com/mafredri/zsh-async/issues/35. 566 | integer errfd=-1 567 | 568 | # Redirect of errfd is broken on zsh 5.0.2. 569 | if is-at-least 5.0.8; then 570 | exec {errfd}>&2 571 | fi 572 | 573 | # Make sure async worker is started without xtrace 574 | # (the trace output interferes with the worker). 575 | [[ -o xtrace ]] && { 576 | has_xtrace=1 577 | unsetopt xtrace 578 | } 579 | 580 | if (( errfd != -1 )); then 581 | zpty -b $worker _async_worker -p $$ $args 2>&$errfd 582 | else 583 | zpty -b $worker _async_worker -p $$ $args 584 | fi 585 | local ret=$? 586 | 587 | # Re-enable it if it was enabled, for debugging. 588 | (( has_xtrace )) && setopt xtrace 589 | (( errfd != -1 )) && exec {errfd}>& - 590 | 591 | if (( ret )); then 592 | async_stop_worker $worker 593 | return 1 594 | fi 595 | 596 | if ! is-at-least 5.0.8; then 597 | # For ZSH versions older than 5.0.8 we delay a bit to give 598 | # time for the worker to start before issuing commands, 599 | # otherwise it will not be ready to receive them. 600 | sleep 0.001 601 | fi 602 | 603 | if [[ -o interactive ]] && [[ -o zle ]]; then 604 | if (( ! ASYNC_ZPTY_RETURNS_FD )); then 605 | REPLY=$zptyfd # Use the guessed value for the file desciptor. 606 | fi 607 | 608 | ASYNC_PTYS[$REPLY]=$worker # Map the file desciptor to the worker. 609 | fi 610 | } 611 | 612 | # 613 | # Stop one or multiple workers that are running, all unfetched and incomplete work will be lost. 614 | # 615 | # usage: 616 | # async_stop_worker [] 617 | # 618 | async_stop_worker() { 619 | setopt localoptions noshwordsplit 620 | 621 | local ret=0 worker k v 622 | for worker in $@; do 623 | # Find and unregister the zle handler for the worker 624 | for k v in ${(@kv)ASYNC_PTYS}; do 625 | if [[ $v == $worker ]]; then 626 | zle -F $k 627 | unset "ASYNC_PTYS[$k]" 628 | fi 629 | done 630 | async_unregister_callback $worker 631 | zpty -d $worker 2>/dev/null || ret=$? 632 | 633 | # Clear any partial buffers. 634 | typeset -gA ASYNC_PROCESS_BUFFER 635 | unset "ASYNC_PROCESS_BUFFER[$worker]" 636 | done 637 | 638 | return $ret 639 | } 640 | 641 | # 642 | # Initialize the required modules for zsh-async. To be called before using the zsh-async library. 643 | # 644 | # usage: 645 | # async_init 646 | # 647 | async_init() { 648 | (( ASYNC_INIT_DONE )) && return 649 | typeset -g ASYNC_INIT_DONE=1 650 | 651 | zmodload zsh/zpty 652 | zmodload zsh/datetime 653 | 654 | # Load is-at-least for reliable version check. 655 | autoload -Uz is-at-least 656 | 657 | # Check if zsh/zpty returns a file descriptor or not, 658 | # shell must also be interactive with zle enabled. 659 | typeset -g ASYNC_ZPTY_RETURNS_FD=0 660 | [[ -o interactive ]] && [[ -o zle ]] && { 661 | typeset -h REPLY 662 | zpty _async_test : 663 | (( REPLY )) && ASYNC_ZPTY_RETURNS_FD=1 664 | zpty -d _async_test 665 | } 666 | } 667 | 668 | async() { 669 | async_init 670 | } 671 | 672 | async "$@" 673 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dracula/zsh/75ea3f5e1055291caf56b4aea6a5d58d00541c41/screenshot.png --------------------------------------------------------------------------------