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.06
05:27
, and the battery is discharging ↓
ymorin
on tty4
of the machine treguer
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