lib/common.sh

9.8 KB · 383 lines · bash

1#!/usr/bin/env bash
2# Shared helpers for brt
3
4# shellcheck source=progress.sh
5source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/progress.sh"
6
7brt_info() { echo -e "\033[36m●\033[0m $*"; }
8brt_ok() { echo -e "\033[32m✓\033[0m $*"; }
9brt_warn() { echo -e "\033[33m!\033[0m $*" >&2; }
10brt_error() { echo -e "\033[31m✗\033[0m $*" >&2; }
11
12# bash 3.2 + set -u treats empty ${arr[@]} as unbound — use these helpers
13brt_arr_append() {
14 local dest_name="$1"
15 local src_name="$2"
16 eval "local _src=(\"\${${src_name}[@]+${src_name}[@]}\")"
17 ((${#_src[@]})) || return 0
18 eval "${dest_name}+=(\"\${_src[@]}\")"
19}
20
21brt_arr_nonempty() {
22 local name="$1"
23 eval "local _n=\${#${name}[@]}"
24 (( _n > 0 ))
25}
26
27brt_require() {
28 local cmd="$1"
29 if ! command -v "$cmd" &>/dev/null; then
30 brt_error "'${cmd}' is required but not installed."
31 exit 1
32 fi
33}
34
35brt_random_token() {
36 if command -v openssl &>/dev/null; then
37 openssl rand -hex 24
38 else
39 head -c 32 /dev/urandom | xxd -p -c 64
40 fi
41}
42
43brt_local_ips() {
44 ifconfig 2>/dev/null | awk '/inet / && $2 != "127.0.0.1" { print $2 }' || \
45 ip -4 addr show 2>/dev/null | awk '/inet / { split($2,a,"/"); print a[1] }' | grep -v '^127\.' || true
46}
47
48brt_get_public_ip() {
49 local ip=""
50 for url in "https://api.ipify.org" "https://ifconfig.me/ip" "https://icanhazip.com"; do
51 ip=$(curl -fsSL --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]') && [[ -n "$ip" ]] && break
52 done
53 echo "$ip"
54}
55
56brt_read_version() {
57 local dir="${1:-}"
58 local version=""
59
60 if [[ -n "$dir" && -f "${dir}/version" ]]; then
61 version=$(tr -d '[:space:]' < "${dir}/version")
62 fi
63
64 if [[ -z "$version" && -n "$dir" && -f "${dir}/brt" ]]; then
65 version=$(grep '^BRT_VERSION=' "${dir}/brt" 2>/dev/null | head -1 | cut -d'"' -f2)
66 fi
67
68 echo "$version"
69}
70
71brt_os_is_windows() {
72 [[ "${OS:-}" == "Windows_NT" ]] && return 0
73 case "$(uname -s 2>/dev/null)" in
74 MINGW*|MSYS*|CYGWIN*) return 0 ;;
75 esac
76 return 1
77}
78
79brt_install_hint_primary() {
80 local base="${BRT_BASE_URL:-https://brt.sushii.dev}"
81 if brt_os_is_windows; then
82 echo "irm ${base}/win/install.ps1 | iex"
83 else
84 echo "curl -fsSL ${base}/install.sh | sh"
85 fi
86}
87
88brt_install_hint_secondary() {
89 local base="${BRT_BASE_URL:-https://brt.sushii.dev}"
90 if brt_os_is_windows; then
91 echo "macOS / Linux: curl -fsSL ${base}/install.sh | sh"
92 else
93 echo "Windows: irm ${base}/win/install.ps1 | iex"
94 fi
95}
96
97brt_print_install_hints() {
98 echo "Install: $(brt_install_hint_primary)"
99 echo "$(brt_install_hint_secondary)"
100 echo " Remote access works across brt and brtwin."
101}
102
103brt_show_pending_update_notice() {
104 local f="${HOME}/.brt/update-available"
105 [[ -f "$f" ]] || return 0
106 local msg
107 msg=$(tr -d '\n' < "$f" 2>/dev/null || true)
108 rm -f "$f"
109 [[ -n "$msg" ]] || return 0
110 brt_warn "brt update available: ${msg} (run: brt -update)"
111}
112
113brt_maybe_check_updates_async() {
114 [[ "${BRT_AUTO_UPDATE:-1}" == "0" ]] && return 0
115 local lib_dir
116 lib_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
117 (
118 # shellcheck source=/dev/null
119 . "${lib_dir}/update.sh"
120 brt_remove_startup_check_hook
121 brt_update_check_startup
122 ) >/dev/null 2>&1 &
123 disown 2>/dev/null || true
124}
125
126brt_realpath() {
127 local path="$1"
128 local dir base
129
130 while [[ -L "$path" ]]; do
131 dir=$(cd "$(dirname "$path")" && pwd)
132 base=$(readlink "$path")
133 [[ "$base" != /* ]] && base="${dir}/${base}"
134 path="$base"
135 done
136
137 dir=$(cd "$(dirname "$path")" && pwd)
138 base=$(basename "$path")
139 echo "${dir}/${base}"
140}
141
142brt_version_cmd() {
143 local ver active active_real self_real
144 ver=$(brt_read_version "$BRT_DIR")
145 ver="${ver:-unknown}"
146 echo "brt ${ver}"
147
148 active=$(command -v brt 2>/dev/null || true)
149 self_real=$(brt_realpath "${BRT_DIR}/brt" 2>/dev/null || echo "${BRT_DIR}/brt")
150 if [[ -n "$active" ]]; then
151 active_real=$(brt_realpath "$active" 2>/dev/null || echo "$active")
152 if [[ "$active_real" != "$self_real" ]]; then
153 brt_warn "PATH points to a different brt: ${active}"
154 brt_info "This binary: ${self_real}"
155 fi
156 fi
157}
158
159brt_parse_ports() {
160 # Outputs one port per line. Accepts: "80", "1-1024", "22,80,443", or empty for defaults.
161 local spec="${1:-}"
162 if [[ -z "$spec" ]]; then
163 echo "21 22 23 25 53 80 110 111 135 139 143 443 445 993 995 1723 3306 3389 5900 8080 8443 8888 9000 9090" | tr ' ' '\n'
164 return
165 fi
166 if [[ "$spec" == *-* ]]; then
167 local start="${spec%-*}" end="${spec#*-}"
168 seq "$start" "$end" 2>/dev/null
169 return
170 fi
171 if [[ "$spec" == *,* ]]; then
172 echo "$spec" | tr ',' '\n'
173 return
174 fi
175 echo "$spec"
176}
177
178# Terminal media size in character cells (columns rows).
179# Pass explicit width/height as $1/$2; 0 means auto-detect from terminal.
180brt_term_media_size() {
181 local req_width="${1:-0}"
182 local req_height="${2:-0}"
183 local cols rows width height
184
185 cols=$(tput cols 2>/dev/null || true)
186 rows=$(tput lines 2>/dev/null || true)
187 cols=${cols:-${COLUMNS:-80}}
188 rows=${rows:-${LINES:-24}}
189
190 if (( req_width > 0 )); then
191 width=$req_width
192 else
193 width=$cols
194 fi
195
196 if (( req_height > 0 )); then
197 height=$req_height
198 else
199 height=$rows
200 fi
201
202 if (( width < 1 )); then width=1; fi
203 if (( height < 1 )); then height=1; fi
204 if (( width > cols )); then width=$cols; fi
205 if (( height > rows )); then height=$rows; fi
206
207 printf '%s %s\n' "$width" "$height"
208}
209
210# Largest cols x rows that fit video aspect within terminal cells.
211# $1/$2 = video pixels, $3/$4 = max cols/rows, $5/$6 = cell pixel size.
212brt_term_media_size_fit() {
213 local vid_w="${1:-0}"
214 local vid_h="${2:-0}"
215 local max_cols="${3:-80}"
216 local max_rows="${4:-24}"
217 local cell_w="${5:-9}"
218 local cell_h="${6:-18}"
219 local cols rows
220
221 if (( vid_w < 1 || vid_h < 1 || max_cols < 1 || max_rows < 1 )); then
222 printf '%s %s\n' "$max_cols" "$max_rows"
223 return 0
224 fi
225
226 cols=$max_cols
227 rows=$(( cols * cell_w * vid_h / ( vid_w * cell_h ) ))
228 (( rows < 1 )) && rows=1
229
230 if (( rows > max_rows )); then
231 rows=$max_rows
232 cols=$(( rows * vid_w * cell_h / ( vid_h * cell_w ) ))
233 fi
234
235 (( cols > max_cols )) && cols=$max_cols
236 (( cols < 1 )) && cols=1
237 (( rows < 1 )) && rows=1
238
239 printf '%s %s\n' "$cols" "$rows"
240}
241
242BRT_STATE_DIR="${HOME}/.brt"
243BRT_CONFIG_FILE="${BRT_STATE_DIR}/.env"
244BRT_LEGACY_CONFIG_FILE="${BRT_STATE_DIR}/config"
245
246brt_config_write_template() {
247 mkdir -p "$BRT_STATE_DIR"
248 cat >"$BRT_CONFIG_FILE" <<'EOF'
249# brt configuration — edit with: brt -config
250
251# Browse search (https://serper.dev)
252# BRT_SERPER_API_KEY=your-key-here
253
254# Google Custom Search (optional)
255# BRT_GOOGLE_API_KEY=
256# BRT_GOOGLE_CX=
257
258# Auto-update on terminal startup (1 = on, 0 = off)
259# BRT_AUTO_UPDATE=1
260EOF
261}
262
263brt_config_migrate_legacy() {
264 local line key val
265 [[ -f "$BRT_LEGACY_CONFIG_FILE" && ! -f "$BRT_CONFIG_FILE" ]] || return 0
266 brt_config_write_template
267 while IFS= read -r line || [[ -n "$line" ]]; do
268 [[ "$line" =~ ^[[:space:]]*# ]] && continue
269 if [[ "$line" =~ ^[[:space:]]*export[[:space:]]+([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
270 key="${BASH_REMATCH[1]}"
271 val="${BASH_REMATCH[2]}"
272 val="${val%\"}"
273 val="${val#\"}"
274 val="${val%\'}"
275 val="${val#\'}"
276 [[ -n "$val" ]] || continue
277 if grep -q "^[[:space:]]*#*[[:space:]]*${key}=" "$BRT_CONFIG_FILE" 2>/dev/null; then
278 sed -i.bak "s|^[[:space:]]*#*[[:space:]]*${key}=.*|${key}=${val}|" "$BRT_CONFIG_FILE" 2>/dev/null || true
279 rm -f "${BRT_CONFIG_FILE}.bak"
280 else
281 printf '%s=%s\n' "$key" "$val" >>"$BRT_CONFIG_FILE"
282 fi
283 fi
284 done <"$BRT_LEGACY_CONFIG_FILE"
285 mv "$BRT_LEGACY_CONFIG_FILE" "${BRT_LEGACY_CONFIG_FILE}.bak" 2>/dev/null || true
286}
287
288brt_ensure_config_file() {
289 mkdir -p "$BRT_STATE_DIR"
290 brt_config_migrate_legacy
291 [[ -f "$BRT_CONFIG_FILE" ]] || brt_config_write_template
292}
293
294brt_env_unquote() {
295 local val="$1"
296 if [[ "$val" =~ ^\"(.*)\"$ ]]; then
297 printf '%s' "${BASH_REMATCH[1]}"
298 elif [[ "$val" =~ ^\'(.*)\'$ ]]; then
299 printf '%s' "${BASH_REMATCH[1]}"
300 else
301 printf '%s' "$val"
302 fi
303}
304
305brt_load_config() {
306 brt_ensure_config_file
307 local line key val
308 while IFS= read -r line || [[ -n "$line" ]]; do
309 line="${line#"${line%%[![:space:]]*}"}"
310 [[ -z "$line" || "$line" == \#* ]] && continue
311 if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
312 key="${BASH_REMATCH[1]}"
313 val="$(brt_env_unquote "${BASH_REMATCH[2]}")"
314 export "${key}=${val}"
315 fi
316 done <"$BRT_CONFIG_FILE"
317}
318
319brt_config_mask() {
320 local value="$1"
321 local len=${#value}
322 if (( len == 0 )); then
323 printf 'not set'
324 elif (( len <= 4 )); then
325 printf 'set (****)'
326 else
327 printf 'set (****%s)' "${value: -4}"
328 fi
329}
330
331brt_config_show() {
332 local serper google_key google_cx auto_update
333
334 serper="${BRT_SERPER_API_KEY:-}"
335 google_key="${BRT_GOOGLE_API_KEY:-}"
336 google_cx="${BRT_GOOGLE_CX:-}"
337 auto_update="${BRT_AUTO_UPDATE:-1}"
338
339 brt_info "Config: ${BRT_CONFIG_FILE}"
340 echo
341 printf ' %-22s %s\n' "BRT_SERPER_API_KEY" "$(brt_config_mask "$serper")"
342 printf ' %-22s %s\n' "BRT_GOOGLE_API_KEY" "$(brt_config_mask "$google_key")"
343 printf ' %-22s %s\n' "BRT_GOOGLE_CX" "$(brt_config_mask "$google_cx")"
344 if [[ -n "$auto_update" ]]; then
345 printf ' %-22s %s\n' "BRT_AUTO_UPDATE" "$auto_update"
346 else
347 printf ' %-22s %s\n' "BRT_AUTO_UPDATE" "1 (default)"
348 fi
349}
350
351brt_config() {
352 local action="${1:-edit}"
353
354 case "$action" in
355 show|status)
356 brt_load_config
357 brt_config_show
358 ;;
359 path)
360 brt_ensure_config_file
361 echo "$BRT_CONFIG_FILE"
362 ;;
363 edit|"")
364 brt_ensure_config_file
365 if [[ ! -t 0 || ! -t 1 ]]; then
366 brt_error "Config editor requires an interactive terminal"
367 brt_info "Edit manually: ${BRT_CONFIG_FILE}"
368 exit 1
369 fi
370 # shellcheck source=edit.sh
371 source "${BRT_LIB}/edit.sh"
372 BRT_EDITOR_FORCE=1 brt_edit "$BRT_CONFIG_FILE"
373 brt_load_config
374 brt_ok "Saved ${BRT_CONFIG_FILE}"
375 brt_config_show
376 ;;
377 *)
378 brt_error "Usage: brt -config [edit|show|path]"
379 exit 1
380 ;;
381 esac
382}
383