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…
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 :
~/dev/hg/hg.upstream, which is a sub-dir of my home ~ directory☿, the working copy is on the default branch, revision #15140bisect-revset is applied up to the #2 patch (the third one)+ and un-tracked files !0.0605:27, and the battery is discharging ↓ymorin on tty4 of the machine treguer42The 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