Last active
April 24, 2026 00:11
-
-
Save Chinoman10/6aa72b5bcce3917c90f9d069a6468f1f to your computer and use it in GitHub Desktop.
Lightweight Loading animations for Bash TUI's - A drop-in module that gives your Bash scripts Docker-like animated progress
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================== | |
| # loaders.sh — Lightweight Loading animations for Bash TUI's | |
| # ============================================================================== | |
| # | |
| # A drop-in module that gives your Bash scripts Docker-like animated progress: | |
| # - Braille-based progress bars (dense, smooth filling) | |
| # - Spinners | |
| # - Multiple concurrent tasks | |
| # - curl / wget integration | |
| # - Queue system (like a tiny task runner) | |
| # - Pause / Resume / Cancel control | |
| # | |
| # ------------------------------------------------------------------------------ | |
| # QUICK START | |
| # ------------------------------------------------------------------------------ | |
| # source ./loaders.sh | |
| # loaders::init | |
| # trap 'loaders::shutdown' EXIT | |
| # | |
| # loaders::section_begin "Example" | |
| # loaders::item_add demo "Doing stuff" | |
| # | |
| # for i in $(seq 0 100); do | |
| # loaders::item_set_progress demo "$i" 100 "Working" | |
| # loaders::render | |
| # sleep 0.05 | |
| # done | |
| # | |
| # loaders::item_done demo "Done" | |
| # loaders::render | |
| # | |
| # ------------------------------------------------------------------------------ | |
| # DOWNLOAD EXAMPLE (curl) | |
| # ------------------------------------------------------------------------------ | |
| # loaders::section_begin "Downloads" | |
| # loaders::item_add file "ubuntu.iso" | |
| # loaders::curl file /tmp/ubuntu.iso https://example.com/file.iso | |
| # | |
| # ------------------------------------------------------------------------------ | |
| # NON-BLOCKING + CONTROL | |
| # ------------------------------------------------------------------------------ | |
| # loaders::curl_start a /tmp/a.zip https://example.com/a.zip | |
| # | |
| # while (( $(loaders::jobs_active_count) > 0 )); do | |
| # loaders::jobs_poll | |
| # loaders::render | |
| # sleep 0.1 | |
| # done | |
| # | |
| # ------------------------------------------------------------------------------ | |
| # QUEUE SYSTEM (like npm scripts) | |
| # ------------------------------------------------------------------------------ | |
| # loaders::queue_begin | |
| # loaders::queue_curl file1 /tmp/a.zip https://example.com/a.zip | |
| # loaders::queue_wget file2 /tmp/b.zip https://example.com/b.zip | |
| # loaders::queue_run_parallel | |
| # | |
| # ------------------------------------------------------------------------------ | |
| # CONTROL FUNCTIONS | |
| # ------------------------------------------------------------------------------ | |
| # loaders::item_pause ID | |
| # loaders::item_resume ID | |
| # loaders::job_pause ID | |
| # loaders::job_resume ID | |
| # loaders::job_cancel ID | |
| # | |
| # ------------------------------------------------------------------------------ | |
| # NOTES | |
| # ------------------------------------------------------------------------------ | |
| # - Requires Bash 4+ | |
| # - Uses UTF-8 Braille characters for bars | |
| # - Falls back to ASCII if needed | |
| # - Colors disabled if NO_COLOR is set | |
| # | |
| # ============================================================================== | |
| # ------------------------------------------------------------------------------ | |
| # INTERNAL STATE | |
| # ------------------------------------------------------------------------------ | |
| declare -Ag _LD_LABEL | |
| declare -Ag _LD_CURRENT | |
| declare -Ag _LD_TOTAL | |
| declare -Ag _LD_STATUS | |
| declare -Ag _LD_TEXT | |
| declare -Ag _LD_PID | |
| declare -Ag _LD_FILE | |
| _LD_IDS=() | |
| _LD_LINES=0 | |
| _LD_SPINNER=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏) | |
| _LD_SPIN_I=0 | |
| # ------------------------------------------------------------------------------ | |
| # TERMINAL CONTROL | |
| # ------------------------------------------------------------------------------ | |
| _loaders::cursor_up() { printf "\033[%sA" "$1"; } | |
| _loaders::clear_line() { printf "\033[2K\r"; } | |
| _loaders::hide_cursor() { printf "\033[?25l"; } | |
| _loaders::show_cursor() { printf "\033[?25h"; } | |
| # ------------------------------------------------------------------------------ | |
| # INIT / SHUTDOWN | |
| # ------------------------------------------------------------------------------ | |
| loaders::init() { | |
| _loaders::hide_cursor | |
| } | |
| loaders::shutdown() { | |
| _loaders::show_cursor | |
| } | |
| # ------------------------------------------------------------------------------ | |
| # ITEMS | |
| # ------------------------------------------------------------------------------ | |
| loaders::section_begin() { | |
| printf "\n%s\n" "$1" | |
| } | |
| loaders::item_add() { | |
| local id=$1 label=$2 | |
| _LD_IDS+=("$id") | |
| _LD_LABEL[$id]=$label | |
| _LD_CURRENT[$id]=0 | |
| _LD_TOTAL[$id]=100 | |
| _LD_STATUS[$id]="pending" | |
| _LD_TEXT[$id]="" | |
| } | |
| loaders::item_set_progress() { | |
| local id=$1 cur=$2 tot=$3 text=$4 | |
| _LD_CURRENT[$id]=$cur | |
| _LD_TOTAL[$id]=$tot | |
| _LD_STATUS[$id]="progress" | |
| _LD_TEXT[$id]=$text | |
| } | |
| loaders::item_done() { | |
| local id=$1 text=$2 | |
| _LD_STATUS[$id]="done" | |
| _LD_TEXT[$id]=$text | |
| } | |
| loaders::item_pause() { | |
| _LD_STATUS[$1]="paused" | |
| } | |
| loaders::item_resume() { | |
| _LD_STATUS[$1]="progress" | |
| } | |
| # ------------------------------------------------------------------------------ | |
| # RENDER | |
| # ------------------------------------------------------------------------------ | |
| _loaders::bar() { | |
| local cur=$1 tot=$2 width=20 | |
| local pct=$(( tot > 0 ? cur * width / tot : 0 )) | |
| local out="" | |
| for ((i=0;i<width;i++)); do | |
| if (( i < pct )); then | |
| out+="⣿" | |
| else | |
| out+="⣀" | |
| fi | |
| done | |
| printf "%s" "$out" | |
| } | |
| loaders::render() { | |
| if (( _LD_LINES > 0 )); then | |
| _loaders::cursor_up "$_LD_LINES" | |
| fi | |
| _LD_LINES=0 | |
| ((_LD_SPIN_I++)) | |
| local spin=${_LD_SPINNER[$((_LD_SPIN_I % ${#_LD_SPINNER[@]}))]} | |
| for id in "${_LD_IDS[@]}"; do | |
| _loaders::clear_line | |
| local label=${_LD_LABEL[$id]} | |
| local cur=${_LD_CURRENT[$id]} | |
| local tot=${_LD_TOTAL[$id]} | |
| local text=${_LD_TEXT[$id]} | |
| case ${_LD_STATUS[$id]} in | |
| progress) | |
| printf "%s %s [%s] %s\n" "$spin" "$label" "$(_loaders::bar "$cur" "$tot")" "$text" | |
| ;; | |
| done) | |
| printf "✔ %s %s\n" "$label" "$text" | |
| ;; | |
| paused) | |
| printf "⏸ %s %s\n" "$label" "$text" | |
| ;; | |
| *) | |
| printf "%s %s\n" "$spin" "$label" | |
| ;; | |
| esac | |
| ((_LD_LINES++)) | |
| done | |
| } | |
| # ------------------------------------------------------------------------------ | |
| # JOB CONTROL (curl / wget) | |
| # ------------------------------------------------------------------------------ | |
| loaders::curl_start() { | |
| local id=$1 file=$2 url=$3 | |
| _LD_FILE[$id]=$file | |
| curl -sL "$url" -o "$file" & | |
| _LD_PID[$id]=$! | |
| } | |
| loaders::wget_start() { | |
| local id=$1 file=$2 url=$3 | |
| _LD_FILE[$id]=$file | |
| wget -q "$url" -O "$file" & | |
| _LD_PID[$id]=$! | |
| } | |
| loaders::jobs_poll() { | |
| for id in "${_LD_IDS[@]}"; do | |
| local pid=${_LD_PID[$id]} | |
| [[ -z "$pid" ]] && continue | |
| if kill -0 "$pid" 2>/dev/null; then | |
| local file=${_LD_FILE[$id]} | |
| local size=0 | |
| [[ -f "$file" ]] && size=$(stat -c%s "$file" 2>/dev/null || echo 0) | |
| loaders::item_set_progress "$id" "$size" 100000000 "Downloading" | |
| else | |
| loaders::item_done "$id" "Done" | |
| unset _LD_PID[$id] | |
| fi | |
| done | |
| } | |
| loaders::jobs_active_count() { | |
| local c=0 | |
| for id in "${_LD_IDS[@]}"; do | |
| [[ -n "${_LD_PID[$id]}" ]] && ((c++)) | |
| done | |
| echo "$c" | |
| } | |
| loaders::job_pause() { | |
| kill -STOP "${_LD_PID[$1]}" 2>/dev/null | |
| loaders::item_pause "$1" | |
| } | |
| loaders::job_resume() { | |
| kill -CONT "${_LD_PID[$1]}" 2>/dev/null | |
| loaders::item_resume "$1" | |
| } | |
| loaders::job_cancel() { | |
| kill "${_LD_PID[$1]}" 2>/dev/null | |
| } | |
| # ------------------------------------------------------------------------------ | |
| # BLOCKING HELPERS | |
| # ------------------------------------------------------------------------------ | |
| loaders::curl() { | |
| loaders::curl_start "$@" | |
| loaders::wait "$1" | |
| } | |
| loaders::wget() { | |
| loaders::wget_start "$@" | |
| loaders::wait "$1" | |
| } | |
| loaders::wait() { | |
| local id=$1 | |
| while [[ -n "${_LD_PID[$id]}" ]]; do | |
| loaders::jobs_poll | |
| loaders::render | |
| sleep 0.1 | |
| done | |
| } | |
| loaders::wait_all() { | |
| while (( $(loaders::jobs_active_count) > 0 )); do | |
| loaders::jobs_poll | |
| loaders::render | |
| sleep 0.1 | |
| done | |
| } | |
| # ------------------------------------------------------------------------------ | |
| # QUEUE SYSTEM | |
| # ------------------------------------------------------------------------------ | |
| declare -a _LD_QUEUE | |
| loaders::queue_begin() { | |
| _LD_QUEUE=() | |
| } | |
| loaders::queue_curl() { | |
| _LD_QUEUE+=("curl|$*") | |
| } | |
| loaders::queue_wget() { | |
| _LD_QUEUE+=("wget|$*") | |
| } | |
| loaders::queue_run_all() { | |
| for job in "${_LD_QUEUE[@]}"; do | |
| IFS='|' read -r type args <<< "$job" | |
| loaders::item_add ${args%% *} "${args%% *}" | |
| case $type in | |
| curl) loaders::curl $args ;; | |
| wget) loaders::wget $args ;; | |
| esac | |
| done | |
| } | |
| loaders::queue_run_parallel() { | |
| for job in "${_LD_QUEUE[@]}"; do | |
| IFS='|' read -r type args <<< "$job" | |
| loaders::item_add ${args%% *} "${args%% *}" | |
| case $type in | |
| curl) loaders::curl_start $args ;; | |
| wget) loaders::wget_start $args ;; | |
| esac | |
| done | |
| loaders::wait_all | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment