Payload/scripts/kaden/utils/PsCliUiKit/PsCliUiKit.psm1
|
Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" function Get-CliStyleForegroundColor { param( [Parameter(Mandatory = $true)] [string]$Style ) switch ($Style) { "Default" { return [ConsoleColor]::Gray } "Info" { return [ConsoleColor]::Cyan } "Success" { return [ConsoleColor]::Green } "Warning" { return [ConsoleColor]::Yellow } "Error" { return [ConsoleColor]::Red } "Muted" { return [ConsoleColor]::DarkGray } "Accent" { return [ConsoleColor]::Magenta } "Code" { return [ConsoleColor]::White } default { return [ConsoleColor]::Gray } } } function Get-CliKnownStyleCatalog { return @( "Default", "Info", "Success", "Warning", "Error", "Muted", "Accent", "Code" ) } function Get-CliAnsiSupportsVirtualTerminal { # Inline color tokens only render correctly when the host supports VT sequences. # This guard allows safe fallback to plain text in limited terminals. $ui = $Host.UI if ($null -eq $ui) { return $false } $vtProp = $ui.PSObject.Properties['SupportsVirtualTerminal'] if ($null -eq $vtProp) { return $false } return [bool]$vtProp.Value } function Get-CliAnsiForegroundCodeMap { return @{ "Black" = 30 "Red" = 31 "Green" = 32 "Yellow" = 33 "Blue" = 34 "Magenta" = 35 "Cyan" = 36 "White" = 37 "Gray" = 90 "Grey" = 90 "BrightBlack" = 90 "BrightRed" = 91 "BrightGreen" = 92 "BrightYellow" = 93 "BrightBlue" = 94 "BrightMagenta" = 95 "BrightCyan" = 96 "BrightWhite" = 97 } } function Get-CliAnsiSemanticStyleCodeMap { return @{ # Layout "header" = "96;1" "subheader" = "36;1" "divider" = "90" # Italic is not consistently supported across terminals. "footer" = "90" "muted" = "90" "code" = "30;47" # Feedback "success" = "92" "error" = "91;1" "warning" = "93" "info" = "94" } } function Get-CliAnsiTokenCodeMap { $tokenCodes = @{} $fgCodes = Get-CliAnsiForegroundCodeMap foreach ($name in $fgCodes.Keys) { $tokenCodes[$name.ToLowerInvariant()] = [string]$fgCodes[$name] } $semanticCodes = Get-CliAnsiSemanticStyleCodeMap foreach ($name in $semanticCodes.Keys) { $tokenCodes[$name.ToLowerInvariant()] = [string]$semanticCodes[$name] } return $tokenCodes } function Expand-CliPrefixAlias { param( [Parameter(Mandatory = $true)] [string]$Message ) $prefixMap = @{ "[+]" = "[BrightGreen]__CLI_STATUS_PLUS__[-]" "[-]" = "[BrightRed]__CLI_STATUS_MINUS__[-]" "[~]" = "[BrightYellow]__CLI_STATUS_TILDE__[-]" "[!]" = "[BrightMagenta]__CLI_STATUS_BANG__[-]" "[*]" = "[BrightCyan]__CLI_STATUS_STAR__[-]" } foreach ($prefix in $prefixMap.Keys) { if ($Message.StartsWith($prefix, [StringComparison]::Ordinal)) { return $prefixMap[$prefix] + $Message.Substring($prefix.Length) } } return $Message } function Restore-CliPrefixAliasText { param( [Parameter(Mandatory = $true)] [string]$Message ) $restored = $Message $restored = $restored.Replace("__CLI_STATUS_PLUS__", "[+]") $restored = $restored.Replace("__CLI_STATUS_MINUS__", "[-]") $restored = $restored.Replace("__CLI_STATUS_TILDE__", "[~]") $restored = $restored.Replace("__CLI_STATUS_BANG__", "[!]") $restored = $restored.Replace("__CLI_STATUS_STAR__", "[*]") return $restored } function ConvertTo-CliAnsiMessage { param( [Parameter(Mandatory = $true)] [string]$Message ) $esc = [char]0x1b $pattern = '\[(?<token>[A-Za-z]+|-)\]' $tokenCodes = Get-CliAnsiTokenCodeMap $expanded = Expand-CliPrefixAlias -Message $Message $evaluator = { param($match) $token = $match.Groups['token'].Value if ($token -eq '-') { return "${esc}[0m" } $key = $token.ToLowerInvariant() if ($tokenCodes.ContainsKey($key)) { return "${esc}[$($tokenCodes[$key])m" } return $match.Value } $ansi = [regex]::Replace($expanded, $pattern, $evaluator) $withSymbols = Restore-CliPrefixAliasText -Message $ansi # Reset at end so subsequent host output keeps its expected color. return "${withSymbols}${esc}[0m" } function ConvertFrom-CliTokenMarkup { # This is the non-VT fallback path: remove known markup tokens but keep # content readable so logs/transcripts still make sense in plain hosts. param( [Parameter(Mandatory = $true)] [string]$Message ) $pattern = '\[(?<token>[A-Za-z]+|-)\]' $tokenCodes = Get-CliAnsiTokenCodeMap $expanded = Expand-CliPrefixAlias -Message $Message $stripEvaluator = { param($match) $token = $match.Groups['token'].Value if ($token -eq "-") { return "" } $key = $token.ToLowerInvariant() if ($tokenCodes.ContainsKey($key)) { return "" } return $match.Value } $stripped = [regex]::Replace($expanded, $pattern, $stripEvaluator) return Restore-CliPrefixAliasText -Message $stripped } function Write-CliColor { <# .SYNOPSIS Write one colored line to the host. .DESCRIPTION Lightweight CLI UI helper for scripts that need readable operator output without polluting the success pipeline. .PARAMETER Message Text to print. .PARAMETER ForegroundColor Console foreground color. Overrides Style when set. .PARAMETER BackgroundColor Optional console background color. .PARAMETER Style Semantic style preset for default foreground color. Known values: Default, Info, Success, Warning, Error, Muted, Accent, Code. An unknown value warns and falls back to Default. -OnInvalidStyle Error throws. Silent falls back without a warning. .PARAMETER OnInvalidStyle Error, Warn, or Silent. Default: Warn. .PARAMETER NoNewline Writes without a trailing newline. .PARAMETER Literal Print Message as plain text. Skips inline color tokens such as [-]. .EXAMPLE Write-CliColor -Message "Starting..." -Style Info .EXAMPLE Write-CliColor -Message "done" -Style Success .EXAMPLE Write-CliColor -Message "Database is [BrightGreen]Online[-]." .EXAMPLE Write-CliColor -Message "[+] done" .EXAMPLE Write-CliColor -Message "[header]Deployment started[-]" .EXAMPLE Write-CliColor -Message "[divider]================================[-]" .EXAMPLE "alpha","beta" | Write-CliColor -Style Warning #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidUsingWriteHost', '', Justification = 'Intentional operator-facing console UI; this function is the host-output boundary.' )] [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [AllowEmptyString()] [string]$Message, [ConsoleColor]$ForegroundColor, [ConsoleColor]$BackgroundColor, [string]$Style = "Default", [ValidateSet("Error", "Warn", "Silent")] [string]$OnInvalidStyle = "Warn", [switch]$NoNewline, [switch]$Literal ) process { $knownStyles = Get-CliKnownStyleCatalog if ($knownStyles -notcontains $Style) { $validList = $knownStyles -join ", " $styleWarning = "Unknown style '$Style'. Valid values: $validList." switch ($OnInvalidStyle) { "Error" { throw $styleWarning } "Warn" { Write-Warning "$styleWarning Falling back to Default." } "Silent" { } } $Style = "Default" } $resolvedForeground = if ($PSBoundParameters.ContainsKey('ForegroundColor')) { $ForegroundColor } else { Get-CliStyleForegroundColor -Style $Style } $containsTokens = [regex]::IsMatch($Message, '\[(?:[A-Za-z]+|-|\+|~|!|\*)\]') $writeHostParams = @{ ForegroundColor = $resolvedForeground } if ($PSBoundParameters.ContainsKey('BackgroundColor')) { $writeHostParams.BackgroundColor = $BackgroundColor } if ($NoNewline) { $writeHostParams.NoNewline = $true } if ($containsTokens -and -not $Literal) { if (Get-CliAnsiSupportsVirtualTerminal) { $writeHostParams.Object = ConvertTo-CliAnsiMessage -Message $Message } else { $writeHostParams.Object = ConvertFrom-CliTokenMarkup -Message $Message } } else { $writeHostParams.Object = $Message } Write-Host @writeHostParams } } function Write-CliSection { <# .SYNOPSIS Write a titled section, with a rule under header and success titles. .EXAMPLE Write-CliSection -Title "Deployment" .EXAMPLE Write-CliSection -Title "Details" -Style subheader #> [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Title, [ValidateSet('header', 'subheader', 'success', 'info', 'warning', 'error')] [string]$Style = "header", [ValidateRange(8, 120)] [int]$Width = 60 ) Write-CliColor -Message '' Write-CliColor -Message "[$Style]$Title[-]" if ($Style -eq "header" -or $Style -eq "success") { Write-CliDivider -Width $Width } } function Write-CliDivider { <# .SYNOPSIS Write a muted horizontal rule. .EXAMPLE Write-CliDivider .EXAMPLE Write-CliDivider -Width 40 -Character '=' #> [CmdletBinding()] param( [ValidateRange(8, 120)] [int]$Width = 60, [ValidateLength(1, 1)] [string]$Character = "-" ) $line = $Character * $Width Write-CliColor -Message "[divider]$line[-]" } function Write-CliKeyValue { <# .SYNOPSIS Write one aligned label and value row. .EXAMPLE Write-CliKeyValue -Label "Version" -Value "1.2.3" .EXAMPLE Write-CliKeyValue -Label "Service" -Value "[BrightCyan]api[-]" -LabelWidth 12 #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Label, [Parameter(Mandatory = $true)] [AllowEmptyString()] [string]$Value, [ValidateRange(4, 40)] [int]$LabelWidth = 16 ) $labelText = $Label.PadRight($LabelWidth) Write-CliColor -Message " [muted]$labelText[-] : $Value" } function Write-CliList { <# .SYNOPSIS Write an indented list. Items may come from the pipeline. .PARAMETER Style Bullet uses -Bullet, which defaults to an asterisk. Corner uses the Corner glyph, the tree-list mark. .PARAMETER Bullet Mark printed before each item. Overrides Style when you pass it. .EXAMPLE Write-CliList -Style Corner -Item "Get-KadenVersion", "Initialize-KadenRepo" .EXAMPLE "one", "two" | Write-CliList -Bullet "-" #> [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [AllowEmptyString()] [string[]]$Item, [ValidateSet("Bullet", "Corner")] [string]$Style = "Bullet", [string]$Bullet = "*", [ValidateRange(0, 12)] [int]$Indent = 2, [switch]$Literal ) begin { $pad = ' ' * $Indent $mark = $Bullet if ((-not $PSBoundParameters.ContainsKey("Bullet")) -and ($Style -eq "Corner")) { $mark = Get-CliGlyph -Name Corner } } process { foreach ($one in @($Item)) { if ($Literal) { Write-CliColor -Message "$pad$mark $one" -Literal } else { Write-CliColor -Message "$pad$mark $one" } } } } function Write-CliStatus { <# .SYNOPSIS Write a status line with a shorthand prefix. .EXAMPLE Write-CliStatus -State Success -Message "Deployment finished" .EXAMPLE Write-CliStatus -State Warning -Message "2 warnings" #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateSet("Success", "Error", "Warning", "Info", "Changed")] [string]$State, [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [string]$Message ) process { $prefix = switch ($State) { "Success" { "[+]" } "Error" { "[-]" } "Warning" { "[!]" } "Info" { "[*]" } "Changed" { "[~]" } } $style = switch ($State) { "Success" { "Success" } "Error" { "Error" } "Warning" { "Warning" } "Info" { "Info" } "Changed" { "Warning" } } Write-CliColor -Message "$prefix $Message" -Style $style } } function Write-CliEnd { <# .SYNOPSIS Write the trailing blank line that separates script output from the next prompt. .EXAMPLE Write-CliEnd #> [CmdletBinding()] param() Write-CliColor -Message "" } # Code points only. A literal glyph in this file fails the PowerShell ASCII lint gate. $script:CliGlyphCatalog = @{ Prompt = @{ CodePoint = 0x276F; Fallback = '>' } Corner = @{ CodePoint = 0x23BF; Fallback = '>' } TreeBar = @{ CodePoint = 0x2502; Fallback = '|' } TreeBranch = @{ CodePoint = 0x251C; Fallback = '+' } TreeLast = @{ CodePoint = 0x2514; Fallback = '+' } Ok = @{ CodePoint = 0x2714; Fallback = '+' } Fail = @{ CodePoint = 0x2716; Fallback = 'x' } Warn = @{ CodePoint = 0x26A0; Fallback = '!' } Bullet = @{ CodePoint = 0x2022; Fallback = '*' } BigDot = @{ CodePoint = 0x25CF; Fallback = 'o' } EnDash = @{ CodePoint = 0x2013; Fallback = '-' } EmDash = @{ CodePoint = 0x2014; Fallback = '-' } MiddleDot = @{ CodePoint = 0x00B7; Fallback = '.' } PlusMinus = @{ CodePoint = 0x00B1; Fallback = '+-' } Guillemet = @{ CodePoint = 0x00BB; Fallback = '>' } AngleLeft = @{ CodePoint = 0x2039; Fallback = '<' } AngleRight = @{ CodePoint = 0x203A; Fallback = '>' } Multiply = @{ CodePoint = 0x00D7; Fallback = 'x' } Star = @{ CodePoint = 0x204E; Fallback = '*' } OutlineStar = @{ CodePoint = 0x2729; Fallback = '*' } Flower = @{ CodePoint = 0x2055; Fallback = '*' } Diamond = @{ CodePoint = 0x1F539; Fallback = '*' } LargeDiamond = @{ CodePoint = 0x1F537; Fallback = '#' } GClef = @{ CodePoint = 0x1D11E; Fallback = 'G' } FClef = @{ CodePoint = 0x1D122; Fallback = 'F' } QuarterNote = @{ CodePoint = 0x1D15F; Fallback = 'q' } Arrow = @{ CodePoint = 0x2192; Fallback = '->' } ArrowLeft = @{ CodePoint = 0x2190; Fallback = '<-' } ArrowUp = @{ CodePoint = 0x2191; Fallback = '^' } ArrowDown = @{ CodePoint = 0x2193; Fallback = 'v' } ArrowLeftRight = @{ CodePoint = 0x2194; Fallback = '<->' } ArrowSwap = @{ CodePoint = 0x21C4; Fallback = '<->' } ArrowUpDown = @{ CodePoint = 0x21C5; Fallback = '^v' } DoubleLeft = @{ CodePoint = 0x21D0; Fallback = '<-' } DoubleUp = @{ CodePoint = 0x21D1; Fallback = '^' } DoubleRight = @{ CodePoint = 0x21D2; Fallback = '->' } DoubleDown = @{ CodePoint = 0x21D3; Fallback = 'v' } DoubleLeftRight = @{ CodePoint = 0x21D4; Fallback = '<->' } DoubleUpDown = @{ CodePoint = 0x21D5; Fallback = '^v' } HeavyLeft = @{ CodePoint = 0x1F870; Fallback = '<-' } HeavyUp = @{ CodePoint = 0x1F871; Fallback = '^' } HeavyRight = @{ CodePoint = 0x1F872; Fallback = '->' } HeavyDown = @{ CodePoint = 0x1F873; Fallback = 'v' } HeavyUpLeft = @{ CodePoint = 0x1F874; Fallback = '^' } HeavyUpRight = @{ CodePoint = 0x1F875; Fallback = '^' } HeavyDownLeft = @{ CodePoint = 0x1F876; Fallback = 'v' } HeavyDownRight = @{ CodePoint = 0x1F877; Fallback = 'v' } BoxHorizontal = @{ CodePoint = 0x2501; Fallback = '-' } BoxDownRight = @{ CodePoint = 0x250D; Fallback = '+' } BoxDownLeft = @{ CodePoint = 0x2511; Fallback = '+' } BoxUpRight = @{ CodePoint = 0x2515; Fallback = '+' } BoxUpLeft = @{ CodePoint = 0x2519; Fallback = '+' } BoxVerticalRight = @{ CodePoint = 0x251D; Fallback = '+' } BoxVerticalLeft = @{ CodePoint = 0x2525; Fallback = '+' } BoxDownHorizontal = @{ CodePoint = 0x252D; Fallback = '+' } BoxUpHorizontal = @{ CodePoint = 0x2535; Fallback = '+' } BoxVerticalHorizontal = @{ CodePoint = 0x253D; Fallback = '+' } } function Test-CliGlyphHost { if ($PSVersionTable.PSVersion.Major -lt 7) { return $false } try { if ([Console]::IsOutputRedirected) { return $false } } catch { return $false } return $true } function Test-CliGlyphScalar { param( [Parameter(Mandatory = $true)] [int]$CodePoint ) if ($CodePoint -lt 0 -or $CodePoint -gt 0x10FFFF) { return $false } if ($CodePoint -ge 0xD800 -and $CodePoint -le 0xDFFF) { return $false } return $true } function Get-CliGlyphFallback { param( [Parameter(Mandatory = $true)] [int]$CodePoint ) foreach ($entry in $script:CliGlyphCatalog.Values) { if ($entry.CodePoint -eq $CodePoint) { return [string]$entry.Fallback } } return '?' } function Get-CliGlyph { <# .SYNOPSIS Return an operator glyph, or its ASCII fallback. .DESCRIPTION Named marks resolve through the in-module code-point table. -Mode Glyph builds the character with ConvertFromUtf32. -Mode Ansi returns the table fallback. A code point that is not in the table returns '?' on Ansi. -Mode Auto returns a glyph only on PowerShell 7 or later when stdout is a real console. Redirected output stays ASCII. .PARAMETER Name Catalog name. Tab completion lists the marks. .PARAMETER CodePoint Unicode scalar value to render. .PARAMETER Mode Auto, Ansi, or Glyph. Default: Auto. .EXAMPLE Write-CliColor -Message "$(Get-CliGlyph -Name Prompt) Next step" -Style Warning .EXAMPLE Get-CliGlyph -Name Ok -Mode Ansi .EXAMPLE Get-CliGlyph -CodePoint 0x2192 -Mode Glyph #> [CmdletBinding(DefaultParameterSetName = 'Name')] param( [Parameter(Mandatory = $true, ParameterSetName = 'Name', Position = 0)] [ValidateSet( 'Prompt', 'Corner', 'TreeBar', 'TreeBranch', 'TreeLast', 'Ok', 'Fail', 'Warn', 'Bullet', 'BigDot', 'EnDash', 'EmDash', 'MiddleDot', 'PlusMinus', 'Guillemet', 'AngleLeft', 'AngleRight', 'Multiply', 'Star', 'OutlineStar', 'Flower', 'Diamond', 'LargeDiamond', 'GClef', 'FClef', 'QuarterNote', 'Arrow', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'ArrowLeftRight', 'ArrowSwap', 'ArrowUpDown', 'DoubleLeft', 'DoubleUp', 'DoubleRight', 'DoubleDown', 'DoubleLeftRight', 'DoubleUpDown', 'HeavyLeft', 'HeavyUp', 'HeavyRight', 'HeavyDown', 'HeavyUpLeft', 'HeavyUpRight', 'HeavyDownLeft', 'HeavyDownRight', 'BoxHorizontal', 'BoxDownRight', 'BoxDownLeft', 'BoxUpRight', 'BoxUpLeft', 'BoxVerticalRight', 'BoxVerticalLeft', 'BoxDownHorizontal', 'BoxUpHorizontal', 'BoxVerticalHorizontal' )] [string]$Name, [Parameter(Mandatory = $true, ParameterSetName = 'CodePoint')] [int]$CodePoint, [ValidateSet('Auto', 'Ansi', 'Glyph')] [string]$Mode = 'Auto' ) $useGlyph = $false if ($Mode -eq 'Glyph') { $useGlyph = $true } elseif ($Mode -eq 'Auto') { $useGlyph = Test-CliGlyphHost } if ($PSCmdlet.ParameterSetName -eq 'Name') { $entry = $script:CliGlyphCatalog[$Name] if ($useGlyph) { return [char]::ConvertFromUtf32($entry.CodePoint) } return [string]$entry.Fallback } if (-not (Test-CliGlyphScalar -CodePoint $CodePoint)) { throw "Code point $($CodePoint.ToString('X')) is not a Unicode scalar." } if ($useGlyph) { return [char]::ConvertFromUtf32($CodePoint) } return Get-CliGlyphFallback -CodePoint $CodePoint } Export-ModuleMember -Function @( "Write-CliColor" "Write-CliSection" "Write-CliDivider" "Write-CliKeyValue" "Write-CliList" "Write-CliStatus" "Write-CliEnd" "Get-CliGlyph" ) |