#!/usr/bin/env bash # term.sh — terminal panel design system for claude-mods skills. # # Source from any skill script: # LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" && pwd)" # . "$LIB/term.sh" # term_init # # Honors: NO_COLOR, FORCE_COLOR, TERM_ASCII=1, FLEET_ASCII=1 (legacy). # Status: experimental — see docs/TERMINAL-DESIGN.md. # Guard against double-sourcing. [[ -n "${__TERM_SH_LOADED:-}" ]] && return 0 __TERM_SH_LOADED=1 # ─── Globals (populated by term_init) ────────────────────────────────────── TERM_TTY=0 TERM_COLOR=0 TERM_ASCII_MODE=0 TERM_WIDTH=80 # ─── ANSI escapes (empty when color disabled) ───────────────────────────── TERM_C_GREEN="" TERM_C_YELLOW="" TERM_C_ORANGE="" TERM_C_RED="" TERM_C_CYAN="" TERM_C_MAGENTA="" TERM_C_DIM="" TERM_C_OFF="" # ─── Tree connectors (set by term_init based on TERM_ASCII_MODE) ────────── TERM_TREE_BRANCH="" # ├─ / +- TERM_TREE_LAST="" # └─ / `- TERM_TREE_VERT="" # │ / | # ─── Panel chrome ───────────────────────────────────────────────────────── TERM_PANEL_TL="" # ╭ / + TERM_PANEL_BL="" # ╰ / + TERM_PANEL_HRULE="" # ─ / - TERM_PANEL_TERM="" # ● / * # ─── Legacy state icons (kept for backwards-compat with fleet.sh) ───────── TERM_ICON_PENDING="" TERM_ICON_READY="" TERM_ICON_DONE="" TERM_ICON_FAILED="" TERM_ICON_WARN="" TERM_ICON_HINT="" # ─── Registries (Unicode|ASCII) ─────────────────────────────────────────── declare -A TERM_BRAND=( [fleet]="⚡|[F]" [forge]="🔨|[B]" [psql]="🐘|[P]" [watch]="📡|[M]" [deploy]="🚀|[D]" [git]="🌿|[G]" [windows-ops]="🩺|[H]" ) declare -A TERM_HEALTH_GLYPH=( [healthy]="•|(+)" [pending]="•|(.)" [warning]="•|(!)" [critical]="•|(!!)" [busted]="⬤|(X)" [unknown]="•|(?)" ) declare -A TERM_DIAGRAM_ICON=( [user]="👤|(U)" [web]="🌐|(W)" [mobile]="📱|(M)" [auth]="🔐|(A)" [database]="🗄|(D)" [cache]="⚡|(C)" [queue]="📨|(Q)" [storage]="📦|(P)" [service]="⚙|*" [api]="🔌|(I)" [search]="🔍|(S)" [timer]="⏱|(T)" [build]="🔨|(B)" [hook]="🪝|(H)" [log]="📄|(F)" ) # Header indicator glyph (branch/⎇) TERM_GLYPH_BRANCH="" # Inline alert glyph (▲) TERM_GLYPH_ALERT="" # Empty-state tip glyph (💡) TERM_GLYPH_TIP="" # Spinner frame banks (set by term_init; arrays keep order). TERM_SPIN_WORKING=() TERM_SPIN_HEARTBEAT=() # ─── term_init ──────────────────────────────────────────────────────────── term_init() { # TTY detection — stdout only. if [[ -t 1 ]]; then TERM_TTY=1; else TERM_TTY=0; fi # ASCII fallback: explicit env, or non-UTF locale. if [[ "${TERM_ASCII:-}" == "1" ]] || [[ "${FLEET_ASCII:-}" == "1" ]]; then TERM_ASCII_MODE=1 elif [[ "${LC_ALL:-${LANG:-}}" != *[Uu][Tt][Ff]* ]] && [[ -z "${LC_ALL:-${LANG:-}}" || "${TERM:-}" == "dumb" ]]; then TERM_ASCII_MODE=1 else TERM_ASCII_MODE=0 fi # Color: TTY + not NO_COLOR, or FORCE_COLOR overrides. if [[ -n "${FORCE_COLOR:-}" ]]; then TERM_COLOR=1 elif [[ -n "${NO_COLOR:-}" ]] || [[ "$TERM_TTY" -eq 0 ]] || [[ "${TERM:-}" == "dumb" ]]; then TERM_COLOR=0 else TERM_COLOR=1 fi # Terminal width — fall back to 80. if [[ "$TERM_TTY" -eq 1 ]] && command -v tput >/dev/null 2>&1; then TERM_WIDTH=$(tput cols 2>/dev/null || echo 80) fi [[ "$TERM_WIDTH" -lt 40 ]] && TERM_WIDTH=80 if [[ "$TERM_ASCII_MODE" -eq 1 ]]; then TERM_ICON_PENDING="[.]" TERM_ICON_READY="[+]" TERM_ICON_DONE="[*]" TERM_ICON_FAILED="[x]" TERM_ICON_WARN="[!]" TERM_ICON_HINT="[i]" TERM_TREE_BRANCH="+-" TERM_TREE_LAST="\`-" TERM_TREE_VERT="|" TERM_PANEL_TL="+" TERM_PANEL_BL="+" TERM_PANEL_HRULE="-" TERM_PANEL_TERM="*" TERM_GLYPH_BRANCH="(b)" TERM_GLYPH_ALERT="!" TERM_GLYPH_TIP="(i)" TERM_SPIN_WORKING=('|' '/' '-' '\') TERM_SPIN_HEARTBEAT=('.' ':' '*' ':') else TERM_ICON_PENDING="⏳" TERM_ICON_READY="✅" TERM_ICON_DONE="🚀" TERM_ICON_FAILED="❌" TERM_ICON_WARN="⚠️ " TERM_ICON_HINT="💡" TERM_TREE_BRANCH="├─" TERM_TREE_LAST="└─" TERM_TREE_VERT="│" TERM_PANEL_TL="╭" TERM_PANEL_BL="╰" TERM_PANEL_HRULE="─" TERM_PANEL_TERM="●" TERM_GLYPH_BRANCH="⎇" TERM_GLYPH_ALERT="▲" TERM_GLYPH_TIP="💡" TERM_SPIN_WORKING=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') TERM_SPIN_HEARTBEAT=('·' '∙' '•' '●' '•' '∙') fi if [[ "$TERM_COLOR" -eq 1 ]]; then TERM_C_GREEN=$'\033[32m' TERM_C_YELLOW=$'\033[33m' TERM_C_ORANGE=$'\033[38;5;208m' TERM_C_RED=$'\033[31m' TERM_C_CYAN=$'\033[36m' TERM_C_MAGENTA=$'\033[35m' TERM_C_DIM=$'\033[2m' TERM_C_OFF=$'\033[0m' else TERM_C_GREEN=""; TERM_C_YELLOW=""; TERM_C_ORANGE="" TERM_C_RED=""; TERM_C_CYAN=""; TERM_C_MAGENTA="" TERM_C_DIM=""; TERM_C_OFF="" fi } # ─── Color helper ───────────────────────────────────────────────────────── # term_color term_color() { local name=$1; shift local code="" case "$name" in green) code="$TERM_C_GREEN" ;; yellow) code="$TERM_C_YELLOW" ;; orange) code="$TERM_C_ORANGE" ;; red) code="$TERM_C_RED" ;; cyan) code="$TERM_C_CYAN" ;; magenta) code="$TERM_C_MAGENTA" ;; dim) code="$TERM_C_DIM" ;; esac printf '%s%s%s' "$code" "$*" "$TERM_C_OFF" } # ─── Registry lookup ────────────────────────────────────────────────────── # term_emoji — returns Unicode glyph or ASCII fallback. # Internal helper; pass "BRAND", "HEALTH_GLYPH", "DIAGRAM_ICON". __term_lookup() { local map=$1 key=$2 entry uni ascii case "$map" in BRAND) entry="${TERM_BRAND[$key]:-}" ;; HEALTH_GLYPH) entry="${TERM_HEALTH_GLYPH[$key]:-}" ;; DIAGRAM_ICON) entry="${TERM_DIAGRAM_ICON[$key]:-}" ;; *) entry="" ;; esac [[ -z "$entry" ]] && { printf '%s' "?"; return; } uni="${entry%|*}" ascii="${entry#*|}" if [[ "$TERM_ASCII_MODE" -eq 1 ]]; then printf '%s' "$ascii" else printf '%s' "$uni"; fi } term_brand_glyph() { __term_lookup BRAND "$1"; } term_health_glyph() { __term_lookup HEALTH_GLYPH "$1"; } term_diagram_icon() { __term_lookup DIAGRAM_ICON "$1"; } # ─── Legacy state-icon helper (used by fleet.sh) ────────────────────────── term_state_icon() { case "$1" in RUNNING|PENDING) printf '%s' "$TERM_ICON_PENDING" ;; READY) printf '%s' "$TERM_ICON_READY" ;; LANDED|DONE|OK) printf '%s' "$TERM_ICON_DONE" ;; FAILED|ERROR) printf '%s' "$TERM_ICON_FAILED" ;; CONFLICT|WARN) printf '%s' "$TERM_ICON_WARN" ;; HINT|INFO) printf '%s' "$TERM_ICON_HINT" ;; *) printf '%s' "?" ;; esac } # ─── Primitives ─────────────────────────────────────────────────────────── # term_repeat term_repeat() { local ch=$1 n=$2 i out="" for (( i=0; i — ellipsis-truncate, append "…" or "..". term_truncate() { local text=$1 max=$2 local len=${#text} if [[ $len -le $max ]]; then printf '%s' "$text"; return; fi local ell="…" [[ "$TERM_ASCII_MODE" -eq 1 ]] && ell=".." local elllen=${#ell} printf '%s%s' "${text:0:$((max - elllen))}" "$ell" } # ─── Panel ──────────────────────────────────────────────────────────────── # term_panel_open [right_indicator] # ╭── ⚡ name ───────── ───● term_panel_open() { local key=$1 name=$2 indicator=${3:-} local emoji emoji=$(term_brand_glyph "$key") local left="${TERM_PANEL_TL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${emoji} $(term_color cyan "$name") " local right="" if [[ -n "$indicator" ]]; then right=" $(term_color dim "$indicator") ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")" else right="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")" fi # Visible (color-stripped) widths to size the rule fill correctly. local left_vis="${TERM_PANEL_TL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${emoji} ${name} " local right_vis="" [[ -n "$indicator" ]] && right_vis=" ${indicator} ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}" \ || right_vis="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}" local fill=$(( TERM_WIDTH - ${#left_vis} - ${#right_vis} )) [[ $fill -lt 4 ]] && fill=4 local rule rule=$(term_repeat "$TERM_PANEL_HRULE" "$fill") printf '%s%s%s\n' "$left" "$(term_color cyan "$rule")" "$right" } # term_panel_close [hotkeys] [health_indicators] # ╰── R refresh · L land · ? help ───── • daemon • 17m ───● # `hotkeys`: pre-formatted "R refresh · L land · ? help" string. # `healths`: pre-formatted "• daemon • 17m" string. term_panel_close() { local hotkeys=${1:-} healths=${2:-} local left="${TERM_PANEL_BL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${hotkeys} " local right="" if [[ -n "$healths" ]]; then right=" ${healths} ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")" else right="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")" fi local left_vis="${TERM_PANEL_BL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${hotkeys} " local right_vis="" [[ -n "$healths" ]] && right_vis=" ${healths} ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}" \ || right_vis="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}" local fill=$(( TERM_WIDTH - ${#left_vis} - ${#right_vis} )) [[ $fill -lt 4 ]] && fill=4 local rule rule=$(term_repeat "$TERM_PANEL_HRULE" "$fill") printf '%s%s%s\n' "$left" "$(term_color cyan "$rule")" "$right" } # term_panel_vert — emit a single body-line spacer "│" term_panel_vert() { printf '%s\n' "$(term_color dim "$TERM_TREE_VERT")" } # ─── Body components ────────────────────────────────────────────────────── # term_section