#!/hint/bash # Copyright © Tavian Barnes # SPDX-License-Identifier: 0BSD ## Colored output # Common escape sequences BLD=$'\e[01m' RED=$'\e[01;31m' GRN=$'\e[01;32m' YLW=$'\e[01;33m' BLU=$'\e[01;34m' MAG=$'\e[01;35m' CYN=$'\e[01;36m' RST=$'\e[0m' # Check if we should color output to the given fd color_fd() { [ -z "${NO_COLOR:-}" ] && [ -t "$1" ] } # Cache the color status for std{out,err} color_fd 1 && COLOR_STDOUT=1 || COLOR_STDOUT=0 color_fd 2 && COLOR_STDERR=1 || COLOR_STDERR=0 # Save this in case the tests unset PATH SED=$(command -v sed) # Filter out escape sequences if necessary color() { if color_fd 1; then "$@" else "$@" | "$SED" $'s/\e\\[[^m]*m//g' fi } ## Status bar # Show the terminal status bar show_bar() { if [ -z "$TTY" ]; then return 1 fi # Name the pipe deterministically based on the ttyname, so that concurrent # tests.sh runs on the same terminal (e.g. make -jN check) cooperate local pipe pipe=$(printf '%s' "$TTY" | tr '/' '-') pipe="${TMPDIR:-/tmp}/bfs$pipe.bar" if mkfifo "$pipe" 2>/dev/null; then # We won the race, create the background process to manage the bar bar_proc "$pipe" & exec {BAR}>"$pipe" elif [ -p "$pipe" ]; then # We lost the race, connect to the existing process. # There is a small TOCTTOU race here but I don't see how to avoid it. exec {BAR}>"$pipe" else return 1 fi } # Print to the terminal status bar print_bar() { printf 'PRINT:%d:%s\0' $$ "$1" >&$BAR } # Hide the terminal status bar hide_bar() { printf 'HIDE:%d:\0' $$ >&$BAR exec {BAR}>&- unset BAR } # The background process that muxes multiple status bars for one TTY bar_proc() { # Read from the pipe, write to the TTY exec <"$1" >"$TTY" # Delete the pipe when done defer rm "$1" # Reset the scroll region when done defer printf '\e7\e[r\e8\e[J' # Workaround for bash 4: checkwinsize is off by default. We can turn it # on, but we also have to explicitly trigger a foreground job to finish # so that it will update the window size before we use $LINES shopt -s checkwinsize (:) BAR_HEIGHT=0 resize_bar # Adjust the bar when the TTY size changes trap resize_bar WINCH # Map from PID to status bar local -A pid2bar # Read commands of the form "OP:PID:STRING\0" while IFS=':' read -r -d '' op pid str; do # Map the pid to a bar, creating a new one if necessary if [ -z "${pid2bar[$pid]:-}" ]; then pid2bar["$pid"]=$((BAR_HEIGHT++)) resize_bar fi bar="${pid2bar[$pid]}" case "$op" in PRINT) printf '\e7\e[%d;0f\e[K%s\e8' $((TTY_HEIGHT - bar)) "$str" ;; HIDE) bar="${pid2bar[$pid]}" # Delete this status bar unset 'pid2bar[$pid]' # Shift all higher status bars down for i in "${!pid2bar[@]}"; do ibar="${pid2bar[$i]}" if ((ibar > bar)); then pid2bar["$i"]=$((ibar - 1)) fi done ((BAR_HEIGHT--)) resize_bar ;; esac done } # Resize the status bar resize_bar() { # Bash gets $LINES from stderr, so if it's redirected use tput instead TTY_HEIGHT="${LINES:-$(tput lines 2>"$TTY")}" if ((BAR_HEIGHT == 0)); then return fi # Hide the bars temporarily local seq='\e7\e[r\e8\e[J' # Print \eD (IND) N times to ensure N blank lines at the bottom for ((i = 0; i < BAR_HEIGHT; ++i)); do seq="${seq}\\eD" done # Go back up N lines seq="${seq}\\e[${BAR_HEIGHT}A" # Create the new scroll region seq="${seq}\\e7\\e[;$((TTY_HEIGHT - BAR_HEIGHT))r\\e8\\e[J" printf "$seq" }