Skip to content

Instantly share code, notes, and snippets.

@jeebak
Last active August 13, 2023 19:08
Show Gist options
  • Save jeebak/2fb31f964669892f6ef457508916bdb3 to your computer and use it in GitHub Desktop.
Save jeebak/2fb31f964669892f6ef457508916bdb3 to your computer and use it in GitHub Desktop.
A three line ZSH prompt, with emacs and vim keybindings
# -----------------------------------------------------------------------------
# https://gist.github.com/jeebak/2fb31f964669892f6ef457508916bdb3
#
# A three line ZSH prompt, with emacs and vim keybindings:
# 1. At-a-glance: ┌─($USER@$HOST:$TTY─(shlvl:$SHLVL)─(jobs:0)─(exit:$?)
# 2. VCS/vim mode: │░(«git/vim mode»)↔[master↔origin↕◇◇◇]░》
# 3. [r]prompt: └─(«zsh»)% [~]
#
# NOTE: SHLVL on macOS seems to be > 1 (it is correctly 1, under Linux.) The
# quick workaround is to: export SHLVL=1 (before starting tmux, for example.)
#
# Plus a REPORTTIME based command timer
#
# Also...
# - sets emacs mode as default, but also
# - binds <esc> to switch to vi-cmd-mode, to get the best of both worlds
# - additional vicmd bindings
# - additional logic to display: INSERT|NORMAL|REPLAC|VISUAL modes
# - ^D bash-ctrl-d
# - ^Z fancy-ctrl-z
#
# -----------------------------------------------------------------------------
# Add VCS into prompt. also, checkout: man zshcontrib
# - http://stackoverflow.com/questions/1128496/to-get-a-prompt-which-indicates-git-branch-in-zsh
# - http://zsh.sourceforge.net/Doc/Release/Prompt-Expansion.html
# - http://zsh.sourceforge.net/Doc/Release/User-Contributions.html#Version-Control-Information
# - http://zsh.sourceforge.net/Doc/Release/Zsh-Line-Editor.html
# TODO: ideas from... ?
# - https://github.com/denysdovhan/spaceship-prompt
# - https://github.com/starship/starship
#
# :help digraph-table
# ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿×÷ʿˇ˘˙˚˛˝‐–—―‗‘’‚“”„†‡…‰‹›※‾⁄⁺⁻ⁿ₊₋₤₧€№™
# Ω⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞←↑→↓↔↕⇒⇔∀∂∆∇∏∑−∙√∞∟∥∧∨∩∪∫∴∵≈≠≡≤≥⊥⌂⌐⌒⌠⌡▀▄█▌▐░▒▓
# ■□▬▲△▼▽◆◇◊○◎●◘◙★☆☺☻☼♀♂♠♣♪♫♭♯✓✗✠
#
# Requires:
setopt PROMPT_SUBST
autoload -Uz vcs_info
# -----------------------------------------------------------------------------
zstyle ':vcs_info:*' disable-patterns "$HOME" # $HOME got *very* slow w/ yadm
# -- http://briancarper.net/blog/570.html -------------------------------------
zstyle ':vcs_info:*' stagedstr '%F{white}●%f' # Traffic Lights:
zstyle ':vcs_info:*' unstagedstr '%F{yellow}●%f' # Amber, Yellow, Red
zstyle ':vcs_info:*' check-for-changes true
# -- "Other" VCSen ------------------------------------------------------------
# "In branchformat these replacements are done:"
# %r The current revision number or the hgrevformat style for hg
# %b The branch name.
function { # Anonymous function
local i
local -a VCSen
for i in git cvs svn fossil; do
command -v $i > /dev/null 2>&1 && VCSen+=($i)
done
zstyle ':vcs_info:(sv[nk]|bzr):*' branchformat '%b%F{red}:%f%B%F{yellow}%r%f%b'
zstyle ':vcs_info:*' enable $VCSen
}
# -- Based on: http://eseth.org/2010/git-in-zsh.html#post-git-in-zsh ----------
zstyle ':vcs_info:git*+set-message:*' hooks git-status git-stash
# VSC_info "hides" these function names w/ "+vi-" prefix
# Show remote ref name and number of commits ahead-of or behind
function +vi-git-status() {
local branch remote ahead behind
branch=$hook_com[branch]
hook_com[branch]="%F{green}${hook_com[branch]}%f"
# Are we on a remote-tracking branch?
remote=${$(command git rev-parse --verify $branch@{upstream} --symbolic-full-name --abbrev-ref 2> /dev/null)}
if [[ -n ${remote} ]] ; then
read ahead <<< $(command git rev-list $branch@{upstream}..HEAD 2> /dev/null | wc -l)
read behind <<< $(command git rev-list HEAD..$branch@{upstream} 2> /dev/null | wc -l)
(( $ahead )) && ahead="%B%F{green}+${ahead}%f%b" || unset ahead
(( $behind )) && behind="%B%F{red}-${behind}%f%b" || unset behind
# Only show 'origin' if the remote branch name is the same as the local,
# otherwise keep the full 'origin/different-remote-branch-name' name
[[ ${remote#*/} == $branch ]] && remote="${remote%%/*}"
hook_com[branch]+="$ahead%F{cyan}↔${remote}%f$behind"
fi
# NOTE: having these in their own +vi-git-staged() etc. functions lead to
# some weird behavior
# Process (un)staged (Amber, Yellow, Red Traffic Lights) prompts
[[ -z $hook_com[staged] ]] && hook_com[staged]='%F{cyan}◇%f'
[[ -z $hook_com[unstaged] ]] && hook_com[unstaged]='%F{cyan}◇%f'
# Add red circle if there are any untracked files
[[ ! $(command git config --get status.showUntrackedFiles) =~ no &&
-n $(command git ls-files --other --exclude-standard 2> /dev/null) ]] &&
hook_com[unstaged]+='%F{red}●%f' || hook_com[unstaged]+='%F{cyan}◇%f'
}
# Show count of stashed changes
function +vi-git-stash() {
local -a count
if [[ -s $(command git rev-parse --git-dir)/refs/stash ]] ; then
read count <<< $(command git stash list 2> /dev/null | grep '^stash@' | wc -l)
hook_com[misc]+="%F{magenta}(%f«${count}:stashed»%F{magenta})%f"
fi
}
# -- Define Hook to Build PROMPT ----------------------------------------------
__dotmatrix::prompt-precmd-hook() {
local -A p # "at-a-glance" prompt elements
local -A vp # vcs prompt elements
local ranger NL
NL=$'\n'
p=(
user '%F{cyan}%n%f' # %n $USERNAME.
at '%F{red}@%f'
host '%F{cyan}%m%f' # %m The hostname up to the first ‘.’...
colon '%F{red}:%f'
tty '%F{cyan}%l%f' # %l The line (tty) the user is logged in on...
# hist '─(hist#:%!)' # %! Current history event number.
# 13.3 Conditional Substrings in Prompts
# Bold black when "normal" and bold yellow upon alert
shlvl '─(%Ushlvl%u:%B%2(L.%F{yellow}.%F{black})%L%f%b)' # %L Current $SHLVL
jobs '─(%Ujobs%u:%B%1(j.%F{yellow}.%F{black})%j%f%b)' # %j The # of jobs
exit '─(%Uexit%u:%B%0(?.%F{black}.%F{yellow})%?%f%b)' # %? The exit code
char '%B%0(#.%F{yellow}.%F{black})%#%f%b' # %# '#' for root, '%' if not
)
vp=(
# %s The VCS in use (git, hg, svn, etc.).
vcs '%F{magenta}(%f«%s»%F{magenta})%f%F{cyan}↔%f'
# actionformats:
# A list of formats, used if there is a special action going on in your
# current repository; like an interactive rebase or a merge conflict
branch '%F{magenta}[%f%F{green}%b%f'
# %a An identifier that describes the action. Only makes sense in actionformats.
action '%F{yellow}|%f%F{red}%a%f%F{magenta}]%f'
# %b hook_com[branch], %c hook_com[staged], %u hook_com[unstaged]
info '%F{magenta}[%f%b%F{cyan}↕%f%c%u%F{magenta}]%f'
# %m hook_com[misc]
misc '%m'
)
# Display vim prompt instead of vcs, if it's set
[[ -n "$__dotmatrix__VIM_PROMPT" ]] && vp[vcs]="${__dotmatrix__VIM_PROMPT}"
zstyle ':vcs_info:*' actionformats "$vp[vcs]$vp[branch]$vp[action]"
zstyle ':vcs_info:*' formats "$vp[vcs]$vp[info]$vp[misc]"
# Do not enable for https://github.com/TheLocehiliosan/yadm
if [[ "$HOME" = "$PWD" ]] || command git rev-parse --git-dir > /dev/null 2>&1; then
[[ -z $NO_VCS_INFO ]] && vcs_info
fi
[[ -n "$RANGER_LEVEL" ]] && ranger="[*** in ranger ***]"
# First line
PROMPT="%{┌─($p[user]$p[at]$p[host]$p[colon]$p[tty])$p[shlvl]$p[jobs]$p[exit]%}$NL"
# Second line
if [[ -n "$vcs_info_msg_0_" ]]; then
PROMPT+="│░${vcs_info_msg_0_}░》 ${ranger}${NL}"
elif [[ -n "$__dotmatrix__VIM_PROMPT" ]]; then
# Replace trailing '%%b' with '%b' for non-vcs directories
PROMPT+="│░${__dotmatrix__VIM_PROMPT/\%\%b/%b} ${ranger}${NL}"
fi
# Third line
PROMPT+="└─(«$ZSH_NAME»)$p[char] "
# Clean slate
__dotmatrix__VIM_PROMPT=
vcs_info_msg_0_=
}
# Right Prompt
RPROMPT='[%B%F{cyan}%~%f%b]'
# -- Build Vim Prompt ---------------------------------------------------------
# Modified from: http://www.zsh.org/mla/users/2002/msg00108.html
# Example from: https://dougblack.io/words/zsh-vi-mode.html set "NORMAL" only
function { # Anonymous function
local widget mode
local -A widgets
# https://stackoverflow.com/questions/18042685/list-of-zsh-bindkey-commands
# for i in $(bindkey -l); do bindkey -M $i; done | less
# zle -la # to list "hidden" functions
# Extend widgets
widgets=(
vi-add-eol INSERT
vi-add-next INSERT
vi-change INSERT
vi-change-eol INSERT
vi-change-whole-line INSERT
vi-insert INSERT
vi-insert-bol INSERT
vi-open-line-above INSERT
vi-open-line-below INSERT
vi-substitute INSERT
vi-replace REPLAC # I can live with this
vi-cmd-mode NORMAL
# Run: bindkey -M visual # to get a listing of bound widgets
visual-mode VISUAL
visual-line-mode VISUAL
deactivate-region NORMAL
vi-delete-char NORMAL
vi-delete NORMAL
vi-down-case NORMAL
vi-oper-swap-case NORMAL
vi-put-after NORMAL
vi-put-before NORMAL
vi-up-case NORMAL
vi-yank NORMAL
)
for widget mode in ${(kv)widgets}; do
# Create new function
eval "$widget() {
zle .$widget
# Use: %%b to end bold, to discern it from %b (branch info) in vcs_info
__dotmatrix__VIM_PROMPT='%B%F{yellow}«$mode»%f%%b'
__dotmatrix::prompt-precmd-hook
zle reset-prompt
}"
# Create new widget
zle -N "$widget"
done
}
# -----------------------------------------------------------------------------
# http://chneukirchen.org/blog/archive/2013/03/10-fresh-zsh-tricks-you-may-not-know.html
# Bonus item: This is more for fun than serious use. An updating clock in
# your prompt:
# _prompt_and_resched() { sched +1 _prompt_and_resched; zle && zle reset-prompt }
# _prompt_and_resched
# PS1="%D{%H:%M:%S} $PS1"
# RPROMPT=$'[%B%F{cyan}%~%f%b][%D{%H:%M:%S}]'
# -- Define Hooks for Command timer -------------------------------------------
# Based on: https://superuser.com/questions/553564/is-there-a-way-to-make-zsh-run-a-command-after-reporttime
REPORTTIME=10
# This needs to be [g]lobal
typeset -gA __dotmatrix__cmdtimer_vars
__dotmatrix__cmdtimer_vars=(
cmd_seq ''
start_time "$(date +%s)"
start_date "$(date)"
)
# An init() function
__dotmatrix::cmdtimer-preexec-hook() {
__dotmatrix__cmdtimer_vars[cmd_seq]="$1"
__dotmatrix__cmdtimer_vars[start_time]="$(date +%s)"
__dotmatrix__cmdtimer_vars[start_date]="$(date '+%a %b %d %H:%M:%S %Z %Y')"
print -P "%B%F{black}«Started: ${__dotmatrix__cmdtimer_vars[start_date]}»%f%b"
}
__dotmatrix::cmdtimer-precmd-hook() {
local elapsed h m s
if [[ -n "$__dotmatrix__cmdtimer_vars[cmd_seq]" ]]; then
elapsed=$(($(date +%s) - $__dotmatrix__cmdtimer_vars[start_time]))
if (($elapsed > $REPORTTIME)); then
# Using the "let" since the ()'s seem to confuse vim's syntax hightlighting
((h=${elapsed}/3600)); let "m=(${elapsed}%3600)/60"; ((s=${elapsed}%60));
elapsed="$(printf "%02d:%02d:%02d" $h $m $s)"
print -P "\
┌──────────────────────────────────────────┐
│░ ⌠%F{magenta}Start: %F{yellow}${__dotmatrix__cmdtimer_vars[start_date]}%f ░│
│░ ⌡%F{magenta} End: %F{yellow}$(date '+%a %b %d %H:%M:%S %Z %Y')%f ░│
│░ %F{magenta}Elapsed: %F{yellow}${elapsed}%f, for cmd sequence... ░│
└──────────────────────────────────────────┘
》%F{cyan}${__dotmatrix__cmdtimer_vars[cmd_seq]}%f"
else
print -P "%B%F{black}«Ended: $(date)»%f%b"
fi
fi
__dotmatrix__cmdtimer_vars[cmd_seq]=
}
# -- Vimification -------------------------------------------------------------
# http://chneukirchen.org/blog/archive/2013/03/10-fresh-zsh-tricks-you-may-not-know.html
# "8. ^X^V swiches to vi-cmd-mod/^X^E to switch back to emacs..."
# "... and 'i' will put you back into Emacs mode again."
bindkey -e # Set to Emacs mode, by default, but map escape to...
bindkey '^[' vi-cmd-mode # ...to get the best of both worlds
# (since you'd have to hit escape to get into vi mode anyway.)
# http://www.johnhawthorn.com/2012/09/vi-escape-delays/
KEYTIMEOUT=1
# http://stratus3d.com/blog/2017/10/26/better-vi-mode-in-zshell/
autoload -Uz edit-command-line
zle -N edit-command-line
bindkey -M vicmd '^v' edit-command-line # Starts $EDITOR session on command
# These are unbound by default in vicmd mode. Some nice to haves.
bindkey -M vicmd '^a' beginning-of-line
bindkey -M vicmd '^e' end-of-line
bindkey -M vicmd '^d' __dotmatrix::bash-ctrl-d
bindkey -M vicmd '^f' forward-char
bindkey -M vicmd '^b' backward-char
bindkey -M vicmd '^w' backward-kill-word
bindkey -M vicmd '^z' __dotmatrix::fancy-ctrl-z
bindkey -M vicmd 'ZZ' accept-line
bindkey -M vicmd ':w' accept-line
bindkey -M vicmd ':x' accept-line
# Remove binding for: execute-named-cmd
bindkey -M vicmd -r ':'
# -- ^D bash-ctrl-d -----------------------------------------------------------
# Emulate Bash $IGNOREEOF behavior, based on:
# http://www.zsh.org/mla/users/2001/msg00240.html
# https://superuser.com/questions/1243138/why-does-ignoreeof-not-work-in-zsh
autoload -Uz colors && colors
setopt ignore_eof
IGNOREEOF=1
__dotmatrix::bash-ctrl-d() {
local suspended_jobs
if [[ $CURSOR == 0 && -z $BUFFER ]]; then
suspended_jobs="$(jobs -s)"
if [[ -n "$suspended_jobs" ]]; then
echo -n "${fg_bold[yellow]}Exit aborted! You have suspended jobs:${reset_color}
${fg[magenta]}$suspended_jobs${reset_color}"
else
[[ -z $IGNOREEOF || $IGNOREEOF == 0 ]] && exit
[[ -z $__dotmatrix__IGNOREEOF ]] && __dotmatrix::bash-ctrl-d-reset
(( --__dotmatrix__IGNOREEOF <= 0 )) && exit
echo -n "Hit $__dotmatrix__IGNOREEOF more ^D to really exit"
fi
zle send-break
else
zle delete-char-or-list
fi
}
__dotmatrix::bash-ctrl-d-reset() {
(( __dotmatrix__IGNOREEOF = IGNOREEOF + 1 ))
}
zle -N __dotmatrix::bash-ctrl-d
bindkey "^D" __dotmatrix::bash-ctrl-d
# This is probably not the best place to define this function, but placing it
# here since we're doing the same thing as above.
exec() {
local suspended_jobs
suspended_jobs="$(jobs -s)"
if [[ -z "$suspended_jobs" || -n "$ZLE_STATE" ]]; then
builtin exec "$@"
else
echo "${fg_bold[yellow]}Exec aborted! You have suspended jobs:${reset_color}
'${fg[magenta]}$suspended_jobs${reset_color}'"
fi
}
# -- ^Z fancy-ctrl-z ----------------------------------------------------------
# http://sheerun.net/2014/03/21/how-to-boost-your-vim-productivity/
# was: foreground-vi() { fg %${EDITOR:-vim} }
# from: http://chneukirchen.org/blog/archive/2012/02/10-new-zsh-tricks-you-may-not-know.html
__dotmatrix::fancy-ctrl-z() {
local suspended_jobs="$(jobs -s)"
if [[ -n "$suspended_jobs" ]] && [[ $#BUFFER -eq 0 ]]; then
builtin fg
zle accept-line
else
zle push-input
zle clear-screen
zle get-line
fi
}
zle -N __dotmatrix::fancy-ctrl-z
bindkey '^Z' __dotmatrix::fancy-ctrl-z
# -- Add Zsh Hooks ------------------------------------------------------------
autoload -Uz add-zsh-hook
add-zsh-hook precmd __dotmatrix::prompt-precmd-hook
add-zsh-hook precmd __dotmatrix::cmdtimer-precmd-hook
add-zsh-hook preexec __dotmatrix::cmdtimer-preexec-hook
add-zsh-hook preexec __dotmatrix::bash-ctrl-d-reset
# -----------------------------------------------------------------------------
# vim: set ft=zsh:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment