Skip to content

Instantly share code, notes, and snippets.

@rawiriblundell
Last active April 11, 2021 07:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rawiriblundell/2c4631611052509a55c658cd0efaab35 to your computer and use it in GitHub Desktop.
Save rawiriblundell/2c4631611052509a55c658cd0efaab35 to your computer and use it in GitHub Desktop.
A .bashrc fragment with all required code for setprompt - a PS1 colouriser and state changer
# shellcheck shell=bash
################################################################################
# setprompt and related/required functions
################################################################################
# Wrap 'cd' to automatically update GIT_BRANCH when necessary
cd() {
command cd "${@}" || return 1
if is_gitdir; then
PS1_GIT_MODE=True
GIT_BRANCH="$(git branch 2>/dev/null| sed -n '/\* /s///p')"
export GIT_BRANCH
else
PS1_GIT_MODE=False
fi
}
# Let 'git' take the perf hit of setting GIT_BRANCH rather than PROMPT_COMMAND
# There's no one true way to get the current git branch, they all have pros/cons
# See e.g. https://stackoverflow.com/q/6245570
if is_command git; then
git() {
command git "${@}"
GIT_BRANCH="$(command git branch 2>/dev/null| sed -n '/\* /s///p')"
export GIT_BRANCH
}
fi
# Small function to try and ensure setprompt etc behaves when escalating to root
# I don't want to override the default behaviour of 'sudo', hence the name
godmode() {
if [[ -z "${1}" ]]; then
# Testing for 'sudo -E' is hackish, let's just use this
sudo bash --rcfile "${HOME}"/.bashrc
else
sudo "$@"
fi
}
# Functionalise 'command -v' to allow 'if is_command [command]' idiom
is_command() { command -v "${1}" &>/dev/null; }
# Are we within a directory that's tracked by git?
is_gitdir() {
if [[ -e .git ]]; then
return 0
else
git rev-parse --git-dir 2>&1 | grep -Eq '^.git|/.git'
fi
}
# This function prints the terminfo details for 'xterm-256color'
# This is for importing this into systems that don't have this
# If you'll only ever run this code on newer systems,
# you can rip this and tput() out
print-xterm-256color() {
cat <<'NEWTERM'
xterm-256color|xterm with 256 colors,
am, bce, ccc, km, mc5i, mir, msgr, npc, xenl,
colors#256, cols#80, it#8, lines#24, pairs#32767,
acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
bel=^G, blink=\E[5m, bold=\E[1m, cbt=\E[Z, civis=\E[?25l,
clear=\E[H\E[2J, cnorm=\E[?12l\E[?25h, cr=^M,
csr=\E[%i%p1%d;%p2%dr, cub=\E[%p1%dD, cub1=^H,
cud=\E[%p1%dB, cud1=^J, cuf=\E[%p1%dC, cuf1=\E[C,
cup=\E[%i%p1%d;%p2%dH, cuu=\E[%p1%dA, cuu1=\E[A,
cvvis=\E[?12;25h, dch=\E[%p1%dP, dch1=\E[P, dl=\E[%p1%dM,
dl1=\E[M, ech=\E[%p1%dX, ed=\E[J, el=\E[K, el1=\E[1K,
flash=\E[?5h$<100/>\E[?5l, home=\E[H, hpa=\E[%i%p1%dG,
ht=^I, hts=\EH, ich=\E[%p1%d@, il=\E[%p1%dL, il1=\E[L,
ind=^J, indn=\E[%p1%dS,
initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\,
invis=\E[8m, is2=\E[!p\E[?3;4l\E[4l\E>, kDC=\E[3;2~,
kEND=\E[1;2F, kHOM=\E[1;2H, kIC=\E[2;2~, kLFT=\E[1;2D,
kNXT=\E[6;2~, kPRV=\E[5;2~, kRIT=\E[1;2C, kb2=\EOE,
kbs=\177, kcbt=\E[Z, kcub1=\EOD, kcud1=\EOB, kcuf1=\EOC,
kcuu1=\EOA, kdch1=\E[3~, kend=\EOF, kent=\EOM, kf1=\EOP,
kf10=\E[21~, kf11=\E[23~, kf12=\E[24~, kf13=\E[1;2P,
kf14=\E[1;2Q, kf15=\E[1;2R, kf16=\E[1;2S, kf17=\E[15;2~,
kf18=\E[17;2~, kf19=\E[18;2~, kf2=\EOQ, kf20=\E[19;2~,
kf21=\E[20;2~, kf22=\E[21;2~, kf23=\E[23;2~,
kf24=\E[24;2~, kf25=\E[1;5P, kf26=\E[1;5Q, kf27=\E[1;5R,
kf28=\E[1;5S, kf29=\E[15;5~, kf3=\EOR, kf30=\E[17;5~,
kf31=\E[18;5~, kf32=\E[19;5~, kf33=\E[20;5~,
kf34=\E[21;5~, kf35=\E[23;5~, kf36=\E[24;5~,
kf37=\E[1;6P, kf38=\E[1;6Q, kf39=\E[1;6R, kf4=\EOS,
kf40=\E[1;6S, kf41=\E[15;6~, kf42=\E[17;6~,
kf43=\E[18;6~, kf44=\E[19;6~, kf45=\E[20;6~,
kf46=\E[21;6~, kf47=\E[23;6~, kf48=\E[24;6~,
kf49=\E[1;3P, kf5=\E[15~, kf50=\E[1;3Q, kf51=\E[1;3R,
kf52=\E[1;3S, kf53=\E[15;3~, kf54=\E[17;3~,
kf55=\E[18;3~, kf56=\E[19;3~, kf57=\E[20;3~,
kf58=\E[21;3~, kf59=\E[23;3~, kf6=\E[17~, kf60=\E[24;3~,
kf61=\E[1;4P, kf62=\E[1;4Q, kf63=\E[1;4R, kf7=\E[18~,
kf8=\E[19~, kf9=\E[20~, khome=\EOH, kich1=\E[2~,
kind=\E[1;2B, kmous=\E[M, knp=\E[6~, kpp=\E[5~,
kri=\E[1;2A, mc0=\E[i, mc4=\E[4i, mc5=\E[5i, meml=\El,
memu=\Em, op=\E[39;49m, rc=\E8, rev=\E[7m, ri=\EM,
rin=\E[%p1%dT, rmacs=\E(B, rmam=\E[?7l, rmcup=\E[?1049l,
rmir=\E[4l, rmkx=\E[?1l\E>, rmm=\E[?1034l, rmso=\E[27m,
rmul=\E[24m, rs1=\Ec, rs2=\E[!p\E[?3;4l\E[4l\E>, sc=\E7,
setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m,
sgr0=\E(B\E[m, smacs=\E(0, smam=\E[?7h, smcup=\E[?1049h,
smir=\E[4h, smkx=\E[?1h\E=, smm=\E[?1034h, smso=\E[7m,
smul=\E[4m, tbc=\E[3g, u6=\E[%i%d;%dR, u7=\E[6n,
u8=\E[?1;2c, u9=\E[c, vpa=\E[%i%p1%dd,
NEWTERM
}
# Standardise the terminal window title header
# reference: http://www.faqs.org/docs/Linux-mini/Xterm-Title.html#s3
settitle() {
case $(tty) in
(*tty*)
: # Physical terminal, so no-op.
;;
(*pts*)
# shellcheck disable=SC2059,SC1117
printf "\033]0;${HOSTNAME%%.*}:${PWD}\007"
# This might also need to be expressed as
#printf "\\033]2;${HOSTNAME}:${PWD}\\007\\003"
# I possibly need to test and figure out a way to auto-switch between these two
;;
esac
}
# Test if a string contains a substring
# Example: string-contains needle haystack
string-contains() {
case "${2}" in
(*${1}*) return 0 ;;
(*) return 1 ;;
esac
}
# Detect if our version of 'tput' is so old that it uses termcap syntax
# This is a subset of a fuller gist
# https://gist.github.com/rawiriblundell/83ed9408a7e3032c780ed56b7c9026f2
# For performance we only implement if 'tput ce' (a harmless test) works
if tput ce 2>/dev/null; then
tput() {
ctput-null() { command tput "${@}" 2>/dev/null; }
ctput() { command tput "${@}"; }
case "${1}" in
(bold) ctput-null bold || ctput md;;
(civis) ctput-null civis || ctput vi;;
(cnorm) ctput-null cnorm || ctput ve;;
(cols) ctput-null cols || ctput co;;
(dim) ctput-null dim || ctput mh;;
(lines) ctput-null lines || ctput li;;
(setaf)
case $(uname) in
(FreeBSD) ctput AF "${2}";;
(OpenBSD) ctput AF "${2}" 0 0;;
(*) ctput setaf "${2}";;
esac
;;
(setab)
case $(uname) in
(FreeBSD) ctput AB "${2}";;
(OpenBSD) ctput AB "${2}" 0 0;;
(*) ctput setab "${2}";;
esac
;;
(sgr0) ctput-null sgr0 || ctput me;;
(*) ctput "${@}";;
esac
}
fi
################################################################################
# Figure out the correct TERM value
# Function to test indicated terminfo entries
termtest() {
if exists infocmp; then
infocmp "${1}" &>/dev/null
return "$?"
else
oldTerm="${TERM}"
TERM="${1}"
tput colors &>/dev/null
rc="$?"
TERM="${oldTerm}"
return "${rc}"
fi
}
# Firstly, we assume a PuTTY connection identified as 'putty-256color'
if [[ "${TERM}" = "putty-256color" ]]; then
# We check whether an appropriate terminfo entry exists
# If not, failover to 'xterm-256color'
termtest putty-256color || TERM=xterm-256color
# If we're not using 'putty-256color', then we want 'xterm-256color'
else
TERM=xterm-256color
fi
# Next, we test for a 'xterm-256color' terminfo entry
# If not, we set up ~/.terminfo appropriately (usually Solaris)
if [[ "${TERM}" = "xterm-256color" ]]; then
if ! termtest xterm-256color; then
if exists tic; then
mkdir -p "${HOME}"/.terminfo
print-xterm-256color > "${HOME}"/.terminfo/xterm-256color
TERMINFO="${HOME}"/.terminfo
export TERMINFO
tic "${HOME}"/.terminfo/xterm-256color 2>/dev/null
TERM=xterm-256color
else
printf '%s\n' "'tic' is required to setup xterm-256color but was not found" \
"Usually this can be found in the 'ncurses' package"
# Set a dummy TERM to invoke the next block
TERM=pants
fi
fi
fi
# If we get to this point, we take what we can get
if ! string-contains 256color "${TERM}"; then
for termType in xterm-color xtermc dtterm sun-color xterm; do
if termtest "${termType}"; then
TERM="${termType}"
break
fi
done
fi
# Finally, lock in the TERM setting
export TERM
################################################################################
# Standardise the Command Prompt
setprompt-help() {
printf -- '%s\n' "setprompt - configure state and colourisation of the bash prompt" ""
printf '\t%s\n' "Usage: setprompt [-ahfmrs|rand|safe|[0-255]] [rand|[0-255]]" ""
printf '\t%s\n' "Options:" \
" -a Automatic type selection (width based)" \
" -g Enable/disable git branch in the first text block" \
" -h Help, usage information" \
" -f Full prompt" \
" -m Minimal prompt" \
" -r Restore prompt colours to defaults" \
" -s Simplified prompt" \
" rand Select a random colour. Can be used for 1st and 2nd colours" \
" e.g. 'setprompt rand rand'" \
" safe Sets 1st and 2nd colours to white. In case of weird behaviour" \
"" \
"The first and second parameters will accept human readable colour codes." \
"These are represented in short and full format e.g." \
"For 'blue', you can enter 'bl', 'Bl', 'blue' or 'Blue'." \
"This applies for Black, Red, Green, Yellow, Blue, Magenta, Cyan, White and Orange." \
"ANSI Numerical codes (0-255) can also be supplied e.g. 'setprompt 143 76'." \
"" \
"256 colours is assumed at all times. If you find issues, run 'setprompt safe'." \
"" \
"'setprompt -a' enables automatic prompt mode selection based on the terminal width" \
"If less than 60 columns is detected, the prompt is set to minimal mode." \
"If less than 80 columns is detected, the prompt is set to simple mode." \
"When the columns exceed 80, the prompt is set to the full mode."
return 0
}
setprompt() {
# NOTE for customisation: Any non-printing escape characters must be enclosed,
# otherwise bash will miscount and get confused about where the prompt starts.
# All sorts of line wrapping weirdness and prompt overwrites will then occur.
# This is why all the escape codes have '\]' enclosing them. Don't mess with that.
# First, we map some basic colours:
ps1Blk="\[$(tput setaf 0)\]" # Black - \[\e[0;30m\]
ps1Red="\[$(tput bold)\]\[$(tput setaf 9)\]" # Bold Red - \[\e[1;31m\]
ps1Grn="\[$(tput setaf 10)\]" # Normal Green - \[\e[0;32m\]
ps1Ylw="\[$(tput bold)\]\[$(tput setaf 11)\]" # Bold Yellow - \[\e[1;33m\]
ps1Blu="\[$(tput setaf 32)\]" # Blue - \[\e[38;5;32m\]
ps1Mag="\[$(tput bold)\]\[$(tput setaf 13)\]" # Bold Magenta - \[\e[1;35m\]
ps1Cyn="\[$(tput bold)\]\[$(tput setaf 14)\]" # Bold Cyan - \[\e[1;36m\]
ps1Wte="\[$(tput bold)\]\[$(tput setaf 15)\]" # Bold White - \[\e[1;37m\]
ps1Ora="\[$(tput setaf 208)\]" # Orange - \[\e[38;5;208m\]
ps1Rst="\[$(tput sgr0)\]" # Reset text - \[\e[0m\]
# Map out some block characters
#block100="\xe2\x96\x88" # u2588\0xe2 0x96 0x88 Solid Block 100%
block75="\xe2\x96\x93" # u2593\0xe2 0x96 0x93 Dark shade 75%
block50="\xe2\x96\x92" # u2592\0xe2 0x96 0x92 Half shade 50%
block25="\xe2\x96\x91" # u2591\0xe2 0x96 0x91 Light shade 25%
# Put those block characters in ascending and descending triplets
blockAsc="$(printf '%b\n' "${block25}${block50}${block75}")"
blockDwn="$(printf '%b\n' "${block75}${block50}${block25}")"
# If we want to define our colours with a dotfile, we load it here
[[ -f "${HOME}/.setpromptrc" ]] && . "${HOME}/.setpromptrc"
# Let's setup some default primary and secondary colours for root/sudo
if (( EUID == 0 )); then
ps1Pri="${ps1Red}"
ps1Sec="${ps1Red}"
ps1Block="${blockAsc}"
ps1Char='#'
fi
case "${1}" in
(-a|--auto|-|_|'')
# Figure out the appropriate mode to use
if (( "${COLUMNS:-$(tput cols)}" < 60 )); then
export PS1_MODE=Minimal
elif (( "${COLUMNS:-$(tput cols)}" > 80 )); then
export PS1_MODE=Full
else
export PS1_MODE=Simple
fi
;;
(-g|--git)
case "${PS1_GIT_MODE}" in
(True) PS1_GIT_MODE=False ;;
(False) PS1_GIT_MODE=True ;;
(''|*) PS1_GIT_MODE=True ;;
esac
export PS1_GIT_MODE
;;
(-h|--help) setprompt-help; return 0;;
(-f|--full) export PS1_MODE=Full;;
(-m|--mini) export PS1_MODE=Minimal;;
(-r|--reset)
if [[ -r "${HOME}/.setpromptrc" ]]; then
unset ps1Pri ps1Sec
. "${HOME}/.setpromptrc"
[[ -z "${ps1Pri}" ]] && ps1Pri="${ps1Red}"
[[ -z "${ps1Sec}" ]] && ps1Sec="${ps1Grn}"
else
ps1Pri="${ps1Red}"
ps1Sec="${ps1Grn}"
fi
;;
(-s|--simple) export PS1_MODE=Simple;;
(b|B|black|Black) ps1Pri="${ps1Blk}";;
(r|R|red|Red) ps1Pri="${ps1Red}";;
(g|G|green|Green) ps1Pri="${ps1Grn}";;
(y|Y|yellow|Yellow) ps1Pri="${ps1Ylw}";;
(bl|Bl|blue|Blue) ps1Pri="${ps1Blu}";;
(m|M|magenta|Magenta) ps1Pri="${ps1Mag}";;
(c|C|cyan|Cyan) ps1Pri="${ps1Cyn}";;
(w|W|white|White) ps1Pri="${ps1Wte}";;
(o|O|orange|Orange) ps1Pri="${ps1Ora}";;
(rand)
ps1Pri="\[$(tput setaf $((RANDOM%255)))\]"
;;
(safe)
ps1Pri="${ps1Wte}"
ps1Sec="${ps1Wte}"
;;
(*[0-9]*)
if (( "${1//[^0-9]/}" > 255 )); then
setprompt-help; return 1
else
ps1Pri="\[\e[38;5;${1//[^0-9]/}m\]"
fi
;;
esac
case "${2}" in
(b|B|black|Black) ps1Sec="${ps1Blk}";;
(r|R|red|Red) ps1Sec="${ps1Red}";;
(g|G|green|Green) ps1Sec="${ps1Grn}";;
(y|Y|yellow|Yellow) ps1Sec="${ps1Ylw}";;
(bl|Bl|blue|Blue) ps1Sec="${ps1Blu}";;
(m|M|magenta|Magenta) ps1Sec="${ps1Mag}";;
(c|C|cyan|Cyan) ps1Sec="${ps1Cyn}";;
(w|W|white|White) ps1Sec="${ps1Wte}";;
(o|O|orange|Orange) ps1Sec="${ps1Ora}";;
(rand)
ps1Sec="\[$(tput setaf $((RANDOM%255)))\]"
;;
(*[0-9]*)
if (( "${2//[^0-9]/}" > 255 )); then
setprompt-help; return 1
else
ps1Sec="\[\e[38;5;${2//[^0-9]/}m\]"
fi
;;
esac
# Setup sane defaults for the following variables
export "${PS1_MODE:=Auto}"
: "${ps1Pri:=$ps1Red}"
: "${ps1Sec:=$ps1Grn}"
: "${ps1Block:=$blockDwn}"
: "${ps1Char:=$}"
ps1Triplet="${ps1Pri}${ps1Block}"
ps1Main="${ps1Sec}[\u@\h${ps1Rst} \W${ps1Sec}]${ps1Rst}${ps1Char}"
# Throw it all together, based on the selected mode
# shellcheck disable=SC1117
case "${PS1_MODE}" in
(Minimal)
export PS1="${ps1Triplet}${ps1Rst}${ps1Char} "
;;
(Simple)
export PS1="${ps1Triplet}${ps1Main} "
;;
(Full)
if [[ "${PS1_GIT_MODE}" = "True" ]]; then
if is_gitdir; then
if [[ -z "${GIT_BRANCH}" ]]; then
if is_gitdir; then
GIT_BRANCH="$(git branch 2>/dev/null| sed -n '/\* /s///p')"
fi
fi
: "[${GIT_BRANCH:-UNKNOWN}]"
else
: "[NOT-GIT]"
fi
else
: "[\$(date +%y%m%d/%H:%M)]"
fi
PS1="${ps1Triplet}${_}${ps1Rst}${ps1Main} "
export PS1
;;
esac
# After each command, append to the history file and reread it
# This attempts to keep history sync'd across multiple sessions
history -a; history -c; history -r
}
# Useful for debugging
export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME:-}: '
################################################################################
# Set the PROMPT_COMMAND
# This updates the terminal emulator title and the prompt
PROMPT_COMMAND="settitle; setprompt"
################################################################################
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment