diff --git a/zsh.d/30-prompt.zsh b/zsh.d/30-prompt.zsh index 6494d3b..c220b88 100644 --- a/zsh.d/30-prompt.zsh +++ b/zsh.d/30-prompt.zsh @@ -1,5 +1,11 @@ fpath=($0:h/prompts $fpath) +fpath+=$0:h/prompts/pure autoload -U promptinit;promptinit +zstyle :prompt:pure:user color blue +zstyle :prompt:pure:host color blue + +prompt pure + # vim: set ts=4 sw=4 tw=0 ft=zsh : diff --git a/zsh.d/prompts/pure/async b/zsh.d/prompts/pure/async new file mode 120000 index 0000000..27d2d7e --- /dev/null +++ b/zsh.d/prompts/pure/async @@ -0,0 +1 @@ +async.zsh \ No newline at end of file diff --git a/zsh.d/prompts/pure/async.zsh b/zsh.d/prompts/pure/async.zsh new file mode 100644 index 0000000..d0f3f00 --- /dev/null +++ b/zsh.d/prompts/pure/async.zsh @@ -0,0 +1,574 @@ +#!/usr/bin/env zsh + +# +# zsh-async +# +# version: 1.7.2 +# author: Mathias Fredriksson +# url: https://github.com/mafredri/zsh-async +# + +typeset -g ASYNC_VERSION=1.7.2 +# Produce debug output from zsh-async when set to 1. +typeset -g ASYNC_DEBUG=${ASYNC_DEBUG:-0} + +# Execute commands that can manipulate the environment inside the async worker. Return output via callback. +_async_eval() { + local ASYNC_JOB_NAME + # Rename job to _async_eval and redirect all eval output to cat running + # in _async_job. Here, stdout and stderr are not separated for + # simplicity, this could be improved in the future. + { + eval "$@" + } &> >(ASYNC_JOB_NAME=[async/eval] _async_job 'cat') +} + +# 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 for job. + float -F duration=$EPOCHREALTIME + + # 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 jobname=${ASYNC_JOB_NAME:-$1} + local stdout stderr ret tok + { + stdout=$(eval "$@") + ret=$? + duration=$(( EPOCHREALTIME - duration )) # Calculate duration. + + # Grab mutex lock, stalls until token is available. + read -r -k 1 -p tok || exit 1 + + # Return output ( ). + print -r -n - $'\0'${(q)jobname} $ret ${(q)stdout} $duration + } 2> >(stderr=$(cat) && print -r -n - " "${(q)stderr}$'\0') + + # 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. + + close_idle_coproc() { + 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 + } + + child_exit() { + close_idle_coproc + + # 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 + n) notify_parent=1;; + p) parent_pid=$OPTARG;; + u) unique=1;; + esac + done + + killjobs() { + local tok + local -a pids + pids=(${${(v)jobstates##*:*:}%\=*}) + + # 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 do_eval=0 + 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 $request in + _unset_trap) notify_parent=0; continue;; + _killjobs) killjobs; continue;; + _async_eval*) do_eval=1;; + 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 + for pid in ${${(v)jobstates##*:*:}%\=*}; do + if [[ ${storage[$job]} == $pid ]]; then + continue 2 + fi + done + fi + + # 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 + + if (( do_eval )); then + shift cmd # Strip _async_eval from cmd. + _async_eval $cmd + else + # 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]="$!" + fi + + processing=0 # Disable guard. + + if (( do_eval )); then + do_eval=0 + + # When there are no active jobs we can't rely on the CHLD trap to + # manage the coproc lifetime. + close_idle_coproc + fi + done +} + +# +# Get results from finished jobs and pass it to the to callback function. This is the only way to reliably return the +# job name, return code, output and execution time and with minimal effort. +# +# If the async process buffer becomes corrupt, the callback will be invoked with the first argument being `[async]` (job +# name), non-zero return code and fifth argument describing the error (stderr). +# +# usage: +# async_process_results +# +# callback_function is called with the following parameters: +# $1 = job name, e.g. the function passed to async_job +# $2 = return code +# $3 = resulting stdout from execution +# $4 = execution time, floating point e.g. 2.05 seconds +# $5 = resulting stderr from execution +# $6 = has next result in buffer (0 = buffer empty, 1 = yes) +# +async_process_results() { + setopt localoptions unset noshwordsplit noksharrays noposixidentifiers noposixstrings + + local worker=$1 + local callback=$2 + local caller=$3 + local -a items + local null=$'\0' data + integer -l len pos num_processed has_next + + typeset -gA ASYNC_PROCESS_BUFFER + + # 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). + + # 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]} + + len=${#ASYNC_PROCESS_BUFFER[$worker]} + if (( len > 1 )); then + pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter). + fi + + has_next=$(( len != 0 )) + if (( $#items == 5 )); then + items+=($has_next) + $callback "${(@)items}" # Send all parsed items to the callback. + (( num_processed++ )) + elif [[ -z $items ]]; then + # Empty items occur between results due to double-null ($'\0\0') + # caused by commands being both pre and suffixed with null. + 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})" $has_next + fi + done + done + + (( 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 +} + +# Watch worker for output +_async_zle_watcher() { + setopt localoptions noshwordsplit + typeset -gA ASYNC_PTYS ASYNC_CALLBACKS + local worker=$ASYNC_PTYS[$1] + local callback=$ASYNC_CALLBACKS[$worker] + + if [[ -n $2 ]]; then + # from man zshzle(1): + # `hup' for a disconnect, `nval' for a closed or otherwise + # invalid descriptor, or `err' for any other condition. + # Systems that support only the `select' system call always use + # `err'. + + # this has the side effect to unregister the broken file descriptor + async_stop_worker $worker + + if [[ -n $callback ]]; then + $callback '[async]' 2 "" 0 "$worker:zle -F $1 returned error $2" 0 + fi + return + fi; + + if [[ -n $callback ]]; then + async_process_results $worker $callback watcher + fi +} + +# +# Start a new asynchronous job on specified worker, assumes the worker is running. +# +# usage: +# async_job [] +# +async_job() { + setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings + + local worker=$1; shift + + local -a cmd + cmd=("$@") + if (( $#cmd > 1 )); then + cmd=(${(q)cmd}) # Quote special characters in multi argument commands. + fi + + # Quote the cmd in case RC_EXPAND_PARAM is set. + zpty -w $worker "$cmd"$'\0' +} + +# +# Evaluate a command (like async_job) inside the async worker, then worker environment can be manipulated. For example, +# issuing a cd command will change the PWD of the worker which will then be inherited by all future async jobs. +# +# Output will be returned via callback, job name will be [async/eval]. +# +# usage: +# async_worker_eval [] +# +async_worker_eval() { + setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings + + local worker=$1; shift + + local -a cmd + cmd=("$@") + if (( $#cmd > 1 )); then + cmd=(${(q)cmd}) # Quote special characters in multi argument commands. + fi + + # Quote the cmd in case RC_EXPAND_PARAM is set. + zpty -w $worker "_async_eval $cmd"$'\0' +} + +# This function traps notification signals and calls all registered callbacks +_async_notify_trap() { + setopt localoptions noshwordsplit + + local k + for k in ${(k)ASYNC_CALLBACKS}; do + async_process_results $k ${ASYNC_CALLBACKS[$k]} trap + done +} + +# +# Register a callback for completed jobs. As soon as a job is finnished, async_process_results will be called with the +# specified callback function. This requires that a worker is initialized with the -n (notify) option. +# +# usage: +# async_register_callback +# +async_register_callback() { + setopt localoptions noshwordsplit nolocaltraps + + typeset -gA ASYNC_CALLBACKS + local worker=$1; shift + + ASYNC_CALLBACKS[$worker]="$*" + + # 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 +} + +# +# Unregister the callback for a specific worker. +# +# usage: +# async_unregister_callback +# +async_unregister_callback() { + typeset -gA ASYNC_CALLBACKS + + unset "ASYNC_CALLBACKS[$1]" +} + +# +# Flush all current jobs running on a worker. This will terminate any and all running processes under the worker, use +# with caution. +# +# usage: +# async_flush_jobs +# +async_flush_jobs() { + setopt localoptions noshwordsplit + + local worker=$1; shift + + # Check if the worker exists + zpty -t $worker &>/dev/null || return 1 + + # Send kill command to worker + async_job $worker "_killjobs" + + # 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 + + # Finally, clear the process buffer in case of partially parsed responses. + typeset -gA ASYNC_PROCESS_BUFFER + unset "ASYNC_PROCESS_BUFFER[$worker]" +} + +# +# Start a new async worker with optional parameters, a worker can be told to only run unique tasks and to notify a +# process when tasks are complete. +# +# usage: +# async_start_worker [-u] [-n] [-p ] +# +# opts: +# -u unique (only unique job names can run) +# -n notify through SIGWINCH signal +# -p pid to notify (defaults to current pid) +# +async_start_worker() { + setopt localoptions noshwordsplit + + local worker=$1; shift + zpty -t $worker &>/dev/null && return + + 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 + } + + # Re-enable it if it was enabled, for debugging. + (( has_xtrace )) && setopt xtrace + + 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 +} + +# +# Stop one or multiple workers that are running, all unfetched and incomplete work will be lost. +# +# usage: +# async_stop_worker [] +# +async_stop_worker() { + setopt localoptions noshwordsplit + + local ret=0 worker k v + for worker in $@; do + # Find and unregister the zle handler for the worker + for k v in ${(@kv)ASYNC_PTYS}; do + if [[ $v == $worker ]]; then + zle -F $k + unset "ASYNC_PTYS[$k]" + fi + 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 +} + +# +# Initialize the required modules for zsh-async. To be called before using the zsh-async library. +# +# usage: +# async_init +# +async_init() { + (( ASYNC_INIT_DONE )) && return + typeset -g ASYNC_INIT_DONE=1 + + zmodload zsh/zpty + zmodload zsh/datetime + + # Check if zsh/zpty returns a file descriptor or not, + # shell must also be interactive with zle enabled. + typeset -g ASYNC_ZPTY_RETURNS_FD=0 + [[ -o interactive ]] && [[ -o zle ]] && { + typeset -h REPLY + zpty _async_test : + (( REPLY )) && ASYNC_ZPTY_RETURNS_FD=1 + zpty -d _async_test + } +} + +async() { + async_init +} + +async "$@" diff --git a/zsh.d/prompts/pure/prompt_pure_setup b/zsh.d/prompts/pure/prompt_pure_setup new file mode 120000 index 0000000..f5f60e7 --- /dev/null +++ b/zsh.d/prompts/pure/prompt_pure_setup @@ -0,0 +1 @@ +pure.zsh \ No newline at end of file diff --git a/zsh.d/prompts/pure/pure.plugin.zsh b/zsh.d/prompts/pure/pure.plugin.zsh new file mode 120000 index 0000000..f5f60e7 --- /dev/null +++ b/zsh.d/prompts/pure/pure.plugin.zsh @@ -0,0 +1 @@ +pure.zsh \ No newline at end of file diff --git a/zsh.d/prompts/pure/pure.zsh b/zsh.d/prompts/pure/pure.zsh new file mode 100644 index 0000000..2d3e0f1 --- /dev/null +++ b/zsh.d/prompts/pure/pure.zsh @@ -0,0 +1,797 @@ +# Pure +# by Sindre Sorhus +# https://github.com/sindresorhus/pure +# MIT License + +# For my own and others sanity +# git: +# %b => current branch +# %a => current action (rebase/merge) +# prompt: +# %F => color dict +# %f => reset color +# %~ => current path +# %* => time +# %n => username +# %m => shortname host +# %(?..) => prompt conditional - %(condition.true.false) +# terminal codes: +# \e7 => save cursor position +# \e[2A => move cursor 2 lines up +# \e[1G => go to position 1 in terminal +# \e8 => restore cursor position +# \e[K => clears everything after the cursor on the current line +# \e[2K => clear everything on the current line + + +# Turns seconds into human readable time. +# 165392 => 1d 21h 56m 32s +# https://github.com/sindresorhus/pretty-time-zsh +prompt_pure_human_time_to_var() { + local human total_seconds=$1 var=$2 + local days=$(( total_seconds / 60 / 60 / 24 )) + local hours=$(( total_seconds / 60 / 60 % 24 )) + local minutes=$(( total_seconds / 60 % 60 )) + local seconds=$(( total_seconds % 60 )) + (( days > 0 )) && human+="${days}d " + (( hours > 0 )) && human+="${hours}h " + (( minutes > 0 )) && human+="${minutes}m " + human+="${seconds}s" + + # Store human readable time in a variable as specified by the caller + typeset -g "${var}"="${human}" +} + +# Stores (into prompt_pure_cmd_exec_time) the execution +# time of the last command if set threshold was exceeded. +prompt_pure_check_cmd_exec_time() { + integer elapsed + (( elapsed = EPOCHSECONDS - ${prompt_pure_cmd_timestamp:-$EPOCHSECONDS} )) + typeset -g prompt_pure_cmd_exec_time= + (( elapsed > ${PURE_CMD_MAX_EXEC_TIME:-5} )) && { + prompt_pure_human_time_to_var $elapsed "prompt_pure_cmd_exec_time" + } +} + +prompt_pure_set_title() { + setopt localoptions noshwordsplit + + # Emacs terminal does not support settings the title. + (( ${+EMACS} || ${+INSIDE_EMACS} )) && return + + case $TTY in + # Don't set title over serial console. + /dev/ttyS[0-9]*) return;; + esac + + # Show hostname if connected via SSH. + local hostname= + if [[ -n $prompt_pure_state[username] ]]; then + # Expand in-place in case ignore-escape is used. + hostname="${(%):-(%m) }" + fi + + local -a opts + case $1 in + expand-prompt) opts=(-P);; + ignore-escape) opts=(-r);; + esac + + # Set title atomically in one print statement so that it works when XTRACE is enabled. + print -n $opts $'\e]0;'${hostname}${2}$'\a' +} + +prompt_pure_preexec() { + if [[ -n $prompt_pure_git_fetch_pattern ]]; then + # Detect when Git is performing pull/fetch, including Git aliases. + local -H MATCH MBEGIN MEND match mbegin mend + if [[ $2 =~ (git|hub)\ (.*\ )?($prompt_pure_git_fetch_pattern)(\ .*)?$ ]]; then + # We must flush the async jobs to cancel our git fetch in order + # to avoid conflicts with the user issued pull / fetch. + async_flush_jobs 'prompt_pure' + fi + fi + + typeset -g prompt_pure_cmd_timestamp=$EPOCHSECONDS + + # Shows the current directory and executed command in the title while a process is active. + prompt_pure_set_title 'ignore-escape' "$PWD:t: $2" + + # Disallow Python virtualenv from updating the prompt. Set it to 12 if + # untouched by the user to indicate that Pure modified it. Here we use + # the magic number 12, same as in `psvar`. + export VIRTUAL_ENV_DISABLE_PROMPT=${VIRTUAL_ENV_DISABLE_PROMPT:-12} +} + +# Change the colors if their value are different from the current ones. +prompt_pure_set_colors() { + local color_temp key value + for key value in ${(kv)prompt_pure_colors}; do + zstyle -t ":prompt:pure:$key" color "$value" + case $? in + 1) # The current style is different from the one from zstyle. + zstyle -s ":prompt:pure:$key" color color_temp + prompt_pure_colors[$key]=$color_temp ;; + 2) # No style is defined. + prompt_pure_colors[$key]=$prompt_pure_colors_default[$key] ;; + esac + done +} + +prompt_pure_preprompt_render() { + setopt localoptions noshwordsplit + + # Set color for Git branch/dirty status and change color if dirty checking has been delayed. + local git_color=$prompt_pure_colors[git:branch] + local git_dirty_color=$prompt_pure_colors[git:dirty] + [[ -n ${prompt_pure_git_last_dirty_check_timestamp+x} ]] && git_color=$prompt_pure_colors[git:branch:cached] + + # Initialize the preprompt array. + local -a preprompt_parts + + # Set the path. + preprompt_parts+=('%F{${prompt_pure_colors[path]}}%~%f') + + # Add Git branch and dirty status info. + typeset -gA prompt_pure_vcs_info + if [[ -n $prompt_pure_vcs_info[branch] ]]; then + local branch="%F{$git_color}"'${prompt_pure_vcs_info[branch]}' + if [[ -n $prompt_pure_vcs_info[action] ]]; then + branch+="|%F{$prompt_pure_colors[git:action]}"'$prompt_pure_vcs_info[action]'"%F{$git_color}" + fi + preprompt_parts+=("$branch""%F{$git_dirty_color}"'${prompt_pure_git_dirty}%f') + fi + # Git pull/push arrows. + if [[ -n $prompt_pure_git_arrows ]]; then + preprompt_parts+=('%F{$prompt_pure_colors[git:arrow]}${prompt_pure_git_arrows}%f') + fi + + # Username and machine, if applicable. + [[ -n $prompt_pure_state[username] ]] && preprompt_parts+=($prompt_pure_state[username]) + # Execution time. + [[ -n $prompt_pure_cmd_exec_time ]] && preprompt_parts+=('%F{$prompt_pure_colors[execution_time]}${prompt_pure_cmd_exec_time}%f') + + local cleaned_ps1=$PROMPT + local -H MATCH MBEGIN MEND + if [[ $PROMPT = *$prompt_newline* ]]; then + # Remove everything from the prompt until the newline. This + # removes the preprompt and only the original PROMPT remains. + cleaned_ps1=${PROMPT##*${prompt_newline}} + fi + unset MATCH MBEGIN MEND + + # Construct the new prompt with a clean preprompt. + local -ah ps1 + ps1=( + ${(j. .)preprompt_parts} # Join parts, space separated. + $prompt_newline # Separate preprompt and prompt. + $cleaned_ps1 + ) + + PROMPT="${(j..)ps1}" + + # Expand the prompt for future comparision. + local expanded_prompt + expanded_prompt="${(S%%)PROMPT}" + + if [[ $1 == precmd ]]; then + # Initial newline, for spaciousness. + print + elif [[ $prompt_pure_last_prompt != $expanded_prompt ]]; then + # Redraw the prompt. + prompt_pure_reset_prompt + fi + + typeset -g prompt_pure_last_prompt=$expanded_prompt +} + +prompt_pure_precmd() { + # Check execution time and store it in a variable. + prompt_pure_check_cmd_exec_time + unset prompt_pure_cmd_timestamp + + # Shows the full path in the title. + prompt_pure_set_title 'expand-prompt' '%~' + + # Modify the colors if some have changed.. + prompt_pure_set_colors + + # Perform async Git dirty check and fetch. + prompt_pure_async_tasks + + # Check if we should display the virtual env. We use a sufficiently high + # index of psvar (12) here to avoid collisions with user defined entries. + psvar[12]= + # Check if a Conda environment is active and display its name. + if [[ -n $CONDA_DEFAULT_ENV ]]; then + psvar[12]="${CONDA_DEFAULT_ENV//[$'\t\r\n']}" + fi + # When VIRTUAL_ENV_DISABLE_PROMPT is empty, it was unset by the user and + # Pure should take back control. + if [[ -n $VIRTUAL_ENV ]] && [[ -z $VIRTUAL_ENV_DISABLE_PROMPT || $VIRTUAL_ENV_DISABLE_PROMPT = 12 ]]; then + psvar[12]="${VIRTUAL_ENV:t}" + export VIRTUAL_ENV_DISABLE_PROMPT=12 + fi + + # Make sure VIM prompt is reset. + prompt_pure_reset_prompt_symbol + + # Print the preprompt. + prompt_pure_preprompt_render "precmd" + + if [[ -n $ZSH_THEME ]]; then + print "WARNING: Oh My Zsh themes are enabled (ZSH_THEME='${ZSH_THEME}'). Pure might not be working correctly." + print "For more information, see: https://github.com/sindresorhus/pure#oh-my-zsh" + unset ZSH_THEME # Only show this warning once. + fi +} + +prompt_pure_async_git_aliases() { + setopt localoptions noshwordsplit + local -a gitalias pullalias + + # List all aliases and split on newline. + gitalias=(${(@f)"$(command git config --get-regexp "^alias\.")"}) + for line in $gitalias; do + parts=(${(@)=line}) # Split line on spaces. + aliasname=${parts[1]#alias.} # Grab the name (alias.[name]). + shift parts # Remove `aliasname` + + # Check alias for pull or fetch. Must be exact match. + if [[ $parts =~ ^(.*\ )?(pull|fetch)(\ .*)?$ ]]; then + pullalias+=($aliasname) + fi + done + + print -- ${(j:|:)pullalias} # Join on pipe, for use in regex. +} + +prompt_pure_async_vcs_info() { + setopt localoptions noshwordsplit + + # Configure `vcs_info` inside an async task. This frees up `vcs_info` + # to be used or configured as the user pleases. + zstyle ':vcs_info:*' enable git + zstyle ':vcs_info:*' use-simple true + # Only export three message variables from `vcs_info`. + zstyle ':vcs_info:*' max-exports 3 + # Export branch (%b), Git toplevel (%R), and action (rebase/cherry-pick) (%a). + zstyle ':vcs_info:git*' formats '%b' '%R' + zstyle ':vcs_info:git*' actionformats '%b' '%R' '%a' + + vcs_info + + local -A info + info[pwd]=$PWD + info[top]=$vcs_info_msg_1_ + info[branch]=$vcs_info_msg_0_ + info[action]=$vcs_info_msg_2_ + + print -r - ${(@kvq)info} +} + +# Fastest possible way to check if a Git repo is dirty. +prompt_pure_async_git_dirty() { + setopt localoptions noshwordsplit + local untracked_dirty=$1 + + 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 + + return $? +} + +prompt_pure_async_git_fetch() { + setopt localoptions noshwordsplit + + # Sets `GIT_TERMINAL_PROMPT=0` to disable authentication prompt for Git fetch (Git 2.3+). + 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" + + local ref + ref=$(command git symbolic-ref -q HEAD) + local -a remote + remote=($(command git for-each-ref --format='%(upstream:remotename) %(refname)' $ref)) + + if [[ -z $remote[1] ]]; then + # No remote specified for this branch, skip fetch. + return 97 + fi + + # Default return code, which indicates Git fetch failure. + local fail_code=99 + + # Guard against all forms of password prompts. By setting the shell into + # MONITOR mode we can notice when a child process prompts for user input + # because it will be suspended. Since we are inside an async worker, we + # have no way of transmitting the password and the only option is to + # kill it. If we don't do it this way, the process will corrupt with the + # async worker. + setopt localtraps monitor + + # Make sure local HUP trap is unset to allow for signal propagation when + # the async worker is flushed. + trap - HUP + + trap ' + # Unset trap to prevent infinite loop + trap - CHLD + if [[ $jobstates = suspended* ]]; then + # Set fail code to password prompt and kill the fetch. + fail_code=98 + kill %% + fi + ' CHLD + + # Only fetch information for the current branch and avoid + # fetching tags or submodules to speed up the process. + command git -c gc.auto=0 fetch \ + --quiet \ + --no-tags \ + --recurse-submodules=no \ + $remote &>/dev/null & + wait $! || return $fail_code + + unsetopt monitor + + # Check arrow status after a successful `git fetch`. + prompt_pure_async_git_arrows +} + +prompt_pure_async_git_arrows() { + setopt localoptions noshwordsplit + command git rev-list --left-right --count HEAD...@'{u}' +} + +# Try to lower the priority of the worker so that disk heavy operations +# like `git status` has less impact on the system responsivity. +prompt_pure_async_renice() { + setopt localoptions noshwordsplit + + if command -v renice >/dev/null; then + command renice +15 -p $$ + fi + + if command -v ionice >/dev/null; then + command ionice -c 3 -p $$ + fi +} + +prompt_pure_async_tasks() { + setopt localoptions noshwordsplit + + # Initialize the async worker. + ((!${prompt_pure_async_init:-0})) && { + async_start_worker "prompt_pure" -u -n + async_register_callback "prompt_pure" prompt_pure_async_callback + typeset -g prompt_pure_async_init=1 + async_job "prompt_pure" prompt_pure_async_renice + } + + # Update the current working directory of the async worker. + async_worker_eval "prompt_pure" builtin cd -q $PWD + + typeset -gA prompt_pure_vcs_info + + local -H MATCH MBEGIN MEND + if [[ $PWD != ${prompt_pure_vcs_info[pwd]}* ]]; then + # Stop any running async jobs. + async_flush_jobs "prompt_pure" + + # Reset Git preprompt variables, switching working tree. + unset prompt_pure_git_dirty + unset prompt_pure_git_last_dirty_check_timestamp + unset prompt_pure_git_arrows + unset prompt_pure_git_fetch_pattern + prompt_pure_vcs_info[branch]= + prompt_pure_vcs_info[top]= + fi + unset MATCH MBEGIN MEND + + async_job "prompt_pure" prompt_pure_async_vcs_info + + # Only perform tasks inside a Git working tree. + [[ -n $prompt_pure_vcs_info[top] ]] || return + + prompt_pure_async_refresh +} + +prompt_pure_async_refresh() { + setopt localoptions noshwordsplit + + if [[ -z $prompt_pure_git_fetch_pattern ]]; then + # We set the pattern here to avoid redoing the pattern check until the + # working three has changed. Pull and fetch are always valid patterns. + typeset -g prompt_pure_git_fetch_pattern="pull|fetch" + async_job "prompt_pure" prompt_pure_async_git_aliases + fi + + async_job "prompt_pure" prompt_pure_async_git_arrows + + # Do not preform `git fetch` if it is disabled or in home folder. + if (( ${PURE_GIT_PULL:-1} )) && [[ $prompt_pure_vcs_info[top] != $HOME ]]; then + # Tell the async worker to do a `git fetch`. + async_job "prompt_pure" prompt_pure_async_git_fetch + fi + + # If dirty checking is sufficiently fast, + # tell the worker to check it again, or wait for timeout. + integer time_since_last_dirty_check=$(( EPOCHSECONDS - ${prompt_pure_git_last_dirty_check_timestamp:-0} )) + 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} + fi +} + +prompt_pure_check_git_arrows() { + setopt localoptions noshwordsplit + local arrows left=${1:-0} right=${2:-0} + + (( 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 next_pending=$6 + local do_render=0 + + case $job in + \[async]) + # Code is 1 for corrupted worker output and 2 for dead worker. + if [[ $code -eq 2 ]]; then + # Our worker died unexpectedly. + typeset -g prompt_pure_async_init=0 + fi + ;; + prompt_pure_async_vcs_info) + local -A info + typeset -gA prompt_pure_vcs_info + + # Parse output (z) and unquote as array (Q@). + info=("${(Q@)${(z)output}}") + local -H MATCH MBEGIN MEND + if [[ $info[pwd] != $PWD ]]; then + # The path has changed since the check started, abort. + return + fi + # Check if Git top-level has changed. + if [[ $info[top] = $prompt_pure_vcs_info[top] ]]; then + # If the stored pwd is part of $PWD, $PWD is shorter and likelier + # to be top-level, so we update pwd. + if [[ $prompt_pure_vcs_info[pwd] = ${PWD}* ]]; then + prompt_pure_vcs_info[pwd]=$PWD + fi + else + # Store $PWD to detect if we (maybe) left the Git path. + prompt_pure_vcs_info[pwd]=$PWD + fi + unset MATCH MBEGIN MEND + + # The update has a Git top-level set, which means we just entered a new + # Git directory. Run the async refresh tasks. + [[ -n $info[top] ]] && [[ -z $prompt_pure_vcs_info[top] ]] && prompt_pure_async_refresh + + # Always update branch and top-level. + prompt_pure_vcs_info[branch]=$info[branch] + prompt_pure_vcs_info[top]=$info[top] + prompt_pure_vcs_info[action]=$info[action] + + do_render=1 + ;; + prompt_pure_async_git_aliases) + if [[ -n $output ]]; then + # Append custom Git aliases to the predefined ones. + prompt_pure_git_fetch_pattern+="|$output" + fi + ;; + prompt_pure_async_git_dirty) + local prev_dirty=$prompt_pure_git_dirty + if (( code == 0 )); then + unset prompt_pure_git_dirty + else + typeset -g prompt_pure_git_dirty="*" + fi + + [[ $prev_dirty != $prompt_pure_git_dirty ]] && do_render=1 + + # 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 the next + # rendering of the preprompt will the result appear in a different color. + (( $exec_time > 5 )) && prompt_pure_git_last_dirty_check_timestamp=$EPOCHSECONDS + ;; + 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. + case $code in + 0) + local REPLY + prompt_pure_check_git_arrows ${(ps:\t:)output} + if [[ $prompt_pure_git_arrows != $REPLY ]]; then + typeset -g prompt_pure_git_arrows=$REPLY + do_render=1 + fi + ;; + 97) + # No remote available, make sure to clear git arrows if set. + if [[ -n $prompt_pure_git_arrows ]]; then + typeset -g prompt_pure_git_arrows= + do_render=1 + fi + ;; + 99|98) + # Git fetch failed. + ;; + *) + # Non-zero exit status from `prompt_pure_async_git_arrows`, + # indicating that there is no upstream configured. + if [[ -n $prompt_pure_git_arrows ]]; then + unset prompt_pure_git_arrows + do_render=1 + fi + ;; + esac + ;; + prompt_pure_async_renice) + ;; + esac + + if (( next_pending )); then + (( do_render )) && typeset -g prompt_pure_async_render_requested=1 + return + fi + + [[ ${prompt_pure_async_render_requested:-$do_render} = 1 ]] && prompt_pure_preprompt_render + unset prompt_pure_async_render_requested +} + +prompt_pure_reset_prompt() { + if [[ $CONTEXT == cont ]]; then + # When the context is "cont", PS2 is active and calling + # reset-prompt will have no effect on PS1, but it will + # reset the execution context (%_) of PS2 which we don't + # want. Unfortunately, we can't save the output of "%_" + # either because it is only ever rendered as part of the + # prompt, expanding in-place won't work. + return + fi + + zle && zle .reset-prompt +} + +prompt_pure_reset_prompt_symbol() { + prompt_pure_state[prompt]=${PURE_PROMPT_SYMBOL:-❯} +} + +prompt_pure_update_vim_prompt_widget() { + setopt localoptions noshwordsplit + prompt_pure_state[prompt]=${${KEYMAP/vicmd/${PURE_PROMPT_VICMD_SYMBOL:-❮}}/(main|viins)/${PURE_PROMPT_SYMBOL:-❯}} + + prompt_pure_reset_prompt +} + +prompt_pure_reset_vim_prompt_widget() { + setopt localoptions noshwordsplit + prompt_pure_reset_prompt_symbol + + # We can't perform a prompt reset at this point because it + # removes the prompt marks inserted by macOS Terminal. +} + +prompt_pure_state_setup() { + setopt localoptions noshwordsplit + + # Check SSH_CONNECTION and the current state. + local ssh_connection=${SSH_CONNECTION:-$PROMPT_PURE_SSH_CONNECTION} + local username hostname + if [[ -z $ssh_connection ]] && (( $+commands[who] )); then + # When changing user on a remote system, the $SSH_CONNECTION + # environment variable can be lost. Attempt detection via `who`. + local who_out + who_out=$(who -m 2>/dev/null) + if (( $? )); then + # Who am I not supported, fallback to plain who. + local -a who_in + who_in=( ${(f)"$(who 2>/dev/null)"} ) + who_out="${(M)who_in:#*[[:space:]]${TTY#/dev/}[[:space:]]*}" + fi + + local reIPv6='(([0-9a-fA-F]+:)|:){2,}[0-9a-fA-F]+' # Simplified, only checks partial pattern. + local reIPv4='([0-9]{1,3}\.){3}[0-9]+' # Simplified, allows invalid ranges. + # Here we assume two non-consecutive periods represents a + # hostname. This matches `foo.bar.baz`, but not `foo.bar`. + local reHostname='([.][^. ]+){2}' + + # Usually the remote address is surrounded by parenthesis, but + # not on all systems (e.g. busybox). + local -H MATCH MBEGIN MEND + if [[ $who_out =~ "\(?($reIPv4|$reIPv6|$reHostname)\)?\$" ]]; then + ssh_connection=$MATCH + + # Export variable to allow detection propagation inside + # shells spawned by this one (e.g. tmux does not always + # inherit the same tty, which breaks detection). + export PROMPT_PURE_SSH_CONNECTION=$ssh_connection + fi + unset MATCH MBEGIN MEND + fi + + hostname='%F{$prompt_pure_colors[host]}@%m%f' + # Show `username@host` if logged in through SSH. + [[ -n $ssh_connection ]] && username='%F{$prompt_pure_colors[user]}%n%f'"$hostname" + + # Show `username@host` if root, with username in default color. + [[ $UID -eq 0 ]] && username='%F{$prompt_pure_colors[user:root]}%n%f'"$hostname" + + typeset -gA prompt_pure_state + prompt_pure_state[version]="1.11.0" + prompt_pure_state+=( + username "$username" + prompt "${PURE_PROMPT_SYMBOL:-❯}" + ) +} + +prompt_pure_system_report() { + setopt localoptions noshwordsplit + + print - "- Zsh: $($SHELL --version) ($SHELL)" + print -n - "- Operating system: " + case "$(uname -s)" in + Darwin) print "$(sw_vers -productName) $(sw_vers -productVersion) ($(sw_vers -buildVersion))";; + *) print "$(uname -s) ($(uname -v))";; + esac + print - "- Terminal program: ${TERM_PROGRAM:-unknown} (${TERM_PROGRAM_VERSION:-unknown})" + print -n - "- Tmux: " + [[ -n $TMUX ]] && print "yes" || print "no" + + local git_version + git_version=($(git --version)) # Remove newlines, if hub is present. + print - "- Git: $git_version" + + print - "- Pure state:" + for k v in "${(@kv)prompt_pure_state}"; do + print - " - $k: \`${(q)v}\`" + done + print - "- PROMPT: \`$(typeset -p PROMPT)\`" + print - "- Colors: \`$(typeset -p prompt_pure_colors)\`" + print - "- Virtualenv: \`$(typeset -p VIRTUAL_ENV_DISABLE_PROMPT)\`" + print - "- Conda: \`$(typeset -p CONDA_CHANGEPS1)\`" + + local ohmyzsh=0 + typeset -la frameworks + (( $+ANTIBODY_HOME )) && frameworks+=("Antibody") + (( $+ADOTDIR )) && frameworks+=("Antigen") + (( $+ANTIGEN_HS_HOME )) && frameworks+=("Antigen-hs") + (( $+functions[upgrade_oh_my_zsh] )) && { + ohmyzsh=1 + frameworks+=("Oh My Zsh") + } + (( $+ZPREZTODIR )) && frameworks+=("Prezto") + (( $+ZPLUG_ROOT )) && frameworks+=("Zplug") + (( $+ZPLGM )) && frameworks+=("Zplugin") + + (( $#frameworks == 0 )) && frameworks+=("None") + print - "- Detected frameworks: ${(j:, :)frameworks}" + + if (( ohmyzsh )); then + print - " - Oh My Zsh:" + print - " - Plugins: ${(j:, :)plugins}" + fi +} + +prompt_pure_setup() { + # Prevent percentage showing up if output doesn't end with a newline. + export PROMPT_EOL_MARK='' + + prompt_opts=(subst percent) + + # Borrowed from `promptinit`. Sets the prompt options in case Pure was not + # initialized via `promptinit`. + setopt noprompt{bang,cr,percent,subst} "prompt${^prompt_opts[@]}" + + if [[ -z $prompt_newline ]]; then + # This variable needs to be set, usually set by promptinit. + typeset -g prompt_newline=$'\n%{\r%}' + fi + + zmodload zsh/datetime + zmodload zsh/zle + zmodload zsh/parameter + zmodload zsh/zutil + + autoload -Uz add-zsh-hook + autoload -Uz vcs_info + autoload -Uz async && async + + # The `add-zle-hook-widget` function is not guaranteed to be available. + # It was added in Zsh 5.3. + autoload -Uz +X add-zle-hook-widget 2>/dev/null + + # Set the colors. + typeset -gA prompt_pure_colors_default prompt_pure_colors + prompt_pure_colors_default=( + execution_time yellow + git:arrow cyan + git:branch 242 + git:branch:cached red + git:action 242 + git:dirty 218 + host 242 + path blue + prompt:error red + prompt:success magenta + prompt:continuation 242 + user 242 + user:root default + virtualenv 242 + ) + prompt_pure_colors=("${(@kv)prompt_pure_colors_default}") + + add-zsh-hook precmd prompt_pure_precmd + add-zsh-hook preexec prompt_pure_preexec + + prompt_pure_state_setup + + zle -N prompt_pure_reset_prompt + zle -N prompt_pure_update_vim_prompt_widget + zle -N prompt_pure_reset_vim_prompt_widget + if (( $+functions[add-zle-hook-widget] )); then + add-zle-hook-widget zle-line-finish prompt_pure_reset_vim_prompt_widget + add-zle-hook-widget zle-keymap-select prompt_pure_update_vim_prompt_widget + fi + + # If a virtualenv is activated, display it in grey. + PROMPT='%(12V.%F{$prompt_pure_colors[virtualenv]}%12v%f .)' + + # Prompt turns red if the previous command didn't exit with 0. + local prompt_indicator='%(?.%F{$prompt_pure_colors[prompt:success]}.%F{$prompt_pure_colors[prompt:error]})${prompt_pure_state[prompt]}%f ' + PROMPT+=$prompt_indicator + + # Indicate continuation prompt by … and use a darker color for it. + PROMPT2='%F{$prompt_pure_colors[prompt:continuation]}… %(1_.%_ .%_)%f'$prompt_indicator + + # Store prompt expansion symbols for in-place expansion via (%). For + # some reason it does not work without storing them in a variable first. + typeset -ga prompt_pure_debug_depth + prompt_pure_debug_depth=('%e' '%N' '%x') + + # Compare is used to check if %N equals %x. When they differ, the main + # prompt is used to allow displaying both filename and function. When + # they match, we use the secondary prompt to avoid displaying duplicate + # information. + local -A ps4_parts + ps4_parts=( + depth '%F{yellow}${(l:${(%)prompt_pure_debug_depth[1]}::+:)}%f' + compare '${${(%)prompt_pure_debug_depth[2]}:#${(%)prompt_pure_debug_depth[3]}}' + main '%F{blue}${${(%)prompt_pure_debug_depth[3]}:t}%f%F{242}:%I%f %F{242}@%f%F{blue}%N%f%F{242}:%i%f' + secondary '%F{blue}%N%f%F{242}:%i' + prompt '%F{242}>%f ' + ) + # Combine the parts with conditional logic. First the `:+` operator is + # used to replace `compare` either with `main` or an ampty string. Then + # the `:-` operator is used so that if `compare` becomes an empty + # string, it is replaced with `secondary`. + local ps4_symbols='${${'${ps4_parts[compare]}':+"'${ps4_parts[main]}'"}:-"'${ps4_parts[secondary]}'"}' + + # Improve the debug prompt (PS4), show depth by repeating the +-sign and + # add colors to highlight essential parts like file and function name. + PROMPT4="${ps4_parts[depth]} ${ps4_symbols}${ps4_parts[prompt]}" + + # Guard against Oh My Zsh themes overriding Pure. + unset ZSH_THEME + + # Guard against (ana)conda changing the PS1 prompt + # (we manually insert the env when it's available). + export CONDA_CHANGEPS1=no +} + +prompt_pure_setup "$@"