Skip to content

Instantly share code, notes, and snippets.

@siddhpant
Created January 23, 2025 10:03
Show Gist options
  • Save siddhpant/e6c40b42ff91bafe174303009d4667a4 to your computer and use it in GitHub Desktop.
Save siddhpant/e6c40b42ff91bafe174303009d4667a4 to your computer and use it in GitHub Desktop.
Custom bash theme
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Siddh Raman Pant <@siddhpant on GitHub>
# Design skeletal inspired from Kali Linux theme PS1: https://superuser.com/a/1781707
# -----------------------------------------------------------------------------
# Check add_starting_line() and add_ending_line()
____custom_ps1_separator="─"
# Colour codes.
____custom_ps1_colour_stop='\[\033[00m\]'
____custom_ps1_bold_yellow_start='\[\033[01;32m\]' # For separator.
____custom_ps1_bold_blue_start='\[\033[01;34m\]' # For dir.
____custom_ps1_bold_red_start='\[\033[01;31m\]' # For shell level.
____custom_ps1_red_start='\[\033[31m\]' # For command info.
____custom_ps1_green_start='\[\033[32m\]' # For command info.
# The text in parenthesis and brackets are not joiners. So stop the yellow
# color before the text, and start after the text.
____custom_ps1_left_parenthesis="($____custom_ps1_colour_stop"
____custom_ps1_right_parenthesis="$____custom_ps1_bold_yellow_start)"
____custom_ps1_left_square_bracket="[$____custom_ps1_colour_stop"
____custom_ps1_right_square_bracket="$____custom_ps1_bold_yellow_start]"
# -----------------------------------------------------------------------------
# Save the command start time using PS0, so we can calculate the amount of time
# taken by a command, and also detect whether we actually executed a command or
# just pressed enter / did Ctrl+C / wrote a comment / etc.
____custom_ps1_previous_command_start_time=''
# Use parameter substitution trick to assign the value to our variable.
# We cannot use a simple function as that will execute in a subshell.
____custom_ps1_start_time_assigner='${____custom_ps1_previous_command_start_time:=${EPOCHREALTIME/[^0-9]/}}'
# Since the above results in a string, but we don't want it to be printed,
# let's use the replace parameter substitution trick on the same variable so
# that the assignment happens, but the initial empty string stays empty.
PS0='${____custom_ps1_previous_command_start_time//?/'
PS0+=$____custom_ps1_start_time_assigner
PS0+='}'
# -----------------------------------------------------------------------------
____custom_ps1_format_duration_us()
{
local us=$1
local day=$(( us / 86400000000 ))
local hr=$(( us / 3600000000 % 24 ))
local min=$(( us / 60000000 % 60 ))
local sec=$(( us / 1000000 % 60 ))
local ms=$(( us / 1000 % 1000))
local formatted_duration=""
if (( $day != 0 )); then formatted_duration+="${day}d "; fi
if (( $hr != 0 )); then formatted_duration+="${hr}h "; fi
if (( $min != 0 )); then formatted_duration+="${min}m "; fi
if [[ "$formatted_duration" == "" ]] && [[ "$sec" == "0" ]] ; then
formatted_duration+="${ms}ms"
else
printf -v ms "%03d" $ms
formatted_duration+="${sec}.${ms}s"
fi
echo $formatted_duration
}
____custom_ps1_exit_code_to_str()
{
local exit_code_int=$1
local exit_code_str="$exit_code_int"
case $exit_code_int in
1) exit_code_str="ERROR";;
2) exit_code_str="USAGE";;
64) exit_code_str="USAGE";;
65) exit_code_str="DATAERR";;
66) exit_code_str="NOINPUT";;
67) exit_code_str="NOUSER";;
68) exit_code_str="NOHOST";;
69) exit_code_str="UNAVAILABLE";;
70) exit_code_str="SOFTWARE";;
71) exit_code_str="OSERR";;
72) exit_code_str="OSFILE";;
73) exit_code_str="CANTCREAT";;
74) exit_code_str="IOERR";;
75) exit_code_str="TEMPFAIL";;
76) exit_code_str="PROTOCOL";;
77) exit_code_str="NOPERM";;
78) exit_code_str="CONFIG";;
126) exit_code_str="NOPERM";;
127) exit_code_str="NOTFOUND";;
# From output of `kill -l`, with codes 128 + n.
129) exit_code_str="SIGHUP";;
130) exit_code_str="SIGINT";;
131) exit_code_str="SIGQUIT";;
132) exit_code_str="SIGILL";;
133) exit_code_str="SIGTRAP";;
134) exit_code_str="SIGABRT";;
135) exit_code_str="SIGBUS";;
136) exit_code_str="SIGFPE";;
137) exit_code_str="SIGKILL";;
138) exit_code_str="SIGUSR1";;
139) exit_code_str="SIGSEGV";;
140) exit_code_str="SIGUSR2";;
141) exit_code_str="SIGPIPE";;
142) exit_code_str="SIGALRM";;
143) exit_code_str="SIGTERM";;
144) exit_code_str="SIGSTKFLT";;
145) exit_code_str="SIGCHLD";;
146) exit_code_str="SIGCONT";;
147) exit_code_str="SIGSTOP";;
148) exit_code_str="SIGTSTP";;
149) exit_code_str="SIGTTIN";;
150) exit_code_str="SIGTTOU";;
151) exit_code_str="SIGURG";;
152) exit_code_str="SIGXCPU";;
153) exit_code_str="SIGXFSZ";;
154) exit_code_str="SIGVTALRM";;
155) exit_code_str="SIGPROF";;
156) exit_code_str="SIGWINCH";;
157) exit_code_str="SIGIO";;
158) exit_code_str="SIGPWR";;
159) exit_code_str="SIGSYS";;
162) exit_code_str="SIGRTMIN";;
163) exit_code_str="SIGRTMIN+1";;
164) exit_code_str="SIGRTMIN+2";;
165) exit_code_str="SIGRTMIN+3";;
166) exit_code_str="SIGRTMIN+4";;
167) exit_code_str="SIGRTMIN+5";;
168) exit_code_str="SIGRTMIN+6";;
169) exit_code_str="SIGRTMIN+7";;
170) exit_code_str="SIGRTMIN+8";;
171) exit_code_str="SIGRTMIN+9";;
172) exit_code_str="SIGRTMIN+10";;
173) exit_code_str="SIGRTMIN+11";;
174) exit_code_str="SIGRTMIN+12";;
175) exit_code_str="SIGRTMIN+13";;
176) exit_code_str="SIGRTMIN+14";;
177) exit_code_str="SIGRTMIN+15";;
178) exit_code_str="SIGRTMAX-14";;
179) exit_code_str="SIGRTMAX-13";;
180) exit_code_str="SIGRTMAX-12";;
181) exit_code_str="SIGRTMAX-11";;
182) exit_code_str="SIGRTMAX-10";;
183) exit_code_str="SIGRTMAX-9";;
184) exit_code_str="SIGRTMAX-8";;
185) exit_code_str="SIGRTMAX-7";;
186) exit_code_str="SIGRTMAX-6";;
187) exit_code_str="SIGRTMAX-5";;
188) exit_code_str="SIGRTMAX-4";;
189) exit_code_str="SIGRTMAX-3";;
190) exit_code_str="SIGRTMAX-2";;
191) exit_code_str="SIGRTMAX-1";;
192) exit_code_str="SIGRTMAX";;
esac
echo $exit_code_str
}
# -----------------------------------------------------------------------------
# Initialise PS1 here so we can set it in functions later.
# This is the only place where it will be an empty string.
# So it will be empty only before the first prompt of the terminal window.
PS1=''
____custom_ps1_previous_command_exit_status=''
____custom_ps1_show_previous_command_info()
{
____custom_ps1_previous_command_exit_status=$?
# First prompt of terminal doesn't need to account for last command.
if [[ "$PS1" == '' ]] && (( $SHLVL == 1 )); then
return
fi
# We don't need to account for "previous" command if the last prompt
# had no command execution. In that case, there will be no PS0 and thus
# no previous_command_start_time.
if [[ $____custom_ps1_previous_command_start_time == '' ]]; then
# Add a newline regardless.
PS1="\n"
return
fi
# Save previous info in local variables for better naming.
local previous_start_time=$____custom_ps1_previous_command_start_time
local previous_exit_status=$(
____custom_ps1_exit_code_to_str \
$____custom_ps1_previous_command_exit_status
)
# Reset global variables now itself.
PS1=''
____custom_ps1_previous_command_start_time=''
# If the last command didn't end with a newline (like a printf), then
# our right prompt for info will get messed up. So in that case, we
# will add a new line and just make it explicit like GitHub that there
# is no newline using a symbol.
local row_pos
local col_pos
local newline_start=" "
# https://unix.stackexchange.com/a/183121
IFS=';' read -sdR -p $'\E[6n' row_pos col_pos
if [[ "${col_pos#*[}" != "1" ]]; then
# In the above check, we stripped escape codes from col_pos.
PS1+="\n"
newline_start=$____custom_ps1_bold_red_start
newline_start+="⊖"
newline_start+=$____custom_ps1_colour_stop
fi
local current_time=${EPOCHREALTIME/[^0-9]/}
local runtime_us=$(( current_time - previous_start_time ))
local time_taken_str=$(____custom_ps1_format_duration_us $runtime_us)
local info=""
local info_colour=""
if [[ "$previous_exit_status" == "0" ]]; then
info_colour=$____custom_ps1_green_start
info="[$time_taken_str]"
else
info_colour=$____custom_ps1_red_start
info="[$previous_exit_status / $time_taken_str]"
fi
# Right align info (acounting for extra char we will add later).
printf -v info "%$(( $COLUMNS - 1 ))s" "$info"
PS1+=$newline_start
PS1+=$info_colour
PS1+=$info
PS1+=$____custom_ps1_colour_stop
PS1+="\n"
}
____custom_ps1_add_starting_line()
{
PS1+=$____custom_ps1_bold_yellow_start
PS1+='┌──'
PS1+='${debian_chroot:+($debian_chroot)}'
}
____custom_ps1_add_ending_line()
{
PS1+='\n'
PS1+='└─'
PS1+=$____custom_ps1_colour_stop
PS1+='\$ ' # \$ is an escape sequence for PS1 (# if UID 0, else $).
}
____custom_ps1_add_shell_level()
{
if (( $SHLVL == 1 )); then
return
fi
PS1+=$____custom_ps1_separator
PS1+=$____custom_ps1_left_parenthesis
PS1+=$____custom_ps1_bold_red_start
PS1+=" Lvl $SHLVL"
PS1+=$____custom_ps1_colour_stop
PS1+=$____custom_ps1_right_parenthesis
}
____custom_ps1_add_pwd()
{
PS1+=$____custom_ps1_separator
PS1+=$____custom_ps1_left_square_bracket
PS1+=$____custom_ps1_bold_blue_start
PS1+='\w'
PS1+=$____custom_ps1_colour_stop
PS1+=$____custom_ps1_right_square_bracket
}
____custom_ps1_add_git()
{
____custom_ps1_git_ps1="$(
GIT_PS1_SHOWDIRTYSTATE=1
GIT_PS1_SHOWSTASHSTATE=1
GIT_PS1_SHOWCONFLICTSTATE=1
GIT_PS1_SHOWUNTRACKEDFILES=1
GIT_PS1_SHOWUPSTREAM="verbose"
GIT_PS1_DESCRIBE_STYLE=default
__git_ps1
)"
if [[ "$____custom_ps1_git_ps1" == "" ]]; then
return
fi
# Remove starting space and parentheses.
____custom_ps1_git_ps1=${____custom_ps1_git_ps1:2:-1}
PS1+=$____custom_ps1_separator
PS1+=$____custom_ps1_left_square_bracket
PS1+=$____custom_ps1_git_ps1
PS1+=$____custom_ps1_right_square_bracket
}
____custom_ps1_build_ps1()
{
____custom_ps1_show_previous_command_info
____custom_ps1_add_starting_line
____custom_ps1_add_shell_level
____custom_ps1_add_pwd
____custom_ps1_add_git
____custom_ps1_add_ending_line
}
PROMPT_COMMAND="____custom_ps1_build_ps1"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment