Skip to content

Instantly share code, notes, and snippets.

@Chinoman10
Last active April 24, 2026 00:11
Show Gist options
  • Select an option

  • Save Chinoman10/6aa72b5bcce3917c90f9d069a6468f1f to your computer and use it in GitHub Desktop.

Select an option

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
#!/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