From 1049999a504ec176735d26b2af631ee0fe65deb6 Mon Sep 17 00:00:00 2001 From: Clayton Auld Date: Tue, 22 Mar 2016 02:45:31 -0800 Subject: [PATCH] first customizations --- modules/prompt/functions/async | 346 ++++++++++++++++- .../prompt/functions/prompt_agnoster_setup | 154 +++++++- .../prompt/functions/prompt_powerline_setup | 186 ++++++++- modules/prompt/functions/prompt_pure_setup | 355 +++++++++++++++++- 4 files changed, 1037 insertions(+), 4 deletions(-) mode change 120000 => 100644 modules/prompt/functions/async mode change 120000 => 100644 modules/prompt/functions/prompt_agnoster_setup mode change 120000 => 100644 modules/prompt/functions/prompt_powerline_setup mode change 120000 => 100644 modules/prompt/functions/prompt_pure_setup diff --git a/modules/prompt/functions/async b/modules/prompt/functions/async deleted file mode 120000 index d4b591e1..00000000 --- a/modules/prompt/functions/async +++ /dev/null @@ -1 +0,0 @@ -../external/pure/async.zsh \ No newline at end of file diff --git a/modules/prompt/functions/async b/modules/prompt/functions/async new file mode 100644 index 00000000..c891f113 --- /dev/null +++ b/modules/prompt/functions/async @@ -0,0 +1,345 @@ +#!/usr/bin/env zsh + +# +# zsh-async +# +# version: 1.1.0 +# author: Mathias Fredriksson +# url: https://github.com/mafredri/zsh-async +# + +# Wrapper for jobs executed by the async worker, gives output in parseable format with execution time +_async_job() { + # 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) + )" + + # Calculate duration + duration=$(( EPOCHREALTIME - duration )) + + # stip all null-characters from stdout and stderr + stdout=${stdout//$'\0'/} + stderr=${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" +} + +# The background worker manages all tasks and runs them without interfering with other processes +_async_worker() { + local -A storage + local unique=0 + + # 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;; + u) unique=1;; + esac + done + + # Create a mutex for writing to the terminal through coproc + coproc cat + # Insert token into coproc + print -p "t" + + while read -r cmd; do + # Separate on spaces into an array + cmd=(${=cmd}) + local job=$cmd[1] + + # 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 + ;; + esac + + # 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 + + # Run task in background + _async_job $cmd & + # Store pid because zsh job manager is extremely unflexible (show jobname as non-unique '$job')... + storage[$job]=$! + done +} + +# +# Get results from finnished 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. +# +# 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 +# +async_process_results() { + setopt localoptions noshwordsplit + + integer count=0 + local worker=$1 + local callback=$2 + local -a items + local IFS=$'\0' + + 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 + + # Work through all results + while (( ${#items} > 0 )); do + $callback "${(@)=items[1,5]}" + shift 5 items + count+=1 + done + + # Empty the buffer + unset "ASYNC_PROCESS_BUFFER[$worker]" + done + + # If we processed any results, return success + (( count )) && 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 $callback ]]; then + async_process_results $worker $callback + fi +} + +# +# Start a new asynchronous job on specified worker, assumes the worker is running. +# +# usage: +# async_job [] +# +async_job() { + setopt localoptions noshwordsplit + + local worker=$1; shift + zpty -w $worker $@ +} + +# This function traps notification signals and calls all registered callbacks +_async_notify_trap() { + setopt localoptions noshwordsplit + + for k in ${(k)ASYNC_CALLBACKS}; do + async_process_results $k ${ASYNC_CALLBACKS[$k]} + 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]="$*" + + if (( ! ASYNC_USE_ZLE_HANDLER )); 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 + zpty -w $worker "_killjobs" + + # Clear all output buffers + while zpty -r $worker line; do true; done + + # Clear any partial buffers + 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 + 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 + + # If worker was called with -n, disable trap in favor of zle handler + 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 + 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=$? + 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 + 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 + ASYNC_USE_ZLE_HANDLER=0 + [[ -o interactive ]] && { + typeset -h REPLY + zpty _async_test cat + (( REPLY )) && ASYNC_USE_ZLE_HANDLER=1 + zpty -d _async_test + } +} + +async() { + async_init +} + +async "$@" diff --git a/modules/prompt/functions/prompt_agnoster_setup b/modules/prompt/functions/prompt_agnoster_setup deleted file mode 120000 index 34d8bc6d..00000000 --- a/modules/prompt/functions/prompt_agnoster_setup +++ /dev/null @@ -1 +0,0 @@ -../external/agnoster/agnoster.zsh-theme \ No newline at end of file diff --git a/modules/prompt/functions/prompt_agnoster_setup b/modules/prompt/functions/prompt_agnoster_setup new file mode 100644 index 00000000..93045562 --- /dev/null +++ b/modules/prompt/functions/prompt_agnoster_setup @@ -0,0 +1,153 @@ +# vim:ft=zsh ts=2 sw=2 sts=2 +# +# agnoster's Theme - https://gist.github.com/3712874 +# A Powerline-inspired theme for ZSH +# +# # README +# +# In order for this theme to render correctly, you will need a +# [Powerline-patched font](https://gist.github.com/1595572). +# +# In addition, I recommend the +# [Solarized theme](https://github.com/altercation/solarized/) and, if you're +# using it on Mac OS X, [iTerm 2](http://www.iterm2.com/) over Terminal.app - +# it has significantly better color fidelity. +# +# # Goals +# +# The aim of this theme is to only show you *relevant* information. Like most +# prompts, it will only show git information when in a git working directory. +# However, it goes a step further: everything from the current user and +# hostname to whether the last call exited with an error to whether background +# jobs are running in this shell will all be displayed automatically when +# appropriate. + +### Segment drawing +# A few utility functions to make it easy and re-usable to draw segmented prompts + +CURRENT_BG='NONE' +PRIMARY_FG=black + +# Characters +SEGMENT_SEPARATOR="\ue0b0" +PLUSMINUS="\u00b1" +BRANCH="\ue0a0" +DETACHED="\u27a6" +CROSS="\u2718" +LIGHTNING="\u26a1" +GEAR="\u2699" + +# Begin a segment +# Takes two arguments, background and foreground. Both can be omitted, +# rendering default background/foreground. +prompt_segment() { + local bg fg + [[ -n $1 ]] && bg="%K{$1}" || bg="%k" + [[ -n $2 ]] && fg="%F{$2}" || fg="%f" + if [[ $CURRENT_BG != 'NONE' && $1 != $CURRENT_BG ]]; then + print -n "%{$bg%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR%{$fg%}" + else + print -n "%{$bg%}%{$fg%}" + fi + CURRENT_BG=$1 + [[ -n $3 ]] && print -n $3 +} + +# End the prompt, closing any open segments +prompt_end() { + if [[ -n $CURRENT_BG ]]; then + print -n "%{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR" + else + print -n "%{%k%}" + fi + print -n "%{%f%}" + CURRENT_BG='' +} + +### Prompt components +# Each component will draw itself, and hide itself if no information needs to be shown + +# Context: user@hostname (who am I and where am I) +prompt_context() { + local user=`whoami` + + if [[ "$user" != "$DEFAULT_USER" || -n "$SSH_CONNECTION" ]]; then + prompt_segment $PRIMARY_FG default " %(!.%{%F{yellow}%}.)$user@%m " + fi +} + +# Git: branch/detached head, dirty status +prompt_git() { + local color ref + is_dirty() { + test -n "$(git status --porcelain --ignore-submodules)" + } + ref="$vcs_info_msg_0_" + if [[ -n "$ref" ]]; then + if is_dirty; then + color=yellow + ref="${ref} $PLUSMINUS" + else + color=green + ref="${ref} " + fi + if [[ "${ref/.../}" == "$ref" ]]; then + ref="$BRANCH $ref" + else + ref="$DETACHED ${ref/.../}" + fi + prompt_segment $color $PRIMARY_FG + print -Pn " $ref" + fi +} + +# Dir: current working directory +prompt_dir() { + prompt_segment blue $PRIMARY_FG ' %~ ' +} + +# Status: +# - was there an error +# - am I root +# - are there background jobs? +prompt_status() { + local symbols + symbols=() + [[ $RETVAL -ne 0 ]] && symbols+="%{%F{red}%}$CROSS" + [[ $UID -eq 0 ]] && symbols+="%{%F{yellow}%}$LIGHTNING" + [[ $(jobs -l | wc -l) -gt 0 ]] && symbols+="%{%F{cyan}%}$GEAR" + + [[ -n "$symbols" ]] && prompt_segment $PRIMARY_FG default " $symbols " +} + +## Main prompt +prompt_agnoster_main() { + RETVAL=$? + CURRENT_BG='NONE' + prompt_status + prompt_context + prompt_dir + prompt_git + prompt_end +} + +prompt_agnoster_precmd() { + vcs_info + PROMPT='%{%f%b%k%}$(prompt_agnoster_main) ' +} + +prompt_agnoster_setup() { + autoload -Uz add-zsh-hook + autoload -Uz vcs_info + + prompt_opts=(cr subst percent) + + add-zsh-hook precmd prompt_agnoster_precmd + + zstyle ':vcs_info:*' enable git + zstyle ':vcs_info:*' check-for-changes false + zstyle ':vcs_info:git*' formats '%b' + zstyle ':vcs_info:git*' actionformats '%b (%a)' +} + +prompt_agnoster_setup "$@" diff --git a/modules/prompt/functions/prompt_powerline_setup b/modules/prompt/functions/prompt_powerline_setup deleted file mode 120000 index 3715d75b..00000000 --- a/modules/prompt/functions/prompt_powerline_setup +++ /dev/null @@ -1 +0,0 @@ -../external/powerline/prompt_powerline_setup \ No newline at end of file diff --git a/modules/prompt/functions/prompt_powerline_setup b/modules/prompt/functions/prompt_powerline_setup new file mode 100644 index 00000000..1f13956b --- /dev/null +++ b/modules/prompt/functions/prompt_powerline_setup @@ -0,0 +1,185 @@ +# +# A ZSH theme based on a combination of the skwp prezto theme and the robl ohmyzsh theme. +# * RVM info shown on the right +# * Git branch info on the left +# * Single line prompt +# * Time since last commit on the left +# * Time in place of user@hostname +# +# Authors: +# David Rice + +ZSH_THEME_REP_TIME_SINCE_COMMIT_SHORT="%{$fg[green]%}" +ZSH_THEME_REP_TIME_SINCE_COMMIT_MEDIUM="%{$fg[yellow]%}" +ZSH_THEME_REP_TIME_SINCE_COMMIT_LONG="%{$fg[red]%}" +ZSH_THEME_REP_TIME_SINCE_COMMIT_NEUTRAL="%{$fg[cyan]%}" + +# returns the time since last git commit +function git_time_details() { + # only proceed if there is actually a git repository + if `git rev-parse --git-dir > /dev/null 2>&1`; then + # only proceed if there is actually a commit + if [[ $(git log 2>&1 > /dev/null | grep -c "^fatal: bad default revision") == 0 ]]; then + # get the last commit hash + # lc_hash=`git log --pretty=format:'%h' -1 2> /dev/null` + # get the last commit time + lc_time=`git log --pretty=format:'%at' -1 2> /dev/null` + + now=`date +%s` + seconds_since_last_commit=$((now-lc_time)) + lc_time_since=`time_since_commit $seconds_since_last_commit` + + echo "$lc_time_since" + else + echo "" + fi + else + echo "" + fi +} + +# returns the time by given seconds +function time_since_commit() { + seconds_since_last_commit=$(($1 + 0)) + + # totals + MINUTES=$((seconds_since_last_commit / 60)) + HOURS=$((seconds_since_last_commit/3600)) + + # sub-hours and sub-minutes + DAYS=$((seconds_since_last_commit / 86400)) + SUB_HOURS=$((HOURS % 24)) + SUB_MINUTES=$((MINUTES % 60)) + + if [ "$HOURS" -gt 24 ]; then + echo "${DAYS}d${SUB_HOURS}h${SUB_MINUTES}m" + elif [ "$MINUTES" -gt 60 ]; then + echo "${HOURS}h${SUB_MINUTES}m" + else + echo "${MINUTES}m" + fi +} + +function rvm_info_for_prompt { + if [[ -d ~/.rvm/ ]]; then + local ruby_version=$(~/.rvm/bin/rvm-prompt) + if [ -n "$ruby_version" ]; then + echo "$ruby_version" + fi + else + echo "" + fi +} + +function prompt_powerline_precmd { + # Check for untracked files or updated submodules since vcs_info doesn't. + if [[ ! -z $(git ls-files --other --exclude-standard 2> /dev/null) ]]; then + fmt_branch="%b%u%c${__PROMPT_SKWP_COLORS[4]}●%f" + else + fmt_branch="%b%u%c" + fi + zstyle ':vcs_info:*:prompt:*' formats "${fmt_branch}" + + vcs_info 'prompt' + RVM_PRECMD_INFO=$(rvm_info_for_prompt) + + # zstyle ':prezto:module:ruby' rvm '%r' +} + +function prompt_powerline_setup { + setopt LOCAL_OPTIONS + unsetopt XTRACE KSH_ARRAYS + prompt_opts=(cr percent subst) + + autoload -Uz add-zsh-hook + autoload -Uz vcs_info + + add-zsh-hook precmd prompt_powerline_precmd + + # Use extended color pallete if available. + if [[ $TERM = *256color* || $TERM = *rxvt* ]]; then + __PROMPT_SKWP_COLORS=( + "%F{81}" # turquoise + "%F{166}" # orange + "%F{135}" # purple + "%F{161}" # hotpink + "%F{118}" # limegreen + ) + else + __PROMPT_SKWP_COLORS=( + "%F{cyan}" + "%F{yellow}" + "%F{magenta}" + "%F{red}" + "%F{green}" + ) + fi + + # Enable VCS systems you use. + zstyle ':vcs_info:*' enable bzr git hg svn + + # check-for-changes can be really slow. + # You should disable it if you work with large repositories. + zstyle ':vcs_info:*:prompt:*' check-for-changes true + + # Formats: + # %b - branchname + # %u - unstagedstr (see below) + # %c - stagedstr (see below) + # %a - action (e.g. rebase-i) + # %R - repository path + # %S - path in the repository + # %n - user + # %m - machine hostname + + # local fmt_branch="(${__PROMPT_SKWP_COLORS[1]}%b%f%u%c)" + local fmt_branch="${__PROMPT_SKWP_COLORS[2]}%b%f%u%c" + local fmt_action="${__PROMPT_SKWP_COLORS[5]}%a%f" + local fmt_unstaged="${__PROMPT_SKWP_COLORS[2]}●%f" + local fmt_staged="${__PROMPT_SKWP_COLORS[5]}●%f" + + zstyle ':vcs_info:*:prompt:*' unstagedstr "${fmt_unstaged}" + zstyle ':vcs_info:*:prompt:*' stagedstr "${fmt_staged}" + zstyle ':vcs_info:*:prompt:*' actionformats "${fmt_branch}${fmt_action}" + zstyle ':vcs_info:*:prompt:*' formats "${fmt_branch}" + zstyle ':vcs_info:*:prompt:*' nvcsformats "" + + # SPLIT RVM PROMPT INFO + # TODO: should assign this to local variable? somehow doesn't work correctly. + rvm_split=("${(s/@/)$(rvm_info_for_prompt)}") + + # if [ "$POWERLINE_RIGHT_B" = "" ]; then + # POWERLINE_RIGHT_B=%D{%H:%M:%S} + local powerline_right_b=$rvm_split[1] + # fi + + # if [ "$POWERLINE_RIGHT_A" = "" ]; then + local powerline_right_a=$rvm_split[2] + # fi + + # Setup powerline style colouring + POWERLINE_COLOR_BG_GRAY=%K{240} + POWERLINE_COLOR_BG_LIGHT_GRAY=%K{240} + POWERLINE_COLOR_BG_WHITE=%K{255} + + POWERLINE_COLOR_FG_GRAY=%F{240} + POWERLINE_COLOR_FG_LIGHT_GRAY=%F{240} + POWERLINE_COLOR_FG_WHITE=%F{255} + + POWERLINE_SEPARATOR=$'\u2b80' + POWERLINE_R_SEPARATOR=$'\u2b82' + + POWERLINE_LEFT_A="%K{green}%F{white} %~ %k%f%F{green}%K{blue}"$POWERLINE_SEPARATOR + POWERLINE_LEFT_B="%k%f%F{white}%K{blue} "'${vcs_info_msg_0_}'" %k%f%F{blue}%K{black}"$POWERLINE_SEPARATOR + POWERLINE_LEFT_C=" %k%f%F{white}%K{black}"'$(git_time_details)'" %k%f%F{black}"$POWERLINE_SEPARATOR"%f " + + PROMPT=$POWERLINE_LEFT_A$POWERLINE_LEFT_B$POWERLINE_LEFT_C + # RPROMPT=$POWERLINE_COLOR_FG_WHITE$POWERLINE_R_SEPARATOR"%f$POWERLINE_COLOR_BG_WHITE $POWERLINE_COLOR_FG_GRAY$powerline_right_b "$POWERLINE_R_SEPARATOR"%f%k$POWERLINE_COLOR_BG_GRAY$POWERLINE_COLOR_FG_WHITE $powerline_right_a %f%k" + # RPROMPT=$POWERLINE_COLOR_FG_WHITE$POWERLINE_R_SEPARATOR"%f$POWERLINE_COLOR_BG_WHITE $POWERLINE_COLOR_FG_GRAY"'$powerline_right_b'" "$POWERLINE_R_SEPARATOR"%f%k$POWERLINE_COLOR_BG_GRAY$POWERLINE_COLOR_FG_WHITE "'$powerline_right_a'" %f%k" + RPROMPT=$POWERLINE_COLOR_FG_WHITE$POWERLINE_R_SEPARATOR"%f$POWERLINE_COLOR_BG_WHITE $POWERLINE_COLOR_FG_GRAY"'$(rvm_info_for_prompt)'" " +} + +prompt_powerline_setup "$@" + + + diff --git a/modules/prompt/functions/prompt_pure_setup b/modules/prompt/functions/prompt_pure_setup deleted file mode 120000 index 1480fcfd..00000000 --- a/modules/prompt/functions/prompt_pure_setup +++ /dev/null @@ -1 +0,0 @@ -../external/pure/pure.zsh \ No newline at end of file diff --git a/modules/prompt/functions/prompt_pure_setup b/modules/prompt/functions/prompt_pure_setup new file mode 100644 index 00000000..d1de2200 --- /dev/null +++ b/modules/prompt/functions/prompt_pure_setup @@ -0,0 +1,354 @@ +# 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 variable as specified by caller + typeset -g "${var}"="${human}" +} + +# stores (into prompt_pure_cmd_exec_time) the exec 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} )) + 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_clear_screen() { + # enable output to terminal + zle -I + # clear screen and move cursor to (0, 0) + print -n '\e[2J\e[0;0H' + # print preprompt + 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 + [[ -n $SSH_CONNECTION ]] && print -Pn '(%m) ' + case $1 in + expand-prompt) + print -Pn $2;; + ignore-escape) + print -rn $2;; + esac + # end set title + print -n '\a' +} + +prompt_pure_preexec() { + # attempt to detect and prevent prompt_pure_async_git_fetch from interfering with user initiated git or hub fetch + [[ $2 =~ (git|hub)\ .*(pull|fetch) ]] && async_flush_jobs 'prompt_pure' + + prompt_pure_cmd_timestamp=$EPOCHSECONDS + + # shows the current dir and executed command in the title while a process is active + prompt_pure_set_title 'ignore-escape' "$PWD:t: $2" +} + +# string length ignoring ansi escapes +prompt_pure_string_length_to_var() { + local str=$1 var=$2 length + # perform expansion on str and check length + length=$(( ${#${(S%%)str//(\%([KF1]|)\{*\}|\%[Bbkf])}} )) + + # store string length in variable as specified by caller + typeset -g "${var}"="${length}" +} + +prompt_pure_preprompt_render() { + # 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 + + # set color for git branch/dirty status, change color if dirty checking has been delayed + local git_color=242 + [[ -n ${prompt_pure_git_last_dirty_check_timestamp+x} ]] && git_color=red + + # construct preprompt, beginning with path + local preprompt="%F{blue}%~%f" + # git info + preprompt+="%F{$git_color}${vcs_info_msg_0_}${prompt_pure_git_dirty}%f" + # git pull/push arrows + preprompt+="%F{cyan}${prompt_pure_git_arrows}%f" + # username and machine if applicable + preprompt+=$prompt_pure_username + # execution time + preprompt+="%F{yellow}${prompt_pure_cmd_exec_time}%f" + + # if executing through precmd, do not perform fancy terminal editing + if [[ "$1" == "precmd" ]]; then + print -P "\n${preprompt}" + else + # only redraw if preprompt has changed + [[ "${prompt_pure_last_preprompt}" != "${preprompt}" ]] || return + + # calculate length of preprompt and store it locally in preprompt_length + integer preprompt_length lines + prompt_pure_string_length_to_var "${preprompt}" "preprompt_length" + + # calculate number of preprompt lines for redraw purposes + (( lines = ( preprompt_length - 1 ) / COLUMNS + 1 )) + + # 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" + (( last_lines = ( last_preprompt_length - 1 ) / COLUMNS + 1 )) + + # clr_prev_preprompt erases visual artifacts from previous preprompt + local clr_prev_preprompt + if (( last_lines > lines )); then + # move cursor up by last_lines, clear the line and move it down by one line + clr_prev_preprompt="\e[${last_lines}A\e[2K\e[1B" + while (( last_lines - lines > 1 )); do + # clear the line and move cursor down by one + clr_prev_preprompt+='\e[2K\e[1B' + (( last_lines-- )) + done + + # move cursor into correct position for preprompt update + clr_prev_preprompt+="\e[${lines}B" + # create more space for preprompt if new preprompt has more lines than last + 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 ))} + fi + + # disable clearing of line if last char of preprompt is last column of terminal + local clr='\e[K' + (( COLUMNS * lines == preprompt_length )) && clr= + + # modify previous preprompt + print -Pn "${clr_prev_preprompt}\e[${lines}A\e[${COLUMNS}D${preprompt}${clr}\n" + + # redraw prompt (also resets cursor position) + zle && zle .reset-prompt + fi + + # store previous preprompt for comparison + prompt_pure_last_preprompt=$preprompt +} + +prompt_pure_precmd() { + # check exec time and store it in a variable + prompt_pure_check_cmd_exec_time + + # by making sure that prompt_pure_cmd_timestamp is defined here the async functions are prevented from interfering + # 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' '%~' + + # get vcs info + vcs_info + + # preform async git dirty check and fetch + prompt_pure_async_tasks + + # print the preprompt + prompt_pure_preprompt_render "precmd" + + # remove the prompt_pure_cmd_timestamp, indicating that precmd has completed + unset prompt_pure_cmd_timestamp +} + +# fastest possible way to check if repo is dirty +prompt_pure_async_git_dirty() { + local untracked_dirty=$1; shift + + # use cd -q to avoid side effects of changing directory, e.g. chpwd hooks + builtin cd -q "$*" + + 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 "*" +} + +prompt_pure_async_git_fetch() { + # use cd -q to avoid side effects of changing directory, e.g. chpwd hooks + builtin cd -q "$*" + + # 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 +} + +prompt_pure_async_tasks() { + # initialize async worker + ((!${prompt_pure_async_init:-0})) && { + async_start_worker "prompt_pure" -u -n + async_register_callback "prompt_pure" prompt_pure_async_callback + prompt_pure_async_init=1 + } + + # store working_tree without the "x" prefix + local working_tree="${vcs_info_msg_1_#x}" + + # check if the working tree changed (prompt_pure_current_working_tree is prefixed by "x") + if [[ ${prompt_pure_current_working_tree#x} != $working_tree ]]; 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 + + # 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}" + fi + + # only perform tasks inside git working tree + [[ -n $working_tree ]] || return + + # 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}" + fi + + # if dirty checking is sufficiently fast, tell 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}" "${working_tree}" + fi +} + +prompt_pure_async_callback() { + local job=$1 + local output=$3 + local exec_time=$4 + + case "${job}" in + prompt_pure_async_git_dirty) + prompt_pure_git_dirty=$output + 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 + ;; + esac +} + +prompt_pure_setup() { + # prevent percentage showing up + # if output doesn't end with a newline + export PROMPT_EOL_MARK='' + + prompt_opts=(subst percent) + + zmodload zsh/datetime + zmodload zsh/zle + autoload -Uz add-zsh-hook + autoload -Uz vcs_info + autoload -Uz async && async + + add-zsh-hook precmd prompt_pure_precmd + add-zsh-hook preexec prompt_pure_preexec + + zstyle ':vcs_info:*' enable git + zstyle ':vcs_info:*' use-simple true + # only export two msg variables from vcs_info + zstyle ':vcs_info:*' max-exports 2 + # vcs_info_msg_0_ = ' %b' (for branch) + # vcs_info_msg_1_ = 'x%R' git top level (%R), x-prefix prevents creation of a named path (AUTO_NAME_DIRS) + zstyle ':vcs_info:git*' formats ' %b' 'x%R' + zstyle ':vcs_info:git*' actionformats ' %b|%a' 'x%R' + + # if the user has not registered a custom zle widget for clear-screen, + # override the builtin one so that the preprompt is displayed correctly when + # ^L is issued. + if [[ $widgets[clear-screen] == 'builtin' ]]; then + zle -N clear-screen prompt_pure_clear_screen + fi + + # show username@host if logged in through SSH + [[ "$SSH_CONNECTION" != '' ]] && prompt_pure_username=' %F{242}%n@%m%f' + + # show username@host if root, with username in white + [[ $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_pure_setup "$@"