diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 0000000..6e09db0 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,34 @@ + + +### General information + +- Pure version: 1.x.x +- ZSH version: 5.x.x +- Terminal program & version: +- Operating system: +- ZSH framework: + +I have: +- [ ] Tested with another terminal program and can reproduce the issue: +- [ ] Followed the [Integration](https://github.com/sindresorhus/pure#integration) instructions for my framework + +### Problem description + + +### Reproduction steps + +1. +2. +3. + +### My `.zshrc`: + + + +```shell +autoload -U promptinit; promptinit +prompt pure +``` diff --git a/async.zsh b/async.zsh index c891f11..c1a4f68 100644 --- a/async.zsh +++ b/async.zsh @@ -3,96 +3,160 @@ # # zsh-async # -# version: 1.1.0 +# version: 1.5.0 # author: Mathias Fredriksson # url: https://github.com/mafredri/zsh-async # +# Produce debug output from zsh-async when set to 1. +ASYNC_DEBUG=${ASYNC_DEBUG:-0} + # Wrapper for jobs executed by the async worker, gives output in parseable format with execution time _async_job() { + # Disable xtrace as it would mangle the output. + setopt localoptions noxtrace + # Store start time as double precision (+E disables scientific notation) float -F duration=$EPOCHREALTIME - # Run the command - # - # What is happening here is that we are assigning stdout, stderr and ret to - # variables, and then we are printing out the variable assignment through - # typeset -p. This way when we run eval we get something along the lines of: - # eval " - # typeset stdout=' M async.test.sh\n M async.zsh' - # typeset ret=0 - # typeset stderr='' - # " - unset stdout stderr ret - eval "$( - { - stdout=$(eval "$@") - ret=$? - typeset -p stdout ret - } 2> >(stderr=$(cat); typeset -p stderr) - )" + # Run the command and capture both stdout (`eval`) and stderr (`cat`) in + # separate subshells. When the command is complete, we grab write lock + # (mutex token) and output everything except stderr inside the command + # block, after the command block has completed, the stdin for `cat` is + # closed, causing stderr to be appended with a $'\0' at the end to mark the + # end of output from this job. + local stdout stderr ret tok + { + stdout=$(eval "$@") + ret=$? + duration=$(( EPOCHREALTIME - duration )) # Calculate duration. - # Calculate duration - duration=$(( EPOCHREALTIME - duration )) + # Grab mutex lock, stalls until token is available. + read -r -k 1 -p tok || exit 1 - # stip all null-characters from stdout and stderr - stdout=${stdout//$'\0'/} - stderr=${stderr//$'\0'/} + # Return output ( ). + print -r -n - ${(q)1} $ret ${(q)stdout} $duration + } 2> >(stderr=$(cat) && print -r -n - " "${(q)stderr}$'\0') - # if ret is missing for some unknown reason, set it to -1 to indicate we - # have run into a bug - ret=${ret:--1} - - # Grab mutex lock - read -ep >/dev/null - - # return output ( ) - print -r -N -n -- "$1" "$ret" "$stdout" "$duration" "$stderr"$'\0' - - # Unlock mutex - print -p "t" + # Unlock mutex by inserting a token. + print -n -p $tok } # The background worker manages all tasks and runs them without interfering with other processes _async_worker() { + # Reset all options to defaults inside async worker. + emulate -R zsh + + # Make sure monitor is unset to avoid printing the + # pids of child processes. + unsetopt monitor + + # Redirect stderr to `/dev/null` in case unforseen errors produced by the + # worker. For example: `fork failed: resource temporarily unavailable`. + # Some older versions of zsh might also print malloc errors (know to happen + # on at least zsh 5.0.2 and 5.0.8) likely due to kill signals. + exec 2>/dev/null + + # When a zpty is deleted (using -d) all the zpty instances created before + # the one being deleted receive a SIGHUP, unless we catch it, the async + # worker would simply exit (stop working) even though visible in the list + # of zpty's (zpty -L). + TRAPHUP() { + return 0 # Return 0, indicating signal was handled. + } + local -A storage local unique=0 + local notify_parent=0 + local parent_pid=0 + local coproc_pid=0 + local processing=0 + + local -a zsh_hooks zsh_hook_functions + zsh_hooks=(chpwd periodic precmd preexec zshexit zshaddhistory) + zsh_hook_functions=(${^zsh_hooks}_functions) + unfunction $zsh_hooks &>/dev/null # Deactivate all zsh hooks inside the worker. + unset $zsh_hook_functions # And hooks with registered functions. + unset zsh_hooks zsh_hook_functions # Cleanup. + + child_exit() { + local -a pids + pids=(${${(v)jobstates##*:*:}%\=*}) + + # If coproc (cat) is the only child running, we close it to avoid + # leaving it running indefinitely and cluttering the process tree. + if (( ! processing )) && [[ $#pids = 1 ]] && [[ $coproc_pid = $pids[1] ]]; then + coproc : + coproc_pid=0 + fi + + # On older version of zsh (pre 5.2) we notify the parent through a + # SIGWINCH signal because `zpty` did not return a file descriptor (fd) + # prior to that. + if (( notify_parent )); then + # We use SIGWINCH for compatibility with older versions of zsh + # (pre 5.1.1) where other signals (INFO, ALRM, USR1, etc.) could + # cause a deadlock in the shell under certain circumstances. + kill -WINCH $parent_pid + fi + } + + # Register a SIGCHLD trap to handle the completion of child processes. + trap child_exit CHLD # Process option parameters passed to worker while getopts "np:u" opt; do case $opt in - # Use SIGWINCH since many others seem to cause zsh to freeze, e.g. ALRM, INFO, etc. - n) trap 'kill -WINCH $ASYNC_WORKER_PARENT_PID' CHLD;; - p) ASYNC_WORKER_PARENT_PID=$OPTARG;; + n) notify_parent=1;; + p) parent_pid=$OPTARG;; u) unique=1;; esac done - # Create a mutex for writing to the terminal through coproc - coproc cat - # Insert token into coproc - print -p "t" + killjobs() { + local tok + local -a pids + pids=(${${(v)jobstates##*:*:}%\=*}) - while read -r cmd; do - # Separate on spaces into an array - cmd=(${=cmd}) - local job=$cmd[1] + # No need to send SIGHUP if no jobs are running. + (( $#pids == 0 )) && continue + (( $#pids == 1 )) && [[ $coproc_pid = $pids[1] ]] && continue + + # Grab lock to prevent half-written output in case a child + # process is in the middle of writing to stdin during kill. + (( coproc_pid )) && read -r -k 1 -p tok + + kill -HUP -$$ # Send to entire process group. + coproc : # Quit coproc. + coproc_pid=0 # Reset pid. + } + + local request + local -a cmd + while :; do + # Wait for jobs sent by async_job. + read -r -d $'\0' request || { + # Since we handle SIGHUP above (and thus do not know when `zpty -d`) + # occurs, a failure to read probably indicates that stdin has + # closed. This is why we propagate the signal to all children and + # exit manually. + kill -HUP -$$ # Send SIGHUP to all jobs. + exit 0 + } # Check for non-job commands sent to worker - case $job in - _unset_trap) - trap - CHLD; continue;; - _killjobs) - # Do nothing in the worker when receiving the TERM signal - trap '' TERM - # Send TERM to the entire process group (PID and all children) - kill -TERM -$$ &>/dev/null - # Reset trap - trap - TERM - continue - ;; + case $request in + _unset_trap) notify_parent=0; continue;; + _killjobs) killjobs; continue;; esac + # Parse the request using shell parsing (z) to allow commands + # to be parsed from single strings and multi-args alike. + cmd=("${(z)request}") + + # Name of the job (first argument). + local job=$cmd[1] + # If worker should perform unique jobs if (( unique )); then # Check if a previous job is still running, if yes, let it finnish @@ -103,10 +167,25 @@ _async_worker() { done fi - # Run task in background + # Guard against closing coproc from trap before command has started. + processing=1 + + # Because we close the coproc after the last job has completed, we must + # recreate it when there are no other jobs running. + if (( ! coproc_pid )); then + # Use coproc as a mutex for synchronized output between children. + coproc cat + coproc_pid="$!" + # Insert token into coproc + print -n -p "t" + fi + + # Run job in background, completed jobs are printed to stdout. _async_job $cmd & # Store pid because zsh job manager is extremely unflexible (show jobname as non-unique '$job')... - storage[$job]=$! + storage[$job]="$!" + + processing=0 # Disable guard. done } @@ -127,38 +206,55 @@ _async_worker() { async_process_results() { setopt localoptions noshwordsplit - integer count=0 local worker=$1 local callback=$2 + local caller=$3 local -a items - local IFS=$'\0' + local null=$'\0' data + integer -l len pos num_processed typeset -gA ASYNC_PROCESS_BUFFER - # Read output from zpty and parse it if available - while zpty -rt $worker line 2>/dev/null; do - # Remove unwanted \r from output - ASYNC_PROCESS_BUFFER[$worker]+=${line//$'\r'$'\n'/$'\n'} - # Split buffer on null characters, preserve empty elements - items=("${(@)=ASYNC_PROCESS_BUFFER[$worker]}") - # Remove last element since it's due to the return string separator structure - items=("${(@)items[1,${#items}-1]}") - # Continue until we receive all information - (( ${#items} % 5 )) && continue + # Read output from zpty and parse it if available. + while zpty -r -t $worker data 2>/dev/null; do + ASYNC_PROCESS_BUFFER[$worker]+=$data + len=${#ASYNC_PROCESS_BUFFER[$worker]} + pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter). - # Work through all results - while (( ${#items} > 0 )); do - $callback "${(@)=items[1,5]}" - shift 5 items - count+=1 + # Keep going until we find a NULL-character. + if (( ! len )) || (( pos > len )); then + continue + fi + + while (( pos <= len )); do + # Take the content from the beginning, until the NULL-character and + # perform shell parsing (z) and unquoting (Q) as an array (@). + items=("${(@Q)${(z)ASYNC_PROCESS_BUFFER[$worker][1,$pos-1]}}") + + # Remove the extracted items from the buffer. + ASYNC_PROCESS_BUFFER[$worker]=${ASYNC_PROCESS_BUFFER[$worker][$pos+1,$len]} + + if (( $#items == 5 )); then + $callback "${(@)items}" # Send all parsed items to the callback. + else + # In case of corrupt data, invoke callback with *async* as job + # name, non-zero exit status and an error message on stderr. + $callback "async" 1 "" 0 "$0:$LINENO: error: bad format, got ${#items} items (${(@q)items})" + fi + + (( num_processed++ )) + + len=${#ASYNC_PROCESS_BUFFER[$worker]} + if (( len > 1 )); then + pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter). + fi done - - # Empty the buffer - unset "ASYNC_PROCESS_BUFFER[$worker]" done - # If we processed any results, return success - (( count )) && return 0 + (( num_processed )) && return 0 + + # Avoid printing exit value when `setopt printexitvalue` is active.` + [[ $caller = trap || $caller = watcher ]] && return 0 # No results were processed return 1 @@ -172,7 +268,7 @@ _async_zle_watcher() { local callback=$ASYNC_CALLBACKS[$worker] if [[ -n $callback ]]; then - async_process_results $worker $callback + async_process_results $worker $callback watcher fi } @@ -186,7 +282,14 @@ async_job() { setopt localoptions noshwordsplit local worker=$1; shift - zpty -w $worker $@ + + local -a cmd + cmd=("$@") + if (( $#cmd > 1 )); then + cmd=(${(q)cmd}) # Quote special characters in multi argument commands. + fi + + zpty -w $worker $cmd$'\0' } # This function traps notification signals and calls all registered callbacks @@ -194,7 +297,7 @@ _async_notify_trap() { setopt localoptions noshwordsplit for k in ${(k)ASYNC_CALLBACKS}; do - async_process_results $k ${ASYNC_CALLBACKS[$k]} + async_process_results $k ${ASYNC_CALLBACKS[$k]} trap done } @@ -213,7 +316,9 @@ async_register_callback() { ASYNC_CALLBACKS[$worker]="$*" - if (( ! ASYNC_USE_ZLE_HANDLER )); then + # Enable trap when the ZLE watcher is unavailable, allows + # workers to notify (via -n) when a job is done. + if [[ ! -o interactive ]] || [[ ! -o zle ]]; then trap '_async_notify_trap' WINCH fi } @@ -246,12 +351,19 @@ async_flush_jobs() { zpty -t $worker &>/dev/null || return 1 # Send kill command to worker - zpty -w $worker "_killjobs" + async_job $worker "_killjobs" - # Clear all output buffers - while zpty -r $worker line; do true; done + # Clear the zpty buffer. + local junk + if zpty -r -t $worker junk '*'; then + (( ASYNC_DEBUG )) && print -n "async_flush_jobs $worker: ${(V)junk}" + while zpty -r -t $worker junk '*'; do + (( ASYNC_DEBUG )) && print -n "${(V)junk}" + done + (( ASYNC_DEBUG )) && print + fi - # Clear any partial buffers + # Finally, clear the process buffer in case of partially parsed responses. typeset -gA ASYNC_PROCESS_BUFFER unset "ASYNC_PROCESS_BUFFER[$worker]" } @@ -276,16 +388,47 @@ async_start_worker() { typeset -gA ASYNC_PTYS typeset -h REPLY + typeset has_xtrace=0 + + # Make sure async worker is started without xtrace + # (the trace output interferes with the worker). + [[ -o xtrace ]] && { + has_xtrace=1 + unsetopt xtrace + } + + if (( ! ASYNC_ZPTY_RETURNS_FD )) && [[ -o interactive ]] && [[ -o zle ]]; then + # When zpty doesn't return a file descriptor (on older versions of zsh) + # we try to guess it anyway. + integer -l zptyfd + exec {zptyfd}>&1 # Open a new file descriptor (above 10). + exec {zptyfd}>&- # Close it so it's free to be used by zpty. + fi + zpty -b $worker _async_worker -p $$ $@ || { async_stop_worker $worker return 1 } - if (( ASYNC_USE_ZLE_HANDLER )); then - ASYNC_PTYS[$REPLY]=$worker - zle -F $REPLY _async_zle_watcher + # Re-enable it if it was enabled, for debugging. + (( has_xtrace )) && setopt xtrace - # If worker was called with -n, disable trap in favor of zle handler + if [[ $ZSH_VERSION < 5.0.8 ]]; then + # For ZSH versions older than 5.0.8 we delay a bit to give + # time for the worker to start before issuing commands, + # otherwise it will not be ready to receive them. + sleep 0.001 + fi + + if [[ -o interactive ]] && [[ -o zle ]]; then + if (( ! ASYNC_ZPTY_RETURNS_FD )); then + REPLY=$zptyfd # Use the guessed value for the file desciptor. + fi + + ASYNC_PTYS[$REPLY]=$worker # Map the file desciptor to the worker. + zle -F $REPLY _async_zle_watcher # Register the ZLE handler. + + # Disable trap in favor of ZLE handler when notify is enabled (-n). async_job $worker _unset_trap fi } @@ -310,6 +453,10 @@ async_stop_worker() { done async_unregister_callback $worker zpty -d $worker 2>/dev/null || ret=$? + + # Clear any partial buffers. + typeset -gA ASYNC_PROCESS_BUFFER + unset "ASYNC_PROCESS_BUFFER[$worker]" done return $ret @@ -328,12 +475,13 @@ async_init() { zmodload zsh/zpty zmodload zsh/datetime - # Check if zsh/zpty returns a file descriptor or not, shell must also be interactive - ASYNC_USE_ZLE_HANDLER=0 - [[ -o interactive ]] && { + # Check if zsh/zpty returns a file descriptor or not, + # shell must also be interactive with zle enabled. + ASYNC_ZPTY_RETURNS_FD=0 + [[ -o interactive ]] && [[ -o zle ]] && { typeset -h REPLY - zpty _async_test cat - (( REPLY )) && ASYNC_USE_ZLE_HANDLER=1 + zpty _async_test : + (( REPLY )) && ASYNC_ZPTY_RETURNS_FD=1 zpty -d _async_test } } diff --git a/package.json b/package.json index a7096f3..0e534e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pure-prompt", - "version": "1.3.0", + "version": "1.5.0", "description": "Pretty, minimal and fast ZSH prompt", "license": "MIT", "repository": "sindresorhus/pure", @@ -19,7 +19,7 @@ "node": ">=0.10.0" }, "scripts": { - "postinstall": "dest=/usr/local/share/zsh/site-functions/; mkdir -p $dest && ln -sf \"$PWD/pure.zsh\" $dest/prompt_pure_setup && ln -sf \"$PWD/async.zsh\" $dest/async || echo 'Could not automagically symlink the prompt. Check out the readme on how to do it manually: https://github.com/sindresorhus/pure#manually'" + "postinstall": "dest=/usr/local/share/zsh/site-functions; mkdir -p $dest && ln -sf \"$PWD/pure.zsh\" $dest/prompt_pure_setup && ln -sf \"$PWD/async.zsh\" $dest/async || echo 'Could not automagically symlink the prompt. Check out the readme on how to do it manually: https://github.com/sindresorhus/pure#manually'" }, "files": [ "pure.zsh", diff --git a/pure.zsh b/pure.zsh index 29bf2ea..7f367eb 100644 --- a/pure.zsh +++ b/pure.zsh @@ -61,30 +61,10 @@ prompt_pure_clear_screen() { prompt_pure_preprompt_render precmd } -prompt_pure_check_git_arrows() { - # reset git arrows - prompt_pure_git_arrows= - - # check if there is an upstream configured for this branch - command git rev-parse --abbrev-ref @'{u}' &>/dev/null || return - - local arrow_status - # check git left and right arrow_status - arrow_status="$(command git rev-list --left-right --count HEAD...@'{u}' 2>/dev/null)" - # exit if the command failed - (( !$? )) || return - - # left and right are tab-separated, split on tab and store as array - arrow_status=(${(ps:\t:)arrow_status}) - local arrows left=${arrow_status[1]} right=${arrow_status[2]} - - (( ${right:-0} > 0 )) && arrows+="${PURE_GIT_DOWN_ARROW:-⇣}" - (( ${left:-0} > 0 )) && arrows+="${PURE_GIT_UP_ARROW:-⇡}" - - [[ -n $arrows ]] && prompt_pure_git_arrows=" ${arrows}" -} - prompt_pure_set_title() { + # emacs terminal does not support settings the title + (( ${+EMACS} )) && return + # tell the terminal we are setting the title print -n '\e]0;' # show hostname if connected through ssh @@ -120,6 +100,12 @@ prompt_pure_string_length_to_var() { } prompt_pure_preprompt_render() { + # store the current prompt_subst setting so that it can be restored later + local prompt_subst_status=$options[prompt_subst] + + # make sure prompt_subst is unset to prevent parameter expansion in preprompt + setopt local_options no_prompt_subst + # check that no command is currently running, the preprompt will otherwise be rendered in the wrong place [[ -n ${prompt_pure_cmd_timestamp+x} && "$1" != "precmd" ]] && return @@ -138,12 +124,15 @@ prompt_pure_preprompt_render() { # execution time preprompt+="%F{yellow}${prompt_pure_cmd_exec_time}%f" + # make sure prompt_pure_last_preprompt is a global array + typeset -g -a prompt_pure_last_preprompt + # if executing through precmd, do not perform fancy terminal editing if [[ "$1" == "precmd" ]]; then print -P "${preprompt}" else - # only redraw if preprompt has changed - [[ "${prompt_pure_last_preprompt}" != "${preprompt}" ]] || return + # only redraw if the expanded preprompt has changed + [[ "${prompt_pure_last_preprompt[2]}" != "${(S%%)preprompt}" ]] || return # calculate length of preprompt and store it locally in preprompt_length integer preprompt_length lines @@ -154,7 +143,7 @@ prompt_pure_preprompt_render() { # calculate previous preprompt lines to figure out how the new preprompt should behave integer last_preprompt_length last_lines - prompt_pure_string_length_to_var "${prompt_pure_last_preprompt}" "last_preprompt_length" + prompt_pure_string_length_to_var "${prompt_pure_last_preprompt[1]}" "last_preprompt_length" (( last_lines = ( last_preprompt_length - 1 ) / COLUMNS + 1 )) # clr_prev_preprompt erases visual artifacts from previous preprompt @@ -174,9 +163,6 @@ prompt_pure_preprompt_render() { elif (( last_lines < lines )); then # move cursor using newlines because ansi cursor movement can't push the cursor beyond the last line printf $'\n'%.0s {1..$(( lines - last_lines ))} - - # redraw the prompt since it has been moved by print - zle && zle .reset-prompt fi # disable clearing of line if last char of preprompt is last column of terminal @@ -184,11 +170,19 @@ prompt_pure_preprompt_render() { (( COLUMNS * lines == preprompt_length )) && clr= # modify previous preprompt - print -Pn "\e7${clr_prev_preprompt}\e[${lines}A\e[1G${preprompt}${clr}\e8" + print -Pn "${clr_prev_preprompt}\e[${lines}A\e[${COLUMNS}D${preprompt}${clr}\n" + + if [[ $prompt_subst_status = 'on' ]]; then + # re-eanble prompt_subst for expansion on PS1 + setopt prompt_subst + fi + + # redraw prompt (also resets cursor position) + zle && zle .reset-prompt fi - # store previous preprompt for comparison - prompt_pure_last_preprompt=$preprompt + # store both unexpanded and expanded preprompt for comparison + prompt_pure_last_preprompt=("$preprompt" "${(S%%)preprompt}") } prompt_pure_precmd() { @@ -199,9 +193,6 @@ prompt_pure_precmd() { # with the initial preprompt rendering prompt_pure_cmd_timestamp= - # check for git arrows - prompt_pure_check_git_arrows - # shows the full path in the title prompt_pure_set_title 'expand-prompt' '%~' @@ -220,29 +211,46 @@ prompt_pure_precmd() { # fastest possible way to check if repo is dirty prompt_pure_async_git_dirty() { - local untracked_dirty=$1; shift + setopt localoptions noshwordsplit + local untracked_dirty=$1 dir=$2 # use cd -q to avoid side effects of changing directory, e.g. chpwd hooks - cd -q "$*" + builtin cd -q $dir - if [[ "$untracked_dirty" == "0" ]]; then + if [[ $untracked_dirty = 0 ]]; then command git diff --no-ext-diff --quiet --exit-code else test -z "$(command git status --porcelain --ignore-submodules -unormal)" fi - (( $? )) && echo "%F{red}*%f" + return $? } prompt_pure_async_git_fetch() { + setopt localoptions noshwordsplit # use cd -q to avoid side effects of changing directory, e.g. chpwd hooks - cd -q "$*" + builtin cd -q $1 # set GIT_TERMINAL_PROMPT=0 to disable auth prompting for git fetch (git 2.3+) - GIT_TERMINAL_PROMPT=0 command git -c gc.auto=0 fetch + export GIT_TERMINAL_PROMPT=0 + # set ssh BachMode to disable all interactive ssh password prompting + export GIT_SSH_COMMAND=${GIT_SSH_COMMAND:-"ssh -o BatchMode=yes"} + + command git -c gc.auto=0 fetch &>/dev/null || return 1 + + # check arrow status after a successful git fetch + prompt_pure_async_git_arrows $1 +} + +prompt_pure_async_git_arrows() { + setopt localoptions noshwordsplit + builtin cd -q $1 + command git rev-list --left-right --count HEAD...@'{u}' } prompt_pure_async_tasks() { + setopt localoptions noshwordsplit + # initialize async worker ((!${prompt_pure_async_init:-0})) && { async_start_worker "prompt_pure" -u -n @@ -261,6 +269,7 @@ prompt_pure_async_tasks() { # reset git preprompt variables, switching working tree unset prompt_pure_git_dirty unset prompt_pure_git_last_dirty_check_timestamp + prompt_pure_git_arrows= # set the new working tree and prefix with "x" to prevent the creation of a named path by AUTO_NAME_DIRS prompt_pure_current_working_tree="x${working_tree}" @@ -269,10 +278,12 @@ prompt_pure_async_tasks() { # only perform tasks inside git working tree [[ -n $working_tree ]] || return + async_job "prompt_pure" prompt_pure_async_git_arrows $working_tree + # do not preform git fetch if it is disabled or working_tree == HOME if (( ${PURE_GIT_PULL:-1} )) && [[ $working_tree != $HOME ]]; then # tell worker to do a git fetch - async_job "prompt_pure" prompt_pure_async_git_fetch "${working_tree}" + async_job "prompt_pure" prompt_pure_async_git_fetch $working_tree fi # if dirty checking is sufficiently fast, tell worker to check it again, or wait for timeout @@ -280,41 +291,76 @@ prompt_pure_async_tasks() { if (( time_since_last_dirty_check > ${PURE_GIT_DELAY_DIRTY_CHECK:-1800} )); then unset prompt_pure_git_last_dirty_check_timestamp # check check if there is anything to pull - async_job "prompt_pure" prompt_pure_async_git_dirty "${PURE_GIT_UNTRACKED_DIRTY:-1}" "${working_tree}" + async_job "prompt_pure" prompt_pure_async_git_dirty ${PURE_GIT_UNTRACKED_DIRTY:-1} $working_tree fi } -prompt_pure_async_callback() { - local job=$1 - local output=$3 - local exec_time=$4 +prompt_pure_check_git_arrows() { + setopt localoptions noshwordsplit + local arrows left=${1:-0} right=${2:-0} - case "${job}" in + (( right > 0 )) && arrows+=${PURE_GIT_DOWN_ARROW:-⇣} + (( left > 0 )) && arrows+=${PURE_GIT_UP_ARROW:-⇡} + + [[ -n $arrows ]] || return + typeset -g REPLY=" $arrows" +} + +prompt_pure_async_callback() { + setopt localoptions noshwordsplit + local job=$1 code=$2 output=$3 exec_time=$4 + + case $job in prompt_pure_async_git_dirty) - prompt_pure_git_dirty=$output - prompt_pure_preprompt_render + local prev_dirty=$prompt_pure_git_dirty + if (( code == 0 )); then + prompt_pure_git_dirty= + else + prompt_pure_git_dirty="%F{red}*%f" + fi + + [[ $prev_dirty != $prompt_pure_git_dirty ]] && prompt_pure_preprompt_render # When prompt_pure_git_last_dirty_check_timestamp is set, the git info is displayed in a different color. # To distinguish between a "fresh" and a "cached" result, the preprompt is rendered before setting this # variable. Thus, only upon next rendering of the preprompt will the result appear in a different color. (( $exec_time > 2 )) && prompt_pure_git_last_dirty_check_timestamp=$EPOCHSECONDS ;; - prompt_pure_async_git_fetch) - prompt_pure_check_git_arrows - prompt_pure_preprompt_render + prompt_pure_async_git_fetch|prompt_pure_async_git_arrows) + # prompt_pure_async_git_fetch executes prompt_pure_async_git_arrows + # after a successful fetch. + if (( code == 0 )); then + local REPLY + prompt_pure_check_git_arrows ${(ps:\t:)output} + if [[ $prompt_pure_git_arrows != $REPLY ]]; then + prompt_pure_git_arrows=$REPLY + prompt_pure_preprompt_render + fi + fi ;; esac } prompt_pure_setup() { + local autoload_name=$1; shift + # prevent percentage showing up # if output doesn't end with a newline export PROMPT_EOL_MARK='' prompt_opts=(subst percent) + # if autoload_name or eval context differ, pure wasn't autoloaded via + # promptinit and we need to take care of setting the options ourselves + if [[ $autoload_name != prompt_pure_setup ]] || [[ $zsh_eval_context[-2] != loadautofunc ]]; then + # borrowed from `promptinit`, set the pure prompt options + setopt noprompt{bang,cr,percent,subst} "prompt${^prompt_opts[@]}" + fi + zmodload zsh/datetime zmodload zsh/zle + zmodload zsh/parameter + autoload -Uz add-zsh-hook autoload -Uz vcs_info autoload -Uz async && async @@ -345,7 +391,7 @@ prompt_pure_setup() { [[ $UID -eq 0 ]] && prompt_pure_username=' %F{white}%n%f%F{242}@%m%f' # prompt turns red if the previous command didn't exit with 0 - PROMPT="%(?.%F{magenta}.%F{red})${PURE_PROMPT_SYMBOL:-❯}%f " + PROMPT='%(?.%F{magenta}.%F{red})${PURE_PROMPT_SYMBOL:-❯}%f ' } -prompt_pure_setup "$@" +prompt_pure_setup "$0" "$@" diff --git a/readme.md b/readme.md index 326a397..3a60e9a 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ > Pretty, minimal and fast ZSH prompt -![](screenshot.png) + ## Overview @@ -11,10 +11,10 @@ Most prompts are cluttered, ugly and slow. I wanted something visually pleasing ### Why? -- Comes with the perfect prompt character. +- Comes with the perfect prompt character. Author went through the whole Unicode range to find it. - Shows `git` branch and whether it's dirty (with a `*`). -- Indicates when you have unpushed/unpulled `git` commits with up/down arrows. +- Indicates when you have unpushed/unpulled `git` commits with up/down arrows. *(Check is done asynchronously!)* - Prompt character turns red if the last command didn't exit with `0`. - Command execution time will be displayed if it exceeds the set threshold. - Username and host only displayed when in an SSH session. @@ -24,11 +24,11 @@ Most prompts are cluttered, ugly and slow. I wanted something visually pleasing ## Install -Can be installed with `npm` or manually. Requires git 2.0.0+ and ZSH 5.0.0+. +Can be installed with `npm` or manually. Requires Git 2.0.0+ and ZSH 5.2+. Older versions of ZSH are known to work, but they are **not** recommended. ### npm -``` +```console $ npm install --global pure-prompt ``` @@ -39,7 +39,7 @@ That's it. Skip to [Getting started](#getting-started). 1. Either… - Clone this repo - add it as a submodule, or - - just download `pure.zsh` + - just download `pure.zsh` and `async.zsh` 2. Symlink `pure.zsh` to somewhere in [`$fpath`](http://www.refining-linux.org/archives/46/ZSH-Gem-12-Autoloading-functions/) with the name `prompt_pure_setup`. @@ -47,7 +47,7 @@ That's it. Skip to [Getting started](#getting-started). #### Example -``` +```console $ ln -s "$PWD/pure.zsh" /usr/local/share/zsh/site-functions/prompt_pure_setup $ ln -s "$PWD/async.zsh" /usr/local/share/zsh/site-functions/async ``` @@ -62,7 +62,7 @@ fpath=( "$HOME/.zfunctions" $fpath ) Then install the theme there: -```sh +```console $ ln -s "$PWD/pure.zsh" "$HOME/.zfunctions/prompt_pure_setup" $ ln -s "$PWD/async.zsh" "$HOME/.zfunctions/async" ``` @@ -74,7 +74,7 @@ Initialize the prompt system (if not so already) and choose `pure`: ```sh # .zshrc -autoload -U promptinit && promptinit +autoload -U promptinit; promptinit prompt pure ``` @@ -114,7 +114,7 @@ Defines the git up arrow symbol. The default value is `⇡`. ```sh # .zshrc -autoload -U promptinit && promptinit +autoload -U promptinit; promptinit # optionally define some options PURE_CMD_MAX_EXEC_TIME=10 @@ -125,33 +125,66 @@ prompt pure ## Tips -[Tomorrow Night Eighties](https://github.com/chriskempson/tomorrow-theme) theme with the [Droid Sans Mono](http://www.google.com/webfonts/specimen/Droid+Sans+Mono) font (15pt) is a beautiful combination, as seen in the screenshot above. Just make sure you have anti-aliasing enabled in your Terminal. +In the screenshot you see Pure running in [Hyper](https://hyper.is) with the [hyper-snazzy](https://github.com/sindresorhus/hyper-snazzy) theme and Menlo font. -To have commands colorized as seen in the screenshot install [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting). +The [Tomorrow Night Eighties](https://github.com/chriskempson/tomorrow-theme) theme with the [Droid Sans Mono](https://fonts.google.com/specimen/Droid+Sans+Mono) font (15pt) is also a [nice combination](https://github.com/sindresorhus/pure/blob/95ee3e7618c6e2162a1e3cdac2a88a20ac3beb27/screenshot.png).
+*Just make sure you have anti-aliasing enabled in your terminal.* + +To have commands colorized as seen in the screenshot, install [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting). ## Integration ### [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) -1. Remove competing theme included in oh-my-zsh `~/.oh-my-zsh/themes/pure.zsh-theme` -2. Symlink (or copy) `pure.zsh` to `~/.oh-my-zsh/custom/pure.zsh-theme` -3. Symlink (or copy) `async.zsh` to `~/.oh-my-zsh/custom/async.zsh` -4. Add `ZSH_THEME="pure"` to your `.zshrc` file. +1. Symlink (or copy) `pure.zsh` to `~/.oh-my-zsh/custom/pure.zsh-theme`. +2. Symlink (or copy) `async.zsh` to `~/.oh-my-zsh/custom/async.zsh`. +3. Set `ZSH_THEME="pure"` in your `.zshrc` file. -### [prezto](https://github.com/sorin-ionescu/prezto) +Or skip the `oh-my-zsh` integration above and simply: + +1. Set `ZSH_THEME=""` in your `.zshrc` to disable oh-my-zsh themes. +2. Follow the Pure [Install](#install) instructions. + +### [prezto](https://github.com/zsh-users/prezto) + +Pure is bundled with Prezto. No need to install it. Set `zstyle ':prezto:module:prompt' theme 'pure'` in `~/.zpreztorc`. +### [zim](https://github.com/Eriner/zim) + +Pure is bundled with Zim. No need to install it. + +Set `zprompt_theme='pure'` in `~/.zimrc`. + ### [antigen](https://github.com/zsh-users/antigen) Update your `.zshrc` file with the following two lines (order matters). Do not use the `antigen theme` function. -``` +```sh antigen bundle mafredri/zsh-async antigen bundle sindresorhus/pure ``` +### [antibody](https://github.com/getantibody/antibody) + +Update your `.zshrc` file with the following two lines (order matters): + +```sh +antibody bundle mafredri/zsh-async +antibody bundle sindresorhus/pure +``` + +### [zplug](https://github.com/zplug/zplug) + +Update your `.zshrc` file with the following two lines: + +```sh +zplug mafredri/zsh-async, from:github +zplug sindresorhus/pure, use:pure.zsh, from:github, as:theme +``` + ## FAQ ### My preprompt is missing when I clear the screen with Ctrl+L @@ -171,15 +204,26 @@ Using `git pull` when you get the username prompt should help you to break the l #### Gentoo -``` -sudo sh -c "echo 'SANDBOX_WRITE=\"/dev/ptmx\"' > /etc/sandbox.d/10zsh" -sudo emerge -1 zsh +```console +$ sudo sh -c "echo 'SANDBOX_WRITE=\"/dev/ptmx\"' > /etc/sandbox.d/10zsh" +$ sudo emerge -1 zsh ``` #### FreeBSD 10.1 On a default setup, running the command `kldload pty` should do the trick. If you have a custom kernel, you might need to add `device pty` to the configuration file ([example](https://github.com/nbari/freebsd/blob/58646a9c3c4aaabf6f6467ff505f27f09e29dc75/kernels/xen.kernel#L188)). +## Ports + +* **Bash** + * [sapegin/dotfiles](https://github.com/sapegin/dotfiles)’s [prompt](https://github.com/sapegin/dotfiles/blob/dd063f9c30de7d2234e8accdb5272a5cc0a3388b/includes/bash_prompt.bash) and [color theme](https://github.com/sapegin/dotfiles/tree/master/color) for `Terminal.app`. +* **Fish** + * [brandonweiss/pure.fish](https://github.com/brandonweiss/pure.fish): a Pure-inspired prompt for Fish, not intended to have feature parity. + * [rafaelrinaldi/pure](https://github.com/rafaelrinaldi/pure), support for bare Fish and various framework ([Oh-My-Fish](https://github.com//oh-my-fish/oh-my-fish), [Fisherman](https://github.com//fisherman/fisherman) and [Wahoo](https://github.com//bucaran/wahoo)). +* **Zsh** + * [therealklanni/purity](https://github.com/therealklanni/purity): a more compact current working directory, important details on the main prompt line, and extra Git indicators. + * [intelfx/pure](https://github.com/intelfx/pure): Solarized-friendly colors, highly verbose and fully async Git integration + ## Team [![Sindre Sorhus](https://avatars.githubusercontent.com/u/170270?v=3&s=100)](http://sindresorhus.com) | [![Mathias Fredriksson](https://avatars.githubusercontent.com/u/147409?v=3&s=100)](https://github.com/mafredri) diff --git a/screenshot.png b/screenshot.png index dd7db79..1a27678 100644 Binary files a/screenshot.png and b/screenshot.png differ