summaryrefslogtreecommitdiffstats
path: root/tests/color.sh
blob: 4f4312e009e7f157812afdd25c9e474a4c8d16ef (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#!/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
    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"
}