Private/ColorNames.ps1
|
# Helpers for the color name and style profile commands function Get-ColorRegisterRgb { <# .SYNOPSIS The RGB color Register-ColorName takes a value as, or $null for a value that is not a color: a hex code, rgb(), hsl(), three numbers 0-255, or a name in the color table. #> param([object]$Value, [hashtable]$Table) if ($Value -is [array]) { if ($Value.Count -ne 3) { return $null } $rgb = [System.Collections.Generic.List[object]]::new() foreach ($channel in $Value) { if (-not ($channel -is [int] -or $channel -is [long] -or $channel -is [int16] -or $channel -is [byte]) -or $channel -lt 0 -or $channel -gt 255) { return $null } $rgb.Add([int]$channel) } return ,$rgb.ToArray() } if ($Value -isnot [string]) { return $null } $entry = $Table[$Value] if ($entry) { return ,@($entry[4][0], $entry[4][1], $entry[4][2]) } $form = ConvertTo-ColorForm -Value $Value return ,[ColorCode]::HexToRgb($form) } function Get-ColorNameOrder { <# .SYNOPSIS Color names in family order: by the name without Dark or Light, then Dark, the name, Light. Names compare by their letters in upper case, ordinal, so the order is the same everywhere. #> param([string[]]$Names) if ($null -eq $Names -or $Names.Count -eq 0) { return } $keys = [string[]]::new($Names.Count) for ($i = 0; $i -lt $Names.Count; $i++) { $name = $Names[$i] $variant = 1 $family = $name if ($name -match '^(?i)dark(.+)$') { $variant = 0 $family = $Matches[1] } elseif ($name -match '^(?i)light(.+)$') { $variant = 2 $family = $Matches[1] } $keys[$i] = '{0}|{1}|{2}' -f $family.ToUpperInvariant(), $variant, $name.ToUpperInvariant() } $sorted = [string[]]$Names.Clone() [Array]::Sort($keys, $sorted, [System.StringComparer]::Ordinal) $sorted } function ConvertTo-ColorJson { <# .SYNOPSIS A value as JSON, indented two spaces: an ordered dictionary as an object, an array as an array, and strings, integers, booleans and $null. Written here rather than with ConvertTo-Json, whose layout differs between Windows PowerShell 5.1 and PowerShell 7, so a profile file has the same bytes everywhere. #> param([object]$Value, [int]$Depth = 0) $indent = ' ' * ($Depth + 1) $close = ' ' * $Depth if ($null -eq $Value) { return 'null' } if ($Value -is [bool]) { return $(if ($Value) { 'true' } else { 'false' }) } if ($Value -is [int] -or $Value -is [long]) { return $Value.ToString([System.Globalization.CultureInfo]::InvariantCulture) } if ($Value -is [string] -or $Value -is [char]) { $builder = [System.Text.StringBuilder]::new('"') foreach ($character in ([string]$Value).ToCharArray()) { switch ($character) { '"' { [void]$builder.Append('\"'); continue } '\' { [void]$builder.Append('\\'); continue } "`n" { [void]$builder.Append('\n'); continue } "`r" { [void]$builder.Append('\r'); continue } "`t" { [void]$builder.Append('\t'); continue } default { if ([int]$character -lt 0x20) { [void]$builder.Append(('\u{0:x4}' -f [int]$character)) } else { [void]$builder.Append($character) } } } } return $builder.Append('"').ToString() } if ($Value -is [System.Collections.IDictionary]) { if ($Value.Count -eq 0) { return '{}' } $members = foreach ($key in $Value.Keys) { $indent + (ConvertTo-ColorJson -Value ([string]$key)) + ': ' + (ConvertTo-ColorJson -Value $Value[$key] -Depth ($Depth + 1)) } return "{`n" + ($members -join ",`n") + "`n$close}" } if ($Value -is [System.Collections.IEnumerable]) { $items = @($Value) if ($items.Count -eq 0) { return '[]' } # A list of numbers, such as an RGB color, stays on one line $simple = $true foreach ($item in $items) { if ($item -isnot [int] -and $item -isnot [long]) { $simple = $false } } if ($simple) { return '[' + (@(foreach ($item in $items) { ConvertTo-ColorJson -Value $item }) -join ', ') + ']' } $members = foreach ($item in $items) { $indent + (ConvertTo-ColorJson -Value $item -Depth ($Depth + 1)) } return "[`n" + ($members -join ",`n") + "`n$close]" } return ConvertTo-ColorJson -Value ([string]$Value) -Depth $Depth } function ConvertFrom-ColorJsonValue { <# .SYNOPSIS A value ConvertFrom-Json read, with whole numbers as [int] as Write-ColorEX takes them, and arrays as object arrays. PowerShell 7 reads whole numbers as [long]. #> param([object]$Value) if ($Value -is [long] -and $Value -ge [int]::MinValue -and $Value -le [int]::MaxValue) { return [int]$Value } if ($Value -is [array]) { $items = [System.Collections.Generic.List[object]]::new() foreach ($item in $Value) { $items.Add((ConvertFrom-ColorJsonValue -Value $item)) } return ,$items.ToArray() } return $Value } # The PSColorStyle properties a profile file holds, in the order it holds them $script:ColorProfileProperties = @( 'ForegroundColor', 'BackgroundColor', 'Gradient', 'BackgroundGradient', 'GradientSpace', 'GradientHue', 'GradientEasing', 'GradientRepeat', 'GradientDirection', 'GradientSteps', 'Style', 'Bold', 'Italic', 'Underline', 'Blink', 'Faint', 'CrossedOut', 'DoubleUnderline', 'Overline', 'Reverse', 'UnderlineColor', 'UnderlineStyle', 'StartTab', 'StartSpaces', 'LinesBefore', 'LinesAfter', 'ShowTime', 'NoNewLine', 'HorizontalCenter', 'AutoPad', 'PadLeft', 'PadCenter', 'PadChar', 'Truncate', 'Wrap' ) function Get-PaletteTextColor { <# .SYNOPSIS The 256-color code of the number Show-ColorTable -Palette writes on a code: 16, black, or 231, white, whichever has the higher contrast on the code's color. On the 16 colors, which the terminal's scheme sets, white on 0-6 and 8 and black on the others. #> param([int]$Code) if ($Code -lt 16) { if ($Code -le 6 -or $Code -eq 8) { return 231 } return 16 } $rgb = [ColorMath]::Ansi8Rgb($Code) if ([ColorMath]::BlackTextReads($rgb[0], $rgb[1], $rgb[2])) { return 16 } return 231 } function Format-ColorSettingValue { <# .SYNOPSIS A color setting as Show-ColorTable -Profiles writes it: a name, hex code or number as it is, an RGB array as @(r, g, b), and a list of colors with commas between them. #> param([object]$Value) if ($null -eq $Value) { return '' } if ($Value -is [array]) { $numbers = $Value.Count -eq 3 foreach ($item in $Value) { if (-not ($item -is [int] -or $item -is [long])) { $numbers = $false } } if ($numbers) { return '@(' + ($Value -join ', ') + ')' } return (@(foreach ($item in $Value) { Format-ColorSettingValue -Value $item }) -join ', ') } return [string]$Value } function Get-ColorStyleSummary { <# .SYNOPSIS A profile's settings in words, as Show-ColorTable -Profiles writes them: its colors ("White on DarkRed"), then each style and layout setting it has by property name, then "(the default style)" for the default style; "no settings" for a profile with none. #> param([PSColorStyle]$Style) $parts = [System.Collections.Generic.List[string]]::new() $foreground = Format-ColorSettingValue -Value $Style.ForegroundColor $background = Format-ColorSettingValue -Value $Style.BackgroundColor if ($foreground -and $background) { $parts.Add("$foreground on $background") } elseif ($foreground) { $parts.Add($foreground) } elseif ($background) { $parts.Add("on $background") } foreach ($property in 'Bold', 'Faint', 'Italic', 'Underline', 'DoubleUnderline', 'Blink', 'Reverse', 'CrossedOut', 'Overline') { if ($Style.$property) { $parts.Add($property) } } if ($Style.UnderlineStyle) { $parts.Add("UnderlineStyle $($Style.UnderlineStyle)") } if ($null -ne $Style.UnderlineColor) { $parts.Add("UnderlineColor $(Format-ColorSettingValue -Value $Style.UnderlineColor)") } if ($Style.Style.Count -gt 0) { $parts.Add("Style $($Style.Style -join ', ')") } if ($null -ne $Style.Gradient -and $Style.Gradient.Count -gt 0) { $parts.Add("Gradient $(Format-ColorSettingValue -Value $Style.Gradient)") } if ($null -ne $Style.BackgroundGradient -and $Style.BackgroundGradient.Count -gt 0) { $parts.Add("BackgroundGradient $(Format-ColorSettingValue -Value $Style.BackgroundGradient)") } foreach ($property in 'GradientSpace', 'GradientHue', 'GradientEasing', 'GradientRepeat', 'GradientDirection') { if ($Style.$property) { $parts.Add("$property $($Style.$property)") } } foreach ($property in 'GradientSteps', 'StartTab', 'StartSpaces', 'LinesBefore', 'LinesAfter', 'AutoPad') { if ($Style.$property -gt 0) { $parts.Add("$property $($Style.$property)") } } foreach ($property in 'ShowTime', 'NoNewLine', 'HorizontalCenter', 'PadLeft', 'PadCenter', 'Truncate', 'Wrap') { if ($Style.$property) { $parts.Add($property) } } if ($Style.PadChar -ne ' ') { $parts.Add("PadChar '$($Style.PadChar)'") } $text = $parts -join ', ' if ([object]::ReferenceEquals($Style, [PSColorStyle]::Default)) { return ("$text (the default style)").TrimStart() } if ($parts.Count -eq 0) { return 'no settings' } return $text } function New-SgrStyleProperties { <# .SYNOPSIS The PSColorStyle properties ConvertFrom-SgrStyle sets, each with no color or style. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'It returns a new set of properties and changes nothing.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'It returns several properties, which the plural names.')] param() [ordered]@{ ForegroundColor = $null; BackgroundColor = $null; UnderlineColor = $null Bold = $false; Faint = $false; Italic = $false; Underline = $false; DoubleUnderline = $false UnderlineStyle = ''; Blink = $false; Reverse = $false; CrossedOut = $false; Overline = $false } } function ConvertFrom-SgrColor { <# .SYNOPSIS The color of a 256-color number (Kind 5) or an RGB color (Kind 2, the last three values) in an escape code: a console color name for the numbers 0-15, otherwise '#RRGGBB'. $null for values that are not a color. #> param([string]$Kind, [string[]]$Values, [hashtable]$PaletteNames) if ($Kind -eq '5' -and $Values.Count -ge 1) { $number = 0 if (-not [int]::TryParse($Values[0], [ref]$number) -or $number -lt 0 -or $number -gt 255) { return $null } if ($number -lt 16) { return $PaletteNames[$number] } $rgb = [ColorMath]::Ansi8Rgb($number) return '#{0:X2}{1:X2}{2:X2}' -f $rgb[0], $rgb[1], $rgb[2] } if ($Kind -eq '2' -and $Values.Count -ge 3) { $rgb = [int[]]::new(3) for ($i = 0; $i -lt 3; $i++) { $channel = 0 if (-not [int]::TryParse($Values[$Values.Count - 3 + $i], [ref]$channel) -or $channel -lt 0 -or $channel -gt 255) { return $null } $rgb[$i] = $channel } return '#{0:X2}{1:X2}{2:X2}' -f $rgb[0], $rgb[1], $rgb[2] } return $null } function ConvertFrom-SgrStyle { <# .SYNOPSIS The colors and styles a string of SGR escape codes sets, as PSColorStyle property values, and the codes and text no property holds. .DESCRIPTION Returns @{ Properties; Ignored }: the properties New-SgrStyleProperties lists, and a list of what was left out. A 16-color code, or a 256-color number 0-15, becomes its console color name, which Write-ColorEX writes without a mode switch as that same code; a 256-color number 16-255 or an RGB color becomes '#RRGGBB'. The codes apply in order, a later one over an earlier one, and 0 clears them all. An underline color is kept only with an underline, since Write-ColorEX underlines a segment that has an underline color. #> param([string]$Sequence) $consoleNames = @{} foreach ($name in [ColorCode]::ConsoleSgr.Keys) { $consoleNames[[int][ColorCode]::ConsoleSgr[$name]] = $name } $paletteNames = @{} foreach ($name in [ColorCode]::ConsoleAnsiIndex.Keys) { $paletteNames[[int][ColorCode]::ConsoleAnsiIndex[$name]] = $name } $properties = New-SgrStyleProperties $ignored = [System.Collections.Generic.List[string]]::new() if ([string]::IsNullOrEmpty($Sequence)) { return @{ Properties = $properties; Ignored = $ignored } } foreach ($match in [regex]::Matches($Sequence, '\x1b\[([0-9;:]*)m')) { $parameters = $match.Groups[1].Value.Split(';') $i = 0 while ($i -lt $parameters.Count) { $parameter = $parameters[$i] $parts = $parameter.Split(':') $code = 0 if ($parts[0].Length -gt 0 -and -not [int]::TryParse($parts[0], [ref]$code)) { $ignored.Add($parameter) $i++ continue } $underline = $null if ($code -eq 0) { $properties = New-SgrStyleProperties } elseif ($code -eq 1) { $properties.Bold = $true } elseif ($code -eq 2) { $properties.Faint = $true } elseif ($code -eq 3) { $properties.Italic = $true } elseif ($code -eq 4) { $kind = '1' if ($parts.Count -gt 1) { $kind = $parts[1] } switch ($kind) { '0' { $underline = 'None' } '1' { $underline = 'Single' } '2' { $underline = 'Double' } '3' { $underline = 'Curly' } '4' { $underline = 'Dotted' } '5' { $underline = 'Dashed' } default { $ignored.Add($parameter) } } } elseif ($code -eq 5 -or $code -eq 6) { $properties.Blink = $true } elseif ($code -eq 7) { $properties.Reverse = $true } elseif ($code -eq 9) { $properties.CrossedOut = $true } elseif ($code -eq 21) { $underline = 'Double' } elseif ($code -eq 22) { $properties.Bold = $false $properties.Faint = $false } elseif ($code -eq 23) { $properties.Italic = $false } elseif ($code -eq 24) { $underline = 'None' } elseif ($code -eq 25) { $properties.Blink = $false } elseif ($code -eq 27) { $properties.Reverse = $false } elseif ($code -eq 29) { $properties.CrossedOut = $false } elseif (($code -ge 30 -and $code -le 37) -or ($code -ge 90 -and $code -le 97)) { $properties.ForegroundColor = $consoleNames[$code] } elseif ($code -eq 39) { $properties.ForegroundColor = $null } elseif (($code -ge 40 -and $code -le 47) -or ($code -ge 100 -and $code -le 107)) { $properties.BackgroundColor = $consoleNames[$code - 10] } elseif ($code -eq 49) { $properties.BackgroundColor = $null } elseif ($code -eq 53) { $properties.Overline = $true } elseif ($code -eq 55) { $properties.Overline = $false } elseif ($code -eq 59) { $properties.UnderlineColor = $null } elseif ($code -eq 38 -or $code -eq 48 -or $code -eq 58) { # 38;5;N and 38;2;R;G;B take the parameters after them; 38:5:N and 38:2:R:G:B, # with or without a color space before R, hold them in one $color = $null $taken = @($parameter) if ($parts.Count -gt 1) { $color = ConvertFrom-SgrColor -Kind $parts[1] -Values ([string[]]@($parts | Select-Object -Skip 2)) -PaletteNames $paletteNames } elseif ($i + 2 -lt $parameters.Count -and $parameters[$i + 1] -eq '5') { $taken = $parameters[$i..($i + 2)] $color = ConvertFrom-SgrColor -Kind '5' -Values ([string[]]@($parameters[$i + 2])) -PaletteNames $paletteNames $i += 2 } elseif ($i + 4 -lt $parameters.Count -and $parameters[$i + 1] -eq '2') { $taken = $parameters[$i..($i + 4)] $color = ConvertFrom-SgrColor -Kind '2' -Values ([string[]]$parameters[($i + 2)..($i + 4)]) -PaletteNames $paletteNames $i += 4 } if ($null -eq $color) { $ignored.Add($taken -join ';') } elseif ($code -eq 38) { $properties.ForegroundColor = $color } elseif ($code -eq 48) { $properties.BackgroundColor = $color } else { $properties.UnderlineColor = $color } } else { $ignored.Add($parameter) } if ($null -ne $underline) { $properties.Underline = $underline -eq 'Single' $properties.DoubleUnderline = $underline -eq 'Double' $properties.UnderlineStyle = '' if ($underline -eq 'Curly' -or $underline -eq 'Dotted' -or $underline -eq 'Dashed') { $properties.UnderlineStyle = $underline } } $i++ } } $text = [regex]::Replace($Sequence, '\x1b\[[0-9;:]*m', '') if ($text.Length -gt 0) { $ignored.Add("'" + ($text -replace '\x1b', '`e') + "'") } if (-not ($properties.Underline -or $properties.DoubleUnderline -or $properties.UnderlineStyle)) { $properties.UnderlineColor = $null } return @{ Properties = $properties; Ignored = $ignored } } function ConvertFrom-HostColor { <# .SYNOPSIS A color setting of the host as a color Write-ColorEX takes: a console color as its name, a color with R, G and B properties, as the ISE's are, as '#RRGGBB', and $null for a transparent one or none. #> param([object]$Value) if ($null -eq $Value) { return $null } if ($Value -is [System.ConsoleColor]) { return $Value.ToString() } $members = $Value.PSObject.Properties if ($null -ne $members['R'] -and $null -ne $members['G'] -and $null -ne $members['B']) { if ($null -ne $members['A'] -and [int]$Value.A -eq 0) { return $null } return '#{0:X2}{1:X2}{2:X2}' -f [int]$Value.R, [int]$Value.G, [int]$Value.B } Write-Warning "The host's color '$Value' is not a color a profile takes, so it is left out." return $null } function Get-PowerShellColorSetting { <# .SYNOPSIS PowerShell's own color settings: @{ PSStyle; PrivateData }, $PSStyle $null where PowerShell has none (before 7.2). #> @{ PSStyle = Get-Variable -Name PSStyle -ValueOnly -ErrorAction Ignore PrivateData = $Host.PrivateData } } function Get-PowerShellThemeStyle { <# .SYNOPSIS The profiles Import-ColorProfile -Theme PowerShell sets, each exactly one of PowerShell's own settings: from $PSStyle where there is one (PowerShell 7.2 and later), otherwise from the host's PrivateData. .DESCRIPTION With $PSStyle: Error, Warning, Info and Debug from $PSStyle.Formatting's Error, Warning, Verbose and Debug, Critical from ErrorAccent and Default from FormatAccent. Without it: Error, Warning, Info and Debug from the host's ErrorForegroundColor and ErrorBackgroundColor and the like. A setting the host lacks leaves its profile out, with a warning. #> param([object]$PSStyleValue, [object]$PrivateData) $styles = [System.Collections.Generic.List[object]]::new() if ($null -ne $PSStyleValue) { $settings = [ordered]@{ Error = 'Error'; Warning = 'Warning'; Info = 'Verbose'; Debug = 'Debug'; Critical = 'ErrorAccent'; Default = 'FormatAccent' } foreach ($name in $settings.Keys) { $setting = $settings[$name] $member = $null if ($null -ne $PSStyleValue.Formatting) { $member = $PSStyleValue.Formatting.PSObject.Properties[$setting] } if ($null -eq $member) { Write-Warning "PowerShell has no $setting setting here, so the $name profile keeps its colors." continue } $read = ConvertFrom-SgrStyle -Sequence ([string]$member.Value) if ($read.Ignored.Count -gt 0) { Write-Warning "PowerShell's $setting setting has codes no profile holds, left out of the $name profile: $($read.Ignored -join ', ')" } $style = [PSColorStyle]::new($name, $read.Properties.ForegroundColor, $read.Properties.BackgroundColor) foreach ($property in $read.Properties.Keys) { if ($property -ne 'ForegroundColor' -and $property -ne 'BackgroundColor') { $style.$property = $read.Properties[$property] } } $styles.Add($style) } return ,$styles } $settings = [ordered]@{ Error = 'Error'; Warning = 'Warning'; Info = 'Verbose'; Debug = 'Debug' } foreach ($name in $settings.Keys) { $setting = $settings[$name] $foreground = $null $background = $null if ($null -ne $PrivateData) { $foreground = $PrivateData.PSObject.Properties["$($setting)ForegroundColor"] $background = $PrivateData.PSObject.Properties["$($setting)BackgroundColor"] } if ($null -eq $foreground -and $null -eq $background) { Write-Warning "The host has no $setting colors, so the $name profile keeps its colors." continue } $foregroundColor = $null $backgroundColor = $null if ($null -ne $foreground) { $foregroundColor = ConvertFrom-HostColor -Value $foreground.Value } if ($null -ne $background) { $backgroundColor = ConvertFrom-HostColor -Value $background.Value } $styles.Add([PSColorStyle]::new($name, $foregroundColor, $backgroundColor)) } return ,$styles } |