diff options
Diffstat (limited to 'tests/color.sh')
-rw-r--r-- | tests/color.sh | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/tests/color.sh b/tests/color.sh new file mode 100644 index 0000000..9e2e0f6 --- /dev/null +++ b/tests/color.sh @@ -0,0 +1,151 @@ +#!/hint/bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# 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="${TMPDIR:-/tmp}/bfs${TTY//\//-}.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" >/dev/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>/dev/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" +} |