summaryrefslogtreecommitdiffstats
path: root/tests/color.sh
diff options
context:
space:
mode:
Diffstat (limited to 'tests/color.sh')
-rw-r--r--tests/color.sh151
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"
+}