refactor: optimize terminal progress bar library - simplify terminal cursor and scroll-region handling - replace echo/let with printf and Bash arithmetic - localize function variables - centralize progress-bar rendering - validate and clamp progress percentages - handle narrow terminal widths safely - improve Ctrl-C cleanup and exit status - retain existing public API - expand inline documentation
238 lines
5.3 KiB
Bash
Executable File
238 lines
5.3 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Terminal Progress Bar Library
|
|
#
|
|
# Description:
|
|
# Provides a reusable progress bar that occupies the last terminal row
|
|
# while allowing normal command output to continue scrolling above it.
|
|
#
|
|
# Usage:
|
|
# Source this file from another Bash script:
|
|
#
|
|
# source ./progress_bar.sh
|
|
#
|
|
# Then:
|
|
#
|
|
# enable_trapping
|
|
# setup_scroll_area
|
|
#
|
|
# draw_progress_bar 10
|
|
# draw_progress_bar 40
|
|
#
|
|
# # Indicates that user interaction/action is required.
|
|
# block_progress_bar 45
|
|
#
|
|
# draw_progress_bar 90
|
|
# destroy_scroll_area
|
|
#
|
|
# Functions:
|
|
# enable_trapping
|
|
# Enable Ctrl-C cleanup for the progress bar.
|
|
#
|
|
# setup_scroll_area
|
|
# Create the terminal scrolling area and display an empty progress bar.
|
|
#
|
|
# draw_progress_bar <percentage>
|
|
# Draw a normal green progress bar.
|
|
#
|
|
# block_progress_bar <percentage>
|
|
# Draw a yellow progress bar to indicate that user action is required.
|
|
#
|
|
# clear_progress_bar
|
|
# Clear the progress bar without destroying the scroll area.
|
|
#
|
|
# destroy_scroll_area
|
|
# Restore the terminal scrolling region and remove the progress bar.
|
|
#
|
|
# Notes:
|
|
# - Bash is required.
|
|
# - Requires `tput` and a terminal supporting ANSI escape sequences.
|
|
# - This script is intended to be sourced, not executed directly.
|
|
#
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Terminal escape sequences
|
|
# ---------------------------------------------------------------------------
|
|
|
|
readonly CODE_SAVE_CURSOR=$'\033[s'
|
|
readonly CODE_RESTORE_CURSOR=$'\033[u'
|
|
readonly CODE_CURSOR_IN_SCROLL_AREA=$'\033[1A'
|
|
|
|
readonly COLOR_FG=$'\e[30m'
|
|
readonly COLOR_BG=$'\e[42m'
|
|
readonly COLOR_BG_BLOCKED=$'\e[43m'
|
|
readonly RESTORE_FG=$'\e[39m'
|
|
readonly RESTORE_BG=$'\e[49m'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# State
|
|
# ---------------------------------------------------------------------------
|
|
|
|
PROGRESS_BLOCKED=false
|
|
TRAPPING_ENABLED=false
|
|
TRAP_SET=false
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_terminal_rows() {
|
|
tput lines
|
|
}
|
|
|
|
_terminal_columns() {
|
|
tput cols
|
|
}
|
|
|
|
_move_to_progress_row() {
|
|
local rows
|
|
rows=$(_terminal_rows)
|
|
|
|
printf '%s' "${CODE_SAVE_CURSOR}"
|
|
printf '\033[%d;0f' "$rows"
|
|
tput el
|
|
}
|
|
|
|
_restore_cursor() {
|
|
printf '%s' "${CODE_RESTORE_CURSOR}"
|
|
}
|
|
|
|
_validate_percentage() {
|
|
local percentage=$1
|
|
|
|
if ! [[ "$percentage" =~ ^[0-9]+$ ]]; then
|
|
percentage=0
|
|
elif (( percentage > 100 )); then
|
|
percentage=100
|
|
fi
|
|
|
|
printf '%d' "$percentage"
|
|
}
|
|
|
|
_print_repeated() {
|
|
local character=$1
|
|
local count=$2
|
|
|
|
(( count <= 0 )) && return
|
|
|
|
printf '%*s' "$count" '' | tr ' ' "$character"
|
|
}
|
|
|
|
_print_bar() {
|
|
local percentage
|
|
local columns
|
|
local bar_size
|
|
local complete_size
|
|
local remainder_size
|
|
local background
|
|
|
|
percentage=$(_validate_percentage "$1")
|
|
columns=$(_terminal_columns)
|
|
|
|
# 17 characters are reserved for:
|
|
# " Progress XXX% []"
|
|
bar_size=$((columns - 17))
|
|
|
|
# Prevent negative printf field widths on very small terminals.
|
|
(( bar_size < 1 )) && bar_size=1
|
|
|
|
complete_size=$((bar_size * percentage / 100))
|
|
remainder_size=$((bar_size - complete_size))
|
|
|
|
if [[ "$PROGRESS_BLOCKED" == true ]]; then
|
|
background=$COLOR_BG_BLOCKED
|
|
else
|
|
background=$COLOR_BG
|
|
fi
|
|
|
|
printf ' Progress %3d%% [' "$percentage"
|
|
printf '%s%s' "$COLOR_FG" "$background"
|
|
_print_repeated '#' "$complete_size"
|
|
printf '%s%s' "$RESTORE_FG" "$RESTORE_BG"
|
|
_print_repeated '.' "$remainder_size"
|
|
printf ']'
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
setup_scroll_area() {
|
|
if [[ "$TRAPPING_ENABLED" == true ]]; then
|
|
trap_on_interrupt
|
|
fi
|
|
|
|
local rows
|
|
rows=$(_terminal_rows)
|
|
(( rows > 1 )) || return 1
|
|
|
|
# Create a line for the progress bar and avoid terminal resize glitches.
|
|
printf '\n'
|
|
|
|
# Reserve the final terminal row for the progress bar.
|
|
printf '%s' "$CODE_SAVE_CURSOR"
|
|
printf '\033[0;%dr' "$((rows - 1))"
|
|
|
|
# Restore the cursor inside the scrolling region.
|
|
printf '%s%s' "$CODE_RESTORE_CURSOR" "$CODE_CURSOR_IN_SCROLL_AREA"
|
|
|
|
draw_progress_bar 0
|
|
}
|
|
|
|
destroy_scroll_area() {
|
|
local rows
|
|
rows=$(_terminal_rows)
|
|
|
|
# Restore the full terminal scrolling region.
|
|
printf '%s' "$CODE_SAVE_CURSOR"
|
|
printf '\033[0;%dr' "$rows"
|
|
printf '%s%s' "$CODE_RESTORE_CURSOR" "$CODE_CURSOR_IN_SCROLL_AREA"
|
|
|
|
clear_progress_bar
|
|
|
|
# Allow the terminal to resume normal scrolling.
|
|
printf '\n\n'
|
|
|
|
if [[ "$TRAP_SET" == true ]]; then
|
|
trap - INT
|
|
TRAP_SET=false
|
|
fi
|
|
}
|
|
|
|
draw_progress_bar() {
|
|
PROGRESS_BLOCKED=false
|
|
|
|
_move_to_progress_row
|
|
_print_bar "$1"
|
|
_restore_cursor
|
|
}
|
|
|
|
block_progress_bar() {
|
|
PROGRESS_BLOCKED=true
|
|
|
|
_move_to_progress_row
|
|
_print_bar "$1"
|
|
_restore_cursor
|
|
}
|
|
|
|
clear_progress_bar() {
|
|
_move_to_progress_row
|
|
_restore_cursor
|
|
}
|
|
|
|
enable_trapping() {
|
|
TRAPPING_ENABLED=true
|
|
}
|
|
|
|
trap_on_interrupt() {
|
|
TRAP_SET=true
|
|
trap cleanup_on_interrupt INT
|
|
}
|
|
|
|
cleanup_on_interrupt() {
|
|
destroy_scroll_area
|
|
exit 130
|
|
}
|