term.sh 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. #!/usr/bin/env bash
  2. # term.sh — terminal panel design system for claude-mods skills.
  3. #
  4. # Source from any skill script:
  5. # LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" && pwd)"
  6. # . "$LIB/term.sh"
  7. # term_init
  8. #
  9. # Honors: NO_COLOR, FORCE_COLOR, TERM_ASCII=1, FLEET_ASCII=1 (legacy).
  10. # Status: experimental — see docs/TERMINAL-DESIGN.md.
  11. # Guard against double-sourcing.
  12. [[ -n "${__TERM_SH_LOADED:-}" ]] && return 0
  13. __TERM_SH_LOADED=1
  14. # ─── Globals (populated by term_init) ──────────────────────────────────────
  15. TERM_TTY=0
  16. TERM_COLOR=0
  17. TERM_ASCII_MODE=0
  18. TERM_WIDTH=80
  19. # ─── ANSI escapes (empty when color disabled) ─────────────────────────────
  20. TERM_C_GREEN=""
  21. TERM_C_YELLOW=""
  22. TERM_C_ORANGE=""
  23. TERM_C_RED=""
  24. TERM_C_CYAN=""
  25. TERM_C_MAGENTA=""
  26. TERM_C_DIM=""
  27. TERM_C_OFF=""
  28. # ─── Tree connectors (set by term_init based on TERM_ASCII_MODE) ──────────
  29. TERM_TREE_BRANCH="" # ├─ / +-
  30. TERM_TREE_LAST="" # └─ / `-
  31. TERM_TREE_VERT="" # │ / |
  32. # ─── Panel chrome ─────────────────────────────────────────────────────────
  33. TERM_PANEL_TL="" # ╭ / +
  34. TERM_PANEL_BL="" # ╰ / +
  35. TERM_PANEL_HRULE="" # ─ / -
  36. TERM_PANEL_TERM="" # ● / *
  37. # ─── Legacy state icons (kept for backwards-compat with fleet.sh) ─────────
  38. TERM_ICON_PENDING=""
  39. TERM_ICON_READY=""
  40. TERM_ICON_DONE=""
  41. TERM_ICON_FAILED=""
  42. TERM_ICON_WARN=""
  43. TERM_ICON_HINT=""
  44. # ─── Registries (Unicode|ASCII) ───────────────────────────────────────────
  45. declare -A TERM_BRAND=(
  46. [fleet]="⚡|[F]"
  47. [forge]="🔨|[B]"
  48. [psql]="🐘|[P]"
  49. [watch]="📡|[M]"
  50. [deploy]="🚀|[D]"
  51. [git]="🌿|[G]"
  52. [windows-ops]="🩺|[H]"
  53. )
  54. declare -A TERM_HEALTH_GLYPH=(
  55. [healthy]="•|(+)"
  56. [pending]="•|(.)"
  57. [warning]="•|(!)"
  58. [critical]="•|(!!)"
  59. [busted]="⬤|(X)"
  60. [unknown]="•|(?)"
  61. )
  62. declare -A TERM_DIAGRAM_ICON=(
  63. [user]="👤|(U)"
  64. [web]="🌐|(W)"
  65. [mobile]="📱|(M)"
  66. [auth]="🔐|(A)"
  67. [database]="🗄|(D)"
  68. [cache]="⚡|(C)"
  69. [queue]="📨|(Q)"
  70. [storage]="📦|(P)"
  71. [service]="⚙|*"
  72. [api]="🔌|(I)"
  73. [search]="🔍|(S)"
  74. [timer]="⏱|(T)"
  75. [build]="🔨|(B)"
  76. [hook]="🪝|(H)"
  77. [log]="📄|(F)"
  78. )
  79. # Header indicator glyph (branch/⎇)
  80. TERM_GLYPH_BRANCH=""
  81. # Inline alert glyph (▲)
  82. TERM_GLYPH_ALERT=""
  83. # Empty-state tip glyph (💡)
  84. TERM_GLYPH_TIP=""
  85. # Spinner frame banks (set by term_init; arrays keep order).
  86. TERM_SPIN_WORKING=()
  87. TERM_SPIN_HEARTBEAT=()
  88. # ─── term_init ────────────────────────────────────────────────────────────
  89. term_init() {
  90. # TTY detection — stdout only.
  91. if [[ -t 1 ]]; then TERM_TTY=1; else TERM_TTY=0; fi
  92. # ASCII fallback: explicit env, or non-UTF locale.
  93. if [[ "${TERM_ASCII:-}" == "1" ]] || [[ "${FLEET_ASCII:-}" == "1" ]]; then
  94. TERM_ASCII_MODE=1
  95. elif [[ "${LC_ALL:-${LANG:-}}" != *[Uu][Tt][Ff]* ]] && [[ -z "${LC_ALL:-${LANG:-}}" || "${TERM:-}" == "dumb" ]]; then
  96. TERM_ASCII_MODE=1
  97. else
  98. TERM_ASCII_MODE=0
  99. fi
  100. # Color: TTY + not NO_COLOR, or FORCE_COLOR overrides.
  101. if [[ -n "${FORCE_COLOR:-}" ]]; then
  102. TERM_COLOR=1
  103. elif [[ -n "${NO_COLOR:-}" ]] || [[ "$TERM_TTY" -eq 0 ]] || [[ "${TERM:-}" == "dumb" ]]; then
  104. TERM_COLOR=0
  105. else
  106. TERM_COLOR=1
  107. fi
  108. # Terminal width — fall back to 80.
  109. if [[ "$TERM_TTY" -eq 1 ]] && command -v tput >/dev/null 2>&1; then
  110. TERM_WIDTH=$(tput cols 2>/dev/null || echo 80)
  111. fi
  112. [[ "$TERM_WIDTH" -lt 40 ]] && TERM_WIDTH=80
  113. if [[ "$TERM_ASCII_MODE" -eq 1 ]]; then
  114. TERM_ICON_PENDING="[.]"
  115. TERM_ICON_READY="[+]"
  116. TERM_ICON_DONE="[*]"
  117. TERM_ICON_FAILED="[x]"
  118. TERM_ICON_WARN="[!]"
  119. TERM_ICON_HINT="[i]"
  120. TERM_TREE_BRANCH="+-"
  121. TERM_TREE_LAST="\`-"
  122. TERM_TREE_VERT="|"
  123. TERM_PANEL_TL="+"
  124. TERM_PANEL_BL="+"
  125. TERM_PANEL_HRULE="-"
  126. TERM_PANEL_TERM="*"
  127. TERM_GLYPH_BRANCH="(b)"
  128. TERM_GLYPH_ALERT="!"
  129. TERM_GLYPH_TIP="(i)"
  130. TERM_SPIN_WORKING=('|' '/' '-' '\')
  131. TERM_SPIN_HEARTBEAT=('.' ':' '*' ':')
  132. else
  133. TERM_ICON_PENDING="⏳"
  134. TERM_ICON_READY="✅"
  135. TERM_ICON_DONE="🚀"
  136. TERM_ICON_FAILED="❌"
  137. TERM_ICON_WARN="⚠️ "
  138. TERM_ICON_HINT="💡"
  139. TERM_TREE_BRANCH="├─"
  140. TERM_TREE_LAST="└─"
  141. TERM_TREE_VERT="│"
  142. TERM_PANEL_TL="╭"
  143. TERM_PANEL_BL="╰"
  144. TERM_PANEL_HRULE="─"
  145. TERM_PANEL_TERM="●"
  146. TERM_GLYPH_BRANCH="⎇"
  147. TERM_GLYPH_ALERT="▲"
  148. TERM_GLYPH_TIP="💡"
  149. TERM_SPIN_WORKING=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
  150. TERM_SPIN_HEARTBEAT=('·' '∙' '•' '●' '•' '∙')
  151. fi
  152. if [[ "$TERM_COLOR" -eq 1 ]]; then
  153. TERM_C_GREEN=$'\033[32m'
  154. TERM_C_YELLOW=$'\033[33m'
  155. TERM_C_ORANGE=$'\033[38;5;208m'
  156. TERM_C_RED=$'\033[31m'
  157. TERM_C_CYAN=$'\033[36m'
  158. TERM_C_MAGENTA=$'\033[35m'
  159. TERM_C_DIM=$'\033[2m'
  160. TERM_C_OFF=$'\033[0m'
  161. else
  162. TERM_C_GREEN=""; TERM_C_YELLOW=""; TERM_C_ORANGE=""
  163. TERM_C_RED=""; TERM_C_CYAN=""; TERM_C_MAGENTA=""
  164. TERM_C_DIM=""; TERM_C_OFF=""
  165. fi
  166. }
  167. # ─── Color helper ─────────────────────────────────────────────────────────
  168. # term_color <name> <text...>
  169. term_color() {
  170. local name=$1; shift
  171. local code=""
  172. case "$name" in
  173. green) code="$TERM_C_GREEN" ;;
  174. yellow) code="$TERM_C_YELLOW" ;;
  175. orange) code="$TERM_C_ORANGE" ;;
  176. red) code="$TERM_C_RED" ;;
  177. cyan) code="$TERM_C_CYAN" ;;
  178. magenta) code="$TERM_C_MAGENTA" ;;
  179. dim) code="$TERM_C_DIM" ;;
  180. esac
  181. printf '%s%s%s' "$code" "$*" "$TERM_C_OFF"
  182. }
  183. # ─── Registry lookup ──────────────────────────────────────────────────────
  184. # term_emoji <registry_name> <key> — returns Unicode glyph or ASCII fallback.
  185. # Internal helper; pass "BRAND", "HEALTH_GLYPH", "DIAGRAM_ICON".
  186. __term_lookup() {
  187. local map=$1 key=$2 entry uni ascii
  188. case "$map" in
  189. BRAND) entry="${TERM_BRAND[$key]:-}" ;;
  190. HEALTH_GLYPH) entry="${TERM_HEALTH_GLYPH[$key]:-}" ;;
  191. DIAGRAM_ICON) entry="${TERM_DIAGRAM_ICON[$key]:-}" ;;
  192. *) entry="" ;;
  193. esac
  194. [[ -z "$entry" ]] && { printf '%s' "?"; return; }
  195. uni="${entry%|*}"
  196. ascii="${entry#*|}"
  197. if [[ "$TERM_ASCII_MODE" -eq 1 ]]; then printf '%s' "$ascii"
  198. else printf '%s' "$uni"; fi
  199. }
  200. term_brand_glyph() { __term_lookup BRAND "$1"; }
  201. term_health_glyph() { __term_lookup HEALTH_GLYPH "$1"; }
  202. term_diagram_icon() { __term_lookup DIAGRAM_ICON "$1"; }
  203. # ─── Legacy state-icon helper (used by fleet.sh) ──────────────────────────
  204. term_state_icon() {
  205. case "$1" in
  206. RUNNING|PENDING) printf '%s' "$TERM_ICON_PENDING" ;;
  207. READY) printf '%s' "$TERM_ICON_READY" ;;
  208. LANDED|DONE|OK) printf '%s' "$TERM_ICON_DONE" ;;
  209. FAILED|ERROR) printf '%s' "$TERM_ICON_FAILED" ;;
  210. CONFLICT|WARN) printf '%s' "$TERM_ICON_WARN" ;;
  211. HINT|INFO) printf '%s' "$TERM_ICON_HINT" ;;
  212. *) printf '%s' "?" ;;
  213. esac
  214. }
  215. # ─── Primitives ───────────────────────────────────────────────────────────
  216. # term_repeat <char> <n>
  217. term_repeat() {
  218. local ch=$1 n=$2 i out=""
  219. for (( i=0; i<n; i++ )); do out="$out$ch"; done
  220. printf '%s' "$out"
  221. }
  222. # term_truncate <text> <max_cols> — ellipsis-truncate, append "…" or "..".
  223. term_truncate() {
  224. local text=$1 max=$2
  225. local len=${#text}
  226. if [[ $len -le $max ]]; then printf '%s' "$text"; return; fi
  227. local ell="…"
  228. [[ "$TERM_ASCII_MODE" -eq 1 ]] && ell=".."
  229. local elllen=${#ell}
  230. printf '%s%s' "${text:0:$((max - elllen))}" "$ell"
  231. }
  232. # ─── Panel ────────────────────────────────────────────────────────────────
  233. # term_panel_open <emoji_key> <name> [right_indicator]
  234. # ╭── ⚡ name ───────── <indicator> ───●
  235. term_panel_open() {
  236. local key=$1 name=$2 indicator=${3:-}
  237. local emoji
  238. emoji=$(term_brand_glyph "$key")
  239. local left="${TERM_PANEL_TL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${emoji} $(term_color cyan "$name") "
  240. local right=""
  241. if [[ -n "$indicator" ]]; then
  242. right=" $(term_color dim "$indicator") ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")"
  243. else
  244. right="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")"
  245. fi
  246. # Visible (color-stripped) widths to size the rule fill correctly.
  247. local left_vis="${TERM_PANEL_TL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${emoji} ${name} "
  248. local right_vis=""
  249. [[ -n "$indicator" ]] && right_vis=" ${indicator} ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}" \
  250. || right_vis="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}"
  251. local fill=$(( TERM_WIDTH - ${#left_vis} - ${#right_vis} ))
  252. [[ $fill -lt 4 ]] && fill=4
  253. local rule
  254. rule=$(term_repeat "$TERM_PANEL_HRULE" "$fill")
  255. printf '%s%s%s\n' "$left" "$(term_color cyan "$rule")" "$right"
  256. }
  257. # term_panel_close [hotkeys] [health_indicators]
  258. # ╰── R refresh · L land · ? help ───── • daemon • 17m ───●
  259. # `hotkeys`: pre-formatted "R refresh · L land · ? help" string.
  260. # `healths`: pre-formatted "• daemon • 17m" string.
  261. term_panel_close() {
  262. local hotkeys=${1:-} healths=${2:-}
  263. local left="${TERM_PANEL_BL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${hotkeys} "
  264. local right=""
  265. if [[ -n "$healths" ]]; then
  266. right=" ${healths} ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")"
  267. else
  268. right="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")"
  269. fi
  270. local left_vis="${TERM_PANEL_BL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${hotkeys} "
  271. local right_vis=""
  272. [[ -n "$healths" ]] && right_vis=" ${healths} ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}" \
  273. || right_vis="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}"
  274. local fill=$(( TERM_WIDTH - ${#left_vis} - ${#right_vis} ))
  275. [[ $fill -lt 4 ]] && fill=4
  276. local rule
  277. rule=$(term_repeat "$TERM_PANEL_HRULE" "$fill")
  278. printf '%s%s%s\n' "$left" "$(term_color cyan "$rule")" "$right"
  279. }
  280. # term_panel_vert — emit a single body-line spacer "│"
  281. term_panel_vert() {
  282. printf '%s\n' "$(term_color dim "$TERM_TREE_VERT")"
  283. }
  284. # ─── Body components ──────────────────────────────────────────────────────
  285. # term_section <state> <label> <count>
  286. # ├── LABEL (n) (label colored by state)
  287. term_section() {
  288. local state=$1 label=$2 count=$3
  289. local color=""
  290. case "$state" in
  291. RUNNING|PENDING|CONFLICT|WARN|warning) color="yellow" ;;
  292. READY|LANDED|DONE|OK|healthy) color="green" ;;
  293. FAILED|ERROR|critical|alarm) color="red" ;;
  294. *) color="" ;;
  295. esac
  296. local rendered_label="$label"
  297. [[ -n "$color" ]] && rendered_label=$(term_color "$color" "$label")
  298. printf '%s%s %s %s\n' \
  299. "$(term_color dim "$TERM_TREE_VERT")" \
  300. "$(term_color dim "$TERM_TREE_BRANCH$TERM_PANEL_HRULE")" \
  301. "$rendered_label" \
  302. "$(term_color dim "($count)")"
  303. }
  304. # term_summary_line <text> — dim metadata branch
  305. # ├── text
  306. term_summary_line() {
  307. printf '%s%s %s\n' \
  308. "$(term_color dim "$TERM_TREE_VERT")" \
  309. "$(term_color dim "$TERM_TREE_BRANCH$TERM_PANEL_HRULE")" \
  310. "$(term_color dim "$*")"
  311. }
  312. # term_leaf_line <connector> <name> <leaf_glyph> <meta> <age>
  313. # │ ├── name ●─●─●─◉ M4 ?1 12m
  314. # `connector` = ├── or └──
  315. term_leaf_line() {
  316. local conn=$1 name=$2 leaf=$3 meta=${4:-} age=${5:-}
  317. local trunc_name
  318. trunc_name=$(term_truncate "$name" 28)
  319. printf '%s %s %-28s %-14s %-10s %s\n' \
  320. "$(term_color dim "$TERM_TREE_VERT")" \
  321. "$(term_color dim "$conn$TERM_PANEL_HRULE")" \
  322. "$trunc_name" \
  323. "$leaf" \
  324. "$(term_color dim "$meta")" \
  325. "$(term_color dim "$age")"
  326. }
  327. # term_toast <emoji_key> <text> — ├── ⚡ text (dim cyan)
  328. term_toast() {
  329. local key=$1; shift
  330. local emoji
  331. emoji=$(term_brand_glyph "$key")
  332. printf '%s%s %s\n' \
  333. "$(term_color dim "$TERM_TREE_VERT")" \
  334. "$(term_color dim "$TERM_TREE_BRANCH$TERM_PANEL_HRULE")" \
  335. "$(term_color cyan "$emoji $*")"
  336. }
  337. # term_alert <severity> <text> — ▲ message (orange/red), as a sub-row
  338. # `severity` = warning | critical
  339. term_alert() {
  340. local sev=$1; shift
  341. local color="orange"
  342. [[ "$sev" == "critical" ]] && color="red"
  343. printf '%s %s %s %s\n' \
  344. "$(term_color dim "$TERM_TREE_VERT")" \
  345. "$(term_color dim "$TERM_TREE_VERT")" \
  346. "$(term_color "$color" "$TERM_GLYPH_ALERT")" \
  347. "$*"
  348. }
  349. # ─── Leaf glyph builders ──────────────────────────────────────────────────
  350. # term_rail <commits_ahead> <head_state>
  351. # head_state: HEAD | CONFLICT | EMPTY
  352. # Examples:
  353. # term_rail 3 HEAD → ●─●─●─◉
  354. # term_rail 4 HEAD → ●─●─●─●─◉
  355. # term_rail 1 HEAD → ●─◉
  356. # term_rail 3 CONFLICT → ●─●─⊗
  357. # term_rail 0 EMPTY → ─
  358. term_rail() {
  359. local n=$1 head=${2:-HEAD}
  360. local commit="●"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && commit="*"
  361. local link="─"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && link="-"
  362. local headg="◉"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && headg="@"
  363. local conflict="⊗"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && conflict="X"
  364. if [[ $n -le 0 && "$head" == "EMPTY" ]]; then printf '%s' "$link"; return; fi
  365. local out=""
  366. local i
  367. # n landed commits, joined by links
  368. for (( i=0; i<n-1; i++ )); do
  369. out="${out}$(term_color green "$commit")${link}"
  370. done
  371. # final glyph
  372. case "$head" in
  373. HEAD)
  374. if [[ $n -ge 1 ]]; then out="${out}$(term_color green "$commit")${link}"; fi
  375. out="${out}$(term_color yellow "$headg")"
  376. ;;
  377. CONFLICT)
  378. if [[ $n -ge 1 ]]; then out="${out}$(term_color green "$commit")${link}"; fi
  379. out="${out}$(term_color red "$conflict")"
  380. ;;
  381. *)
  382. [[ $n -ge 1 ]] && out="${out}$(term_color green "$commit")"
  383. ;;
  384. esac
  385. printf '%s' "$out"
  386. }
  387. # term_pip_bar <metric_type> <filled> <total>
  388. # metric_type: progress | score | capacity
  389. # filled / total are integers (e.g., 30, 100)
  390. term_pip_bar() {
  391. local kind=$1 filled=$2 total=$3
  392. local pip_full="▰"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && pip_full="#"
  393. local pip_empty="▱"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && pip_empty="-"
  394. local width=10
  395. [[ "$total" -ne 100 && "$total" -gt 0 && "$total" -le 12 ]] && width=$total
  396. # Pip count
  397. local pips
  398. if [[ "$total" -eq 100 ]]; then
  399. pips=$(( filled / 10 ))
  400. else
  401. pips=$filled
  402. fi
  403. [[ $pips -lt 0 ]] && pips=0
  404. [[ $pips -gt $width ]] && pips=$width
  405. # Color selection
  406. local color="green"
  407. local pct=$(( total > 0 ? filled * 100 / total : 0 ))
  408. case "$kind" in
  409. progress) color="yellow"; [[ $pct -ge 100 ]] && color="green" ;;
  410. score) if [[ $pct -lt 33 ]]; then color="red"
  411. elif [[ $pct -lt 66 ]]; then color="yellow"
  412. else color="green"; fi ;;
  413. capacity) if [[ $pct -ge 80 ]]; then color="red"
  414. elif [[ $pct -ge 60 ]]; then color="yellow"
  415. else color="green"; fi ;;
  416. esac
  417. local i out=""
  418. for (( i=0; i<pips; i++ )); do out="${out}$(term_color "$color" "$pip_full")"; done
  419. for (( i=pips; i<width; i++ )); do out="${out}$(term_color dim "$pip_empty")"; done
  420. printf '%s' "$out"
  421. }
  422. # ─── Right-side furniture ─────────────────────────────────────────────────
  423. # term_health <state> <text> — • text (colored bullet, with ⬤ for busted)
  424. # state: healthy|pending|warning|critical|busted|unknown
  425. term_health() {
  426. local state=$1; shift
  427. local glyph
  428. glyph=$(term_health_glyph "$state")
  429. local color=""
  430. case "$state" in
  431. healthy) color="green" ;;
  432. pending) color="yellow" ;;
  433. warning) color="orange" ;;
  434. critical) color="red" ;;
  435. busted) color="dim" ;;
  436. *) color="dim" ;;
  437. esac
  438. printf '%s %s' "$(term_color "$color" "$glyph")" "$*"
  439. }
  440. # term_hotkey <key> <verb> — "R refresh" (key in cyan)
  441. term_hotkey() {
  442. printf '%s %s' "$(term_color cyan "$1")" "$2"
  443. }
  444. # ─── Spinners (live mode) ─────────────────────────────────────────────────
  445. # term_spinner_frame <family> <tick> — return frame at `tick % frames`.
  446. # family: working | heartbeat
  447. term_spinner_frame() {
  448. local fam=$1 tick=$2
  449. local -a frames
  450. case "$fam" in
  451. working) frames=("${TERM_SPIN_WORKING[@]}") ;;
  452. heartbeat) frames=("${TERM_SPIN_HEARTBEAT[@]}") ;;
  453. *) printf '?'; return ;;
  454. esac
  455. local n=${#frames[@]}
  456. printf '%s' "${frames[$(( tick % n ))]}"
  457. }
  458. # ─── Legacy / kept-for-compat helpers (used by older scripts) ─────────────
  459. # term_header <title> [meta] — "── title ────── meta" (legacy)
  460. term_header() {
  461. local title=$1 meta=${2:-}
  462. local glyph="─"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && glyph="-"
  463. local pad=$(( TERM_WIDTH - ${#title} - 6 ))
  464. [[ $pad -lt 4 ]] && pad=4
  465. local line
  466. line="$(term_repeat "$glyph" 2) $(term_color cyan "$title") $(term_repeat "$glyph" "$pad")"
  467. if [[ -n "$meta" ]]; then
  468. printf '%s %s\n' "$line" "$(term_color dim "$meta")"
  469. else
  470. printf '%s\n' "$line"
  471. fi
  472. }
  473. term_divider() {
  474. local w=${1:-$TERM_WIDTH}
  475. local glyph="─"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && glyph="-"
  476. printf '%s\n' "$(term_repeat "$glyph" "$w")"
  477. }
  478. term_tree_item() {
  479. local icon=$1 label=$2 meta=${3:-}
  480. if [[ -n "$meta" ]]; then
  481. printf ' %s %-32s %s\n' "$icon" "$label" "$(term_color dim "$meta")"
  482. else
  483. printf ' %s %s\n' "$icon" "$label"
  484. fi
  485. }
  486. term_tree_connector() {
  487. if [[ "$1" -eq "$2" ]]; then printf '%s' "$TERM_TREE_LAST"
  488. else printf '%s' "$TERM_TREE_BRANCH"; fi
  489. }
  490. term_tree_indent() {
  491. if [[ "$1" -eq 1 ]]; then printf ' '
  492. else printf '%s ' "$TERM_TREE_VERT"; fi
  493. }
  494. term_tree_node() {
  495. local prefix=$1 conn=$2 label=$3 meta=${4:-}
  496. if [[ -n "$meta" ]]; then
  497. printf '%s%s %-32s %s\n' "$prefix" "$conn" "$label" "$(term_color dim "$meta")"
  498. else
  499. printf '%s%s %s\n' "$prefix" "$conn" "$label"
  500. fi
  501. }
  502. term_table_row() {
  503. printf ' %-2s %-32s %-10s %s\n' "${1:-}" "${2:-}" "${3:-}" "${4:-}"
  504. }
  505. term_empty() {
  506. printf ' %s\n' "$(term_color dim "($*)")"
  507. }