PRbL/functions
pyr0ball 7b63dbca46 fix: terminate truncated ANSI codes, honor ASCII fallback in box borders
- blk/nln/unbl/rsinv/hid/unh were missing the trailing 'm', producing
  invalid escape sequences. Concatenated with text (e.g. temperature
  readouts using ${blk}), strict-parsing terminals would read the
  digits/letters that followed as CSI parameters, causing garbled
  output or unwanted cursor movement.
- norm used bash's $"..." locale-translation syntax on an ANSI code
  instead of a plain string.
- set-boxtype() always ran box-norm/double/heavy/light/rounded, which
  draw with hardcoded Unicode corner glyphs, even when
  check_unicode_support() determined the terminal/locale couldn't
  render Unicode. This defeated box_chars_simple()'s ASCII fallback
  entirely. Now the Unicode style dispatch is skipped when
  unicode_supported is false.
- Removed a duplicate box-singlechar() definition.
2026-07-17 16:01:11 -07:00

1860 lines
55 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# pyr0ball script functions library
# Initial Vars
functionsrev=2.1.2
#scriptname="${BASH_SOURCE[0]##*/}"
#rundir="${BASH_SOURCE[0]%/*}"
#runuser="$(whoami)"
pretty_date="$(date +%Y-%m-%d_%H-%M-%S)"
short_date="$(date +%Y-%m-%d)"
# rundir_absolute=$(pushd $rundir ; pwd ; popd)
# escape_dir=$(printf %q "${rundir_absolute}")
# logfile="${rundir}/${pretty_date}_${scriptname}.log"
# ensure ctrl-c to cancel script ends the entire process and not just the current function
# trap ctrl_c INT # Commented out by default to prevent abnormal background exits
# return any cursor mods to normal on exit
cleanup(){
tput cnorm
}
trap cleanup EXIT
# Escape characters (if your shell uses a different one, you can modify it here)
# By default this is using the usual bash escape code
ESC=$( printf '\033')
# Detect OS type
case $OSTYPE in
linux-gnu*|cygwin|msys) ESC=$( printf '\033') ;;
darwin* ) ESC=$( printf '\e') ;;
esac
# Utilities (Setting up colors and bounding boxes)
# boxtype changes what borders are used for the boxborder functions.
# options are "single" "double" "char"
# for the "char" option, change the "borderchar" variable to
# whichever unicode character you wish to use
# boxtype="${boxtype:-single}" # Sets default 'single' if not already set
# borderchar="${borderchar:-#}" # Sets default '#' if not already set
boxtype="norm" # Sets default 'single' if not already set
borderchar="#" # Sets default '#' if not already set
# Colorization options
if [[ "$TERM" != "linux" ]] ; then
red="${ESC}[38;5;1m" # red
grn="${ESC}[38;5;2m" # green
ylw="${ESC}[38;5;3m" # yellow
blu="${ESC}[38;5;27m" # blue
lbl="${ESC}[38;5;69m" # light blue
mag="${ESC}[38;5;5m" # magenta
cyn="${ESC}[38;5;6m" # cyan
pur="${ESC}[38;5;135m" # purple
ong="${ESC}[38;5;166m" # orange
lyl="${ESC}[38;5;228m" # light yellow
lrd="${ESC}[38;5;196m" # light red
gry="${ESC}[38;5;240m" # Grey
norm="${ESC}[39m" # default/normal
#
bld="${ESC}[1m" # bold
unb="${ESC}[21m" # un-bold
dim="${ESC}[2m" # dim
und="${ESC}[22m" # un-dim
unl="${ESC}[4m" # underline
nln="${ESC}[24m" # not-underline
blk="${ESC}[5m" # blinking
unbl="${ESC}[25m" # stop blinking
inv="${ESC}[7m" # invert
rsinv="${ESC}[27m" # reset invert
hid="${ESC}[8m" # hidden
unh="${ESC}[28m" # unhide
dfl="${ESC}[0m" # restore default
fi
check_unicode_support() {
# Check if LANG indicates UTF-8
[[ "${LANG,,}" == *".utf-8"* || "${LANG,,}" == *".utf8"* ]] || return 1
# Check if terminal can display Unicode
echo -ne "\u2714" >/dev/null 2>&1 || return 1
return 0
}
box_chars_simple() {
# Simple ASCII box characters
top_border="-"
bottom_border="-"
left_border="|"
right_border="|"
left_top_border="+"
right_top_border="+"
left_bottom_border="+"
right_bottom_border="+"
box_break_line="-"
}
set_box_chars() {
if check_unicode_support; then
unicode_supported=true
case "$OSTYPE" in
linux-gnu*|cygwin|msys)
return_arrow=$(echo -e "\u2BAC")
enter_arrow=$(echo -e "\u21B5")
green_check=${grn}$(echo -e "\u2714")${dfl}
red_x=${lrd}$(echo -e "\u00D7")${dfl}
selected_opt=$(echo -e "\u25C9")
deselected_opt=$(echo -e "\u25CE")
light_h='─'
norm_h='━'
double_h='═'
light_v='│'
norm_v='┃'
double_v='║'
;;
darwin*)
return_arrow="⮬"
return_arrow=$(echo -e "⮬")
enter_arrow=$(echo -e "↵")
green_check=${grn}$(echo -e "✔")${dfl}
red_x=${lrd}$(echo -e "×")${dfl}
selected_opt=$(echo -e "◉")
deselected_opt=$(echo -e "◎")
light_h='─'
norm_h='━'
double_h='═'
light_v='│'
norm_v='┃'
double_v='║'
;;
*)
# Fallback to simple ASCII if OS is unknown
return_arrow=">"
green_check="+"
red_x="x"
selected_opt="*"
deselected_opt="o"
;;
esac
else
# ASCII fallbacks
unicode_supported=false
return_arrow=">"
enter_arrow="<"
green_check="+"
red_x="x"
selected_opt="*"
deselected_opt="o"
box_chars_simple
fi
}
# For drawing pretty boxes
update_terminal_width() {
local min_width=40 # Minimum usable width
local max_width=100 # Maximum usable width
local default_width=80
# Try to get terminal width, fallback to default if failed
terminal_width=$(tput cols 2>/dev/null || echo "$default_width")
# Ensure minimum width
if (( terminal_width < min_width )); then
terminal_width=$min_width
fi
if (( terminal_width > max_width )); then
terminal_width=$max_width
fi
# Set box width with 1 character margin
BOXWIDTH=$((terminal_width - 1))
}
# Add trap for terminal resize
trap 'update_terminal_width' WINCH
set_borders() {
top_border=$1
bottom_border=$2
left_border=$3
right_border=$4
left_top_border=$5
right_top_border=$6
left_bottom_border=$7
right_bottom_border=$8
box_break_line=$9
}
# Box border type single-line
box-norm() {
case $OSTYPE in
linux-gnu*|cygwin|msys)
set_borders "${norm_h}" "${norm_h}" "${norm_v}" "${norm_v}" "┏" "┓" "┗" "┛" "▫"
;;&
darwin* )
set_borders "${norm_h}" "${norm_h}" "${norm_v}" "${norm_v}" "┏" "┓" "┗" "┛" "▫"
;;
esac
}
# Box border type double-line
box-double() {
case $OSTYPE in
linux-gnu*|cygwin|msys)
set_borders "${double_h}" "${double_h}" "${double_v}" "${double_v}" "╔" "╗" "╚" "╝" "▫"
;;&
darwin* )
set_borders "${double_h}" "${double_h}" "${double_v}" "${double_v}" "╔" "╗" "╚" "╝" "▫"
;;
esac
}
# Box border type thick-line
box-heavy(){
case $OSTYPE in
linux-gnu*|cygwin|msys)
set_borders "▀" "▄" "▌" "▐" "▛" "▜" "▙" "▟" "▫"
;;&
darwin* )
set_borders "▀" "▄" "▌" "▐" "▛" "▜" "▙" "▟" "▫"
;;
esac
}
# Box border type thin-line
box-light(){
case $OSTYPE in
linux-gnu*|cygwin|msys)
set_borders "${light_h}" "${light_h}" "${light_v}" "${light_v}" "▫" "▫" "▫" "▫" "⎯"
;;&
darwin* )
set_borders "${light_h}" "${light_h}" "${light_v}" "${light_v}" "▫" "▫" "▫" "▫" "⎯"
;;
esac
}
box-rounded(){
case $OSTYPE in
linux-gnu*|cygwin|msys)
set_borders "${light_h}" "${light_h}" "${light_v}" "${light_v}" "╭" "╮" "╰" "╯" "▢"
;;&
darwin* )
set_borders "${light_h}" "${light_h}" "${light_v}" "${light_v}" "╭" "╮" "╰" "╯" "▢"
;;
esac
}
box-singlechar(){
set_borders "$borderchar" "$borderchar" "$borderchar" "$borderchar" "$borderchar" "$borderchar" "$borderchar" "$borderchar" "-"
}
set-boxtype() {
# Accept a parameter if provided, otherwise use the global variable
local style="${1:-$boxtype}"
# Update the global variable to match what we're using
boxtype="$style"
# Ensure characters are defined
set_box_chars
update_terminal_width
# box-norm/double/heavy/light/rounded all draw with hardcoded Unicode corner
# glyphs; on a terminal without Unicode support, box_chars_simple() (called
# from set_box_chars above) already set full ASCII borders/corners, so skip
# the style dispatch entirely rather than clobbering them.
if [[ "$unicode_supported" != true && "$style" != "char" ]]; then
return
fi
# Set the box style
case $style in
norm) box-norm ;;
double) box-double ;;
heavy) box-heavy ;;
light) box-light ;;
rounded) box-rounded ;;
char) box-singlechar ;;
*)
# Handle unknown box types by falling back to normal
echo "Warning: Unknown box type '$style', falling back to 'norm'" >&2
boxtype="norm"
if [[ "$unicode_supported" == true ]]; then
box-norm
fi
;;
esac
}
format_boxborder() {
local alignment="left"
local box_type="norm"
# Parse options
while [[ "$1" == --* ]]; do
case "$1" in
--center)
alignment="center"
;;
--right)
alignment="right"
;;
--box-type=*)
box_type="${1#--box-type=}"
;;
esac
shift
done
# Set box type
set-boxtype
boxtop
for line in "$@"; do
case "$alignment" in
center)
center "$line"
;;
right)
right_align "$line"
;;
*)
boxline "$line"
;;
esac
done
boxbottom
}
update_terminal_width
set-boxtype
repchar() {
# More robust parameter checking
if [[ $# -lt 2 ]]; then
echo "Error: repchar() Incorrect usage. Usage: repchar <char> <count>" >&2
# Return a safe fallback instead of failing
echo "----"
return 0
fi
local char="$1"
local count="$2"
# Validate count is a number
if ! [[ "$count" =~ ^[0-9]+$ ]]; then
echo "Error: repchar() count must be a number. Got: '$count'" >&2
# Return a safe fallback instead of failing
echo "----"
return 0
fi
# Create and output the repeated character string
printf -v result "%*s" "$count" ""
echo "${result// /$char}"
}
boxtop() {
printf "%s%s%s\n" "${brdr}$left_top_border" "$(repchar "$top_border" "$((BOXWIDTH-1))")" "$right_top_border${dfl}"
}
boxbottom() {
printf "%s%s%s\n" "${brdr}$left_bottom_border" "$(repchar "$bottom_border" "$((BOXWIDTH-1))")" "$right_bottom_border${dfl}"
}
boxlinelog() {
local content="$1"
logger printf "%s%s\r${ESC}[%sC%s\n" "$left_border" "$content" "$BOXWIDTH" "$right_border"
}
boxline() {
local content="$1"
if ! printf "%s%s\r${ESC}[%sC%s\n" "$left_border" "$content" "$BOXWIDTH" "$right_border" 2>/dev/null; then
# Fallback to simple output if fancy formatting fails
printf "%s %s %s\n" "$left_border" "$content" "$right_border"
fi
}
boxseparator(){
printf '%s%s%s\n' "${brdr}" "$(repchar "$box_break_line" $((BOXWIDTH-3)))" "${dfl}"
}
boxborder(){
boxtop
for line in "$@" ; do
boxline "$line"
done
boxbottom
}
# Experimental!
subboxborder() {
level=${1:-0}
local indent=$((level*2))
local border_char="━"
local top_border=""
local bottom_border=""
# Set the border characters based on the level of nesting
if ((level==0)); then
left_border="$left_top_border"
right_border="$right_top_border"
top_border="$rep_char $border_char $indent"
bottom_border="$rep_char $border_char $indent"
else
left_border="$left_border"
right_border="$right_border"
top_border="$rep_char $border_char $indent"
bottom_border="$rep_char $border_char $indent"
fi
# Generate the top border
boxline "$(boxborder "$top_border")"
((level++))
# Generate the output lines
for line in "${@:2}" ; do
boxline "$(repchar " " $indent)$(boxline "$line")"
boxseparator
done
# Generate the bottom border
((level--))
boxline "$(boxborder "$bottom_border")"
}
# Helper function to get visual length of string (accounting for ANSI escape codes)
get_visual_length() {
local text="$1"
# Remove ANSI escape sequences and count remaining characters
local stripped
stripped=$(printf "%s" "$text" | sed 's/\x1b\[[0-9;]*m//g')
printf "%s" "${#stripped}"
}
# For printing center-justified text. To change the padding character, replace the ' ' <~~ whitespace in front of the '{' in the padding variable
center() {
local text="$1"
# Early return for non-interactive/debug mode
if (( !interactive )) || (( xtrace )); then
printf "%s %s\n" "$text"
return
fi
# Calculate available inner width (total width minus borders)
local inner_width=$(( BOXWIDTH - 2 ))
local text_length=$(get_visual_length "$text")
# If text is too long, just print it with borders
if (( text_length >= inner_width )); then
printf "%s%s%s\n" "$left_border" "$text" "$right_border"
return
fi
# Calculate padding sizes
local padding_left=$(( (inner_width - text_length) / 2 ))
local padding_right=$(( inner_width - text_length - padding_left ))
# Create padding strings
local left_pad right_pad
printf -v left_pad "%*s" "$padding_left" ""
printf -v right_pad "%*s" "$padding_right" ""
# Print with consistent borders
printf "%s%s%s%s%s\n" \
"$left_border" \
"$left_pad" \
"$text" \
"$right_pad" \
"$right_border"
}
right_align() {
local text="$1"
# Early return for non-interactive/debug mode
if (( !interactive )) || (( xtrace )); then
printf "%s %s\n" "$text"
return
fi
# Calculate available inner width (total width minus borders)
local inner_width=$(( BOXWIDTH - 2 ))
local text_length=$(get_visual_length "$text")
# If text is too long, just print it with borders
if (( text_length >= inner_width )); then
printf "%s%s%s\n" "$left_border" "$text" "$right_border"
return
fi
# Calculate padding size
local padding=$(( inner_width - text_length ))
# Create padding string
local pad
printf -v pad "%*s" "$padding" ""
# Print with consistent borders
printf "%s%s%s%s\n" \
"$left_border" \
"$pad" \
"$text" \
"$right_border"
}
# For printing spanned text, e.g. single-pair lists ($name...$title)
spanner() {
local left="$1"
local right="$2"
local spacer="${3:-.}" # Default to dot if no spacer provided
# Early return for non-interactive/debug mode
if (( !interactive )) || (( xtrace )); then
printf "%s %s\n" "$left" "$right"
return
fi
# Calculate available inner width (total width minus borders)
local inner_width=$(( BOXWIDTH - 2 ))
local left_length=$(get_visual_length "$left")
local right_length=$(get_visual_length "$right")
# Minimum space between left and right text
local min_space=1
# Calculate space needed for spacer
local spacer_width=$(( inner_width - left_length - right_length ))
# If not enough space, just print with minimal formatting
if (( spacer_width < min_space )); then
printf "%s%s %s%s\n" \
"$left_border" \
"$left" \
"$right" \
"$right_border"
return
fi
# Create spacer string
local spacer_string
printf -v spacer_string "%*s" "$spacer_width" ""
spacer_string=${spacer_string// /$spacer}
# Print with consistent borders
printf "%s%s%s%s%s\n" \
"$left_border" \
"$left" \
"$spacer_string" \
"$right" \
"$right_border"
}
# Workflow and logging
pushd(){
command pushd "$@" > /dev/null
}
popd(){
command popd "$@" > /dev/null
}
popdfail(){
ec=$?
popd
$(exit $ec) #restore the exit code
fail "$@"
}
run(){
_cmd=$@
if [[ $dry_run != true ]] ; then
logger $_cmd
else
logger boxline "DryRun: $_cmd"
fi
}
logger(){
if [[ -w "$logfile" ]]; then
"$@" 2>&1 | tee -a "$logfile" | while IFS= read -r line; do
# strip any escaped strings for logging output
_line="${line//[$'\t\r\x01-\x1F\x7F-\xFF']}"
# only print out non-empty lines to log
[[ -n $_line ]] && printf '[%s][%s] %s\n' "$scriptname" "$pretty_date" "$_line" >> "$logfile"
done
else
"$@"
fi
}
# File handling functions
take-backup(){
name="$1"
if [[ $update_run != true ]] ; then
# Check if a backup file or symbolic link already exists
if [[ -e "$name.bak" || -L "$name.bak" ]]; then
run boxline " $name.bak backup already exists"
else
# Check if the file is a hidden file (starts with a dot)
if [[ "$name" == .* ]]; then
# Add a dot to the beginning of the backup file name
backup_name=".${name}.bak"
else
# Create the backup file name by appending ".bak" to the original file name
backup_name="${name}.bak"
fi
# Copy the file to the backup file with preservation of file attributes
run cp -p "$name" "$backup_name"
# Add the original file to the list of backup files
backup_files+=("$name")
# Log the original file name to the backup file list file
run echo "$name" >> "$rundir/backup_files.list"
fi
fi
}
restore-backup(){
run echo "${#backup_files[@]}"
for file in "${backup_files[@]}" ; do
run cp "$file".bak $file
run echo "$file is restored"
done
backup_files=()
if [ -f $rundir/backup_files.list ] ; then
run rm $rundir/backup_files.list
fi
}
install-file(){
local _source="$1"
local _destination="$2"
local _source_root="$3"
local _filename=${_source##*/}
local _destination_file=${_destination}/${_filename#${_source_root}}
installed_files+=("${_destination_file}")
if [[ $update_run == true ]] ; then
run boxline "$scriptname: added file ${_destination_file} to list"
else
run cp -p $_source $_destination_file && boxline "Installed ${_filename}" || warn "Unable to install ${_filename}"
run echo "${_destination_file}" >> $rundir/installed_files.list
fi
}
install-dir() {
local _source="$1"
local _destination="$2"
installed_dirs+=("$_source -> $_destination")
# Iterate through all files in the source directory recursively
while IFS= read -r -d '' source_file; do
# Construct the destination file path by removing the source directory path
# and appending it to the destination directory path
local _filename="${source_file#${_source}}"
local destination_file="${_destination}/${source_file#${_source}}"
# Create the parent directory of the destination file if it doesn't exist
# Log the destination file path to the logfile
#echo "$destination_file" >> "$logfile"
installed_files+=($destination_file)
if [[ $update_run == true ]] ; then
run boxline "$scriptname: added file ${destination_file} to list"
else
run mkdir -p "$(dirname "$destination_file")"
run cp -p ${_source}${_filename} $destination_file && boxline "Installed ${_filename}" || warn "Unable to install ${_filename}"
run echo "${destination_file}" >> $rundir/installed_files.list
fi
done < <(find "$_source" -type f -print0)
}
# Utilities
success(){
_line="$@"
echo -e "\n"
boxtop
boxline "${scriptname} ${grn}SUCCESS${dfl} ${_line}"
boxbottom
exit 0
}
warn(){
ec=$?
_line="$@"
echo -e "\n"
boxtop
boxline "${lrd}WARNING${lyl}[${ong}code=${red}$ec${lyl}]: ${_line}${dfl}"
boxbottom
}
fail(){
ec=$?
_line="$@"
echo -e "\n"
boxtop
boxline "${lrd}FAILED${lyl}[${ong}code=${red}$ec${lyl}]: ${_line}${dfl}"
boxbottom
exit $ec
}
clone-repo(){
# git clone <source url> <destination>
local _url=$1
local _destination=$2
if [ ! -z $_destination ] ; then
run mkdir -p $_destination
fi
run git clone --recurse-submodules $_url $_destination
}
# Executes: grep "$1" | grep "$2" | grep "$3" | ...
chained-grep(){
local pattern="$1"
if [[ -z "$pattern" ]]; then
cat
return
fi
shift
grep -- "$pattern" | chained-grep "$@"
}
# Remove leading/trailing whitespace
trim(){
local var=${@:2}
var="${var#"${var%%[![:space:]]*}"}"
var="${var%"${var##*[![:space:]]}"}"
printf '%s' "$var"
}
strip-html-tags(){
local input="$1"
local stripped=$(echo "$input" | sed 's/<[^>]*>//g' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
echo "$stripped"
}
# Retrieve and run a script from url
run-from-url(){
url=$1
# boxline "url=$url"
args=${*:2}
# boxline "args=$args"
# check the interpreter using the shebang at the top of the file
interpreter=$(curl -ks $url | head -n 1 | sed -n -e 's\/\#//g' 2>/dev/null)
# boxline "interpreter=$interpreter"
# In case of missing shebang, default to local shells
if [ -z $interpreter ] ; then
if [ -x /bin/bash ] ; then
interpreter="/bin/bash"
elif [ -x /bin/sh ] ; then
interpreter="/bin/sh"
elif [ -x /bin/zsh ] ; then
interpreter="/bin/zsh"
fi
fi
logger boxline "Running online script $url with args $args"
if [[ "$interpreter" == "/bin/bash" ]] ; then
run curl -H 'Cache-Control: no-cache' -kLs $url | $interpreter -s -- $args
else
run curl -H 'Cache-Control: no-cache' -kLs $url > ${url##*/}
run $interpreter ${url##*/} $args
ec=$?
run rm ${url##*/}
fi
return $ec
}
check-git-repository(){
repodir=${1:-$rundir}
if [ -z $repodir ] ; then
warn "check-git-repository() Incorrect usage. Usage: check-git-repository </path/to/dir>"
return 2
fi
if git -C "$repodir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Directory '$repodir' is a valid Git repository."
return 0
# Your script logic here
else
echo "Directory '$repodir' is not a valid Git repository"
return 1
fi
}
# fancy script exits
ctrl_c(){
echo -e "\n"
fail "User interrupted with Ctrl-C"
}
# Cosmetics
PRBL_LOGO="
${ESC}[38;5;202m_!*(xL}}xvr!,.${dfl}
${ESC}[38;5;202m>}KM3${ong}nLL}}}}}LLTh${ESC}[38;5;202m%5u>.${dfl}
${ESC}[38;5;202m<FE${ong}nvi${ESC}[38;5;202mT3Egg8888888DGn${ong}ivn${ESC}[38;5;202mOKr.${dfl}
${ESC}[38;5;202m.vEP${ong}?v${ESC}[38;5;202mZ88888${ong}Q${ESC}[38;5;178mB${ylw}B####B${ESC}[38;5;178mQ${ESC}[38;5;202m88888Zx${ong}rj${ESC}[38;5;202mNT.${dfl}
${ESC}[38;5;202m!MW${ong}^L${ESC}[38;5;202mD888${ESC}[38;5;178mQ${ylw}#@@${ESC}[38;5;178m@${ong}@${ESC}[38;5;202m######${ESC}[38;5;178m@${ylw}@@#B${ESC}[38;5;202m888g${ong}n>P${ESC}[38;5;202m8}.${dfl}
${ESC}[38;5;202m?g${ong}2!${ESC}[38;5;202m%888${ESC}[38;5;178mB${ylw}B${ESC}[38;5;178mev*${ong}<!!!!!!!!!>${ESC}[38;5;178m***^${ong}!!!!!.L${ESC}[38;5;202mB&?${dfl}
${ESC}[38;5;202m>gF${ong}=${ESC}[38;5;202mRg8${ong}8${ylw}#5${dfl} ${ESC}[38;5;202m,${ong}^}${ESC}[38;5;202m#Be-${dfl}
${ESC}[38;5;202mPg${ong}'${ESC}[38;5;202ma88${ong}8${ylw}#B ${ESC}[38;5;202m,unnnnn}- .ZO${ong}_${ESC}[38;5;202mN${ESC}[38;5;208m#B${ESC}[38;5;202mgr${dfl}
${ESC}[38;5;202m-gZ${ong}-${ESC}[38;5;202mg88${ong}8${ylw}B, ${ESC}[38;5;202m.U88${ong}!${ESC}[38;5;202mj${ESC}[38;5;208m#B#${ESC}[38;5;202m${ESC}[38;5;202mQs.${dfl}
${ESC}[38;5;202m.NM${ong}-${ESC}[38;5;202mD88${ong}N${ylw}! .${ESC}[38;5;202mj888${ong}=${ESC}[38;5;202mX${ESC}[38;5;208mQQ${ESC}[38;5;202mQ${ong}O${ESC}[38;5;202mQQ=${dfl}
${ESC}[38;5;202m28${ong}>u${ESC}[38;5;202m8g< ${ESC}[38;5;178m,${ong}!!!!!!!!!!!!!!!!${ESC}[38;5;178m>Lp${ESC}[38;5;202mQ888K${ong},${ESC}[38;5;202mQQ${ESC}[38;5;208mg${ESC}[38;5;202my ,V>${dfl}
${ESC}[38;5;202m_OM${ong}:K${ESC}[38;5;202m? ${ylw}.&#${ong}Q${ESC}[38;5;202m88888888888888${ESC}[38;5;178m#${ylw}@@${ESC}[38;5;178m#${ESC}[38;5;202m888M${ong}:p${ESC}[38;5;202m#${ESC}[38;5;208m#Q${ESC}[38;5;202m8G! .${dfl}
${ESC}[38;5;202m,WM${ong}!.!!${ESC}[38;5;202m^p${ESC}[38;5;178m#${ylw}@@${ESC}[38;5;178m#${ong}#${ESC}[38;5;202mQ88g8888Q${ong}B${ESC}[38;5;178m#${ylw}@@${ESC}[38;5;178m#Q${ESC}[38;5;202m888e${ong}!W${ESC}[38;5;202m#${ESC}[38;5;208m####${ESC}[38;5;202mBBg2!${dfl}
${ESC}[38;5;202m.u&n${ong}^y${ESC}[38;5;202mg88${ong}8${ESC}[38;5;178mQ${ylw}##@${ESC}[38;5;178m@${ong}@${ESC}[38;5;202m@@@${ong}@${ESC}[38;5;178m@@${ylw}@#${ESC}[38;5;178mB8${ong}8${ESC}[38;5;202m88K${ong}*}${ESC}[38;5;202mQ${ESC}[38;5;208m###${ESC}[38;5;202mBQ${ong}Q${ESC}[38;5;202mQ${ESC}[38;5;208mBB${ESC}[38;5;202m##gL.${dfl}
${ESC}[38;5;202m_nD${ong}j?${ESC}[38;5;202mxmPggg${ESC}[38;5;178m8${ylw}88Q${ESC}[38;5;178mQQQ${ylw}Q8${ESC}[38;5;178m88${ong}88${ESC}[38;5;202m8Wx${ong}?z${ESC}[38;5;202mQ${ESC}[38;5;208m#########${ESC}[38;5;202mQ${ESC}[38;5;208m#${ong}@${ESC}[38;5;202mQ8Z.${dfl}
${ESC}[38;5;202m.^y${ong}Oj${ESC}[38;5;202mLLL${ESC}[38;5;208m}${ESC}[38;5;202mjP${ong}%OEOM${ESC}[38;5;202m5suLxL${ong}jQ${ESC}[38;5;202m#${ESC}[38;5;208m######${ESC}[38;5;214mQ${ESC}[38;5;208m###${ESC}[38;5;202mB88&2${ong}vvn${ESC}[38;5;202mWQ(${dfl}
${ESC}[38;5;202m.!}${ong}%QQNWKessaG%gB${ESC}[38;5;202m###${ESC}[38;5;208m########${ESC}[38;5;202mB${ESC}[38;5;214mQ@@@${ESC}[38;5;208m#${ESC}[38;5;202mR?. r<${dfl}
${ESC}[38;5;202m.!LP${ESC}[38;5;208m8B${ESC}[38;5;214m##${ESC}[38;5;202m#Q${ong}5${ESC}[38;5;202mV%8${ESC}[38;5;208mB##B88${ong}QQ${ESC}[38;5;202mB${ESC}[38;5;208m##B${ESC}[38;5;202mB${ESC}[38;5;214m#@@@@@${ong}Q${ESC}[38;5;202me?,${dfl}
${ESC}[38;5;202m_*${ESC}[38;5;208mTW${ESC}[38;5;214m#${ESC}[38;5;202mP_ !K${ESC}[38;5;208mQ${ESC}[38;5;214m#${ESC}[38;5;202m@${ong}BM^^}${ESC}[38;5;202mOB${ESC}[38;5;208m##BB#${ESC}[38;5;202m#${ESC}[38;5;214m@@@#B${ESC}[38;5;202ma<${dfl}
${ESC}[38;5;202m-${ESC}[38;5;208m<${ESC}[38;5;202m!. :j${ESC}[38;5;208mg${ESC}[38;5;214m##${ESC}[38;5;202mh: _ry${ESC}[38;5;208mW8BBB${ESC}[38;5;202mB#${ESC}[38;5;214m#@@@@@${ESC}[38;5;202m#}.${dfl}
${ESC}[38;5;202m'!?${ESC}[38;5;214mn${ESC}[38;5;202mn?=. .-_,${ong}=${ESC}[38;5;202m^x${ESC}[38;5;214mAQ#@@${ESC}[38;5;202mB>${dfl}
${ESC}[38;5;202m>s${ESC}[38;5;214mQ@${ESC}[38;5;202m@?${dfl}
${ESC}[38;5;202m_n${ESC}[38;5;214mB${ESC}[38;5;202m#,${dfl}
${ESC}[38;5;202m-hv${dfl}
"
prbl-logo(){
echo "${PRBL_LOGO}"
}
# Function for spinner status
# usage: spin "command [args]"
set_spinner() {
case $1 in
spinner1)
FRAME="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
FRAME_INTERVAL=0.05
;;
spinner2)
FRAME="-\|/"
FRAME_INTERVAL=0.1
;;
spinner3)
FRAME="◐◓◑◒"
FRAME_INTERVAL=0.2
;;
spinner4)
FRAME="⇐⇖⇑⇗⇒⇘⇓⇙"
FRAME_INTERVAL=0.1
;;
spinner5)
FRAME="◇◈◆◈"
FRAME_INTERVAL=0.2
;;
spinner6)
FRAME="⚬⚭⚮⚯⚮⚭"
FRAME_INTERVAL=0.15
;;
spinner7)
FRAME="░▒▓█▓▒"
FRAME_INTERVAL=0.2
;;
spinner8)
FRAME="☉◎◉◎☉"
FRAME_INTERVAL=0.1
;;
spinner9)
FRAME="♡♥❤♥♡"
FRAME_INTERVAL=0.15
;;
spinner10)
FRAME="▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"
FRAME_INTERVAL=0.1
;;
spinner11)
FRAME="←↖↑↗→↘↓↙"
FRAME_INTERVAL=0.1
;;
spinner12)
FRAME="▖▘▝▗"
FRAME_INTERVAL=0.2
;;
spinner13)
FRAME="▙▛▜▟"
FRAME_INTERVAL=0.2
;;
spinner14)
FRAME="▚▞"
FRAME_INTERVAL=0.5
;;
spinner15)
FRAME="☰☱☳☶☵☳☶☴☰☰"
FRAME_INTERVAL=0.1
;;
spinner16)
FRAME="▤▧▥▨"
FRAME_INTERVAL=0.2
;;
spinner17)
FRAME="▉▊▋▌▍▎▏▎▍▌▋▊▉"
FRAME_INTERVAL=0.1
;;
spinner18)
FRAME="⣾⣽⣻⢿⡿⣟⣯⣷"
FRAME_INTERVAL=0.1
;;
spinner19)
FRAME="┤┘┴└├┌┬┐"
FRAME_INTERVAL=0.1
;;
*)
warn "No spinner is defined for $1"
esac
}
spin() {
CMD=$@
tput civis
sc=0
$CMD &
_PID=$!
while [ -d /proc/$_PID ] ; do
for i in "${FRAME[@]}" ; do
printf "\b${FRAME:sc++:1}"
((sc==${#FRAME})) && sc=0
#echo -ne "\b${FRAME[$i]}"
sleep $FRAME_INTERVAL
done
done
printf "\r\n"
tput cnorm
}
spinstart() {
sstop=false
tput civis
sc=0
until [[ $sstop != false ]] ; do
for i in "${FRAME[@]}" ; do
printf "\b${FRAME:sc++:1}"
((sc==${#FRAME})) && sc=0
#echo -ne "\b${FRAME[$i]}"
sleep $FRAME_INTERVAL
done
done
}
spinstop() {
export sstop=true
printf "\r\n"
tput cnorm
}
# https://github.com/fearside/ProgressBar/blob/master/progressbar.sh
progress-bar() {
# Process data
let _progress=(${1}*100/${2}*100)/100
let _done=(${_progress}*4)/10
let _left=40-$_done
# Build progressbar string lengths
_done=$(printf "%${_done}s")
_left=$(printf "%${_left}s")
# 1.2 Build progressbar strings and print the ProgressBar line
# 1.2.1 Output example:
# 1.2.1.1 Progress : [########################################] 100%
printf "\rProgress : [${_done// /#}${_left// /-}] ${_progress}%%"
}
# Menu functions
# Renders a text based list of options that can be selected by the
# user using up, down and enter keys and returns the chosen option.
#
# Arguments : list of options, maximum of 256
# "opt1" "opt2" ...
# Return value: selected index (0 for opt1, 1 for opt2 ...)
select_option(){
# little helpers for terminal print control and key input
cursor_blink_on() { printf "$ESC[?25h"; }
cursor_blink_off() { printf "$ESC[?25l"; }
cursor_to() { printf "$ESC[$1;${2:-1}H"; }
print_option() { printf " $1 "; }
print_selected() { printf " $ESC[7m $1 $ESC[27m"; }
get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
key_input() { read -s -n3 key 2>/dev/null >&2
if [[ $key = $ESC[A ]]; then echo up; fi
if [[ $key = $ESC[B ]]; then echo down; fi
if [[ $key = "" ]]; then echo enter; fi; }
# initially print empty new lines (scroll down if at bottom of screen)
for opt; do printf "\n"; done
# determine current screen position for overwriting the options
local lastrow=`get_cursor_row`
local startrow=$(($lastrow - $#))
# ensure cursor and input echoing back on upon a ctrl+c during read -s
trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
cursor_blink_off
local selected=0
while true; do
# print options by overwriting the last lines
local idx=0
for opt; do
cursor_to $(($startrow + $idx))
if [ $idx -eq $selected ]; then
print_selected "$opt"
else
print_option "$opt"
fi
((idx++))
done
# user key control
case `key_input` in
enter) break;;
up) ((selected--));
if [ $selected -lt 0 ]; then selected=$(($# - 1)); fi;;
down) ((selected++));
if [ $selected -ge $# ]; then selected=0; fi;;
esac
done
# cursor position back to normal
cursor_to $lastrow
printf "\n"
cursor_blink_on
return $selected
}
# Example for above select_option
#echo "Select one option using up/down keys and enter to confirm:"
#echo
#options=("one" "two" "three")
#select_option "${options[@]}"
#choice=$?
#echo "Choosen index = $choice"
#echo " value = ${options[$choice]}"
select_opt(){
select_option "$@" 1>&2
local result=$?
echo $result
return $result
}
# Examples for above select_opt
#case `select_opt "Yes" "No" "Cancel"` in
# 0) echo "selected Yes";;
# 1) echo "selected No";;
# 2) echo "selected Cancel";;
#esac
#options=("Yes" "No" "${array[@]}") # join arrays to add some variable array
#case `select_opt "${options[@]}"` in
# 0) echo "selected Yes";;
# 1) echo "selected No";;
# *) echo "selected ${options[$?]}";;
#esac
multiselect(){
# little helpers for terminal print control and key input
cursor_blink_on() { printf "$ESC[?25h"; }
cursor_blink_off() { printf "$ESC[?25l"; }
cursor_to() { printf "$ESC[$1;${2:-1}H"; }
print_inactive() { printf "$2 $1 "; }
print_active() { printf "$2 $ESC[7m $1 $ESC[27m"; }
get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
local return_value=$1
local -n options=$2
local -n defaults=$3
local selected=()
for ((i=0; i<${#options[@]}; i++)); do
if [[ ${defaults[i]} = "true" ]]; then
selected+=("true")
else
selected+=("false")
fi
printf "\n"
done
# determine current screen position for overwriting the options
local lastrow=`get_cursor_row`
local startrow=$(($lastrow - ${#options[@]}))
# ensure cursor and input echoing back on upon a ctrl+c during read -s
trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
cursor_blink_off
key_input() {
local key
IFS= read -rsn1 key 2>/dev/null >&2
if [[ $key = "" ]]; then echo enter; fi;
if [[ $key = $'\x20' ]]; then echo space; fi;
if [[ $key = "k" ]]; then echo up; fi;
if [[ $key = "j" ]]; then echo down; fi;
if [[ $key = $'\x1b' ]]; then
read -rsn2 key
if [[ $key = [A || $key = k ]]; then echo up; fi;
if [[ $key = [B || $key = j ]]; then echo down; fi;
fi
}
toggle_option() {
local option=$1
if [[ ${selected[option]} == true ]]; then
selected[option]=false
else
selected[option]=true
fi
}
print_options() {
# print options by overwriting the last lines
local idx=0
for option in "${options[@]}"; do
#local prefix="[ ]"
local prefix="${gry}${deselected_opt}${dfl}"
if [[ ${selected[idx]} == true ]]; then
#prefix="[\e[38;5;46m✔\e[0m]"
prefix="${selected_opt}"
fi
cursor_to $(($startrow + $idx))
if [ $idx -eq $1 ]; then
print_active "$option" "$prefix"
else
print_inactive "$option" "$prefix"
fi
((idx++))
done
}
local active=0
while true; do
print_options $active
# user key control
case `key_input` in
space) toggle_option $active;;
enter) print_options -1; break;;
up) ((active--));
if [ $active -lt 0 ]; then active=$((${#options[@]} - 1)); fi;;
down) ((active++));
if [ $active -ge ${#options[@]} ]; then active=0; fi;;
esac
done
# cursor position back to normal
cursor_to $lastrow
printf "\n"
cursor_blink_on
eval $return_value='("${selected[@]}")'
}
# Example for above multiselect functions
#my_options=( "Option 1" "Option 2" "Option 3" )
#preselection=( "true" "true" "false" )
#multiselect result my_options preselection
#idx=0
#for option in "${my_options[@]}"; do
# echo -e "$option\t=> ${result[idx]}"
# ((idx++))
#done
### IP validation function
# returns 0 (true) for valid, 1 (false) for invalid
valid-ip()
{
local ip=$1
local stat=1
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
OIFS=$IFS
IFS='.'
ip=($ip)
IFS=$OIFS
[[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \
&& ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
stat=$?
fi
return $stat
}
# Version comparison function
# usage:
# vercomp [first version] [second version]
# output:
# 0 =
# 1 >
# 2 <
vercomp() {
if [[ $1 == $2 ]]
then
return 0
fi
local IFS=.
local i ver1=($1) ver2=($2)
# fill empty fields in ver1 with zeros
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++))
do
ver1[i]=0
done
for ((i=0; i<${#ver1[@]}; i++))
do
if [[ -z ${ver2[i]} ]]
then
# fill empty fields in ver2 with zeros
ver2[i]=0
fi
if ((10#${ver1[i]} > 10#${ver2[i]}))
then
# version 1 is greater than version 2
return 1
fi
if ((10#${ver1[i]} < 10#${ver2[i]}))
then
# version 1 is less than version 2
return 2
fi
done
return 0
}
check-prbl-version(){
# TODO: Finish writing this function
vercomp
}
compare-versions(){
vercomp $1 $2
case $? in
0) op='=';;
1) op='>';;
2) op='<';;
esac
if [[ $op != $3 ]]
then
echo "Fail: Expected '$3', Actual '$op', Arg1 '$1', Arg2 '$2'"
else
echo "Pass: '$1 $op $2'"
fi
}
identify-system(){
if type lsb_release >/dev/null 2>&1; then
ID=$(lsb_release -si)
OS=$(lsb_release -si)
VER=$(lsb_release -sr)
elif [ -f /etc/os-release ]; then
. /etc/os-release
ID=${ID:-$VERSION_ID}
ID=${ID:-$VERSION}
OS=$NAME
VER=$VERSION_ID
VER=${VER:-$VERSION}
elif [ -f /etc/lsb-release ]; then
. /etc/lsb-release
ID=$DISTRIB_ID
OS=$DISTRIB_ID
VER=$DISTRIB_RELEASE
VER=${VER:-$LSB_VERSION}
elif [ -f /etc/debian_version ]; then
ID=debian
OS=Debian
VER=$(cat /etc/debian_version)
elif [ -f /etc/redhat-release ]; then
OS=$(awk '{print $1}' /etc/redhat-release)
else
OS=$(uname -s)
VER=$(uname -r)
fi
}
ID(){
boxborder \
"ID: $ID" \
"OS: $OS" \
"VER: $VER"
}
install-packages(){
declare -a packages=()
declare -a offline_packages=()
for arg in "$@"; do
if [[ -f $arg ]]; then
offline_packages+=("$arg")
else
packages+=("$arg")
fi
done
identify-system
case $ID in
amzn | centos | fedora | rhel)
[ "${#packages[@]}" -gt 0 ] && run sudo yum install -y "${packages[@]}"
[ "${#offline_packages[@]}" -gt 0 ] && run sudo rpm -i "${packages[@]}"
;;
arch | Arch | manjaro)
if [ "${#packages[@]}" -gt 0 ]; then
run sudo pacman -Syu
run sudo pacman -S "${packages[@]}"
fi
[ "${#offline_packages[@]}" -gt 0 ] && run sudo pacman -U "${offline_packages[@]}"
;;
debian | ubuntu | Ubuntu)
if [ "${#packages[@]}" -gt 0 ]; then
run sudo apt-get update
run sudo apt-get install -y "${packages[@]}"
fi
[ "${#offline_packages[@]}" -gt 0 ] && run sudo dpkg -i "${offline_packages[@]}"
;;
opensuse | sles)
if [ "${#packages[@]}" -gt 0 ]; then
run sudo zypper refresh
run sudo zypper install "${packages[@]}"
fi
[ "${#offline_packages[@]}" -gt 0 ] && run sudo rpm -i "${offline_packages[@]}"
;;
slackware)
if [ "${#packages[@]}" -gt 0 ]; then
for package in "${packages[@]}"; do
run sudo installpkg "$package"
done
fi
if [ "${#offline_packages[@]}" -gt 0 ]; then
for package_file in "${offline_packages[@]}"; do
run sudo installpkg "$package_file"
done
fi
;;
brew)
[ "${#packages[@]}" -gt 0 ] && run brew install "${packages[@]}"
if [ "${#offline_packages[@]}" -gt 0 ]; then
for package_file in "${offline_packages[@]}"; do
run brew install "$package_file"
done
fi
;;
esac
}
check-packages() {
# Utilizes system package manager to check for installed packages
# Declare arrays for online and offline packages
declare -a packages=()
declare -a offline_packages=()
# Separate online and offline packages
for arg in "$@"; do
if [[ -f $arg ]]; then
offline_packages+=("$arg")
else
packages+=("$arg")
fi
done
# Identify the system
identify-system
# Check packages based on system type
case $ID in
amzn | centos | fedora | rhel)
# Check online packages
if [ "${#packages[@]}" -gt 0 ]; then
for package in "${packages[@]}"; do
if yum list installed "$package" >/dev/null 2>&1; then
installed_version=$(yum list installed "$package" | grep "$package" | awk '{print $2}')
available_version=$(yum list available "$package" | grep "$package" | awk '{print $2}')
if [ "$installed_version" = "$available_version" ]; then
return 0
elif [ "$installed_version" \> "$available_version" ]; then
return 3
else
return 1
fi
else
return 2
fi
done
fi
# Check offline packages
if [ "${#offline_packages[@]}" -gt 0 ]; then
for package_file in "${offline_packages[@]}"; do
if rpm -q --quiet "$package_file"; then
installed_version=$(rpm -q "$package_file" --qf "%{VERSION}")
return 0
else
return 2
fi
done
fi
;;
arch | Arch | manjaro)
# Check online packages
if [ "${#packages[@]}" -gt 0 ]; then
for package in "${packages[@]}"; do
if pacman -Q "$package" >/dev/null 2>&1; then
installed_version=$(pacman -Q "$package" | awk '{print $2}')
available_version=$(pacman -Si "$package" | grep "Version" | awk '{print $3}')
if [ "$installed_version" = "$available_version" ]; then
return 0
elif [ "$installed_version" \> "$available_version" ]; then
return 3
else
return 1
fi
else
return 2
fi
done
fi
# Check offline packages
if [ "${#offline_packages[@]}" -gt 0 ]; then
for package_file in "${offline_packages[@]}"; do
if pacman -Qp "$package_file" >/dev/null 2>&1; then
installed_version=$(pacman -Qp "$package_file" | awk '{print $2}')
return 0
else
return 2
fi
done
fi
;;
debian | ubuntu | Ubuntu)
# Check online packages
if [ "${#packages[@]}" -gt 0 ]; then
for package in "${packages[@]}"; do
if dpkg -s "$package" >/dev/null 2>&1; then
installed_version=$(dpkg -s "$package" | grep -E "^Version:" | awk '{print $2}')
available_version=$(apt-cache show "$package" | grep -E "^Version:" | awk '{print $2}')
if [ "$installed_version" = "$available_version" ]; then
return 0
elif [ "$installed_version" \> "$available_version" ]; then
return 3
else
return 1
fi
else
return 2
fi
done
fi
# Check offline packages
if [ "${#offline_packages[@]}" -gt 0 ]; then
for package_file in "${offline_packages[@]}"; do
if dpkg -I "$package_file" >/dev/null 2>&1; then
installed_version=$(dpkg -I "$package_file" | grep -E "^ Version:" | awk '{print $3}')
return 0
else
return 2
fi
done
fi
;;
opensuse | sles)
# Check online packages
if [ "${#packages[@]}" -gt 0 ]; then
for package in "${packages[@]}"; do
if zypper info "$package" >/dev/null 2>&1; then
installed_version=$(zypper info "$package" | grep "Version" | awk '{print $3}')
available_version=$(zypper se -i "$package" | grep "Version" | awk '{print $3}')
if [ "$installed_version" = "$available_version" ]; then
return 0
elif [ "$installed_version" \> "$available_version" ]; then
return 3
else
return 1
fi
else
return 2
fi
done
fi
# Check offline packages
if [ "${#offline_packages[@]}" -gt 0 ]; then
for package_file in "${offline_packages[@]}"; do
if rpm -q --quiet "$package_file"; then
installed_version=$(rpm -q "$package_file" --qf "%{VERSION}")
return 0
else
return 2
fi
done
fi
;;
slackware)
# Check online packages
if [ "${#packages[@]}" -gt 0 ]; then
for package in "${packages[@]}"; do
if slackpkg search "$package" >/dev/null 2>&1; then
installed_version=$(slackpkg search "$package" | awk '{print $3}')
return 0
else
return 2
fi
done
fi
# Check offline packages
if [ "${#offline_packages[@]}" -gt 0 ]; then
for package_file in "${offline_packages[@]}"; do
if slackpkg search "$package_file" >/dev/null 2>&1; then
installed_version=$(slackpkg search "$package_file" | awk '{print $3}')
return 0
else
return 2
fi
done
fi
;;
brew)
# Check online packages
if [ "${#packages[@]}" -gt 0 ]; then
for package in "${packages[@]}"; do
if brew list "$package" >/dev/null 2>&1; then
installed_version=$(brew list --versions "$package" | awk '{print $2}')
available_version=$(brew info "$package" | head -n 1 | awk '{print $3}')
if [ "$installed_version" = "$available_version" ]; then
return 0
elif [ "$installed_version" \> "$available_version" ]; then
return 3
else
return 1
fi
else
return 2
fi
done
fi
# Check offline packages
if [ "${#offline_packages[@]}" -gt 0 ]; then
for package_file in "${offline_packages[@]}"; do
if brew list "$package_file" >/dev/null 2>&1; then
installed_version=$(brew list --versions "$package_file" | awk '{print $2}')
return 0
else
return 2
fi
done
fi
;;
esac
}
# Power Monitoring Functions for PRbL
################################
# Detect and query power usage from various sources
# - NVIDIA GPUs via nvidia-smi
# - AMD GPUs via rocm-smi
# - System-wide power via RAPL (Running Average Power Limit)
# - ACPI battery information
# - Power supply information via hwmon
################################
# Main function to get system power information
get_power_info() {
# Initialize variables
total_power=0
cpu_power="N/A"
gpu_power="N/A"
system_power="N/A"
battery_power="N/A"
power_sources=()
power_values=()
power_units=()
# Try each method and collect the data
get_nvidia_power
get_amd_power
get_rapl_power
get_acpi_power
get_hwmon_power
# Calculate total known power
if [[ "$total_power" -gt 0 ]]; then
system_power="${total_power}W"
fi
# Return results in variables
# These variables will be available after calling this function:
# - system_power: Total system power if available
# - cpu_power: CPU power consumption if available
# - gpu_power: GPU power consumption if available
# - battery_power: Battery charging/discharging rate if available
# - power_sources: Array of power source names
# - power_values: Array of power values (numeric)
# - power_units: Array of power units (W, mW, etc.)
return 0
}
# Get NVIDIA GPU power information using nvidia-smi
get_nvidia_power() {
if command -v nvidia-smi >/dev/null 2>&1; then
# Get power usage for all NVIDIA GPUs
local gpu_count=$(nvidia-smi --query-gpu=count --format=csv,noheader,nounits | head -n 1)
local total_gpu_power=0
# Validate that gpu_count is a number
if ! [[ "$gpu_count" =~ ^[0-9]+$ ]]; then
# If not a number, default to 1
gpu_count=1
fi
# Iterate from 0 to (count-1), not from 0 to count
for ((i=0; i<$gpu_count; i++)); do
# Get power draw and power limit for this GPU
local power_draw=$(nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits -i $i )
local power_limit=$(nvidia-smi --query-gpu=power.limit --format=csv,noheader,nounits -i $i )
local gpu_name=$(nvidia-smi --query-gpu=name --format=csv,noheader -i $i | cut -c -20)
# Check if the value is valid (not N/A or empty)
if [[ "$power_draw" != "N/A" && -n "$power_draw" ]]; then
# Convert to integer watts for consistency (removing decimal)
local power_int=$(printf "%.0f" "$power_draw")
total_gpu_power=$((total_gpu_power + power_int))
# Add to the arrays
power_sources+=("NVIDIA GPU $i ($gpu_name)")
power_values+=($power_int)
power_units+=("W")
fi
done
if [[ $total_gpu_power -gt 0 ]]; then
gpu_power="${total_gpu_power}W"
total_power=$((total_power + total_gpu_power))
fi
fi
}
# Get AMD GPU power information using rocm-smi
get_amd_power() {
if command -v rocm-smi >/dev/null 2>&1; then
# Get power usage for all AMD GPUs
local total_amd_power=0
# Parse the output of rocm-smi to get power readings
local power_data=$(rocm-smi --showpower)
# Process each line to extract power values
while IFS= read -r line; do
if [[ "$line" == *"Average Graphics Package Power"* ]]; then
local power_val=$(echo "$line" | grep -oE '[0-9]+\.[0-9]+' | head -n1)
if [[ -n "$power_val" ]]; then
local power_int=$(printf "%.0f" "$power_val")
total_amd_power=$((total_amd_power + power_int))
# Extract GPU ID
local gpu_id=$(echo "$line" | grep -oE 'GPU\[[0-9]+\]' | grep -oE '[0-9]+')
# Add to the arrays
power_sources+=("AMD GPU $gpu_id")
power_values+=($power_int)
power_units+=("W")
fi
fi
done <<< "$power_data"
if [[ $total_amd_power -gt 0 ]]; then
if [[ -n "$gpu_power" && "$gpu_power" != "N/A" ]]; then
# If we already have NVIDIA GPU power, add this to it
local current_power=$(echo $gpu_power | grep -oE '[0-9]+')
gpu_power="$((current_power + total_amd_power))W"
else
gpu_power="${total_amd_power}W"
fi
total_power=$((total_power + total_amd_power))
fi
fi
}
# Get CPU and memory power information using RAPL (Running Average Power Limit)
get_rapl_power() {
# Check if RAPL sysfs entries exist
if [ -d /sys/class/powercap/intel-rapl ]; then
local total_cpu_power=0
# Find all energy meters
local rapl_dirs=$(find /sys/class/powercap/intel-rapl -type d -name "intel-rapl:*")
for dir in $rapl_dirs; do
# Check if this is a CPU package or a domain within a package
local name=""
if [ -f "$dir/name" ]; then
name=$(cat "$dir/name")
fi
# Only process packages and memory controller, not subdomains like core or uncore
if [[ "$name" == "package-"* || "$name" == "psys" || "$name" == "dram" ]]; then
# Get microjoules over time to calculate power
if [ -f "$dir/energy_uj" ]; then
local start_uj=$(cat "$dir/energy_uj")
sleep 0.5
local end_uj=$(cat "$dir/energy_uj")
# Calculate power in watts: microjoules to watts = (end-start)/(time in microseconds)
local power_w=$(echo "scale=1; ($end_uj - $start_uj) / 500000" | bc)
# If microjoule counter wrapped around (rare but possible)
if (( $(echo "$power_w < 0" | bc -l) )); then
continue
fi
local power_int=$(printf "%.0f" "$power_w")
# Add to total and arrays
total_cpu_power=$((total_cpu_power + power_int))
power_sources+=("RAPL $name")
power_values+=($power_int)
power_units+=("W")
fi
fi
done
if [[ $total_cpu_power -gt 0 ]]; then
cpu_power="${total_cpu_power}W"
total_power=$((total_power + total_cpu_power))
fi
fi
}
# Get battery power information from ACPI
get_acpi_power() {
# Check if system has a battery
if [ -d /sys/class/power_supply ]; then
local batteries=$(find /sys/class/power_supply -type d -name "BAT*")
local total_battery_power=0
for battery in $batteries; do
# Check if power information is available
if [ -f "$battery/power_now" ]; then
# Power now is in microwatts
local power_uw=$(cat "$battery/power_now")
local power_w=$(echo "scale=1; $power_uw / 1000000" | bc)
local power_int=$(printf "%.0f" "$power_w")
# Get battery status (charging/discharging)
local status="unknown"
if [ -f "$battery/status" ]; then
status=$(cat "$battery/status")
fi
# Add to arrays
local bat_name=$(basename "$battery")
power_sources+=("Battery $bat_name ($status)")
power_values+=($power_int)
power_units+=("W")
total_battery_power=$((total_battery_power + power_int))
fi
done
if [[ $total_battery_power -gt 0 ]]; then
battery_power="${total_battery_power}W"
# We don't add battery power to total_power as it might be redundant with system power
fi
fi
}
# Get power supply information from hwmon
get_hwmon_power() {
# Check for hwmon power information
if [ -d /sys/class/hwmon ]; then
for hwmon_dir in /sys/class/hwmon/hwmon*; do
if [ -f "$hwmon_dir/name" ]; then
local device_name=$(cat "$hwmon_dir/name")
# Look for power sensors (usually in_*_input or power*_input)
for power_file in "$hwmon_dir"/power*_input "$hwmon_dir"/in*_input; do
if [ -f "$power_file" ]; then
local label_file="${power_file/_input/_label}"
local power_label="$device_name"
if [ -f "$label_file" ]; then
power_label=$(cat "$label_file")
fi
# Read the power value
local power_val=$(cat "$power_file")
local power_unit="mW" # Most hwmon power values are in milliwatts
# Convert to watts for consistency if needed
local power_w=$power_val
if [[ "$power_unit" == "mW" ]]; then
power_w=$(echo "scale=1; $power_val / 1000" | bc)
fi
local power_int=$(printf "%.0f" "$power_w")
# Only include reasonable values
if [[ $power_int -gt 0 && $power_int -lt 2000 ]]; then
power_sources+=("PSU $power_label")
power_values+=($power_int)
power_units+=("W")
# Don't add to total as it might double-count
# total_power=$((total_power + power_int))
fi
fi
done
fi
done
fi
}
# Format the power info for display
format_power_info() {
local show_details=${1:-false}
if [[ "$system_power" != "N/A" || "$cpu_power" != "N/A" || "$gpu_power" != "N/A" ]]; then
echo "${bld}Power Usage:${dfl}"
if [[ "$system_power" != "N/A" ]]; then
echo " ${bld}System:${dfl} ${ong}${system_power}${dfl}"
fi
if [[ "$cpu_power" != "N/A" ]]; then
echo " ${bld}CPU:${dfl} ${lrd}${cpu_power}${dfl}"
fi
if [[ "$gpu_power" != "N/A" ]]; then
echo " ${bld}GPU:${dfl} ${lrd}${gpu_power}${dfl}"
fi
if [[ "$battery_power" != "N/A" ]]; then
echo " ${bld}Battery:${dfl} ${ylw}${battery_power}${dfl}"
fi
# Show detailed breakdown if requested
if [[ "$show_details" == "true" && ${#power_sources[@]} -gt 0 ]]; then
echo " ${bld}${unl}Power Details:${dfl}"
for ((i=0; i<${#power_sources[@]}; i++)); do
printf " %-25s %3d%s\n" "${power_sources[$i]}:" "${power_values[$i]}" "${power_units[$i]}"
done
fi
return 0
else
echo "${bld}Power Usage:${dfl} Not available"
return 1
fi
}
# Test function to output all power information
test_power_monitoring() {
get_power_info
boxborder $(format_power_info true)
}
# Example usage:
# get_power_info
# boxborder $(format_power_info)
set_box_chars
update_terminal_width
set-boxtype