|
| 1 | +#!/usr/bin/env bash |
| 2 | +# nlt-progress:可复用终端进度条(macOS + Linux)。由其他脚本 source。 |
| 3 | +[[ -n "${_NLT_PROGRESS_LOADED:-}" ]] && return 0 |
| 4 | +_NLT_PROGRESS_LOADED=1 |
| 5 | + |
| 6 | +_nlt_file_size() { |
| 7 | + local f="$1" |
| 8 | + [[ -f "$f" ]] || { echo 0; return 0; } |
| 9 | + if stat -f%z "$f" >/dev/null 2>&1; then |
| 10 | + stat -f%z "$f" |
| 11 | + else |
| 12 | + stat -c%s "$f" 2>/dev/null || echo 0 |
| 13 | + fi |
| 14 | +} |
| 15 | + |
| 16 | +_nlt_pb_now_s() { |
| 17 | + date +%s |
| 18 | +} |
| 19 | + |
| 20 | +_nlt_pb_cols() { |
| 21 | + if [[ -n "${COLUMNS:-}" ]] && [[ "${COLUMNS}" =~ ^[0-9]+$ ]]; then |
| 22 | + echo "${COLUMNS}" |
| 23 | + elif command -v tput >/dev/null 2>&1 && [[ -t 1 ]]; then |
| 24 | + tput cols 2>/dev/null || echo 80 |
| 25 | + else |
| 26 | + echo 80 |
| 27 | + fi |
| 28 | +} |
| 29 | + |
| 30 | +# 字节 → 人类可读(1024 底,一位小数) |
| 31 | +nlt_pb_human_bytes() { |
| 32 | + awk -v n="${1:-0}" 'BEGIN{ |
| 33 | + if (n < 0) n = 0 |
| 34 | + u[1]="KiB"; u[2]="MiB"; u[3]="GiB"; u[4]="TiB" |
| 35 | + if (n < 1024) { printf "%d B", n; exit } |
| 36 | + x = n / 1024.0 |
| 37 | + i = 1 |
| 38 | + while (x >= 1024 && i < 4) { x /= 1024; i++ } |
| 39 | + printf "%.1f %s", x, u[i] |
| 40 | + }' |
| 41 | +} |
| 42 | + |
| 43 | +_nlt_pb_fmt_hms() { |
| 44 | + awk -v s="${1:-0}" 'BEGIN{ |
| 45 | + if (s < 0) s = 0 |
| 46 | + h = int(s / 3600) |
| 47 | + m = int((s % 3600) / 60) |
| 48 | + sec = int(s % 60) |
| 49 | + if (h > 0) printf "%d:%02d:%02d", h, m, sec |
| 50 | + else printf "%d:%02d", m, sec |
| 51 | + }' |
| 52 | +} |
| 53 | + |
| 54 | +_nlt_pb_parse_content_length() { |
| 55 | + local url="$1" |
| 56 | + curl -sI -L "$url" 2>/dev/null | awk 'BEGIN{IGNORECASE=1} /^content-length:/ {v=$2+0} END{print v+0}' |
| 57 | +} |
| 58 | + |
| 59 | +# nlt_pb_render current_bytes total_bytes label start_epoch |
| 60 | +# total_bytes=0 表示总长未知:条形为不确定样式,百分比与 ETA 为 — |
| 61 | +nlt_pb_render() { |
| 62 | + local cur="${1:-0}" total="${2:-0}" label="${3:-}" start="${4:-0}" |
| 63 | + local now cols barw filled pct elapsed rate eta rem slot span |
| 64 | + local cur_h total_h rate_h line bar i ch color denom |
| 65 | + |
| 66 | + [[ "${cur}" =~ ^[0-9]+$ ]] || cur=0 |
| 67 | + [[ "${total}" =~ ^[0-9]+$ ]] || total=0 |
| 68 | + [[ "${start}" =~ ^[0-9]+$ ]] || start="$(_nlt_pb_now_s)" |
| 69 | + |
| 70 | + now="$(_nlt_pb_now_s)" |
| 71 | + elapsed=$((now - start)) |
| 72 | + [[ "$elapsed" -lt 1 ]] && elapsed=1 |
| 73 | + |
| 74 | + cur_h="$(nlt_pb_human_bytes "$cur")" |
| 75 | + |
| 76 | + if [[ ! -t 1 ]]; then |
| 77 | + return 0 |
| 78 | + fi |
| 79 | + |
| 80 | + cols="$(_nlt_pb_cols)" |
| 81 | + barw=$((cols - 52)) |
| 82 | + [[ "$barw" -lt 12 ]] && barw=12 |
| 83 | + [[ "$barw" -gt 40 ]] && barw=40 |
| 84 | + denom=$((barw - 1)) |
| 85 | + [[ "$denom" -lt 1 ]] && denom=1 |
| 86 | + |
| 87 | + rate=$(awk -v c="$cur" -v e="$elapsed" 'BEGIN { if (e > 0) printf "%.0f", c / e; else print 0 }') |
| 88 | + rate_h="$(nlt_pb_human_bytes "$rate")/s" |
| 89 | + |
| 90 | + if [[ "$total" -gt 0 ]]; then |
| 91 | + pct=$(awk -v c="$cur" -v t="$total" 'BEGIN { |
| 92 | + if (t <= 0) { print 0; exit } |
| 93 | + p = 100.0 * c / t |
| 94 | + if (p > 100) p = 100 |
| 95 | + printf "%.1f", p |
| 96 | + }') |
| 97 | + total_h="$(nlt_pb_human_bytes "$total")" |
| 98 | + rem=$((total - cur)) |
| 99 | + [[ "$rem" -lt 0 ]] && rem=0 |
| 100 | + if [[ "$rate" -gt 0 ]]; then |
| 101 | + eta=$(awk -v r="$rem" -v rt="$rate" 'BEGIN { if (rt > 0) printf "%.0f", r / rt; else print 0 }') |
| 102 | + eta="$(_nlt_pb_fmt_hms "$eta")" |
| 103 | + else |
| 104 | + eta="—" |
| 105 | + fi |
| 106 | + filled=$(awk -v c="$cur" -v t="$total" -v w="$barw" 'BEGIN { |
| 107 | + if (t <= 0) { print 0; exit } |
| 108 | + f = int(w * c / t + 0.5) |
| 109 | + if (f > w) f = w |
| 110 | + if (f < 0) f = 0 |
| 111 | + print f |
| 112 | + }') |
| 113 | + else |
| 114 | + pct="—" |
| 115 | + total_h="?" |
| 116 | + eta="—" |
| 117 | + # 不确定:滑动的 4 格亮块 |
| 118 | + span=4 |
| 119 | + slot=$(( (elapsed * 2) % (barw - span + 1) )) |
| 120 | + filled=-1 |
| 121 | + fi |
| 122 | + |
| 123 | + bar="" |
| 124 | + if [[ "$filled" -ge 0 ]]; then |
| 125 | + for ((i = 0; i < barw; i++)); do |
| 126 | + if [[ "$i" -lt "$filled" ]]; then |
| 127 | + color=$((39 + (i * 6 / denom))) |
| 128 | + [[ "$color" -gt 45 ]] && color=45 |
| 129 | + ch=$'█' |
| 130 | + bar+="$(printf '\033[38;5;%dm%s' "$color" "$ch")" |
| 131 | + else |
| 132 | + bar+=$'\033[38;5;238m░' |
| 133 | + fi |
| 134 | + done |
| 135 | + else |
| 136 | + for ((i = 0; i < barw; i++)); do |
| 137 | + if [[ "$i" -ge "$slot" && "$i" -lt $((slot + span)) ]]; then |
| 138 | + color=$((39 + (i * 6 / denom))) |
| 139 | + [[ "$color" -gt 45 ]] && color=45 |
| 140 | + ch=$'█' |
| 141 | + bar+="$(printf '\033[38;5;%dm%s' "$color" "$ch")" |
| 142 | + else |
| 143 | + bar+=$'\033[38;5;238m░' |
| 144 | + fi |
| 145 | + done |
| 146 | + fi |
| 147 | + |
| 148 | + line="$(printf '\r\033[1;36m%s\033[0m [\033[0m%s\033[0m] \033[33m%s%%\033[0m \033[35m%s\033[0m/\033[35m%s\033[0m \033[32m%s\033[0m \033[2melapsed %s\033[0m \033[2mETA %s\033[0m' \ |
| 149 | + "$label" "${bar}" "$pct" "$cur_h" "$total_h" "$rate_h" "$(_nlt_pb_fmt_hms "$elapsed")" "$eta")" |
| 150 | + printf '%s\033[K' "$line" |
| 151 | +} |
| 152 | + |
| 153 | +nlt_pb_done() { |
| 154 | + [[ -t 1 ]] && printf '\n' || true |
| 155 | +} |
| 156 | + |
| 157 | +# nlt_pb_curl_to_file url dest [optional_total_bytes] |
| 158 | +# 可选第三参为已知总字节;否则尝试 HEAD 解析 Content-Length。 |
| 159 | +nlt_pb_curl_to_file() { |
| 160 | + local url="$1" dest="$2" total="${3:-}" |
| 161 | + local start curl_pid ec size last_plain now |
| 162 | + |
| 163 | + [[ -n "$url" && -n "$dest" ]] || return 2 |
| 164 | + command -v curl >/dev/null 2>&1 || return 127 |
| 165 | + |
| 166 | + if [[ -z "$total" || ! "$total" =~ ^[0-9]+$ ]]; then |
| 167 | + total="$(_nlt_pb_parse_content_length "$url")" |
| 168 | + fi |
| 169 | + [[ ! "$total" =~ ^[0-9]+$ ]] && total=0 |
| 170 | + |
| 171 | + start="$(_nlt_pb_now_s)" |
| 172 | + ec=0 |
| 173 | + last_plain=0 |
| 174 | + |
| 175 | + rm -f "$dest" |
| 176 | + curl -fL --connect-timeout 30 -o "$dest" "$url" & |
| 177 | + curl_pid=$! |
| 178 | + |
| 179 | + if [[ -t 1 ]]; then |
| 180 | + while kill -0 "$curl_pid" 2>/dev/null; do |
| 181 | + size="$(_nlt_file_size "$dest")" |
| 182 | + nlt_pb_render "$size" "$total" "${NLT_PB_LABEL:-download}" "$start" |
| 183 | + sleep 0.25 |
| 184 | + done |
| 185 | + else |
| 186 | + while kill -0 "$curl_pid" 2>/dev/null; do |
| 187 | + size="$(_nlt_file_size "$dest")" |
| 188 | + now="$(_nlt_pb_now_s)" |
| 189 | + if [[ $((now - last_plain)) -ge 5 ]]; then |
| 190 | + if [[ "${NONINTERACTIVE:-}" != "1" ]]; then |
| 191 | + if [[ "$total" -gt 0 ]]; then |
| 192 | + printf '%s: %s / %s bytes\n' "${NLT_PB_LABEL:-download}" "$size" "$total" >&2 |
| 193 | + else |
| 194 | + printf '%s: %s bytes\n' "${NLT_PB_LABEL:-download}" "$size" >&2 |
| 195 | + fi |
| 196 | + fi |
| 197 | + last_plain=$now |
| 198 | + fi |
| 199 | + sleep 0.25 |
| 200 | + done |
| 201 | + fi |
| 202 | + |
| 203 | + wait "$curl_pid" || ec=$? |
| 204 | + |
| 205 | + size="$(_nlt_file_size "$dest")" |
| 206 | + if [[ "$total" -eq 0 && "$size" -gt 0 ]]; then |
| 207 | + total=$size |
| 208 | + fi |
| 209 | + if [[ -t 1 ]]; then |
| 210 | + nlt_pb_render "$size" "$total" "${NLT_PB_LABEL:-download}" "$start" |
| 211 | + nlt_pb_done |
| 212 | + fi |
| 213 | + |
| 214 | + return "$ec" |
| 215 | +} |
0 commit comments