#!/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 # Draw a normal green progress bar. # # block_progress_bar # 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 }