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

3
.editorconfig Normal file
View file

@ -0,0 +1,3 @@
[*.sh]
indent_style = space
indent_size = 4

8
LICENSE Normal file
View file

@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright © 2025 Manuele Sarfatti
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

47
Makefile Normal file
View file

@ -0,0 +1,47 @@
# Beddu build Makefile
OUT_DIR = build
OUTPUT = $(OUT_DIR)/beddu.sh
SRC_DIR = src
DEMO_DIR = demo
# Find all direct subdirectories of src and sort them alphabetically
SUBDIRS = $(sort $(dir $(wildcard $(SRC_DIR)/*/)))
# Define a function to get files from a specific directory
get_dir_files = $(wildcard $(1)*.sh)
# Build ALL_SRC_FILES by including files from each subdirectory in order
ALL_SRC_FILES = $(foreach dir,$(SUBDIRS),$(call get_dir_files,$(dir)))
.PHONY: all clean demo build
all: $(OUTPUT)
build:
@$(MAKE) clean
@$(MAKE) all
demo: build
@./$(DEMO_DIR)/demo.sh
$(OUTPUT): $(ALL_SRC_FILES)
@mkdir -p $(OUT_DIR)
@echo '#! /usr/bin/env bash' > $(OUTPUT)
@echo '# shellcheck disable=all' >> $(OUTPUT)
@echo '#' >> $(OUTPUT)
@echo '# beddu.sh - A lightweight bash framework for interactive scripts and pretty output' >> $(OUTPUT)
@echo '# https://github.com/mjsarfatti/beddu' >> $(OUTPUT)
@echo '#' >> $(OUTPUT)
@echo '# Generated on: $(shell date)' >> $(OUTPUT)
@# Process each file, stripping (line) comments and empty lines
@for file in $(ALL_SRC_FILES); do \
echo "" >> $(OUTPUT); \
grep -v '^\s*#' "$$file" | sed '/^[[:space:]]*$$/d' | sed 's/#[a-zA-Z0-9 ]*$$//' >> $(OUTPUT); \
done
@chmod +x $(OUTPUT)
@echo "\nBuild complete: \033[32m$(OUTPUT)\033[0m"
clean:
@rm -rf $(OUT_DIR)
@echo "\nClean up completed."

163
README.md Normal file
View file

@ -0,0 +1,163 @@
# Beddu
A lightweight bash framework for interactive scripts with pretty output.
## Overview
**Beddu** is a minimalist bash library that makes your terminal scripts more interactive and visually appealing. It provides easy-to-use functions for colorful text, spinners, progress indicators, and user interaction.
## Features
- **Text Formatting**: Bold, italic, underline and more
- **Color Support**: Basic colors and full ANSI 256 color support
- **User Interaction**: Ask for input, confirmations, and present menu choices
- **Visual Indicators**: Spinners, checkmarks, and error symbols
- **Line Manipulation**: Replace previous output for dynamic updates
## Installation
Clone the repository or download `beddu.sh` to your project:
```bash
# Clone the repository
git clone https://github.com/mjsarfatti/beddu.git
# Or download just the script
curl -O https://raw.githubusercontent.com/mjsarfatti/beddu/main/beddu.sh
```
## Usage
Source the `beddu.sh` file in your script:
```bash
#!/usr/bin/env bash
source "/path/to/beddu.sh"
# Now use beddu functions
pen bold blue "Hello, world!"
```
## Examples
More can be seen by looking at the [demo](./beddu.sh) file, but here is a quick overview:
### Text Formatting and Colors
```bash
# Basic formatting
pen bold "Bold text"
pen italic "Italic text"
pen underline "Underlined text"
# Colors
pen red "Red text"
pen green "Green text"
pen 39 "ANSI color code 39 (light blue)"
# Combined
pen bold red "Bold red text"
# Inline
echo "This is $(pen yellow "yellow"), and this is $(pen bold "bold")"
```
### Interactive Functions
```bash
# Ask for input
ask name "What's your name?"
pen "Hello, $name!"
# Yes/no confirmation (defaults to "yes")
if confirm "Continue?"; then
pen green "Continuing..."
else
pen red "Aborting."
fi
# Defaulting to "no"
if confirm --default-no "Are you sure?"; then
pen green "Proceeding..."
else
pen red "Cancelled."
fi
# Menu selection
choose color "Select a color:" "Red" "Green" "Blue"
pen "You selected: $color"
```
### Progress Indicators
```bash
# Show a progress spinner
spin "Working on it..."
sleep 2
check "Done!"
# Replace lines dynamically
pen "This will be replaced..."
sleep 1
repen "Processing started..."
sleep 1
repen spin "Almost done..." # Let's add a spinner for good measure
sleep 1
check "Task completed!" # We can directly `check` or `error` after a `spin` call - the message will always replace the spin line
```
## Demo
To see it in action paste the following command in your terminal:
```bash
curl -s https://raw.githubusercontent.com/mjsarfatti/beddu/main/demo.sh | bash
```
## Function Reference
### Text Formatting
- `pen [OPTIONS] TEXT` - Output formatted text
- `-n` - No newline after output (must be the first option if used)
- `bold`, `italic`, `underline` - Text styles
- `red`, `green`, `blue`, etc. - Color names
- `0-255` - ANSI color codes
### User Interaction
- `ask [retval] PROMPT` - Get text input from user, saves the answer in `$retval`
- `confirm [OPTIONS] PROMPT` - Get yes/no input
- `--default-yes` - Set default answer to "yes" (default behavior)
- `--default-no` - Set default answer to "no"
- `choose [retval] PROMPT [OPTIONS...]` - Display a selection menu, saves the answer in `$retval`
### Progress Indicators
- `spin MESSAGE` - Show animated spinner
- `check MESSAGE` - Show success indicator (if called right after a spinner, replaces that line)
- `error MESSAGE` - Show error indicator (if called right after a spinner, replaces that line)
- `repen [OPTIONS] MESSAGE` - Like `pen`, but replace the previous line
- `-n` - No newline after output (must be the first option if used)
- `spin`, `check`, `error` - If passed, use this function to print the message
- `bold`, `italic`, `underline` - Text styles
- `red`, `green`, `blue`, etc. - Color names
- `0-255` - ANSI color codes
## FAQ
### Q: It doesn't work on my computer?
A: **Beddu** requires bash v4+. If your bash version checks out, please file an issue!
### Q: Can you add feature X?
A: Most likely, not. This is meant to be a _minimal_ toolkit to quickly jot down simple interactive scripts, nothing more. If you are looking for something more complete check out [Gum](https://github.com/charmbracelet/gum) (bash), [Inquire](https://github.com/mikaelmello/inquire) (Rust) or [Enquirer](https://github.com/enquirer/enquirer) (Node).
## License
[MIT](./LICENSE)
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.

327
build/beddu.sh Executable file
View file

@ -0,0 +1,327 @@
#! /usr/bin/env bash
# shellcheck disable=all
#
# beddu.sh - A lightweight bash framework for interactive scripts and pretty output
# https://github.com/mjsarfatti/beddu
#
# Generated on: Fri May 9 20:00:08 CEST 2025
_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
up() {
printf "\033[A"
}
down() {
printf "\033[B"
}
bol() {
printf "\r"
}
eol() {
printf "\033[999C"
}
cl() {
printf "\033[2K"
}
upclear() {
up
bol
cl
}
line() {
printf "\n"
}
show_cursor() {
printf "\033[?25h"
}
hide_cursor() {
printf "\033[?25l"
}
export -f up down bol eol cl line show_cursor hide_cursor
pen() {
local new_line="\n"
local text="${*: -1}"
local args=("${@:1:$#-1}")
local format_code=""
local reset_code="\033[0m"
for arg in "${args[@]}"; do
arg=${arg,,}
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]*)
if [[ "$arg" =~ ^[0-9]+$ ]] && [ "$arg" -ge 0 ] && [ "$arg" -le 255 ]; then
format_code+="\033[38;5;${arg}m"
fi
;;
*) ;;
esac
done
printf "%b%s%b%b" "${format_code}" "${text}" "${reset_code}" "${new_line}"
}
export -f pen
run() {
local outvar_name errvar_name
local -n outvar errvar # Declare namerefs (will be assigned below if needed)
local cmd
while [[ $# -gt 0 ]]; do
case "$1" in
--out)
outvar_name="$2"
shift 2
;;
--err)
errvar_name="$2"
shift 2
;;
*)
cmd=("$@")
break
;;
esac
done
[[ -n "${outvar_name}" ]] && local -n outvar="${outvar_name}"
[[ -n "${errvar_name}" ]] && local -n errvar="${errvar_name}"
local stdout_file stderr_file
stdout_file=$(mktemp)
stderr_file=$(mktemp)
"${cmd[@]}" >"${stdout_file}" 2>"${stderr_file}"
local exit_code=$?
[[ -n "${outvar_name}" ]] && outvar="$(<"$stdout_file")"
[[ -n "${errvar_name}" ]] && errvar="$(<"$stderr_file")"
rm -f "${stdout_file}" "${stderr_file}"
return $exit_code
}
export -f run
check() {
if spinning; then
spop
up
bol
cl
fi
pen -n green "${_mark:-} "
pen "$@"
}
export -f check
repen() {
upclear
pen "$@"
}
export -f repen
trap spop EXIT INT TERM
_spinner_frame_duration=0.1
_spinner_pid=""
spin() {
local message=("$@")
_spinner="${_spinner:-⣷⣯⣟⡿⢿⣻⣽⣾}"
if spinning; then
spop --keep-cursor-hidden
fi
(
hide_cursor
trap "show_cursor; exit 0" USR1
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=$!
}
spop() {
local keep_cursor_hidden=false
[[ "$1" == "--keep-cursor-hidden" ]] && keep_cursor_hidden=true
if spinning; then
kill -USR1 "${_spinner_pid}" 2>/dev/null
sleep $_spinner_frame_duration
if ps -p "${_spinner_pid}" >/dev/null 2>&1; then
kill "${_spinner_pid}" 2>/dev/null
if [[ "$keep_cursor_hidden" == false ]]; then
show_cursor
fi
fi
_spinner_pid=""
fi
}
spinning() {
[[ -n "${_spinner_pid}" ]]
}
export -f spin spop spinning
throw() {
if spinning; then
spop
up
bol
cl
fi
pen -n red "${_cross:-} "
pen "$@"
}
export -f throw
warn() {
if spinning; then
spop
up
bol
cl
fi
pen -n yellow bold italic "${_warn:-!} "
pen italic "$@"
}
export -f warn
ask() {
local -n outvar="$1"
local prompt
local answer
prompt=$(
pen -n blue "${_q:-?} "
pen "${2}"
pen -n blue "${_a:-} "
)
show_cursor
while true; do
read -r -p "$prompt" answer
case "$answer" in
"")
echo
warn "Please type your answer."
;;
*) break ;;
esac
done
outvar="$answer"
}
export -f ask
choose() {
local -n outvar="$1"
local prompt
local options=("${@:3}")
local current=0
local count=${#options[@]}
prompt=$(
pen -n blue "${_q:-?} "
pen -n "${2}"
pen gray "[↑↓]"
)
hide_cursor
trap 'show_cursor; return' INT TERM
pen "$prompt"
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 -s -r -n1 key
if [[ $key == $'\e' ]]; then
read -s -r -n2 -t 0.0001 escape
key+="$escape"
fi
case "$key" in
$'\e[A' | 'k')
((current--))
[[ $current -lt 0 ]] && current=$((count - 1))
;;
$'\e[B' | 'j')
((current++))
[[ $current -ge "$count" ]] && current=0
;;
'')
break
;;
esac
echo -en "\e[${count}A\e[J"
done
outvar="${options[$current]}"
}
confirm() {
local default="y"
local hint="[Y/n]"
local prompt
local response
while [[ $# -gt 0 ]]; do
case "$1" in
--default-no)
default="n"
hint="[y/N]"
shift
;;
--default-yes)
shift
;;
*) break ;;
esac
done
prompt=$(
pen -n blue "${_q:-?} "
pen -n "$1"
pen gray " $hint"
pen -n blue "${_a:-} "
)
show_cursor
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 -f confirm

173
demo/demo.sh Executable file
View file

@ -0,0 +1,173 @@
#!/usr/bin/env bash
# Import the beddu.sh library
# shellcheck disable=SC1091
source "$(dirname -- "$0")/../build/beddu.sh"
# Demo function to showcase the framework
demo() {
_violet=99
_pink=219
line
pen $_violet "╔═════════════════════════════════════════════╗"
pen $_violet "║ ║"
pen $_violet "║ ║"
pen -n $_violet "║ "
pen -n $_pink "Beddu.sh Demo"
pen $_violet " ║"
pen $_violet "║ ║"
pen $_violet "║ ║"
pen $_violet "╚═════════════════════════════════════════════╝"
line
line
line
spin $_pink "Loading text formatting..."
sleep 1
spop
upclear
pen $_pink italic "-- Text formatting --"
line
pen bold "This text is bold"
pen italic "This text is italic"
pen underline "This text is underlined"
line
line
spin $_pink "Loading basic colors..."
sleep 1
spop
upclear
pen $_pink italic "-- Basic colors --"
line
pen red "Red text"
pen green "Green text"
pen yellow "Yellow text"
pen blue "Blue text"
pen purple "Purple text"
pen cyan "Cyan text"
pen white "White text"
pen grey "Grey text"
pen -n black "Black text"
pen italic "[Black text - might not be visible]"
line
line
spin $_pink "Loading ANSI 256 colors..."
sleep 1
spop
upclear
pen $_pink italic "-- ANSI 256 colors (examples) --"
line
pen 39 "Light blue text (39)"
pen 208 "Orange text (208)"
pen 82 "Light green text (82)"
line
line
spin $_pink "Loading combined formatting..."
sleep 1
spop
upclear
pen $_pink italic "-- Combined formatting --"
line
pen bold blue "This text is bold and blue"
pen bold italic red "This text is bold, italic and red"
pen underline green "This text is underlined and green"
pen bold 39 "This text is bold and light blue (ANSI 256 color 39)"
pen italic 208 "This text is italic and orange (ANSI 256 color 208)"
pen -n red "This is red "
pen -n green "and this is green, "
pen "all on the same line!"
pen "And this is $(pen yellow "yellow"), and this is $(pen purple "purple")"
line
line
spin $_pink "Loading output utilities..."
sleep 1
spop
upclear
pen $_pink italic "-- Output utilities --"
line
check "Task completed successfully!"
throw "Operation failed."
line
line
spin $_pink "Starting interactive experience..."
sleep 1
spop
upclear
pen $_pink italic "-- Interactive functions --"
line
ask name "How can I call you?"
pen "Hello, $(pen bold cyan "${name:?}")"
line
choose color "What is your favorite color?" "Red" "Green" "Blue"
pen "Nice choice, $(pen bold "${color:?}" "${color:?}")"
line
if confirm "Would you like to continue with the demo?"; then
pen "OK, let's $(pen bold green "continue")!"
else
pen "Too bad, I'll $(pen bold red "continue anyway")"
fi
line
line
spin $_pink "Loading output manipulation..."
sleep 1
spop --keep-cursor-hidden
repen $_pink italic "-- Output manipulation --"
line
pen "This line will be replaced in 1 second..."
sleep 1
spop --keep-cursor-hidden
repen "Processing your request..."
sleep 1
spop --keep-cursor-hidden
upclear
spin "Still working on it..."
sleep 2
check "Task completed successfully!"
spin "Performing an operation that will fail (ask me how I know)"
sleep 2
throw "Operation failed"
line
# This is a 12MB file
local filename="commonswiki-20250501-pages-articles-multistream-index1.txt-p1p1500000.bz2"
local baseurl="https://dumps.wikimedia.org/commonswiki/20250501"
line
spin $_pink "Loading \`run\` utility..."
sleep 1
spop --keep-cursor-hidden
repen $_pink italic "-- Run command output control --"
line
spin "Downloading file..."
# `curl` writes to stderr, so we need to capture that
if run --err output curl -O "$baseurl/$filename"; then
check "Download complete!"
line
pen "${output:-}"
else
throw "Download failed!"
fi
line
if confirm "Would you like to remove the downloaded file?"; then
rm -f "$filename"
check "File removed!"
fi
line
pen bold green "All done!"
line
}
# If this script is executed directly, show the demo
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
demo
fi

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

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