Skip this text, it's but a long rant on my past

I am extensively using terminals and command prompts, on a daily basis. I'd say that I spend about 90% of my time using a terminal. (My second-to-favorite one is yakuake, a konsole-based terminal; obviously, my favorite one is the console!)

My shell is bash, because, well, it fits what I need. Maybe the alternatives would be better, but I got accustomed to bash, and I find it to be enough… bash has long supported the PS1 environment variable to contain a template of the prompt. I won't do a tutorial here, you can find plenty of them every where on the net…

My favorite PS1 was something like [\u@\h:\w] with \u replaced by the login name, \h by the hostname, and \w with the current working directory (there were both \w and \W, one expdanded to the full CWD, while the other only to the basename, but whatever…).

That would tell me what machine I was logged on (hey, remember you can ssh into machines?), what user I was logged in as, and what directory I was in. As a true computer geek, that would relieve me from typing any of who, host or pwd. Every key-press avoided is always a Good Thing (TM).

For a long time, that was pretty enough for me.

But then I came to work on many community projects, and even maintaining my own project. Those projects use different VCS : svn, Hg, or even git. Beside who and where I was, I also needed to know when I was (in terms of the VCS, of course!) : the branch, the revision, changeset, or whatever really usefull info I often needed.

Now, I own many machines, desktops and notebooks and netbooks. And then on the mobile devices I wanted to know how much battery left there was.

And many more informations: load average, time and date, exit status of the previous command…

Here is the really interesting stuff!

So I came up with a feature-full prompt that adapts to many situations. So, here's how it looks like with an UTF-8 (top) and non-UTF-8 (bottom) locales :

┌───┤~/dev/hg/hg.upstream☿default:15140+bisect-revset:2+!├──────────────────┤0.06|05:27↓├──────┤20110921.004709├───
└─┤ymorin@treguer:pts/4│ret=42├────> _

,---[~/dev/hg/hg.upstream☿default:15140+bisect-revset:2+!]------------------[0.06|05:27v]------[20110921.004709]---
`-[ymorin@treguer:pts/4|ret=42]----> _

Which means :

  • the current working directory is ~/dev/hg/hg.upstream, which is a sub-dir of my home ~ directory
  • it is a Mercurial clone , the working copy is on the default branch, revision #15140
  • the MQ bisect-revset is applied up to the #2 patch (the third one)
  • the working copy has un-committed changes + and un-tracked files !
  • the load average is 0.06
  • there is an estimated 5 hours and 27 minutes of battery left 05:27, and the battery is discharging
  • the day is 2011-09-21, the time is 00:47:09
  • I'm logged in as ymorin on tty4 of the machine treguer
  • last command exited with status 42

The prompt extends to the width of the terminal, and if the prompt is too large, it is elided at the begining:

/very/very/very/very/long/path
...ry/long/path

Enough talk, here's the prompt :

# This file is sourced by your .profile script at login.
# (C) 2007 to 2012 Yann E. MORIN <yann.morin.1998@anciens.enib.fr>
# Licensed under the GPL v2 only.
 
# Format of the prompt:
# ,----[~/dev]-----------------------------------[1.27]-------[20060708.112233]---
# `-[ymorin@there:pts/2|ret=4]----> _
#
# Paths too long to fit in the path place-holder are mangled as:
#    ...end/of/very/long/path
#
# When the language (${LANG}) is an UTF-8 locale, then the terminal is
# supposed to handle UTF-8, and then extended characters are used to
# replace: , ` -  [ ] >
#
# In case one (or more) battery is present, the current status
# is also included next to the load average
#
# WARNING! WARNING!
# Does not work with UTF-8 characters that are wider than an ASCII char.
# The first line of the prompt will then span down to the second line,
# and will be quite ugly, a bit like:
# ,----[~/wide.UTF-8/chars]-----------------------------------[1.27]-------[20
# 060708.112233]---
# `-[ymorin@there:pts/2|ret=4]----> _
 
# Cursor movements
CURS_U="\[\033A\]"
CURS_D="\[\033B\]"
CURS_R="\[\033C\]"
CURS_L="\[\033D\]"
 
# Attributes
A_NOR="\[\033[0m\]"
A_BRI="\[\033[1m\]"
A_DIM="\[\033[2m\]"
A_UND="\[\033[4m\]"
A_BRB="\[\033[5m\]"
A_REV="\[\033[7m\]"
A_HID="\[\033[8m\]"
 
# Fore colors
F_BLK="\[\033[30m\]"
F_RED="\[\033[31m\]"
F_GRN="\[\033[32m\]"
F_YEL="\[\033[33m\]"
F_BLU="\[\033[34m\]"
F_MAG="\[\033[35m\]"
F_CYA="\[\033[36m\]"
F_WHI="\[\033[37m\]"
 
# Back colors
B_BLK="\[\033[40m\]"
B_RED="\[\033[41m\]"
B_GRN="\[\033[42m\]"
B_YEL="\[\033[43m\]"
B_BLU="\[\033[44m\]"
B_MAG="\[\033[45m\]"
B_CYA="\[\033[46m\]"
B_WHI="\[\033[47m\]"
 
# Command codes
L_ERA="\[\033[K\]"
 
# Box glyphs
# These are the default values
H_BAR="-"
V_BAR="|"
SQ_UL=","
SQ_UR="+"
SQ_BL="\\\`"
SQ_BR="+"
T_L="["
T_R="]"
U_ARROW="^"
D_ARROW="v"
ARROW=">"
 
# Misc escapes
 
case "${PROMPT_RAW}:${LANG}" in
  ?*:*)
    ;;
  :*.UTF-8)
    H_BAR="$( /usr/bin/printf "\u2500" )"
    V_BAR="$( /usr/bin/printf "\u2502" )"
    SQ_UL="$( /usr/bin/printf "\u250c" )"
    SQ_UR="$( /usr/bin/printf "\u2510" )"
    SQ_BL="$( /usr/bin/printf "\u2514" )"
    SQ_BR="$( /usr/bin/printf "\u2518" )"
    T_L="$(   /usr/bin/printf "\u2524" )"
    T_R="$(   /usr/bin/printf "\u251c" )"
    U_ARROW="$( /usr/bin/printf "\u2191" )"
    D_ARROW="$( /usr/bin/printf "\u2193" )"
    ARROW=">"
    # Curved angles
    #SQ_UL="$( /usr/bin/printf "\u256d" )"
    #SQ_UR="$( /usr/bin/printf "\u256e" )"
    #SQ_BL="$( /usr/bin/printf "\u2570" )"
    #SQ_BR="$( /usr/bin/printf "\u256f" )"
    ;;
#  :cp850)
#    H_BAR="\304"
#    V_BAR="\263"
#    SQ_UL="\332"
#    SQ_UR="\277"
#    SQ_BL="\300"
#    SQ_BR="\331"
#    T_L="\264"
#    T_R="\303"
#    ;;
esac
 
#         .---> PWD_COL
#         |  ..---> BRK_COL <-------------------..
#         |  ||.---> LK1_COL <-----------------.||
#         |  |||.---> LK0_COL <---------------.|||
#         |  ||||              .---> BAR_COL  |||| .---> LOD_COL      .---> TIM_COL
#         |  ||||              |              |||| |                  |
#         v  vvvv.-------------^-------------.vvvv v                  v
# ,----[~/dev]-----------------------------------[1.27]-------[20060708.113233]---
# `-[ymorin@there:pts/2/screen1|ret=4]----> _
#    `--------.-------'   ^       ^
#             |           |       |
#             |           |       '---> RET_COL
#             |           '---> SCR_COL
#             '---> WHO_COL
case "${PROMPT_THEME}" in
    red)
        BRK_COL="${A_BRI}${F_RED}"
        LK1_COL="${A_BRI}${F_RED}"
        LK0_COL="${A_NOR}${F_RED}"
        BAR_COL="${A_BRI}${F_BLK}"
        PWD_COL="${A_BRI}${F_YEL}"
        LOD_COL="${A_NOR}${F_YEL}"
        BAT_OK_COL="${A_NOR}${F_GRN}"
        BAT_LO_COL="${A_BRI}${F_YEL}"
        BAT_CRIT_COL="${A_BRB}${F_RED}"
        TIM_COL="${A_NOR}${F_YEL}"
        WHO_COL="${A_BRI}${F_YEL}"
        PTS_COL="${A_NOR}${F_YEL}"
        SCR_COL="${PTS_COL}"
        RET_COL="${A_BRI}${F_RED}"
        ;;
    blue|*)
        BRK_COL="${A_BRI}${F_WHI}"
        LK1_COL="${A_BRI}${F_CYA}"
        LK0_COL="${A_NOR}${F_CYA}"
        BAR_COL="${A_BRI}${F_BLK}"
        PWD_COL="${A_BRI}${F_BLU}"
        REV_COL="${A_BRI}${F_MAG}"
        LOD_COL="${A_BRI}${F_BLU}"
        BAT_OK_COL="${A_NOR}${F_GRN}"
        BAT_LO_COL="${A_BRI}${F_YEL}"
        BAT_CRIT_COL="${A_BRB}${F_RED}"
        TIM_COL="${A_BRI}${F_BLU}"
        WHO_COL="${A_BRI}${F_GRN}"
        PTS_COL="${A_NOR}${F_GRN}"
        SCR_COL="${PTS_COL}"
        RET_COL="${A_BRI}${F_RED}"
        ;;
esac
 
# NOTIF_MIN_RUN: notify command termination if command ran longer than this (sec)
# NOTIF_OK_ICON: icon to display if $? == 0
# NOTIF_KO_ICON: icon to display if $? != 0
# NOTIF_TIMEOUT: delay to display notifications (sec)
# NOTIF_PRIO   : notification priority
# NOTIF_CMDS   : array of grep-expressions of commands to notify for
NOTIF_MIN_RUN=10
NOTIF_OK_ICON='/usr/share/icons/gnome/48x48/emblems/emblem-default.png'
NOTIF_KO_ICON='/usr/share/icons/gnome/48x48/status/dialog-warning.png'
NOTIF_TIMEOUT=10
NOTIF_PRIO=low
NOTIF_CMDS=(
    "\</?configure'? .*"
    "\</?q?make'? .*"
)
 
do_notify() {
    local ret="${1}"; shift
    local cmd="${*}"
    local pwd="$(pwd)"
    local msg icon
 
    if [ ${ret} -eq 0 ]; then
        title="Command finished successfully"
        icon="${NOTIF_OK_ICON}"
    else
        title="Command terminated in error"
        icon="${NOTIF_KO_ICON}"
    fi
    case "${pwd}" in
        "${HOME}"/?*)   pwd="~${pwd#${HOME}}";;
    esac
    msg="$( printf "CWD: %s\nCMD: %s" "${pwd}" "${cmd}" )"
 
    notify-send -t $((1000*NOTIF_TIMEOUT))  \
                -u ${NOTIF_PRIO}            \
                -i "${icon}"                \
                "${title}"                  \
                "${msg}"
}
 
doPrompt() {
  local RET="${?}"
  local is_net_mntpt=0
  local HGPLAIN
  local LOAD BAT TTY SCREEN_SES COLS REV newPWD DATE promptsize fillsize cutt
  local repoTYPE repoPWD
  local last_run last_run_cmd last_run_date last_run_epoch cur_epoch c
  COLS="${COLUMNS}"
  [ -z "${COLS}" ] && COLS=$(stty size |cut -d ' ' -f 2)
 
  export HGPLAIN=y
 
  # Prepare the values for after
  LOAD=$(cut -d ' ' -f 1 </proc/loadavg)
  TTY=$(tty |cut -d / -f 3-)
  SCREEN_SES=$(LC_ALL=C LANG=C screen -list |awk '$1~/^[[:digit:]]$/ {print $1}')
 
  # Was the last command running for long enough that it warrants a notification?
  last_run="$( history |tail -n 1                                               \
               |sed -r -e 's/^[[:space:]]*[[:digit:]]+[[:space:]]+(.*)$/\1/;'   \
             )"
  last_run_cmd="${last_run#* }"
  last_run_epoch="$( date -d "$( sed -r -e 's/(....)(..)(..)\.(..)(..)(..)/\1-\2-\3 \4:\5:\6/;' <<<"${last_run%% *}" )" '+%s' )"
  cur_epoch="$( date '+%s' )"
  if [ $((cur_epoch-last_run_epoch)) -ge ${NOTIF_MIN_RUN} -a ${prompt_first} -eq 0 ]; then
    for c in "${NOTIF_CMDS[@]}"; do
        if grep -E "${c}" <<<"${last_run_cmd}" >/dev/null; then
            do_notify ${RET} "${last_run_cmd}"
            break
        fi
    done
  fi
 
  # Is there a battery?
  if [ -d /proc/acpi/battery/BAT0 ]; then
    BAT=0
    ok=
    for bat in /proc/acpi/battery/BAT*; do
      state="$(awk '$0~/^charging state:/ {print $3;}' "${bat}/state")"
      rate="$(awk '$0~/^present rate:/ {print $3;}' "${bat}/state")"
      if [ "${state}" = "discharging" -a ${rate} -ne 0 ]; then
        ok=1
        rem_cap="$(awk '$0~/^remaining capacity:/ {print $3;}' "${bat}/state")"
        BAT=$((BAT+((60*rem_cap)/rate)))
      fi
    done
    if [ -n "${ok}" ]; then
      if [ ${BAT} -le 5 ]; then
        bat_col="${BAT_CRIT_COL}"
      elif [ ${BAT} -le 15 ]; then
        bat_col="${BAT_LOW_COL}"
      else
        bat_col="${BAT_OK_COL}"
      fi
      h=$((BAT/60))
      m=$((BAT%60))
      [ $m -ge 10 ] || m="0${m}"
      BAT="${h}:${m}${D_ARROW}"
    else
      BAT=
    fi
  fi
 
  # Replace "${HOME}/" with "~/".
  # Escape special chars (only ` for now...)
  newPWD="$( pwd |sed -r -e "s:^${HOME}/:~/:; s/\`/\\\\\`/g;" )"
 
  if [ $(($(mountpoint -d . 2>/dev/null |cut -d : -f 1)+0)) -eq 0 ]; then
    is_net_mntpt=1
  fi
 
  # Try to find if we are in a ☿/±/@
  # Use the lowest-level repository
  repoPWD="$(pwd)"
  while [ -n "${repoPWD}" ]; do
    if [ -d "${repoPWD}/.hg" ]; then
      repoTYPE=☿
      break
    fi
    if [ -d "${repoPWD}/.git" ]; then
      repoTYPE=±
      break
    fi
    if [ -d "${repoPWD}/.svn" ]; then
      repoTYPE=@
      break
    fi
    repoPWD="${repoPWD%/*}"
  done
 
  # Get repo details
  case "${repoTYPE}:${is_net_mntpt}" in
    ☿:0)
      if [    "${repoPWD}" != "${HOME}"                     \
           -o \(    "${PWD}" = "${HOME}"                    \
                -a -n "$( hg status -dramC |head -n 1 )"    \
              \)                                            \
         ]
      then
        REV+="$( hg id -b ):"
        # If there is an MQ here, also print index in the queue
        rev_qtip="$( hg log -r qtip --template '{rev}\n' 2>/dev/null )"
        if [ -n "${rev_qtip}" ]; then
          REV+="$( hg log -r qparent --template '{rev}\n' )"
          REV+="+$( hg qqueue                                                   \
                    |sed -r -e '/\(active\)$/!d; s/[[:space:]]*\(active\)$//;'  \
                  )"
          REV+=":$( hg qtop -v                                          \
                    |sed -r -e 's/^[[:space:]]*([[:digit:]]+).*$/\1/;'  \
                  )"
          if [ -n "$( hg status |grep -v -E '^\?' )" ]; then
            REV+="+"
          fi
        else
          REV+="$( hg id -n )"
        fi
        if [ -n "$( hg status |grep -E '^\?' )" ]; then
          REV+="!"
        fi
      fi
      ;;
    ☿:1)
      # Don't print anything for the $HOME repository
      if [ "${repoPWD}" != "${HOME}" ]; then
        REV+="$( hg id -b ):"
        # If there is an MQ here, also print index in the queue
        rev_qtip="$( hg log -r qtip --template '{rev}\n' 2>/dev/null )"
        if [ -n "${rev_qtip}" ]; then
          REV+="$( hg log -r qparent --template '{rev}\n' )"
          REV+="+$( hg qqueue                                                     \
                    |sed -r -e '/\(active\)$/!d; s/[[:space:]]*\(active\)$//;'    \
                  )"
          REV+=":$( hg qtop -v 2>/dev/null                                \
                    |sed -r -e 's/^[[:space:]]*([[:digit:]]+).*$/\1/;'    \
                  )"
        else
          REV+="$( hg log -r . --template '{rev}\n' )"
        fi
        REV+="?"
      fi
      ;;
    @:0)
      REV+="$( LC_ALL=C LANG=C svnversion 2>/dev/null   \
               |egrep -v 'exported'                     \
             )"
      ;;
    @:1)
      REV+="$( LC_ALL=C LANG=C svn info 2>/dev/null \
               |egrep '^Revision:'                  \
               |cut -d ' ' -f 2-                    \
             )"
      ;;
  esac
  REV="${REV:+${repoTYPE}${REV}}"
 
  DATE=$(LC_ALL=C LANG=C date +%Y%m%d.%H%M%S)
 
  # Width of variable stuff in the prompt
  promptsize=$((25+${#newPWD}+${#REV}+${#DATE}+${#LOAD}))
  [ ${#BAT} -eq 0 ] || promptsize=$((promptsize+1+${#BAT}))
  # We need to catter for escaped chars, too. For each '\.' pair,
  # we substract one for the computed length
  # This is a subtle, yet ugly, hack to compute the number of
  # escaped chars... Sigh... I can even disgust myself! :-/
  promptsize=$((promptsize-$( printf "${newPWD}"                             \
                              |sed -r -e 's/ //g; s/(\\.)/\n  <<<\1>>>\n/g;' \
                              |sed -r -e '/^  <<<\\.>>>/!d;'                 \
                              |wc -l)))
 
  # Calculate the number of H_BARs to add or how much to truncate the prompt
  fillsize=$((${COLS}-${promptsize}))
  #   Right-truncate PWD if the prompt is going to be wider than the terminal:
  if [ "$fillsize" -lt "0" ]; then
    cutt=$((3-fillsize))
    promptPWD="...$( printf "${newPWD}" | sed -r -e "s/^.{${cutt}}(.*)/\1/" )"
    fillsize=0
  else
    promptPWD="${newPWD}"
  fi
 
  # Trim PWD to display in xterm title
  pwdsize=$((${#newPWD}+${#REV}))
  if [ $pwdsize -gt 56 ]; then
    cutt=$((pwdsize-56))
    xtermPWD="...$( echo "${newPWD}" |sed -r -e 's/^.{'${cutt}'}(.*)/\1/;' )"
  else
    xtermPWD="${newPWD}"
  fi
 
  # Initial prompt
  #  - set xterm title
  PS1="\[\033]0;[\u@\h : ${xtermPWD}${REV}]\007\]"
  #  - rewind to beginning of line, set normal color
  PS1="${PS1}\r${A_NOR}"
 
  # Top line
  #  - first part: angle, 3 hbars and T
  PS1="${PS1}${LK0_COL}${SQ_UL}${LK0_COL}${H_BAR}${LK1_COL}${H_BAR}${BRK_COL}${H_BAR}${T_L}"
  #  - second part: truncated pwd
  PS1="${PS1}${PWD_COL}${promptPWD}${REV_COL}${REV}"
  #  - third part: T, 3 hbars, filling hbars, 3 hbars and T
  PS1="${PS1}${BRK_COL}${T_R}${H_BAR}${LK1_COL}${H_BAR}${LK0_COL}${H_BAR}${BAR_COL}"
  for((i=1;i<=${fillsize};i++)); do PS1="${PS1}${H_BAR}"; done
  PS1="${PS1}${LK0_COL}${H_BAR}${LK1_COL}${H_BAR}${BRK_COL}${H_BAR}${T_L}"
  #  - fourth part: last minute's load average (and battery left)
  PS1="${PS1}${LOD_COL}${LOAD}"
  [ ${#BAT} -eq 0 ] || PS1="${PS1}|${bat_col}${BAT}"
  #  -fifth part: T, 6 hbars, T
  PS1="${PS1}${BRK_COL}${T_R}${H_BAR}${LK1_COL}${H_BAR}${LK0_COL}${H_BAR}${LK0_COL}${H_BAR}${LK1_COL}${H_BAR}${BRK_COL}${H_BAR}${T_L}"
  #  - sixth part: date
  PS1="${PS1}${TIM_COL}${DATE}"
  #  - seventh part: T, 2 hbars and cariage return
  PS1="${PS1}${BRK_COL}${T_R}${H_BAR}${LK1_COL}${H_BAR}${LK0_COL}${H_BAR}"
 
  # Bottom line
  PS1="${PS1}\n"
  #  - first part: angle, hbar and T
  PS1="${PS1}${LK1_COL}${SQ_BL}${BRK_COL}${H_BAR}${T_L}"
  #  - second part: username @ hostname
  PS1="${PS1}${WHO_COL}\u@\h"
  #  - third part: tty if available
  if [ -n "${TTY}" ]; then
    PS1="${PS1}${PTS_COL}:${TTY}"
    [ -n "${STY}" ] && PS1="${PS1}${SCR_COL}/screen${SCREEN_SES}"
    PS1="${PS1}${A_NOR}"
  fi
  #  - fifth part: if ret!=0: vbar, ret=$ret
  if [ ${RET} -ne 0 ]; then
    case "${TERM}" in
      xterm*) attr="${A_UND}";;
      *)      attr="";;
    esac
    PS1="${PS1}${BRK_COL}${V_BAR}${attr}${RET_COL}ret=${RET}${A_NOR}"
  fi
  # - sixth part: T, 2 hbar, > and space
  PS1="${PS1}${BRK_COL}${T_R}${H_BAR}${LK1_COL}${H_BAR}${LK0_COL}${H_BAR}${BAR_COL}${H_BAR}${ARROW} "
 
  PS1="${F_BLU}${PS1}${A_NOR}"
 
  # Save history, to share amongst sessions
  history -a
  prompt_first=0
}
 
export prompt_first=1
export -f doPrompt
export PROMPT_COMMAND=doPrompt

ressources/prompt.txt · Last modified: 20120725.205633 by ymorin
 
Except where otherwise noted, content on this wiki is licensed under the following license: CC Attribution-Noncommercial-Share Alike 3.0 Unported
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki