term.sh 23 KB

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