commit 76d136f64c8e636df82e84fcb2d64282501430be Author: Manuele Sarfatti Date: Fri May 9 20:01:43 2025 +0200 Initial commit - v1.0 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f500456 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.sh] +indent_style = space +indent_size = 4 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..46bd44f --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cff2012 --- /dev/null +++ b/Makefile @@ -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." \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f7c874 --- /dev/null +++ b/README.md @@ -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. diff --git a/build/beddu.sh b/build/beddu.sh new file mode 100755 index 0000000..e028fc6 --- /dev/null +++ b/build/beddu.sh @@ -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 diff --git a/demo/demo.sh b/demo/demo.sh new file mode 100755 index 0000000..c50bc2f --- /dev/null +++ b/demo/demo.sh @@ -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 diff --git a/images/SCR-20250507-taln.png b/images/SCR-20250507-taln.png new file mode 100644 index 0000000..5924a80 Binary files /dev/null and b/images/SCR-20250507-taln.png differ diff --git a/images/SCR-20250507-tbbw.png b/images/SCR-20250507-tbbw.png new file mode 100644 index 0000000..740e30a Binary files /dev/null and b/images/SCR-20250507-tbbw.png differ diff --git a/src/00.utils/_symbols.sh b/src/00.utils/_symbols.sh new file mode 100644 index 0000000..78e0fc8 --- /dev/null +++ b/src/00.utils/_symbols.sh @@ -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 diff --git a/src/00.utils/movements.sh b/src/00.utils/movements.sh new file mode 100644 index 0000000..f2c289f --- /dev/null +++ b/src/00.utils/movements.sh @@ -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 diff --git a/src/01.core/pen.sh b/src/01.core/pen.sh new file mode 100644 index 0000000..a9c5bf3 --- /dev/null +++ b/src/01.core/pen.sh @@ -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 diff --git a/src/01.core/run.sh b/src/01.core/run.sh new file mode 100644 index 0000000..5e34466 --- /dev/null +++ b/src/01.core/run.sh @@ -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 diff --git a/src/02.ui/check.sh b/src/02.ui/check.sh new file mode 100644 index 0000000..d2a3f5d --- /dev/null +++ b/src/02.ui/check.sh @@ -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 diff --git a/src/02.ui/repen.sh b/src/02.ui/repen.sh new file mode 100644 index 0000000..c21641d --- /dev/null +++ b/src/02.ui/repen.sh @@ -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 diff --git a/src/02.ui/spin.sh b/src/02.ui/spin.sh new file mode 100644 index 0000000..007903a --- /dev/null +++ b/src/02.ui/spin.sh @@ -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 diff --git a/src/02.ui/throw.sh b/src/02.ui/throw.sh new file mode 100644 index 0000000..bf21e9a --- /dev/null +++ b/src/02.ui/throw.sh @@ -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 diff --git a/src/02.ui/warn.sh b/src/02.ui/warn.sh new file mode 100644 index 0000000..06d5d55 --- /dev/null +++ b/src/02.ui/warn.sh @@ -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 diff --git a/src/03.prompt/ask.sh b/src/03.prompt/ask.sh new file mode 100644 index 0000000..b96b0bf --- /dev/null +++ b/src/03.prompt/ask.sh @@ -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 diff --git a/src/03.prompt/choose.sh b/src/03.prompt/choose.sh new file mode 100644 index 0000000..58fbea8 --- /dev/null +++ b/src/03.prompt/choose.sh @@ -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]}" +} diff --git a/src/03.prompt/confirm.sh b/src/03.prompt/confirm.sh new file mode 100644 index 0000000..6d167f3 --- /dev/null +++ b/src/03.prompt/confirm.sh @@ -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