| 1 | #!/usr/bin/env bash |
| 2 | # Shared helpers for brt |
| 3 | |
| 4 | # shellcheck source=progress.sh |
| 5 | source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/progress.sh" |
| 6 | |
| 7 | brt_info() { echo -e "\033[36m●\033[0m $*"; } |
| 8 | brt_ok() { echo -e "\033[32m✓\033[0m $*"; } |
| 9 | brt_warn() { echo -e "\033[33m!\033[0m $*" >&2; } |
| 10 | brt_error() { echo -e "\033[31m✗\033[0m $*" >&2; } |
| 11 | |
| 12 | # bash 3.2 + set -u treats empty ${arr[@]} as unbound — use these helpers |
| 13 | brt_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 | |
| 21 | brt_arr_nonempty() { |
| 22 | local name="$1" |
| 23 | eval "local _n=\${#${name}[@]}" |
| 24 | (( _n > 0 )) |
| 25 | } |
| 26 | |
| 27 | brt_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 | |
| 35 | brt_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 | |
| 43 | brt_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 | |
| 48 | brt_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 | |
| 56 | brt_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 | |
| 71 | brt_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 | |
| 79 | brt_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 | |
| 88 | brt_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 | |
| 97 | brt_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 | |
| 103 | brt_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 | |
| 113 | brt_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 | |
| 126 | brt_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 | |
| 142 | brt_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 | |
| 159 | brt_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. |
| 180 | brt_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. |
| 212 | brt_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 | |
| 242 | BRT_STATE_DIR="${HOME}/.brt" |
| 243 | BRT_CONFIG_FILE="${BRT_STATE_DIR}/.env" |
| 244 | BRT_LEGACY_CONFIG_FILE="${BRT_STATE_DIR}/config" |
| 245 | |
| 246 | brt_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 |
| 260 | EOF |
| 261 | } |
| 262 | |
| 263 | brt_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 | |
| 288 | brt_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 | |
| 294 | brt_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 | |
| 305 | brt_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 | |
| 319 | brt_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 | |
| 331 | brt_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 | |
| 351 | brt_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 | |