|
|
@@ -0,0 +1,750 @@
|
|
|
+<#
|
|
|
+.SYNOPSIS
|
|
|
+ PowerShell port of the Terminal Panel Design System (docs/TERMINAL-DESIGN.md).
|
|
|
+
|
|
|
+.DESCRIPTION
|
|
|
+ Mirror of skills/_lib/term.sh for PowerShell scripts in the claude-mods
|
|
|
+ family. Provides chrome rendering (panels, sections, leaves), glyph
|
|
|
+ registries (brand, health, diagram icon), color tokens, and width-aware
|
|
|
+ text utilities so PowerShell-based skills produce output that's visually
|
|
|
+ coherent with bash and Python siblings (fleet-ops, summon, etc.).
|
|
|
+
|
|
|
+ Source from any PowerShell skill script:
|
|
|
+
|
|
|
+ $LibDir = Join-Path $PSScriptRoot '..\..\_lib'
|
|
|
+ . (Join-Path $LibDir 'term.ps1')
|
|
|
+ Initialize-Term
|
|
|
+
|
|
|
+ All component helpers return strings. The caller decides where to write
|
|
|
+ them (stderr for chrome via [Console]::Error.WriteLine, stdout for data
|
|
|
+ payloads). This keeps the ATP stream-separation contract intact.
|
|
|
+
|
|
|
+ Honors: $env:NO_COLOR, $env:FORCE_COLOR, $env:TERM_ASCII, $env:TERM=dumb.
|
|
|
+ Spec: ../docs/TERMINAL-DESIGN.md.
|
|
|
+#>
|
|
|
+
|
|
|
+# Guard against double-sourcing
|
|
|
+if ($Script:__TermPs1Loaded) { return }
|
|
|
+$Script:__TermPs1Loaded = $true
|
|
|
+
|
|
|
+# โโโ Globals (populated by Initialize-Term) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+$Script:TermTty = $false
|
|
|
+$Script:TermColor = $false
|
|
|
+$Script:TermAsciiMode = $false
|
|
|
+$Script:TermWidth = 100 # claude-mods default (TERMINAL-DESIGN open question #1)
|
|
|
+
|
|
|
+# ANSI escape codes (empty when color disabled)
|
|
|
+$Script:TC_Green = ''
|
|
|
+$Script:TC_Yellow = ''
|
|
|
+$Script:TC_Orange = ''
|
|
|
+$Script:TC_Red = ''
|
|
|
+$Script:TC_Cyan = ''
|
|
|
+$Script:TC_Magenta = ''
|
|
|
+$Script:TC_Dim = ''
|
|
|
+$Script:TC_Off = ''
|
|
|
+
|
|
|
+# Tree connectors (set by Initialize-Term)
|
|
|
+$Script:T_Branch = '' # โโ / +-
|
|
|
+$Script:T_Last = '' # โโ / `-
|
|
|
+$Script:T_Vert = '' # โ / |
|
|
|
+
|
|
|
+# Panel chrome
|
|
|
+$Script:P_TL = '' # โญ / +
|
|
|
+$Script:P_BL = '' # โฐ / +
|
|
|
+$Script:P_HRule = '' # โ / -
|
|
|
+$Script:P_Term = '' # โ / *
|
|
|
+
|
|
|
+# Header / alert / tip glyphs
|
|
|
+$Script:G_Branch = '' # โ / (b)
|
|
|
+$Script:G_Alert = '' # โฒ / !
|
|
|
+$Script:G_Tip = '' # ๐ก / (i)
|
|
|
+
|
|
|
+# Spinner frame banks
|
|
|
+$Script:Spin_Working = @()
|
|
|
+$Script:Spin_Heartbeat = @()
|
|
|
+
|
|
|
+# โโโ Registries (Unicode | ASCII fallback) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+$Script:TermBrand = @{
|
|
|
+ fleet = 'โก|[F]'
|
|
|
+ forge = '๐จ|[B]'
|
|
|
+ psql = '๐|[P]'
|
|
|
+ watch = '๐ก|[M]'
|
|
|
+ deploy = '๐|[D]'
|
|
|
+ git = '๐ฟ|[G]'
|
|
|
+ 'windows-ops' = '๐ฉบ|[H]' # stethoscope โ diagnostics is the verb
|
|
|
+}
|
|
|
+
|
|
|
+$Script:TermHealthGlyph = @{
|
|
|
+ healthy = 'โข|(+)'
|
|
|
+ pending = 'โข|(.)'
|
|
|
+ warning = 'โข|(!)'
|
|
|
+ critical = 'โข|(!!)'
|
|
|
+ busted = 'โฌค|(X)'
|
|
|
+ unknown = 'โข|(?)'
|
|
|
+}
|
|
|
+
|
|
|
+$Script:TermDiagramIcon = @{
|
|
|
+ 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)'
|
|
|
+}
|
|
|
+
|
|
|
+# โโโ Initialize-Term โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+function Initialize-Term {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Detect terminal capabilities and populate global glyph/color state.
|
|
|
+ Idempotent โ safe to call multiple times.
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param()
|
|
|
+
|
|
|
+ # TTY detection โ stdout (not stderr; rendering targets stdout-ish)
|
|
|
+ try {
|
|
|
+ $Script:TermTty = -not [Console]::IsOutputRedirected
|
|
|
+ } catch {
|
|
|
+ $Script:TermTty = $false
|
|
|
+ }
|
|
|
+
|
|
|
+ # ASCII fallback: explicit env, or non-UTF environment
|
|
|
+ $asciiEnv = $env:TERM_ASCII -eq '1' -or $env:FLEET_ASCII -eq '1'
|
|
|
+ $lang = if ($env:LC_ALL) { $env:LC_ALL } elseif ($env:LANG) { $env:LANG } else { '' }
|
|
|
+ $nonUtf = $lang -and ($lang -notmatch '[Uu][Tt][Ff]') -and ($env:TERM -eq 'dumb')
|
|
|
+ $Script:TermAsciiMode = $asciiEnv -or $nonUtf
|
|
|
+
|
|
|
+ # Color: TTY + not NO_COLOR, or FORCE_COLOR overrides
|
|
|
+ if ($env:FORCE_COLOR) {
|
|
|
+ $Script:TermColor = $true
|
|
|
+ } elseif ($env:NO_COLOR -or -not $Script:TermTty -or $env:TERM -eq 'dumb') {
|
|
|
+ $Script:TermColor = $false
|
|
|
+ } else {
|
|
|
+ $Script:TermColor = $true
|
|
|
+ }
|
|
|
+
|
|
|
+ # Terminal width โ fall back to 100 (claude-mods default)
|
|
|
+ if ($Script:TermTty) {
|
|
|
+ try {
|
|
|
+ $cols = [Console]::WindowWidth
|
|
|
+ if ($cols -ge 40) { $Script:TermWidth = $cols }
|
|
|
+ } catch {
|
|
|
+ # WindowWidth throws when no console attached; keep default
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ # Allow explicit override
|
|
|
+ if ($env:TERM_WIDTH -match '^\d+$' -and [int]$env:TERM_WIDTH -ge 40) {
|
|
|
+ $Script:TermWidth = [int]$env:TERM_WIDTH
|
|
|
+ }
|
|
|
+
|
|
|
+ # Glyphs by mode
|
|
|
+ if ($Script:TermAsciiMode) {
|
|
|
+ $Script:T_Branch = '+-'
|
|
|
+ $Script:T_Last = '`-'
|
|
|
+ $Script:T_Vert = '|'
|
|
|
+ $Script:P_TL = '+'
|
|
|
+ $Script:P_BL = '+'
|
|
|
+ $Script:P_HRule = '-'
|
|
|
+ $Script:P_Term = '*'
|
|
|
+ $Script:G_Branch = '(b)'
|
|
|
+ $Script:G_Alert = '!'
|
|
|
+ $Script:G_Tip = '(i)'
|
|
|
+ $Script:Spin_Working = @('|', '/', '-', '\')
|
|
|
+ $Script:Spin_Heartbeat = @('.', ':', '*', ':')
|
|
|
+ } else {
|
|
|
+ $Script:T_Branch = 'โโ'
|
|
|
+ $Script:T_Last = 'โโ'
|
|
|
+ $Script:T_Vert = 'โ'
|
|
|
+ $Script:P_TL = 'โญ'
|
|
|
+ $Script:P_BL = 'โฐ'
|
|
|
+ $Script:P_HRule = 'โ'
|
|
|
+ $Script:P_Term = 'โ'
|
|
|
+ $Script:G_Branch = 'โ'
|
|
|
+ $Script:G_Alert = 'โฒ'
|
|
|
+ $Script:G_Tip = '๐ก'
|
|
|
+ $Script:Spin_Working = @('โ ','โ ','โ น','โ ธ','โ ผ','โ ด','โ ฆ','โ ง','โ ','โ ')
|
|
|
+ $Script:Spin_Heartbeat = @('ยท','โ','โข','โ','โข','โ')
|
|
|
+ }
|
|
|
+
|
|
|
+ # ANSI escapes
|
|
|
+ if ($Script:TermColor) {
|
|
|
+ $esc = [char]27
|
|
|
+ $Script:TC_Green = "${esc}[32m"
|
|
|
+ $Script:TC_Yellow = "${esc}[33m"
|
|
|
+ $Script:TC_Orange = "${esc}[38;5;208m"
|
|
|
+ $Script:TC_Red = "${esc}[31m"
|
|
|
+ $Script:TC_Cyan = "${esc}[36m"
|
|
|
+ $Script:TC_Magenta = "${esc}[35m"
|
|
|
+ $Script:TC_Dim = "${esc}[2m"
|
|
|
+ $Script:TC_Off = "${esc}[0m"
|
|
|
+
|
|
|
+ # On Windows, ensure VT processing is enabled so ANSI works in PS 5.1
|
|
|
+ if ($PSVersionTable.PSVersion.Major -le 5) {
|
|
|
+ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ $Script:TC_Green = ''; $Script:TC_Yellow = ''; $Script:TC_Orange = ''
|
|
|
+ $Script:TC_Red = ''; $Script:TC_Cyan = ''; $Script:TC_Magenta = ''
|
|
|
+ $Script:TC_Dim = ''; $Script:TC_Off = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# โโโ Color helper โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+function Get-TermColor {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Wrap text in an ANSI color escape. Returns plain text when color disabled.
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][ValidateSet('green','yellow','orange','red','cyan','magenta','dim')]
|
|
|
+ [string]$Token,
|
|
|
+ [Parameter(Mandatory)][AllowEmptyString()][string]$Text
|
|
|
+ )
|
|
|
+ if (-not $Script:TermColor) { return $Text }
|
|
|
+ $code = switch ($Token) {
|
|
|
+ 'green' { $Script:TC_Green }
|
|
|
+ 'yellow' { $Script:TC_Yellow }
|
|
|
+ 'orange' { $Script:TC_Orange }
|
|
|
+ 'red' { $Script:TC_Red }
|
|
|
+ 'cyan' { $Script:TC_Cyan }
|
|
|
+ 'magenta' { $Script:TC_Magenta }
|
|
|
+ 'dim' { $Script:TC_Dim }
|
|
|
+ }
|
|
|
+ return "${code}${Text}$($Script:TC_Off)"
|
|
|
+}
|
|
|
+
|
|
|
+# โโโ Registry lookup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+function Get-TermGlyph {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Return registered Unicode glyph (or ASCII fallback when in ASCII mode).
|
|
|
+ .PARAMETER Registry
|
|
|
+ Which registry to consult: Brand | Health | Diagram
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][ValidateSet('Brand','Health','Diagram')]$Registry,
|
|
|
+ [Parameter(Mandatory)][string]$Key
|
|
|
+ )
|
|
|
+ $map = switch ($Registry) {
|
|
|
+ 'Brand' { $Script:TermBrand }
|
|
|
+ 'Health' { $Script:TermHealthGlyph }
|
|
|
+ 'Diagram' { $Script:TermDiagramIcon }
|
|
|
+ }
|
|
|
+ $entry = $map[$Key]
|
|
|
+ if (-not $entry) { return '?' }
|
|
|
+ $parts = $entry -split '\|', 2
|
|
|
+ if ($Script:TermAsciiMode) { return $parts[1] } else { return $parts[0] }
|
|
|
+}
|
|
|
+
|
|
|
+# โโโ Display-width helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+function Get-TermDisplayWidth {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Approximate display column count for a string, accounting for emoji
|
|
|
+ double-width and ignoring ANSI color escapes.
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param([Parameter(ValueFromPipeline)][AllowEmptyString()][string]$Text = '')
|
|
|
+ process {
|
|
|
+ if (-not $Text) { return 0 }
|
|
|
+ # Strip ANSI escapes (CSI sequences)
|
|
|
+ $esc = [char]27
|
|
|
+ $clean = $Text -replace "${esc}\[[0-9;]*m", ''
|
|
|
+ $width = 0
|
|
|
+ # EnumerateRunes handles surrogate pairs correctly
|
|
|
+ foreach ($rune in [System.Globalization.StringInfo]::GetTextElementEnumerator($clean)) {
|
|
|
+ $cp = if ([string]$rune.Current.Length -gt 0) { [int][char]([string]$rune.Current)[0] } else { 0 }
|
|
|
+ # Simple wide-emoji heuristic: rune length >1 (surrogate pair) OR in known wide ranges
|
|
|
+ $s = [string]$rune.Current
|
|
|
+ if ($s.Length -gt 1) {
|
|
|
+ $width += 2 # surrogate pair โ almost always wide
|
|
|
+ } elseif (($cp -ge 0x2600 -and $cp -le 0x27BF) -or
|
|
|
+ ($cp -ge 0x2B00 -and $cp -le 0x2BFF) -or
|
|
|
+ $cp -eq 0x26A1 -or # โก
|
|
|
+ $cp -eq 0x2728 -or # โจ
|
|
|
+ $cp -eq 0x2B24) { # โฌค
|
|
|
+ $width += 2
|
|
|
+ } else {
|
|
|
+ $width += 1
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $width
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Get-TermTruncated {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Ellipsis-truncate text to fit in MaxCols display columns.
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory, Position=0)][AllowEmptyString()][string]$Text,
|
|
|
+ [Parameter(Mandatory, Position=1)][int]$MaxCols
|
|
|
+ )
|
|
|
+ $w = Get-TermDisplayWidth $Text
|
|
|
+ if ($w -le $MaxCols) { return $Text }
|
|
|
+ $ell = if ($Script:TermAsciiMode) { '..' } else { 'โฆ' }
|
|
|
+ $ellW = Get-TermDisplayWidth $ell
|
|
|
+ # Naive truncation by character count โ close enough for our needs
|
|
|
+ $maxChars = $MaxCols - $ellW
|
|
|
+ if ($maxChars -lt 0) { $maxChars = 0 }
|
|
|
+ if ($Text.Length -le $maxChars) { return $Text + $ell }
|
|
|
+ return $Text.Substring(0, $maxChars) + $ell
|
|
|
+}
|
|
|
+
|
|
|
+# โโโ Panel chrome โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+function New-TermPanelOpen {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Render the panel header bar: โญโโ ๐ฉบ brand ยท subtitle โโโ INDICATOR โโโโ
|
|
|
+ .PARAMETER Brand
|
|
|
+ Brand key from the registry (e.g. 'windows-ops').
|
|
|
+ .PARAMETER Name
|
|
|
+ Tool name shown after the brand emoji (e.g. 'windows-ops').
|
|
|
+ .PARAMETER Subtitle
|
|
|
+ Optional subtitle after the name (e.g. 'health-audit').
|
|
|
+ .PARAMETER Indicator
|
|
|
+ Optional right-side context indicator (e.g. 'TITAN', 'Y / Disk 1').
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][string]$Brand,
|
|
|
+ [Parameter(Mandatory)][string]$Name,
|
|
|
+ [string]$Subtitle = '',
|
|
|
+ [string]$Indicator = ''
|
|
|
+ )
|
|
|
+ $emoji = Get-TermGlyph -Registry Brand -Key $Brand
|
|
|
+ $title = if ($Subtitle) {
|
|
|
+ "$Name $(Get-TermColor dim "ยท $Subtitle")"
|
|
|
+ } else {
|
|
|
+ $Name
|
|
|
+ }
|
|
|
+ $titleVis = if ($Subtitle) { "$Name ยท $Subtitle" } else { $Name }
|
|
|
+
|
|
|
+ $leftRaw = "$($Script:P_TL)$($Script:P_HRule)$($Script:P_HRule) $emoji "
|
|
|
+ $left = "$leftRaw$(Get-TermColor cyan $title) "
|
|
|
+ $leftVis = "$leftRaw${titleVis} "
|
|
|
+
|
|
|
+ if ($Indicator) {
|
|
|
+ $right = " $(Get-TermColor dim $Indicator) $($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$(Get-TermColor cyan $Script:P_Term)"
|
|
|
+ $rightVis = " $Indicator $($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$($Script:P_Term)"
|
|
|
+ } else {
|
|
|
+ $right = "$($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$(Get-TermColor cyan $Script:P_Term)"
|
|
|
+ $rightVis = "$($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$($Script:P_Term)"
|
|
|
+ }
|
|
|
+
|
|
|
+ $leftW = Get-TermDisplayWidth $leftVis
|
|
|
+ $rightW = Get-TermDisplayWidth $rightVis
|
|
|
+ $fill = $Script:TermWidth - $leftW - $rightW
|
|
|
+ if ($fill -lt 4) { $fill = 4 }
|
|
|
+ $rule = Get-TermColor cyan ($Script:P_HRule * $fill)
|
|
|
+ return "${left}${rule}${right}"
|
|
|
+}
|
|
|
+
|
|
|
+function New-TermPanelClose {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Render the panel footer bar: โฐโโ hotkeys โโโ health1 health2 โโโโ
|
|
|
+ .PARAMETER Hotkeys
|
|
|
+ Pre-rendered hotkey string (use New-TermHotkey + joining with ' ยท ').
|
|
|
+ .PARAMETER Healths
|
|
|
+ Pre-rendered health-indicator string (use New-TermHealth + joining with ' ').
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [string]$Hotkeys = '',
|
|
|
+ [string]$Healths = ''
|
|
|
+ )
|
|
|
+ $leftRaw = "$($Script:P_BL)$($Script:P_HRule)$($Script:P_HRule) "
|
|
|
+ $left = "$leftRaw$Hotkeys "
|
|
|
+ # For width calc strip ANSI from the rendered hotkeys/healths
|
|
|
+ $hotkeysVis = ($Hotkeys -replace "$([char]27)\[[0-9;]*m", '')
|
|
|
+ $leftVis = "$leftRaw$hotkeysVis "
|
|
|
+
|
|
|
+ if ($Healths) {
|
|
|
+ $right = " $Healths $($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$(Get-TermColor cyan $Script:P_Term)"
|
|
|
+ $healthsVis = ($Healths -replace "$([char]27)\[[0-9;]*m", '')
|
|
|
+ $rightVis = " $healthsVis $($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$($Script:P_Term)"
|
|
|
+ } else {
|
|
|
+ $right = "$($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$(Get-TermColor cyan $Script:P_Term)"
|
|
|
+ $rightVis = "$($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$($Script:P_Term)"
|
|
|
+ }
|
|
|
+
|
|
|
+ $leftW = Get-TermDisplayWidth $leftVis
|
|
|
+ $rightW = Get-TermDisplayWidth $rightVis
|
|
|
+ $fill = $Script:TermWidth - $leftW - $rightW
|
|
|
+ if ($fill -lt 4) { $fill = 4 }
|
|
|
+ $rule = Get-TermColor cyan ($Script:P_HRule * $fill)
|
|
|
+ return "${left}${rule}${right}"
|
|
|
+}
|
|
|
+
|
|
|
+function New-TermPanelVert {
|
|
|
+ <# Body-line spacer: a single โ on its own line. #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param()
|
|
|
+ return Get-TermColor dim $Script:T_Vert
|
|
|
+}
|
|
|
+
|
|
|
+# โโโ Body components โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+function New-TermSection {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Section header: โโโ LABEL (n) with label colored by state.
|
|
|
+ .PARAMETER State
|
|
|
+ State token (FAILING/WARN/PASS/INFO or fleet-style RUNNING/READY/FAILED/CONFLICT).
|
|
|
+ .PARAMETER Label
|
|
|
+ Section label text.
|
|
|
+ .PARAMETER Count
|
|
|
+ Item count. Pass -1 to omit the (n).
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][string]$State,
|
|
|
+ [Parameter(Mandatory)][string]$Label,
|
|
|
+ [int]$Count = -1
|
|
|
+ )
|
|
|
+ $color = switch -Regex ($State) {
|
|
|
+ '^(RUNNING|PENDING|WARN|warning|WATCHLIST)$' { 'yellow' }
|
|
|
+ '^(READY|PASS|LANDED|DONE|OK|healthy)$' { 'green' }
|
|
|
+ '^(FAILING|FAILED|ERROR|CRITICAL|critical|alarm|busted)$' { 'red' }
|
|
|
+ '^(CONFLICT)$' { 'magenta' }
|
|
|
+ default { '' }
|
|
|
+ }
|
|
|
+ $renderedLabel = if ($color) { Get-TermColor $color $Label } else { $Label }
|
|
|
+ # Section is a panel-edge attachment โ 'โโโ' IS the left edge, no leading 'โ' prefix.
|
|
|
+ $conn = Get-TermColor dim "$($Script:T_Branch)$($Script:P_HRule)"
|
|
|
+ $countStr = if ($Count -ge 0) { ' ' + (Get-TermColor dim "($Count)") } else { '' }
|
|
|
+ return "${conn} ${renderedLabel}${countStr}"
|
|
|
+}
|
|
|
+
|
|
|
+function New-TermSummary {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Summary line: โโโ text (all dim, metadata-only branch).
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param([Parameter(Mandatory)][string]$Text)
|
|
|
+ # Summary attaches at the panel edge like a section.
|
|
|
+ $conn = Get-TermColor dim "$($Script:T_Branch)$($Script:P_HRule)"
|
|
|
+ $body = Get-TermColor dim $Text
|
|
|
+ return "${conn} ${body}"
|
|
|
+}
|
|
|
+
|
|
|
+function New-TermLeaf {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Single leaf row: โ โโโ name โโโโโ meta age
|
|
|
+ .PARAMETER Name
|
|
|
+ Leaf name (ellipsis-truncated to fit name column).
|
|
|
+ .PARAMETER Rail
|
|
|
+ Pre-rendered leaf-glyph string (rail or pip bar or plain text).
|
|
|
+ .PARAMETER Meta
|
|
|
+ Meta column content (e.g. 'M4 ?1', 'clean', error count).
|
|
|
+ .PARAMETER Age
|
|
|
+ Age column content (right-aligned, e.g. '12m', '2h').
|
|
|
+ .PARAMETER IsLast
|
|
|
+ Use โโโ connector instead of โโโ (for last sibling in a section).
|
|
|
+ .PARAMETER NameColWidth
|
|
|
+ Override name column width (default 32).
|
|
|
+ .PARAMETER RailColWidth
|
|
|
+ Override rail column width (default 14).
|
|
|
+ .PARAMETER MetaColWidth
|
|
|
+ Override meta column width (default 12).
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][string]$Name,
|
|
|
+ [string]$Rail = '',
|
|
|
+ [string]$Meta = '',
|
|
|
+ [string]$Age = '',
|
|
|
+ [switch]$IsLast,
|
|
|
+ [int]$NameColWidth = 32,
|
|
|
+ [int]$RailColWidth = 14,
|
|
|
+ [int]$MetaColWidth = 12
|
|
|
+ )
|
|
|
+ $vert = Get-TermColor dim $Script:T_Vert
|
|
|
+ $connRaw = if ($IsLast) { $Script:T_Last } else { $Script:T_Branch }
|
|
|
+ $conn = Get-TermColor dim "$connRaw$($Script:P_HRule)"
|
|
|
+
|
|
|
+ $truncName = Get-TermTruncated -Text $Name -MaxCols $NameColWidth
|
|
|
+ $nameW = Get-TermDisplayWidth $truncName
|
|
|
+ $namePad = ' ' * [Math]::Max(0, $NameColWidth - $nameW)
|
|
|
+
|
|
|
+ $railW = Get-TermDisplayWidth ($Rail -replace "$([char]27)\[[0-9;]*m", '')
|
|
|
+ $railPad = ' ' * [Math]::Max(0, $RailColWidth - $railW)
|
|
|
+
|
|
|
+ $metaColored = if ($Meta) { Get-TermColor dim $Meta } else { '' }
|
|
|
+ $metaW = Get-TermDisplayWidth $Meta
|
|
|
+ $metaPad = ' ' * [Math]::Max(0, $MetaColWidth - $metaW)
|
|
|
+
|
|
|
+ $ageColored = if ($Age) { Get-TermColor dim $Age } else { '' }
|
|
|
+
|
|
|
+ return "${vert} ${conn} ${truncName}${namePad} ${Rail}${railPad} ${metaColored}${metaPad} ${ageColored}"
|
|
|
+}
|
|
|
+
|
|
|
+function New-TermAlert {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Inline alert sub-row: โ โ โฒ message (orange/red).
|
|
|
+ .PARAMETER Severity
|
|
|
+ warning (orange) or critical (red).
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][ValidateSet('warning','critical')]$Severity,
|
|
|
+ [Parameter(Mandatory)][string]$Text
|
|
|
+ )
|
|
|
+ $color = if ($Severity -eq 'critical') { 'red' } else { 'orange' }
|
|
|
+ $vert = Get-TermColor dim $Script:T_Vert
|
|
|
+ $vert2 = Get-TermColor dim $Script:T_Vert
|
|
|
+ $tri = Get-TermColor $color $Script:G_Alert
|
|
|
+ # Per design ยง 4.7: panel-vert, 3-space section indent, leaf-continuation vert,
|
|
|
+ # 3-space sub-indent (aligns the alert under the leaf's tree connector).
|
|
|
+ return "${vert} ${vert2} ${tri} ${Text}"
|
|
|
+}
|
|
|
+
|
|
|
+function New-TermHint {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Hint row with the tip glyph: โ ๐ก text (dim, no tree connector).
|
|
|
+ Used for "to get started" / "did you know" rows in empty states.
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param([Parameter(Mandatory)][string]$Text)
|
|
|
+ $vert = Get-TermColor dim $Script:T_Vert
|
|
|
+ $tip = $Script:G_Tip
|
|
|
+ return "${vert} ${tip} $(Get-TermColor dim $Text)"
|
|
|
+}
|
|
|
+
|
|
|
+function New-TermToast {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Toast row: โโโ ๐ฉบ message (dim cyan emoji + default fg text).
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][string]$Brand,
|
|
|
+ [Parameter(Mandatory)][string]$Text
|
|
|
+ )
|
|
|
+ $emoji = Get-TermGlyph -Registry Brand -Key $Brand
|
|
|
+ $vert = Get-TermColor dim $Script:T_Vert
|
|
|
+ $conn = Get-TermColor dim "$($Script:T_Branch)$($Script:P_HRule)"
|
|
|
+ $msg = "$(Get-TermColor cyan $emoji) $Text"
|
|
|
+ return "${vert}${conn} ${msg}"
|
|
|
+}
|
|
|
+
|
|
|
+# โโโ Leaf-glyph builders โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+function New-TermRail {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Build a commit-graph rail: โโโโโโโ (HEAD) or โโโโโ (CONFLICT).
|
|
|
+ .PARAMETER Commits
|
|
|
+ Number of landed commits (including the HEAD position).
|
|
|
+ .PARAMETER Head
|
|
|
+ HEAD | CONFLICT | EMPTY
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][ValidateRange(0,99)][int]$Commits,
|
|
|
+ [ValidateSet('HEAD','CONFLICT','EMPTY')][string]$Head = 'HEAD'
|
|
|
+ )
|
|
|
+ $commit = if ($Script:TermAsciiMode) { '*' } else { 'โ' }
|
|
|
+ $link = if ($Script:TermAsciiMode) { '-' } else { 'โ' }
|
|
|
+ $headg = if ($Script:TermAsciiMode) { '@' } else { 'โ' }
|
|
|
+ $conflict = if ($Script:TermAsciiMode) { 'X' } else { 'โ' }
|
|
|
+
|
|
|
+ if ($Commits -le 0 -and $Head -eq 'EMPTY') {
|
|
|
+ return $link
|
|
|
+ }
|
|
|
+
|
|
|
+ $out = ''
|
|
|
+ for ($i = 0; $i -lt ($Commits - 1); $i++) {
|
|
|
+ $out += "$(Get-TermColor green $commit)$link"
|
|
|
+ }
|
|
|
+ switch ($Head) {
|
|
|
+ 'HEAD' {
|
|
|
+ if ($Commits -ge 1) { $out += "$(Get-TermColor green $commit)$link" }
|
|
|
+ $out += Get-TermColor yellow $headg
|
|
|
+ }
|
|
|
+ 'CONFLICT' {
|
|
|
+ if ($Commits -ge 1) { $out += "$(Get-TermColor green $commit)$link" }
|
|
|
+ $out += Get-TermColor red $conflict
|
|
|
+ }
|
|
|
+ 'EMPTY' {
|
|
|
+ if ($Commits -ge 1) { $out += Get-TermColor green $commit }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $out
|
|
|
+}
|
|
|
+
|
|
|
+function New-TermPipBar {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Build a pip bar: โฐโฐโฐโฐโฑโฑโฑโฑโฑโฑ with state-color based on metric type.
|
|
|
+ .PARAMETER Type
|
|
|
+ progress | score | capacity (drives color selection per design ยง 4.10).
|
|
|
+ .PARAMETER Filled
|
|
|
+ Filled count.
|
|
|
+ .PARAMETER Total
|
|
|
+ Total / denominator.
|
|
|
+ .PARAMETER Width
|
|
|
+ Pip count (default 10 = clean 10% increments).
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][ValidateSet('progress','score','capacity')]$Type,
|
|
|
+ [Parameter(Mandatory)][int]$Filled,
|
|
|
+ [Parameter(Mandatory)][int]$Total,
|
|
|
+ [int]$Width = 10
|
|
|
+ )
|
|
|
+ $pipFull = if ($Script:TermAsciiMode) { '#' } else { 'โฐ' }
|
|
|
+ $pipEmpty = if ($Script:TermAsciiMode) { '-' } else { 'โฑ' }
|
|
|
+
|
|
|
+ # Natural denominator override: if total <= 12 and not 100, use total as width
|
|
|
+ if ($Total -ne 100 -and $Total -gt 0 -and $Total -le 12) {
|
|
|
+ $Width = $Total
|
|
|
+ }
|
|
|
+
|
|
|
+ $pct = if ($Total -gt 0) { [int](100 * $Filled / $Total) } else { 0 }
|
|
|
+ $pips = if ($Total -eq 100) { [int]($Filled / 10) } else { $Filled }
|
|
|
+ if ($pips -lt 0) { $pips = 0 }
|
|
|
+ if ($pips -gt $Width) { $pips = $Width }
|
|
|
+
|
|
|
+ $color = switch ($Type) {
|
|
|
+ 'progress' {
|
|
|
+ if ($pct -ge 100) { 'green' } else { 'yellow' }
|
|
|
+ }
|
|
|
+ 'score' {
|
|
|
+ if ($pct -lt 33) { 'red' }
|
|
|
+ elseif ($pct -lt 66) { 'yellow' }
|
|
|
+ else { 'green' }
|
|
|
+ }
|
|
|
+ 'capacity' {
|
|
|
+ if ($pct -ge 80) { 'red' }
|
|
|
+ elseif ($pct -ge 60) { 'yellow' }
|
|
|
+ else { 'green' }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $out = ''
|
|
|
+ for ($i = 0; $i -lt $pips; $i++) { $out += Get-TermColor $color $pipFull }
|
|
|
+ for ($i = $pips; $i -lt $Width; $i++) { $out += Get-TermColor dim $pipEmpty }
|
|
|
+ return $out
|
|
|
+}
|
|
|
+
|
|
|
+# โโโ Right-side furniture โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+function New-TermHealth {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Health indicator: โข text (with โฌค for busted state).
|
|
|
+ .PARAMETER State
|
|
|
+ healthy | pending | warning | critical | busted | unknown
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][ValidateSet('healthy','pending','warning','critical','busted','unknown')]$State,
|
|
|
+ [Parameter(Mandatory)][string]$Text
|
|
|
+ )
|
|
|
+ $glyph = Get-TermGlyph -Registry Health -Key $State
|
|
|
+ $color = switch ($State) {
|
|
|
+ 'healthy' { 'green' }
|
|
|
+ 'pending' { 'yellow' }
|
|
|
+ 'warning' { 'orange' }
|
|
|
+ 'critical' { 'red' }
|
|
|
+ 'busted' { 'dim' }
|
|
|
+ default { 'dim' }
|
|
|
+ }
|
|
|
+ return "$(Get-TermColor $color $glyph) $Text"
|
|
|
+}
|
|
|
+
|
|
|
+function New-TermHotkey {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Hotkey hint: "R refresh" with R in cyan.
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][string]$Key,
|
|
|
+ [Parameter(Mandatory)][string]$Verb
|
|
|
+ )
|
|
|
+ return "$(Get-TermColor cyan $Key) $Verb"
|
|
|
+}
|
|
|
+
|
|
|
+function Join-TermHotkeys {
|
|
|
+ <# Combine hotkeys with the dot separator. Accumulates from pipeline OR -InputObject array. #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param([Parameter(ValueFromPipeline, Position=0)][string]$InputObject)
|
|
|
+ begin { $items = New-Object System.Collections.Generic.List[string] }
|
|
|
+ process { if ($InputObject) { $items.Add($InputObject) } }
|
|
|
+ end { return ($items -join ' ยท ') }
|
|
|
+}
|
|
|
+
|
|
|
+function Join-TermHealths {
|
|
|
+ <# Combine health indicators with two-space separator (per design ยง 4.3). #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param([Parameter(ValueFromPipeline, Position=0)][string]$InputObject)
|
|
|
+ begin { $items = New-Object System.Collections.Generic.List[string] }
|
|
|
+ process { if ($InputObject) { $items.Add($InputObject) } }
|
|
|
+ end { return ($items -join ' ') }
|
|
|
+}
|
|
|
+
|
|
|
+# โโโ Spinners โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+function Get-TermSpinnerFrame {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Return the spinner glyph for the given tick (frame index).
|
|
|
+ .PARAMETER Family
|
|
|
+ working (fast, task-progress) or heartbeat (slow, daemon-alive).
|
|
|
+ .PARAMETER Tick
|
|
|
+ Frame index; modded by family frame count.
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param(
|
|
|
+ [Parameter(Mandatory)][ValidateSet('working','heartbeat')]$Family,
|
|
|
+ [Parameter(Mandatory)][int]$Tick
|
|
|
+ )
|
|
|
+ $frames = switch ($Family) {
|
|
|
+ 'working' { $Script:Spin_Working }
|
|
|
+ 'heartbeat' { $Script:Spin_Heartbeat }
|
|
|
+ }
|
|
|
+ if (-not $frames) { return '?' }
|
|
|
+ return $frames[$Tick % $frames.Count]
|
|
|
+}
|
|
|
+
|
|
|
+# โโโ Convenience: Write a panel-block to stderr โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
+function Write-TermLine {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Write a pre-rendered chrome line to stderr. Use for panel rows so
|
|
|
+ stdout remains data-only (ATP stream separation).
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param([Parameter(ValueFromPipeline)][AllowEmptyString()][string]$Line)
|
|
|
+ process { [Console]::Error.WriteLine($Line) }
|
|
|
+}
|
|
|
+
|
|
|
+function Write-TermData {
|
|
|
+ <#
|
|
|
+ .SYNOPSIS
|
|
|
+ Write payload data to stdout (the data product of the script).
|
|
|
+ Use for JSON, machine-readable records, anything downstream tooling
|
|
|
+ will consume.
|
|
|
+ #>
|
|
|
+ [CmdletBinding()]
|
|
|
+ param([Parameter(ValueFromPipeline)][AllowEmptyString()][string]$Line)
|
|
|
+ process { [Console]::Out.WriteLine($Line) }
|
|
|
+}
|