Last active
August 13, 2023 19:08
-
-
Save jeebak/2fb31f964669892f6ef457508916bdb3 to your computer and use it in GitHub Desktop.
A three line ZSH prompt, with emacs and vim keybindings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ----------------------------------------------------------------------------- | |
# 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