#!/bin/zsh # Headline ZSH Prompt # Copyright (c) 2022 Moarram under the MIT License # To install, source this file from your .zshrc file # Customization variables begin around line 70 # Formatting aliases # (add more if you need) reset=$'\e[0m' bold=$'\e[1m' faint=$'\e[2m' italic=$'\e[3m' underline=$'\e[4m' invert=$'\e[7m' # ... # Foreground color aliases black=$'\e[30m' red=$'\e[31m' green=$'\e[32m' yellow=$'\e[33m' blue=$'\e[34m' magenta=$'\e[35m' cyan=$'\e[36m' white=$'\e[37m' light_black=$'\e[90m' light_red=$'\e[91m' light_green=$'\e[92m' light_yellow=$'\e[93m' light_blue=$'\e[94m' light_magenta=$'\e[95m' light_cyan=$'\e[96m' light_white=$'\e[97m' # Background color aliases black_back=$'\e[40m' red_back=$'\e[41m' green_back=$'\e[42m' yellow_back=$'\e[43m' blue_back=$'\e[44m' magenta_back=$'\e[45m' cyan_back=$'\e[46m' white_back=$'\e[47m' light_black_back=$'\e[100m' light_red_back=$'\e[101m' light_green_back=$'\e[102m' light_yellow_back=$'\e[103m' light_blue_back=$'\e[104m' light_magenta_back=$'\e[105m' light_cyan_back=$'\e[106m' light_white_back=$'\e[107m' # Custom colors # REF: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters # orange_yellow=$'\e[38;5;214m' # example 8-bit color # orange_brown=$'\e[38;2;191;116;46m' # example rgb color # ... # Flags ! [ -z "$SSH_TTY$SSH_CONNECTION$SSH_CLIENT" ] IS_SSH=$? # 0=true, 1=false # ------------------------------------------------------------------------------ # Customization # Use the following variables to customize the theme # These variables can also be set in your ~/.zshrc after sourcing this file # The style aliases for ANSI SGR codes (defined above) can be used there too # Info sources (enclose in single quotes as these will be eval'd, use empty string to hide segment) HEADLINE_USER_CMD='echo $USER' HEADLINE_HOST_CMD='hostname -s' # consider 'basename "$VIRTUAL_ENV"' to replace host with environment HEADLINE_PATH_CMD='print -rP "%~"' HEADLINE_GIT_BRANCH_CMD='headline_git_branch' HEADLINE_GIT_STATUS_CMD='headline_git_status' # Info symbols (optional) HEADLINE_USER_PREFIX='' # consider " " HEADLINE_HOST_PREFIX='' # consider " " HEADLINE_PATH_PREFIX='' # consider " " HEADLINE_BRANCH_PREFIX='' # consider " " # Info joints HEADLINE_USER_BEGIN='' if [ $IS_SSH = 0 ]; then HEADLINE_USER_BEGIN='=> '; fi HEADLINE_USER_TO_HOST=' @ ' HEADLINE_HOST_TO_PATH=': ' HEADLINE_PATH_TO_BRANCH=' | ' # only used when no padding between <path> and <branch> HEADLINE_PATH_TO_PAD='' # used if padding between <path> and <branch> HEADLINE_PAD_TO_BRANCH='' # used if padding between <path> and <branch> HEADLINE_BRANCH_TO_STATUS=' [' HEADLINE_STATUS_TO_STATUS='' # between each status section, consider "]" HEADLINE_STATUS_END=']' # Info padding character HEADLINE_PAD_CHAR=' ' # repeated for space between <path> and <branch> # Info truncation symbol HEADLINE_TRUNC_PREFIX='...' # shown where <path> or <branch> is truncated, consider "…" # Info styles HEADLINE_STYLE_DEFAULT='' # style applied to entire info line HEADLINE_STYLE_JOINT=$light_black HEADLINE_STYLE_USER=$bold$red HEADLINE_STYLE_HOST=$bold$yellow HEADLINE_STYLE_PATH=$bold$blue HEADLINE_STYLE_BRANCH=$bold$cyan HEADLINE_STYLE_STATUS=$bold$magenta # Info options HEADLINE_INFO_MODE=precmd # precmd|prompt (whether info line is in PROMPT or printed by precmd) # use "precmd" for window resize to work properly (but Ctrl+L doesn't show info line) # use "prompt" for Ctrl+L to clear properly (but window resize eats previous output) # Separator options HEADLINE_LINE_MODE=on # on|auto|off (whether to print the line above the prompt) # Separator character HEADLINE_LINE_CHAR='_' # repeated for line above information # Separator styles HEADLINE_STYLE_JOINT_LINE=$HEADLINE_STYLE_JOINT HEADLINE_STYLE_USER_LINE=$HEADLINE_STYLE_USER HEADLINE_STYLE_HOST_LINE=$HEADLINE_STYLE_HOST HEADLINE_STYLE_PATH_LINE=$HEADLINE_STYLE_PATH HEADLINE_STYLE_BRANCH_LINE=$HEADLINE_STYLE_BRANCH HEADLINE_STYLE_STATUS_LINE=$HEADLINE_STYLE_STATUS # Git branch characters HEADLINE_GIT_HASH=':' # hash prefix to distinguish from branch # Git status characters # To set individual status styles use "%{$reset<style>%}<char>" HEADLINE_GIT_STAGED='+' HEADLINE_GIT_CHANGED='!' HEADLINE_GIT_UNTRACKED='?' HEADLINE_GIT_BEHIND='↓' HEADLINE_GIT_AHEAD='↑' HEADLINE_GIT_DIVERGED='↕' HEADLINE_GIT_STASHED='*' HEADLINE_GIT_CONFLICTS='✘' # consider "%{$red%}✘" HEADLINE_GIT_CLEAN='' # consider "✓" or "✔" # Git status options HEADLINE_DO_GIT_STATUS_COUNTS=false # set "true" to show count of each status HEADLINE_DO_GIT_STATUS_OMIT_ONE=false # set "true" to omit the status number when it is 1 # Prompt HEADLINE_PROMPT='%(#.#.%(!.!.$)) ' # consider "%#" HEADLINE_RPROMPT='' # Clock (prepends to RPROMPT) HEADLINE_DO_CLOCK=false # whether to show the clock HEADLINE_STYLE_CLOCK=$faint HEADLINE_CLOCK_FORMAT='%l:%M:%S %p' # consider "%+" for full date (see man strftime) # Exit code HEADLINE_DO_ERR=false # whether to show non-zero exit codes above prompt HEADLINE_DO_ERR_INFO=true # whether to show exit code meaning as well HEADLINE_ERR_PREFIX='→ ' HEADLINE_STYLE_ERR=$italic$faint # ------------------------------------------------------------------------------ # Options for zsh setopt PROMPT_SP # always start prompt on new line setopt PROMPT_SUBST # substitutions autoload -U add-zsh-hook PROMPT_EOL_MARK='' # remove weird % symbol ZLE_RPROMPT_INDENT=0 # remove extra space # Local variables _HEADLINE_LINE_OUTPUT='' # separator line _HEADLINE_INFO_OUTPUT='' # text line _HEADLINE_DO_SEP='false' # whether to show divider this time if [ $IS_SSH = 0 ]; then _HEADLINE_DO_SEP='true' # assume it's not a fresh window fi # Calculate length of string, excluding formatting characters # REF: https://old.reddit.com/r/zsh/comments/cgbm24/multiline_prompt_the_missing_ingredient/ headline_prompt_len() { # (str, num) emulate -L zsh local -i COLUMNS=${2:-COLUMNS} local -i x y=${#1} m if (( y )); then while (( ${${(%):-$1%$y(l.1.0)}[-1]} )); do x=y (( y *= 2 )) done while (( y > x + 1 )); do (( m = x + (y - x) / 2 )) (( ${${(%):-$1%$m(l.x.y)}[-1]} = m )) done fi echo $x } # Repeat character a number of times # (replacing the "${(pl:$num::$char:)}" expansion) headline_repeat_char() { # (char, num) local str='' for (( i = 0; i < $2; i++ )); do str+=$1 done echo $str } # Guess the exit code meaning headline_exit_meaning() { # (num) # REF: https://tldp.org/LDP/abs/html/exitcodes.html # REF: https://man7.org/linux/man-pages/man7/signal.7.html # NOTE: these meanings are not standardized case $1 in 126) echo 'Command cannot execute';; 127) echo 'Command not found';; 129) echo 'Hangup';; 130) echo 'Interrupted';; 131) echo 'Quit';; 132) echo 'Illegal instruction';; 133) echo 'Trapped';; 134) echo 'Aborted';; 135) echo 'Bus error';; 136) echo 'Arithmetic error';; 137) echo 'Killed';; 138) echo 'User signal 1';; 139) echo 'Segmentation fault';; 140) echo 'User signal 2';; 141) echo 'Pipe error';; 142) echo 'Alarm';; 143) echo 'Terminated';; *) ;; esac } # Git command wrapper headline_git() { GIT_OPTIONAL_LOCKS=0 command git "$@" } # Git branch (or hash) headline_git_branch() { local ref ref=$(headline_git symbolic-ref --quiet HEAD 2> /dev/null) local ret=$? if [[ $ret == 0 ]]; then echo ${ref#refs/heads/} # remove "refs/heads/" to get branch else # not on a branch [[ $ret == 128 ]] && return # not a git repo ref=$(headline_git rev-parse --short HEAD 2> /dev/null) || return echo "$HEADLINE_GIT_HASH$ref" # hash prefixed to distingush from branch fi } # Git status headline_git_status() { # Data structures local order; order=('STAGED' 'CHANGED' 'UNTRACKED' 'BEHIND' 'AHEAD' 'DIVERGED' 'STASHED' 'CONFLICTS') local -A totals for key in $order; do totals+=($key 0) done # Retrieve status # REF: https://git-scm.com/docs/git-status local raw lines raw="$(headline_git status --porcelain -b 2> /dev/null)" if [[ $? == 128 ]]; then return 1 # catastrophic failure, abort fi lines=(${(@f)raw}) # Process tracking line if [[ ${lines[1]} =~ '^## [^ ]+ \[(.*)\]' ]]; then local items=("${(@s/,/)match}") for item in $items; do if [[ $item =~ '(behind|ahead|diverged) ([0-9]+)?' ]]; then case $match[1] in 'behind') totals[BEHIND]=$match[2];; 'ahead') totals[AHEAD]=$match[2];; 'diverged') totals[DIVERGED]=$match[2];; esac fi done fi # Process status lines for line in $lines; do if [[ $line =~ '^##|^!!' ]]; then continue elif [[ $line =~ '^U[ADU]|^[AD]U|^AA|^DD' ]]; then totals[CONFLICTS]=$(( ${totals[CONFLICTS]} + 1 )) elif [[ $line =~ '^\?\?' ]]; then totals[UNTRACKED]=$(( ${totals[UNTRACKED]} + 1 )) elif [[ $line =~ '^[MTADRC] ' ]]; then totals[STAGED]=$(( ${totals[STAGED]} + 1 )) elif [[ $line =~ '^[MTARC][MTD]' ]]; then totals[STAGED]=$(( ${totals[STAGED]} + 1 )) totals[CHANGED]=$(( ${totals[CHANGED]} + 1 )) elif [[ $line =~ '^ [MTADRC]' ]]; then totals[CHANGED]=$(( ${totals[CHANGED]} + 1 )) fi done # Check for stashes if $(headline_git rev-parse --verify refs/stash &> /dev/null); then totals[STASHED]=$(headline_git rev-list --walk-reflogs --count refs/stash 2> /dev/null) fi # Build string local prefix status_str status_str='' for key in $order; do if (( ${totals[$key]} > 0 )); then if (( ${#HEADLINE_STATUS_TO_STATUS} && ${#status_str} )); then # not first iteration local style_joint="$reset$HEADLINE_STYLE_DEFAULT$HEADLINE_STYLE_JOINT" local style_status="$resetHEADLINE_STYLE_DEFAULT$HEADLINE_STYLE_STATUS" status_str="$status_str%{$style_joint%}$HEADLINE_STATUS_TO_STATUS%{$style_status%}" fi eval prefix="\$HEADLINE_GIT_${key}" if [[ $HEADLINE_DO_GIT_STATUS_COUNTS == 'true' ]]; then if [[ $HEADLINE_DO_GIT_STATUS_OMIT_ONE == 'true' && (( ${totals[$key]} == 1 )) ]]; then status_str="$status_str$prefix" else status_str="$status_str${totals[$key]}$prefix" fi else status_str="$status_str$prefix" fi fi done # Return if (( ${#status_str} )); then echo $status_str else echo $HEADLINE_GIT_CLEAN fi } # Before executing command add-zsh-hook preexec headline_preexec headline_preexec() { # TODO better way of knowing the prompt is at the top of the terminal if [[ $2 == 'clear' ]]; then _HEADLINE_DO_SEP='false' fi } # Before prompting add-zsh-hook precmd headline_precmd headline_precmd() { local err=$? # Information local user_str host_str path_str branch_str status_str user_str=$(eval $HEADLINE_USER_CMD) host_str=$(eval $HEADLINE_HOST_CMD) path_str=$(eval $HEADLINE_PATH_CMD) branch_str=$(eval $HEADLINE_GIT_BRANCH_CMD) status_str=$(eval $HEADLINE_GIT_STATUS_CMD) # Shared variables _HEADLINE_LEN_REMAIN=$COLUMNS _HEADLINE_INFO_LEFT='' _HEADLINE_LINE_LEFT='' _HEADLINE_INFO_RIGHT='' _HEADLINE_LINE_RIGHT='' # Git status if (( ${#status_str} )); then _headline_part JOINT "$HEADLINE_STATUS_END" right _headline_part STATUS "$HEADLINE_STATUS_PREFIX$status_str" right _headline_part JOINT "$HEADLINE_BRANCH_TO_STATUS" right if (( $_HEADLINE_LEN_REMAIN < ${#HEADLINE_PAD_TO_BRANCH} + ${#HEADLINE_BRANCH_PREFIX} + ${#HEADLINE_TRUNC_PREFIX} )); then user_str=''; host_str=''; path_str=''; branch_str='' fi fi # Git branch local len=$(( $_HEADLINE_LEN_REMAIN - ${#HEADLINE_BRANCH_PREFIX} )) if (( ${#branch_str} )); then if (( $len < ${#HEADLINE_PATH_PREFIX} + ${#HEADLINE_TRUNC_PREFIX} + ${#HEADLINE_PATH_TO_BRANCH} + ${#branch_str} )); then path_str='' fi if (( ${#path_str} )); then len=$(( $len - ${#HEADLINE_PATH_PREFIX} - ${#HEADLINE_PATH_TO_BRANCH} )) else len=$(( $len - ${#HEADLINE_PAD_TO_BRANCH} )) fi _headline_part BRANCH "$HEADLINE_BRANCH_PREFIX%$len<$HEADLINE_TRUNC_PREFIX<$branch_str%<<" right fi # Trimming local joint_len=$(( ${#HEADLINE_USER_BEGIN} + ${#HEADLINE_USER_TO_HOST} + ${#HEADLINE_HOST_TO_PATH} + ${#HEADLINE_PATH_TO_BRANCH} )) local path_min_len=$(( ${#path_str} + ${#HEADLINE_PATH_PREFIX} > 25 ? 25 : ${#path_str} + ${#HEADLINE_PATH_PREFIX} )) len=$(( $_HEADLINE_LEN_REMAIN - $path_min_len - $joint_len )) if (( $len < 2 )); then user_str=''; host_str='' elif (( $len < ${#user_str} + ${#host_str} )); then user_str="${user_str:0:1}" host_str="${host_str:0:1}" fi # User if (( ${#user_str} )); then _headline_part JOINT "$HEADLINE_USER_BEGIN" left _headline_part USER "$HEADLINE_USER_PREFIX$user_str" left fi # Host if (( ${#host_str} )); then if (( ${#_HEADLINE_INFO_LEFT} )); then _headline_part JOINT "$HEADLINE_USER_TO_HOST" left fi _headline_part HOST "$HEADLINE_HOST_PREFIX$host_str" left fi # Path if (( ${#path_str} )); then if (( ${#_HEADLINE_INFO_LEFT} )); then _headline_part JOINT "$HEADLINE_HOST_TO_PATH" left fi len=$(( $_HEADLINE_LEN_REMAIN - ${#HEADLINE_PATH_PREFIX} - ( ${#branch_str} ? ${#HEADLINE_PATH_TO_BRANCH} : 0 ) )) _headline_part PATH "$HEADLINE_PATH_PREFIX%$len<$HEADLINE_TRUNC_PREFIX<$path_str%<<" left fi # Padding if (( ${#branch_str} && ${#path_str} && $_HEADLINE_LEN_REMAIN <= ${#HEADLINE_PATH_TO_BRANCH} )); then _headline_part JOINT "$HEADLINE_PATH_TO_BRANCH" left else if (( ${#branch_str} )); then _headline_part JOINT "$HEADLINE_PAD_TO_BRANCH" right fi _headline_part JOINT "$HEADLINE_PATH_TO_PAD" left _headline_part JOINT "$(headline_repeat_char $HEADLINE_PAD_CHAR $_HEADLINE_LEN_REMAIN)" left fi # Error line if [[ $HEADLINE_DO_ERR == 'true' ]] && (( $err )); then local meaning msg if [[ $HEADLINE_DO_ERR_INFO == 'true' ]]; then meaning=$(headline_exit_meaning $err) (( ${#meaning} )) && msg=" ($meaning)" fi print -rP "$HEADLINE_STYLE_ERR$HEADLINE_ERR_PREFIX$err$msg" fi # Separator line _HEADLINE_LINE_OUTPUT="$_HEADLINE_LINE_LEFT$_HEADLINE_LINE_RIGHT$reset" if [[ $HEADLINE_LINE_MODE == 'on' || ($HEADLINE_LINE_MODE == 'auto' && $_HEADLINE_DO_SEP == 'true' ) ]]; then print -rP $_HEADLINE_LINE_OUTPUT fi _HEADLINE_DO_SEP='true' # Information line _HEADLINE_INFO_OUTPUT="$_HEADLINE_INFO_LEFT$_HEADLINE_INFO_RIGHT$reset" # Prompt if [[ $HEADLINE_INFO_MODE == 'precmd' ]]; then print -rP $_HEADLINE_INFO_OUTPUT PROMPT=$HEADLINE_PROMPT else PROMPT='$(print -rP $_HEADLINE_INFO_OUTPUT; print -rP $HEADLINE_PROMPT)' fi # Right prompt if [[ $HEADLINE_DO_CLOCK == 'true' ]]; then RPROMPT='%{$HEADLINE_STYLE_CLOCK%}$(date +$HEADLINE_CLOCK_FORMAT)%{$reset%}$HEADLINE_RPROMPT' else RPROMPT=$HEADLINE_RPROMPT fi } # Create a part of the prompt _headline_part() { # (name, content, side) local style info info_len line eval style="\$reset\$HEADLINE_STYLE_DEFAULT\$HEADLINE_STYLE_${1}" info="%{$style%}$2" info_len=$(headline_prompt_len $info 9999) _HEADLINE_LEN_REMAIN=$(( $_HEADLINE_LEN_REMAIN - $info_len )) eval style="\$reset\$HEADLINE_STYLE_${1}_LINE" line="%{$style%}$(headline_repeat_char $HEADLINE_LINE_CHAR $info_len)" if [[ $3 == 'right' ]]; then _HEADLINE_INFO_RIGHT="$info$_HEADLINE_INFO_RIGHT" _HEADLINE_LINE_RIGHT="$line$_HEADLINE_LINE_RIGHT" else _HEADLINE_INFO_LEFT="$_HEADLINE_INFO_LEFT$info" _HEADLINE_LINE_LEFT="$_HEADLINE_LINE_LEFT$line" fi } HEADLINE_USER_BEGIN='--' HEADLINE_USER_TO_HOST='-' HEADLINE_HOST_TO_PATH='-' HEADLINE_PATH_TO_BRANCH='-' HEADLINE_PAD_TO_BRANCH='-' HEADLINE_BRANCH_TO_STATUS='-' HEADLINE_STATUS_END='--' HEADLINE_PAD_CHAR='-' HEADLINE_TRUNC_PREFIX='…' HEADLINE_LINE_MODE=off HEADLINE_DO_CLOCK=true