term.sh 24 KB

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