Files
Lucas Vivohost 081210d3ae refactor: optimize terminal progress bar library
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
2026-09-14 12:44:39 +02:00

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
}