Last active
April 11, 2021 07:35
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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