My Nix configuration. Enter at your own risk.
1#!/usr/bin/env bash
2# Usage: run-quiet TITLE -- COMMAND [ARGS...]
3#
4# Runs COMMAND while showing a spinner and the last PREVIEW_LINES of output
5# beneath it. On success, the live area is wiped clean. On failure, the
6# full captured output is printed and the command's exit code is returned.
7#
8# The command is run inside a pseudo-TTY (via util-linux `script`) so that
9# tools which disable color when their stdout isn't a terminal still produce
10# colored output.
11#
12# Configure preview height with the PREVIEW_LINES env var (default: 5).
13
14set -euo pipefail
15
16PREVIEW_LINES=${PREVIEW_LINES:-5}
17
18if [[ $# -lt 3 ]]; then
19 echo "Usage: $0 TITLE -- COMMAND [ARGS...]" >&2
20 exit 2
21fi
22
23title="$1"
24shift
25if [[ "${1:-}" != "--" ]]; then
26 echo "Expected '--' after TITLE" >&2
27 exit 2
28fi
29shift
30
31log=$(mktemp)
32pid=""
33cleanup() {
34 if [[ -n "$pid" ]]; then
35 kill "$pid" 2>/dev/null || true
36 wait "$pid" 2>/dev/null || true
37 fi
38 tput cnorm 2>/dev/null || true
39 rm -f "$log"
40}
41trap cleanup EXIT INT TERM
42
43# Hide the cursor while spinning
44tput civis 2>/dev/null || true
45
46frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
47n_frames=${#frames[@]}
48total=$((PREVIEW_LINES + 1))
49back=$((total - 1))
50
51# Styles
52RESET=$'\033[0m'
53BOLD=$'\033[1m'
54DIM=$'\033[2m'
55MAGENTA=$'\033[35m'
56
57PREFIX="${DIM}│${RESET} "
58PREFIX_WIDTH=2
59
60# Reserve vertical space and move to the top of the live area
61for ((i = 0; i < back; i++)); do echo; done
62printf '\033[%dA\r' "$back"
63
64# Pull leading sudo (+ its short flags) out of the script -c payload so
65# sudo runs in the user's real tty (cache hit) rather than inside the
66# new PTY (cache miss with no stdin to read a password from).
67sudo_prefix=()
68if [[ "${1:-}" == "sudo" ]]; then
69 sudo_prefix+=("$1"); shift
70 while [[ ${#@} -gt 0 && "${1}" == -* ]]; do
71 sudo_prefix+=("$1"); shift
72 done
73fi
74
75# Run inside a PTY so the command produces colored output. BSD `script`
76# (macOS) and util-linux `script` (Linux) take incompatible flags, so
77# branch on OS:
78# -q: quiet (both)
79# -e: return the child's exit status (both)
80# -F (BSD) / -f (util-linux): flush after each write
81# BSD takes the command as positional args after the typescript file;
82# util-linux needs the command as a single string via -c.
83if [[ "$(uname)" == "Darwin" ]]; then
84 ${sudo_prefix[@]+"${sudo_prefix[@]}"} script -qeF /dev/null "$@" > "$log" 2>&1 &
85else
86 cmd_str=""
87 for arg in "$@"; do
88 cmd_str+="$(printf '%q ' "$arg")"
89 done
90 ${sudo_prefix[@]+"${sudo_prefix[@]}"} script -qefc "$cmd_str" /dev/null > "$log" 2>&1 &
91fi
92pid=$!
93
94frame=0
95while kill -0 "$pid" 2>/dev/null; do
96 cols=$(tput cols)
97 content_cols=$((cols - PREFIX_WIDTH))
98 title_cols=$((cols - 2))
99 [ "$content_cols" -lt 1 ] && content_cols=1
100 [ "$title_cols" -lt 1 ] && title_cols=1
101
102 # --- Title line ---
103 spin="${frames[$((frame % n_frames))]}"
104 raw_title="${title:0:title_cols}"
105 title_padded=$(printf '%-*s' "$title_cols" "$raw_title")
106 printf '%s%s%s %s%s%s\n' \
107 "$MAGENTA" "$spin" "$RESET" \
108 "$BOLD" "$title_padded" "$RESET"
109
110 # --- Preview lines ---
111 # Normalize line endings: strip trailing \r (from CRLF), then convert
112 # any standalone \r (progress-bar updates) to \n.
113 sed 's/\r$//' "$log" | tr '\r' '\n' \
114 | awk -v max="$content_cols" \
115 -v prefix="$PREFIX" \
116 -v dim="$DIM" \
117 -v reset="$RESET" \
118 -v n="$PREVIEW_LINES" '
119 function fit(s, m, out, vis, i, c, len, j, esc) {
120 out = ""; vis = 0; esc = ""
121 len = length(s)
122 for (i = 1; i <= len; i++) {
123 c = substr(s, i, 1)
124 if (esc != "") {
125 esc = esc c
126 if (c ~ /[A-Za-z]/) {
127 if (c == "m") out = out esc dim
128 esc = ""
129 }
130 } else if (c == "\033") {
131 esc = c
132 } else {
133 if (vis >= m) break
134 out = out c; vis++
135 }
136 }
137 for (j = vis; j < m; j++) out = out " "
138 return out
139 }
140 # Skip lines that are empty/whitespace-only after stripping ANSI.
141 {
142 v = $0
143 gsub(/\033\[[0-9;]*[a-zA-Z]/, "", v)
144 if (v ~ /^[[:space:]]*$/) next
145 lines[count++] = $0
146 }
147 END {
148 empty = ""
149 for (j = 0; j < max; j++) empty = empty " "
150 start = count > n ? count - n : 0
151 shown = count - start
152 for (i = 0; i < n; i++) {
153 nl = (i < n - 1 ? "\n" : "")
154 if (i < shown) {
155 printf "%s%s%s%s%s", prefix, dim, fit(lines[start + i], max), reset, nl
156 } else {
157 printf "%s%s%s", prefix, empty, nl
158 }
159 }
160 }
161 '
162
163 printf '\033[%dA\r' "$back"
164
165 frame=$((frame + 1))
166 sleep 0.1
167done
168
169ret=0
170wait "$pid" || ret=$?
171pid=""
172
173printf '\033[J'
174
175if [ "$ret" -ne 0 ]; then
176 gum log --level error "$title (exit $ret)"
177 sed 's/\r$//' "$log"
178 exit "$ret"
179fi