Initial commit - v1.0

This commit is contained in:
Manuele Sarfatti 2025-05-09 20:01:43 +02:00
commit 76d136f64c
20 changed files with 1350 additions and 0 deletions

13
src/00.utils/_symbols.sh Normal file
View file

@ -0,0 +1,13 @@
#! /usr/bin/env bash
# @private
_q='?'
_a=''
_o='◌'
_O='●'
_mark='✓'
_warn='!'
_cross='✗'
_spinner='⣷⣯⣟⡿⢿⣻⣽⣾' # See for alternatives: https://antofthy.gitlab.io/info/ascii/Spinners.txt
export _q _a _o _O _mark _warn _cross _spinner

52
src/00.utils/movements.sh Normal file
View file

@ -0,0 +1,52 @@
#! /usr/bin/env bash
# movements.sh - Cursor helper functions
# Move cursor up one line
up() {
printf "\033[A"
}
# Move cursor down one line
down() {
printf "\033[B"
}
# Move cursor to beginning of line
bol() {
printf "\r"
}
# Move cursor to end of line
eol() {
printf "\033[999C"
}
# Clear entire line
cl() {
printf "\033[2K"
}
# Clear line above
upclear() {
up
bol
cl
}
# Print a single newline
line() {
printf "\n"
}
# Show cursor
show_cursor() {
printf "\033[?25h"
}
# Hide cursor
hide_cursor() {
printf "\033[?25l"
}
# Export the functions so they are available to subshells
export -f up down bol eol cl line show_cursor hide_cursor

62
src/01.core/pen.sh Normal file
View file

@ -0,0 +1,62 @@
#! /usr/bin/env bash
# pen.sh - Print pretty text
# @depends on:
# - _symbols.sh
# Print text with ANSI color codes and text formatting
#
# Usage:
# pen [options] text
# Options:
# -n: No newline after text
# bold: Bold text
# italic: Italic text
# underline: Underline text
# black|red|green|yellow|blue|purple|cyan|white|grey|gray: Color text
# [0-9]: ANSI 256 color number
# *: Any other text is printed as is
# Examples:
# pen "Hello, world!"
# pen -n "Hello, world!"
# pen bold "Hello, world!"
# pen italic blue "Hello, world!"
# pen -n 219 underline "Hello, world!"
pen() {
local new_line="\n"
local text="${*: -1}" # Get the last argument as the text
local args=("${@:1:$#-1}") # Get all arguments except the last one
local format_code=""
local reset_code="\033[0m"
for arg in "${args[@]}"; do
arg=${arg,,} # Convert to lowercase
case "$arg" in
-n) new_line="" ;;
bold) format_code+="\033[1m" ;;
italic) format_code+="\033[3m" ;;
underline) format_code+="\033[4m" ;;
black) format_code+="\033[30m" ;;
red) format_code+="\033[31m" ;;
green) format_code+="\033[32m" ;;
yellow) format_code+="\033[33m" ;;
blue) format_code+="\033[34m" ;;
purple) format_code+="\033[35m" ;;
cyan) format_code+="\033[36m" ;;
white) format_code+="\033[37m" ;;
grey | gray) format_code+="\033[90m" ;;
[0-9]*)
# Check if this is a valid ANSI 256 color number
if [[ "$arg" =~ ^[0-9]+$ ]] && [ "$arg" -ge 0 ] && [ "$arg" -le 255 ]; then
format_code+="\033[38;5;${arg}m"
fi
;;
*) ;; # Ignore invalid arguments
esac
done
printf "%b%s%b%b" "${format_code}" "${text}" "${reset_code}" "${new_line}"
}
# Export the pen function so it's available to subshells
export -f pen

59
src/01.core/run.sh Normal file
View file

@ -0,0 +1,59 @@
#! /usr/bin/env bash
# run.sh - Execute commands with output/error capture
# Execute a command with stdout and stderr capture capabilities
#
# Usage:
# run --out output_var --err error_var command [args...]
# run command [args...]
# Examples:
# run --out output --err error echo "Hello, world!"
# pen "You said: $output"
run() {
local outvar_name errvar_name
local -n outvar errvar # Declare namerefs (will be assigned below if needed)
local cmd
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--out)
outvar_name="$2"
shift 2
;;
--err)
errvar_name="$2"
shift 2
;;
*)
cmd=("$@")
break
;;
esac
done
# Set up namerefs if variable names are provided
[[ -n "${outvar_name}" ]] && local -n outvar="${outvar_name}"
[[ -n "${errvar_name}" ]] && local -n errvar="${errvar_name}"
# Temporary files for capture
local stdout_file stderr_file
stdout_file=$(mktemp)
stderr_file=$(mktemp)
# Execute command with redirection
"${cmd[@]}" >"${stdout_file}" 2>"${stderr_file}"
local exit_code=$?
# Assign outputs if requested
# shellcheck disable=SC2034
[[ -n "${outvar_name}" ]] && outvar="$(<"$stdout_file")"
# shellcheck disable=SC2034
[[ -n "${errvar_name}" ]] && errvar="$(<"$stderr_file")"
rm -f "${stdout_file}" "${stderr_file}"
return $exit_code
}
# Export the run function so it's available to subshells
export -f run

38
src/02.ui/check.sh Normal file
View file

@ -0,0 +1,38 @@
#! /usr/bin/env bash
# check.sh - Print a success message
# @depends on:
# - pen.sh
# - movements.sh
# - _symbols.sh
# Print a checkmark with a message, and stop and replace the
# spinner if it's running (relies on the spinner being the last
# thing printed)
#
# Usage:
# check [options] text
# Options:
# [same as pen.sh]
# Examples:
# check "Success, world!"
# check bold "Success, world!"
# --or--
# spin "Installing dependencies..."
# sleep 2
# check "Dependancies installed."
check() {
# If there is a spinner running, stop it and clear the line
if spinning; then
spop
up
bol
cl
fi
pen -n green "${_mark:-} "
pen "$@"
}
# Export the check function so it can be used in other scripts
export -f check

25
src/02.ui/repen.sh Normal file
View file

@ -0,0 +1,25 @@
#! /usr/bin/env bash
# repen.sh - Overwrite the previous line with new text
# @depends on:
# - pen.sh
# - movements.sh
# Move up one line, move to the beginning, clear the line, and print the text.
#
# Usage:
# repen [options] text
# Options:
# [same as pen.sh]
# Examples:
# repen "Hello, world!"
# repen bold "Hello, world!"
# --or--
# repen "Hello, world!"
repen() {
upclear
pen "$@"
}
# Export the repen function so it can be used in other scripts
export -f repen

97
src/02.ui/spin.sh Normal file
View file

@ -0,0 +1,97 @@
#! /usr/bin/env bash
# spin.sh - Print a spinner with a message
# @depends on:
# - pen.sh
# - movements.sh
# - _symbols.sh
# Make sure the cursor is shown and the spinner stopped if the script exits abnormally
trap spop EXIT INT TERM
# Module state variables
_spinner_frame_duration=0.1
_spinner_pid=""
# Print a message with a spinner at the beginning
#
# Usage:
# spin [options] text
# Options:
# [same as pen.sh]
# Examples:
# spin "Installing dependencies..."
# sleep 2
# spop
# pen "Let's do something else now..."
# --or, better--
# spin "Installing dependencies..."
# sleep 2
# check "Dependancies installed."
spin() {
local message=("$@")
_spinner="${_spinner:-⣷⣯⣟⡿⢿⣻⣽⣾}"
# If there is already a spinner running, stop it
if spinning; then
spop --keep-cursor-hidden
fi
# Run the spinner in the background
(
hide_cursor
# Use a trap to catch USR1 signal for clean shutdown
trap "show_cursor; exit 0" USR1
# Print the first frame of the spinner
pen -n cyan "${_spinner:0:1} "
pen "${message[@]}"
while true; do
for ((i = 0; i < ${#_spinner}; i++)); do
frame="${_spinner:$i:1}"
up
bol
pen -n cyan "${frame} "
pen "${message[@]}"
sleep $_spinner_frame_duration
done
done
) &
_spinner_pid=$!
}
# Stop the spinner
spop() {
local keep_cursor_hidden=false
[[ "$1" == "--keep-cursor-hidden" ]] && keep_cursor_hidden=true
if spinning; then
# Signal spinner to exit gracefully
kill -USR1 "${_spinner_pid}" 2>/dev/null
# Wait briefly for cleanup
sleep $_spinner_frame_duration
# Ensure it's really gone
if ps -p "${_spinner_pid}" >/dev/null 2>&1; then
kill "${_spinner_pid}" 2>/dev/null
# Manually clean up display, unless asked not to do so
if [[ "$keep_cursor_hidden" == false ]]; then
show_cursor
fi
fi
_spinner_pid=""
fi
}
# Check if a spinner is running
spinning() {
[[ -n "${_spinner_pid}" ]]
}
# Export the functions so they are available to subshells
export -f spin spop spinning

38
src/02.ui/throw.sh Normal file
View file

@ -0,0 +1,38 @@
#! /usr/bin/env bash
# throw.sh - Print an throw message
# @depends on:
# - pen.sh
# - movements.sh
# - _symbols.sh
# Print an throwmark with a message, and stop and replace the
# spinner if it's running (relies on the spinner being the last
# thing printed)
#
# Usage:
# throw [options] text
# Options:
# [same as pen.sh]
# Examples:
# throw "Failed, world!"
# throw bold "Failed, world!"
# --or--
# spin "Installing dependencies..."
# sleep 2
# throw "Did you forget to feed the cat?"
throw() {
# If there is a spinner running, stop it and clear the line
if spinning; then
spop
up
bol
cl
fi
pen -n red "${_cross:-} "
pen "$@"
}
# Export the throw function so it can be used in other scripts
export -f throw

38
src/02.ui/warn.sh Normal file
View file

@ -0,0 +1,38 @@
#! /usr/bin/env bash
# warn.sh - Print a warning message
# @depends on:
# - pen.sh
# - movements.sh
# - _symbols.sh
# Print a "!" with a message, and stop and replace the
# spinner if it's running (relies on the spinner being
# the last thing printed)
#
# Usage:
# warn [options] text
# Options:
# [same as pen.sh]
# Examples:
# warn "Failed, world!"
# warn bold "Failed, world!"
# --or--
# spin "Installing dependencies..."
# sleep 2
# warn "Did you forget to feed the cat?"
warn() {
# If there is a spinner running, stop it and clear the line
if spinning; then
spop
up
bol
cl
fi
pen -n yellow bold italic "${_warn:-!} "
pen italic "$@"
}
# Export the warn function so it can be used in other scripts
export -f warn

47
src/03.prompt/ask.sh Normal file
View file

@ -0,0 +1,47 @@
#! /usr/bin/env bash
# ask.sh - Get free text input from the user
# @depends on:
# - pen.sh
# - _symbols.sh
# - cursor.sh
# Ask a question and get a free text answer from the user
#
# Usage:
# ask outvar text
# Example:
# ask name "What is your name?"
# echo "Hello, $name!"
ask() {
local -n outvar="$1" # Declare nameref
local prompt
local answer
# Set prompt with default indicator
prompt=$(
pen -n blue "${_q:-?} "
pen "${2}"
pen -n blue "${_a:-} "
)
show_cursor
# Get response
while true; do
read -r -p "$prompt" answer
case "$answer" in
"")
echo
warn "Please type your answer."
;;
*) break ;;
esac
done
# shellcheck disable=SC2034
outvar="$answer"
}
# Export the ask function so it's available to subshells
export -f ask

81
src/03.prompt/choose.sh Normal file
View file

@ -0,0 +1,81 @@
#! /usr/bin/env bash
# choose.sh - Choose from a menu of options
# @depends on:
# - pen.sh
# - _symbols.sh
# - cursor.sh
# Print an interactive menu of options and return the selected option
#
# Usage:
# choose outvar text [choices...]
# Example:
# choose color "What is your favorite color?" "Red" "Blue" "Green"
# pen "You chose $color!"
choose() {
local -n outvar="$1"
local prompt
local options=("${@:3}") # Get options from third argument onwards
local current=0
local count=${#options[@]}
# Set prompt with default indicator
prompt=$(
pen -n blue "${_q:-?} "
pen -n "${2}"
pen gray "[↑↓]"
)
# Hide cursor for cleaner UI
hide_cursor
trap 'show_cursor; return' INT TERM
# Display initial prompt
pen "$prompt"
# Main loop
while true; do
local index=0
for item in "${options[@]}"; do
if ((index == current)); then
pen -n blue "${_O:-} "
pen "${item}"
else
pen gray "${_o:-} ${item}"
fi
((index++))
done
# Read a single key press
read -s -r -n1 key
# Handle arrow/enter keys
if [[ $key == $'\e' ]]; then
read -s -r -n2 -t 0.0001 escape
key+="$escape"
fi
case "$key" in
$'\e[A' | 'k') # Up arrow or k
((current--))
[[ $current -lt 0 ]] && current=$((count - 1))
;;
$'\e[B' | 'j') # Down arrow or j
((current++))
[[ $current -ge "$count" ]] && current=0
;;
'') # Enter
break
;;
esac
# Clear screen and repeat
echo -en "\e[${count}A\e[J"
done
# Pass selected option back to caller
# shellcheck disable=SC2034
outvar="${options[$current]}"
}

79
src/03.prompt/confirm.sh Normal file
View file

@ -0,0 +1,79 @@
#! /usr/bin/env bash
# confirm.sh - Read a yes/no confirmation from the user
# @depends on:
# - pen.sh
# - _symbols.sh
# - movements.sh
# Ask a question and get a yes/no answer from the user
#
# Usage:
# confirm text
# Options:
# --default-yes: Answer 'yes' on ENTER (default)
# --default-no: Answer 'no' on ENTER
# Example:
# if confirm "Would you like to continue?"; then
# pen "Great!"
# else
# pen "Ok, bye!"
# fi
confirm() {
local default="y"
local hint="[Y/n]"
local prompt
local response
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--default-no)
default="n"
hint="[y/N]"
shift
;;
--default-yes)
shift
;;
*) break ;;
esac
done
# Set prompt with default indicator
prompt=$(
pen -n blue "${_q:-?} "
pen -n "$1"
pen gray " $hint"
pen -n blue "${_a:-} "
)
show_cursor
# Get response
while true; do
read -r -p "$prompt" response
response="${response:-$default}"
case "$response" in
[Yy] | [Yy][Ee][Ss])
upclear
pen -n blue "${_a:-} "
pen "yes"
return 0
;;
[Nn] | [Nn][Oo])
upclear
pen -n blue "${_a:-} "
pen "no"
return 1
;;
*)
echo
warn "Please answer yes or no."
;;
esac
done
}
# Export the confirm function so it's available to subshells
export -f confirm