Fancy promptline with git status details

I’ve been using powerline-shell for quite a while and like it a lot. I get aware of that every time I use a terminal which does not tell me which branch I’m on.

Some days ago I stumbled upon promptline.vim and as I’m also using vim-airline I gave it a try. Promptline.vim exports a shell script from the current airline settings.

After sourcing it into a shell you should immediately see the updated promptline (if there are random characters instead of fancy symbols, you need to install powerline symbols first):

:PromptlineSnapshot ~/.shell_prompt.sh airline

Automatically load the promptline when a shell is opened:

# load promptline if available
[ -f ~/.shell_prompt.sh ] && source ~/.shell_prompt.sh

Vim source

I use the light solarized theme created by Ethan Schoonover and invoked the export from vim looking like this:

vim_w_airline

Vim with airline status bar

Result

I tweaked the result a bit. Originally it only indicates whether, and if any, which changes have been made. I added coloring for the git slice, red for pending changes (both staged and unstaged):

  •  +3 indicates three files with unstaged chanegs
  • •2 tells about two files with staged changes
  •  … indicates that there are untracked files
promptline

Bash with customized promptline

In a clean working directory or if there are only untracked files, the slice is green:

untracked_clean

Promptline for a directory with untracked files/no changes

 
 
 
 

Customization

My .shell_prompt.sh (download) looks like this now:

  1. #
  2. # This shell prompt config file was created by promptline.vim
  3. #
  4.  
  5. function __promptline_last_exit_code {
  6.  
  7.   [[ $last_exit_code -gt 0 ]] || return 1;
  8.  
  9.   printf "%s" "$last_exit_code"
  10. }
  11. function __promptline_ps1 {
  12.   local slice_prefix slice_empty_prefix slice_joiner slice_suffix is_prompt_empty=1
  13.  
  14.   # section "aa" header
  15.   slice_prefix="${aa_bg}${sep}${aa_fg}${aa_bg}${space}" slice_suffix="$space${aa_sep_fg}" slice_joiner="${aa_fg}${aa_bg}${alt_sep}${space}" slice_empty_prefix="${aa_fg}${aa_bg}${space}"
  16.   [ $is_prompt_empty -eq 1 ] && slice_prefix="$slice_empty_prefix"
  17.   # section "a" slices
  18.   __promptline_wrapper "$(if [[ -n ${ZSH_VERSION-} ]]; then print %m; elif [[ -n ${FISH_VERSION-} ]]; then hostname -s; else printf "%s" \\A; fi )" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; is_prompt_empty=0; }
  19.  
  20.   # section "a" header
  21.   slice_prefix="${a_bg}${sep}${a_fg}${a_bg}${space}" slice_suffix="$space${a_sep_fg}" slice_joiner="${a_fg}${a_bg}${alt_sep}${space}" slice_empty_prefix="${a_fg}${a_bg}${space}"
  22.   [ $is_prompt_empty -eq 1 ] && slice_prefix="$slice_empty_prefix"
  23.   # section "a" slices
  24.   __promptline_wrapper "$(if [[ -n ${ZSH_VERSION-} ]]; then print %m; elif [[ -n ${FISH_VERSION-} ]]; then hostname -s; else printf "%s" \\h; fi )" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; is_prompt_empty=0; }
  25.  
  26.   # section "b" header
  27.   slice_prefix="${b_bg}${sep}${b_fg}${b_bg}${space}" slice_suffix="$space${b_sep_fg}" slice_joiner="${b_fg}${b_bg}${alt_sep}${space}" slice_empty_prefix="${b_fg}${b_bg}${space}"
  28.   [ $is_prompt_empty -eq 1 ] && slice_prefix="$slice_empty_prefix"
  29.   # section "b" slices
  30.   __promptline_wrapper "$USER" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; is_prompt_empty=0; }
  31.  
  32.   # section "c" header
  33.   slice_prefix="${c_bg}${sep}${c_fg}${c_bg}${space}" slice_suffix="$space${c_sep_fg}" slice_joiner="${c_fg}${c_bg}${alt_sep}${space}" slice_empty_prefix="${c_fg}${c_bg}${space}"
  34.   [ $is_prompt_empty -eq 1 ] && slice_prefix="$slice_empty_prefix"
  35.   # section "c" slices
  36.   __promptline_wrapper "$(__promptline_cwd)" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; is_prompt_empty=0; }
  37.  
  38.   # this will prepare the variables for __promptline_git_status and adjust the bg coloring for section "y"
  39.   __determine_git_colors
  40.   # section "y" header
  41.   slice_prefix="${y_bg}${sep}${y_fg}${y_bg}${space}" slice_suffix="$space${y_sep_fg}" slice_joiner="${y_fg}${y_bg}${alt_sep}${space}" slice_empty_prefix="${y_fg}${y_bg}${space}"
  42.   [ $is_prompt_empty -eq 1 ] && slice_prefix="$slice_empty_prefix"
  43.   # section "y" slices
  44.   __promptline_wrapper "$(__promptline_vcs_branch)" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; is_prompt_empty=0; }
  45.   __promptline_wrapper "$(__promptline_git_status)" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; is_prompt_empty=0; }
  46.  
  47.  
  48.   # section "warn" header
  49.   slice_prefix="${warn_bg}${sep}${warn_fg}${warn_bg}${space}" slice_suffix="$space${warn_sep_fg}" slice_joiner="${warn_fg}${warn_bg}${alt_sep}${space}" slice_empty_prefix="${warn_fg}${warn_bg}${space}"
  50.   [ $is_prompt_empty -eq 1 ] && slice_prefix="$slice_empty_prefix"
  51.   # section "warn" slices
  52.   __promptline_wrapper "$(__promptline_last_exit_code)" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; is_prompt_empty=0; }
  53.  
  54.   # close sections
  55.   printf "%s" "${reset_bg}${sep}$reset$space"
  56. }
  57. function __promptline_vcs_branch {
  58.   local branch
  59.   local branch_symbol=" "
  60.  
  61.   # git
  62.   if hash git 2>/dev/null; then
  63.     if branch=$( { git symbolic-ref --quiet HEAD || git rev-parse --short HEAD; } 2>/dev/null ); then
  64.       branch=${branch##*/}
  65.       printf "%s" "${branch_symbol}${branch:-unknown}"
  66.       return
  67.     fi
  68.   fi
  69.   return 1
  70. }
  71. function __determine_git_colors {
  72.   [[ $(git rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || return 1
  73.  
  74.   __promptline_git_unmerged_count=0 __promptline_git_modified_count=0 __promptline_git_has_untracked_files=0 __promptline_git_added_count=0 __promptline_git_is_clean=""
  75.  
  76.   set -- $(git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null)
  77.   __promptline_git_behind_count=$1
  78.   __promptline_git_ahead_count=$2
  79.  
  80.   # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R), changed (T), Unmerged (U), Unknown (X), Broken (B)
  81.   while read line; do
  82.     case "$line" in
  83.       M*) __promptline_git_modified_count=$(( $__promptline_git_modified_count + 1 )) ;;
  84.       U*) __promptline_git_unmerged_count=$(( $__promptline_git_unmerged_count + 1 )) ;;
  85.     esac
  86.   done < <(git diff --name-status)
  87.  
  88.   while read line; do
  89.     case "$line" in
  90.       *) __promptline_git_added_count=$(( $__promptline_git_added_count + 1 )) ;;
  91.     esac
  92.   done < <(git diff --name-status --cached)
  93.  
  94.   if [ -n "$(git ls-files --others --exclude-standard)" ]; then
  95.     __promptline_git_has_untracked_files=1
  96.   fi
  97.  
  98.   if [ $(( __promptline_git_unmerged_count + __promptline_git_modified_count + __promptline_git_has_untracked_files + __promptline_git_added_count )) -eq 0 ]; then
  99.     __promptline_git_is_clean=1
  100.   fi
  101.  
  102.   y_fg="${wrap}38;5;246${end_wrap}"
  103.   # set green background for the branch info if there are no changes or only untracked files
  104.   if [[ $((__promptline_git_is_clean + __promptline_git_has_untracked_files)) -gt 0 ]]; then
  105.     y_bg="${wrap}48;5;194${end_wrap}"
  106.     y_sep_fg="${wrap}38;5;194${end_wrap}"
  107.   fi
  108.  
  109.   # set red background for the branch info if there are unstaged or staged (but not yet committed) changes
  110.   if [[ $((__promptline_git_modified_count + __promptline_git_added_count)) -gt 0 ]]; then
  111.     y_bg="${wrap}48;5;224${end_wrap}"
  112.     y_sep_fg="${wrap}38;5;224${end_wrap}"
  113.     #y_bg="${wrap}48;5;217${end_wrap}"
  114.     #y_sep_fg="${wrap}38;5;217${end_wrap}"
  115.   fi
  116. }
  117. function __promptline_git_status {
  118.   [[ $(git rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || return 1
  119.  
  120.   local added_symbol="●"
  121.   local unmerged_symbol="✖"
  122.   local modified_symbol="✚"
  123.   local clean_symbol="✔"
  124.   local has_untracked_files_symbol="…"
  125.  
  126.   local ahead_symbol="↑"
  127.   local behind_symbol="↓"
  128.  
  129.   local leading_whitespace=""
  130.   [[ $__promptline_git_ahead_count -gt 0 ]]         && { printf "%s" "$leading_whitespace$ahead_symbol$__promptline_git_ahead_count"; leading_whitespace=" "; }
  131.   [[ $__promptline_git_behind_count -gt 0 ]]        && { printf "%s" "$leading_whitespace$behind_symbol$__promptline_git_behind_count"; leading_whitespace=" "; }
  132.   [[ $__promptline_git_modified_count -gt 0 ]]      && { printf "%s" "$leading_whitespace$modified_symbol$__promptline_git_modified_count"; leading_whitespace=" "; }
  133.   [[ $__promptline_git_unmerged_count -gt 0 ]]      && { printf "%s" "$leading_whitespace$unmerged_symbol$__promptline_git_unmerged_count"; leading_whitespace=" "; }
  134.   [[ $__promptline_git_added_count -gt 0 ]]         && { printf "%s" "$leading_whitespace$added_symbol$__promptline_git_added_count"; leading_whitespace=" "; }
  135.   [[ $__promptline_git_has_untracked_files -gt 0 ]] && { printf "%s" "$leading_whitespace$has_untracked_files_symbol"; leading_whitespace=" "; }
  136.   [[ $__promptline_git_is_clean -gt 0 ]]            && { printf "%s" "$leading_whitespace$clean_symbol"; leading_whitespace=" "; }
  137. }
  138. function __promptline_cwd {
  139.   local dir_limit="3"
  140.   local truncation="⋯"
  141.   local first_char
  142.   local part_count=0
  143.   local formatted_cwd=""
  144.   local dir_sep="  "
  145.   local tilde="~"
  146.  
  147.   local cwd="${PWD/#$HOME/$tilde}"
  148.  
  149.   # get first char of the path, i.e. tilde or slash
  150.   [[ -n ${ZSH_VERSION-} ]] && first_char=$cwd[1,1] || first_char=${cwd::1}
  151.  
  152.   # remove leading tilde
  153.   cwd="${cwd#\~}"
  154.  
  155.   while [[ "$cwd" == */* && "$cwd" != "/" ]]; do
  156.     # pop off last part of cwd
  157.     local part="${cwd##*/}"
  158.     cwd="${cwd%/*}"
  159.  
  160.     formatted_cwd="$dir_sep$part$formatted_cwd"
  161.     part_count=$((part_count+1))
  162.  
  163.     [[ $part_count -eq $dir_limit ]] && first_char="$truncation" && break
  164.   done
  165.  
  166.   printf "%s" "$first_char$formatted_cwd"
  167. }
  168. function __promptline_left_prompt {
  169.   local slice_prefix slice_empty_prefix slice_joiner slice_suffix is_prompt_empty=1
  170.  
  171.   # section "a" header
  172.   slice_prefix="${a_bg}${sep}${a_fg}${a_bg}${space}" slice_suffix="$space${a_sep_fg}" slice_joiner="${a_fg}${a_bg}${alt_sep}${space}" slice_empty_prefix="${a_fg}${a_bg}${space}"
  173.   [ $is_prompt_empty -eq 1 ] && slice_prefix="$slice_empty_prefix"
  174.   # section "a" slices
  175.   __promptline_wrapper "$(if [[ -n ${ZSH_VERSION-} ]]; then print %m; elif [[ -n ${FISH_VERSION-} ]]; then hostname -s; else printf "%s" \\h; fi )" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; is_prompt_empty=0; }
  176.  
  177.   # section "b" header
  178.   slice_prefix="${b_bg}${sep}${b_fg}${b_bg}${space}" slice_suffix="$space${b_sep_fg}" slice_joiner="${b_fg}${b_bg}${alt_sep}${space}" slice_empty_prefix="${b_fg}${b_bg}${space}"
  179.   [ $is_prompt_empty -eq 1 ] && slice_prefix="$slice_empty_prefix"
  180.   # section "b" slices
  181.   __promptline_wrapper "$USER" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; is_prompt_empty=0; }
  182.  
  183.   # section "c" header
  184.   slice_prefix="${c_bg}${sep}${c_fg}${c_bg}${space}" slice_suffix="$space${c_sep_fg}" slice_joiner="${c_fg}${c_bg}${alt_sep}${space}" slice_empty_prefix="${c_fg}${c_bg}${space}"
  185.   [ $is_prompt_empty -eq 1 ] && slice_prefix="$slice_empty_prefix"
  186.   # section "c" slices
  187.   __promptline_wrapper "$(__promptline_cwd)" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; is_prompt_empty=0; }
  188.  
  189.   # close sections
  190.   printf "%s" "${reset_bg}${sep}$reset$space"
  191. }
  192. function __promptline_wrapper {
  193.   # wrap the text in $1 with $2 and $3, only if $1 is not empty
  194.   # $2 and $3 typically contain non-content-text, like color escape codes and separators
  195.  
  196.   [[ -n "$1" ]] || return 1
  197.   printf "%s" "${2}${1}${3}"
  198. }
  199. function __promptline_right_prompt {
  200.   local slice_prefix slice_empty_prefix slice_joiner slice_suffix
  201.  
  202.   # section "warn" header
  203.   slice_prefix="${warn_sep_fg}${rsep}${warn_fg}${warn_bg}${space}" slice_suffix="$space${warn_sep_fg}" slice_joiner="${warn_fg}${warn_bg}${alt_rsep}${space}" slice_empty_prefix=""
  204.   # section "warn" slices
  205.   __promptline_wrapper "$(__promptline_last_exit_code)" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; }
  206.  
  207.   # section "y" header
  208.   slice_prefix="${y_sep_fg}${rsep}${y_fg}${y_bg}${space}" slice_suffix="$space${y_sep_fg}" slice_joiner="${y_fg}${y_bg}${alt_rsep}${space}" slice_empty_prefix=""
  209.   # section "y" slices
  210.   __promptline_wrapper "$(__promptline_vcs_branch)" "$slice_prefix" "$slice_suffix" && { slice_prefix="$slice_joiner"; }
  211.  
  212.   # close sections
  213.   printf "%s" "$reset"
  214. }
  215. function __promptline {
  216.   local last_exit_code="${PROMPTLINE_LAST_EXIT_CODE:-$?}"
  217.  
  218.   local esc=$'[' end_esc=m
  219.   if [[ -n ${ZSH_VERSION-} ]]; then
  220.     local noprint='%{' end_noprint='%}'
  221.   elif [[ -n ${FISH_VERSION-} ]]; then
  222.     local noprint='' end_noprint=''
  223.   else
  224.     local noprint='\[' end_noprint='\]'
  225.   fi
  226.   local wrap="$noprint$esc" end_wrap="$end_esc$end_noprint"
  227.   local space=" "
  228.   local sep=""
  229.   local rsep=""
  230.   local alt_sep=""
  231.   local alt_rsep=""
  232.   local reset="${wrap}0${end_wrap}"
  233.   local reset_bg="${wrap}49${end_wrap}"
  234.   local aa_fg="${wrap}38;5;7${end_wrap}"
  235.   local aa_bg="${wrap}48;5;246${end_wrap}"
  236.   local aa_sep_fg="${wrap}38;5;246${end_wrap}"
  237.   local a_fg="${wrap}38;5;7${end_wrap}"
  238.   local a_bg="${wrap}48;5;11${end_wrap}"
  239.   local a_sep_fg="${wrap}38;5;11${end_wrap}"
  240.   local b_fg="${wrap}38;5;11${end_wrap}"
  241.  
  242.   # set red background for root
  243.   if [ $UID == 0 ]; then
  244.     local b_bg="${wrap}48;5;210${end_wrap}"
  245.     local b_sep_fg="${wrap}38;5;210${end_wrap}"
  246.   # set light green background for anyone else
  247.   else
  248.     local b_bg="${wrap}48;5;187${end_wrap}"
  249.     local b_sep_fg="${wrap}38;5;187${end_wrap}"
  250.   fi
  251.   local c_fg="${wrap}38;5;14${end_wrap}"
  252.   local c_bg="${wrap}48;5;7${end_wrap}"
  253.   local c_sep_fg="${wrap}38;5;7${end_wrap}"
  254.   local warn_fg="${wrap}38;5;15${end_wrap}"
  255.   local warn_bg="${wrap}48;5;9${end_wrap}"
  256.   local warn_sep_fg="${wrap}38;5;9${end_wrap}"
  257.   local y_fg="${wrap}38;5;14${end_wrap}"
  258.   local y_bg="${wrap}48;5;14${end_wrap}"
  259.   local y_sep_fg="${wrap}38;5;14${end_wrap}"
  260.   if [[ -n ${ZSH_VERSION-} ]]; then
  261.     PROMPT="$(__promptline_left_prompt)"
  262.     RPROMPT="$(__promptline_right_prompt)"
  263.   elif [[ -n ${FISH_VERSION-} ]]; then
  264.     if [[ -n "$1" ]]; then
  265.       [[ "$1" = "left" ]] && __promptline_left_prompt || __promptline_right_prompt
  266.     else
  267.       __promptline_ps1
  268.     fi
  269.   else
  270.     PS1="$(__promptline_ps1)"
  271.   fi
  272. }
  273.  
  274. if [[ -n ${ZSH_VERSION-} ]]; then
  275.   if [[ ! ${precmd_functions[(r)__promptline]} == __promptline ]]; then
  276.     precmd_functions+=(__promptline)
  277.   fi
  278. elif [[ -n ${FISH_VERSION-} ]]; then
  279.   __promptline "$1"
  280. else
  281.   if [[ ! "$PROMPT_COMMAND" == *__promptline* ]]; then
  282.     PROMPT_COMMAND='__promptline;'$'\n'"$PROMPT_COMMAND"
  283.   fi
  284. fi

Changes:

  • line 14-16: Add slice with current time (only works in bash as is)
  • line 71-137: Add functions to determine git status
  • line 39: Determine git details and coloring for ‚y‘-section before rendering starts
  • line 45: Call git status rendering
  • line 234-259: Changed some colors, added ‚aa‘-colors, user name background depends on being root or not

I took the logic for the git status slice from promptline.vim and adjusted it so that the coloring for the slice is determined before its rendering starts.

Colors can be adjusted by changing the third value of the tuples, a nice cheat-sheet can be found here.

Comments (4)

GNovember 19th, 2014 at 02:18

Sieht super aus. Ich habe mich ja zugegebenermassen etwas daran aufgehangen, dass das Dreieckszeichen nicht bei normalen Schriftarten dabei ist… Humpf!

jonasNovember 19th, 2014 at 09:08

Dann kann man es ja raus nehmen und hat kantige statt dreieckig Slice-Enden ;). Oder man findet einen anderen „dynamischeren“ Übergang im Satz der Standardzeichen? Die Symbole die für die git-Infos (Branch-Symbol zumindest mal, vermute ich) verwendet werden betrifft das ja eventuell auch.

GJuni 12th, 2015 at 00:23

Ordentliche Übergänge gibt es da nicht, da die meisten Zeichen in Unicode nicht die volle Blockhöhe haben. (Es gibt nur ein paar Zeichen zum Kistenmalen die von alten DOS-Zeichensätzen vermacht wurden, aber davon gibt es wohl keine neuen. Am ehesten haut noch der Übergang mit mehreren Schattierungen hin, aber es ist alles nicht der Weisheit letzter Schluss.)

jonasJuni 12th, 2015 at 00:32

Warum willst du die Zeichen eigentlich nicht nachinstallieren? Das hat bei mir (den wahrnehmbaren Effekten nach zumindest) völlig schmerzfrei geklappt.

Leave a comment

Your comment

(required)