Elizium.Krayola.psm1
Set-StrictMode -Version 1.0 function Get-DefaultHostUiColours { <# .NAME Get-DefaultHostUiColours .SYNOPSIS Get the default foreground and background colours of the console host. .DESCRIPTION Currently there is an open issue <https://github.com/PowerShell/PowerShell/issues/14727> which means that on a mac, the default colours obtained from the host are both incorrectly set to -1. This function takes this deficiency into account and will ensure that sensible colour values are always returned. #> [OutputType([string[]])] param() [string]$rawFgc, [string]$rawBgc = get-RawHostUiColours; [boolean]$isLight = Get-IsKrayolaLightTerminal; [string]$defaultFgc = $isLight ? 'black' : 'gray'; [string]$defaultBgc = $isLight ? 'gray' : 'black'; [string]$fore = Get-EnvironmentVariable 'KRAYOLA_FORE' -Default $rawFgc; [string]$back = Get-EnvironmentVariable 'KRAYOLA_BACK' -Default $rawBgc; [string[]]$colours = [enum]::GetNames("System.ConsoleColor"); if (-not($( $colours -contains $fore ))) { $fore = $defaultFgc; } if (-not($( $colours -contains $back ))) { $back = $defaultBgc; } return $fore, $back; } function Get-EnvironmentVariable { <# .NAME Get-EnvironmentVariable .Synopsis Wrapper around [System.Environment]::GetEnvironmentVariable to support unit testing. .DESCRIPTION Retrieve the value of the environment variable specified. Returns $null if variable is not found. .EXAMPLE Get-EnvironmentVariable 'KRAYOLA_THEME_NAME' #> [CmdletBinding()] [OutputType([string])] Param ( [Parameter(Mandatory = $true, Position=0)] [string]$Variable, [Parameter(Position = 1)] $Default ) $value = [System.Environment]::GetEnvironmentVariable($Variable); if (-not($value) -and ($Default)) { $value = $Default; } return $value; } function Get-IsKrayolaLightTerminal { <# .NAME Get-IsKrayolaLightTerminal .SYNOPSIS Gets the value of KRAYOLA_LIGHT_TERMINAL as a boolean. .DESCRIPTION For use by applications that need to use a Krayola theme that is dependent on whether a light or dark background colour is in effect in the current terminal. #> [OutputType([boolean])] param() return -not([string]::IsNullOrWhiteSpace( (Get-EnvironmentVariable 'KRAYOLA_LIGHT_TERMINAL'))); } function Get-KrayolaTheme { <# .NAME Get-KrayolaTheme .SYNOPSIS Helper function that makes it easier for client applications to get a Krayola theme from the environment, which is compatible with the terminal colours being used. This helps keep output from different applications consistent. .DESCRIPTION If $KrayolaThemeName is specified, then it is used to lookup the theme in the global $KrayolaThemes hash-table exposed by the Krayola module. If either the theme specified does not exist or not specified, then a default theme is used. The default theme created should be compatible with the dark/lightness of the background of the terminal currently in use. By default, a dark terminal is assumed and the colours used show up clearly against a dark background. If KRAYOLA_LIGHT_TERMINAL is defined as an environment variable (can be set to any string apart from empty string/white space), then the colours chosen show up best against a light background. Typically, a user would create their own custom theme and then populate this into the $KrayolaThemes collection. This should be done in the user profile so as to become available in all powershell sessions. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseBOMForUnicodeEncodedFile', '')] [OutputType([hashtable])] param ( [Parameter( Mandatory = $false, Position = 0 )] [AllowEmptyString()] [string]$KrayolaThemeName, [Parameter(Mandatory = $false)] [hashtable]$Themes = $KrayolaThemes, [Parameter(Mandatory = $false)] [hashtable]$DefaultTheme = @{ # DefaultTheme is compatible with dark consoles by default # 'FORMAT' = '"<%KEY%>" => "<%VALUE%>"'; 'KEY-PLACE-HOLDER' = '<%KEY%>'; 'VALUE-PLACE-HOLDER' = '<%VALUE%>'; 'KEY-COLOURS' = @('DarkCyan'); 'VALUE-COLOURS' = @('White'); "AFFIRM-COLOURS" = @("Red"); 'OPEN' = '['; 'CLOSE' = ']'; 'SEPARATOR' = ', '; 'META-COLOURS' = @('Yellow'); 'MESSAGE-COLOURS' = @('Cyan'); 'MESSAGE-SUFFIX' = ' // '; } ) [hashtable]$displayTheme = $DefaultTheme; # Switch to use colours compatible with light consoles if KRAYOLA_LIGHT_TERMINAL # is set. # if (Get-IsKrayolaLightTerminal) { $displayTheme['KEY-COLOURS'] = @('DarkBlue'); $displayTheme['VALUE-COLOURS'] = @('Red'); $displayTheme['AFFIRM-COLOURS'] = @('Magenta'); $displayTheme['META-COLOURS'] = @('DarkMagenta'); $displayTheme['MESSAGE-COLOURS'] = @('Green'); } [string]$themeName = $KrayolaThemeName; # Get the theme name # if ([string]::IsNullOrWhiteSpace($themeName)) { $themeName = Get-EnvironmentVariable 'KRAYOLA_THEME_NAME'; } if ($Themes -and $Themes.ContainsKey($themeName)) { $displayTheme = $Themes[$themeName]; } return $displayTheme; } function Get-Krayon { <# .NAME Get-Krayon .SYNOPSIS Helper factory function that creates Krayon instance. .DESCRIPTION Creates a new krayon instance with the optional krayola theme provided. The krayon contains various methods for writing text directly to the console (See online documentation for more information). #> [OutputType([Krayon])] param( [Parameter()] [hashtable]$theme = $(Get-KrayolaTheme) ) return New-Krayon -Theme $theme; } function Show-ConsoleColours { [Alias('Show-ConsoleColors')] param () <# .NAME Show-ConsoleColours .SYNOPSIS Helper function that shows all the available console colours in the colour they represent. This will assist in the development of colour Themes. #> [Array]$colours = @('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', ` 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White'); foreach ($col in $colours) { Write-Host -ForegroundColor $col $col; } } $Global:KrayolaThemes = @{ 'EMERGENCY-THEME' = @{ 'FORMAT' = '{{<%KEY%>}}={{<%VALUE%>}}'; 'KEY-PLACE-HOLDER' = '<%KEY%>'; 'VALUE-PLACE-HOLDER' = '<%VALUE%>'; 'KEY-COLOURS' = @('White'); 'VALUE-COLOURS' = @('DarkGray'); "AFFIRM-COLOURS" = @("Yellow"); 'OPEN' = '{'; 'CLOSE' = '}'; 'SEPARATOR' = '; '; 'META-COLOURS' = @('Black'); 'MESSAGE-COLOURS' = @('Gray'); 'MESSAGE-SUFFIX' = ' ֎ ' }; 'ROUND-THEME' = @{ 'FORMAT' = '"<%KEY%>"="<%VALUE%>"'; 'KEY-PLACE-HOLDER' = '<%KEY%>'; 'VALUE-PLACE-HOLDER' = '<%VALUE%>'; 'KEY-COLOURS' = @('DarkCyan'); 'VALUE-COLOURS' = @('DarkBlue'); "AFFIRM-COLOURS" = @("Red"); 'OPEN' = '••• ('; 'CLOSE' = ') •••'; 'SEPARATOR' = ' @@ '; 'META-COLOURS' = @('Yellow'); 'MESSAGE-COLOURS' = @('Cyan'); 'MESSAGE-SUFFIX' = ' ~~ ' }; 'SQUARE-THEME' = @{ 'FORMAT' = '"<%KEY%>"="<%VALUE%>"'; 'KEY-PLACE-HOLDER' = '<%KEY%>'; 'VALUE-PLACE-HOLDER' = '<%VALUE%>'; 'KEY-COLOURS' = @('DarkCyan'); 'VALUE-COLOURS' = @('DarkBlue'); "AFFIRM-COLOURS" = @("Blue"); 'OPEN' = '■■■ ['; 'CLOSE' = '] ■■■'; 'SEPARATOR' = ' ## '; 'META-COLOURS' = @('Black'); 'MESSAGE-COLOURS' = @('DarkGreen'); 'MESSAGE-SUFFIX' = ' == ' }; 'ANGULAR-THEME' = @{ 'FORMAT' = '"<%KEY%>"-->"<%VALUE%>"'; 'KEY-PLACE-HOLDER' = '<%KEY%>'; 'VALUE-PLACE-HOLDER' = '<%VALUE%>'; 'KEY-COLOURS' = @('DarkCyan'); 'VALUE-COLOURS' = @('DarkBlue'); "AFFIRM-COLOURS" = @("Blue"); 'OPEN' = '◄◄◄ <'; 'CLOSE' = '> ►►►'; 'SEPARATOR' = ' ^^ '; 'META-COLOURS' = @('Black'); 'MESSAGE-COLOURS' = @('DarkGreen'); 'MESSAGE-SUFFIX' = ' // ' } } $null = $KrayolaThemes; function Write-InColour { <# .NAME Write-InColour .SYNOPSIS Writes a multiple snippets of a line in colour with the provided text, foreground & background colours. .DESCRIPTION :warning: DEPRECATED, use Scribbler/Krayon instead. The user passes in an array of 1,2 or 3 element arrays, which contains any number of text fragments with an optional colour specification (ConsoleColor enumeration). The function will then write a multi coloured text line to the console. Element 0: text Element 1: foreground colour Element 2: background colour If the background colour is required, then the foreground colour must also be specified. Write-InColour -colouredTextLine @( ("some text", "Blue"), ("some more text", "Red", "White") ) Write-InColour -colouredTextLine @( ("some text", "Blue"), ("some more text", "Red") ) Write-InColour -colouredTextLine @( ("some text", "Blue"), ("some more text") ) If you only need to write a single element, use an extra , preceding the array eg: Write-InColour -colouredTextLine @( ,@("some text", "Blue") ) Empty snippets, should not be passed in, it's up to the caller to ensure that this is the case. If an empty snippet is found an ugly warning message is emitted, so this should not go un-noticed. .PARAMETER TextSnippets An array of an array of strings (see description). .PARAMETER NoNewLine Switch to indicate if a new line should be written after the text. #> # This function is supposed to write to the host, because the output is in colour. # Using Write-Host is Krayola's raison d'etre! # [Alias('Write-InColor')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[][]] $TextSnippets, [Switch]$NoNewLine ) foreach ($snippet in $TextSnippets) { if ($snippet.Length -eq 0) { Write-Warning ' * Found malformed line (empty snippet entry), skipping * '; continue; } if ($snippet.Length -eq 1) { Write-Warning " * No colour specified for snippet '$($snippet[0])', skipping * "; continue; } if ($null -eq $snippet[0]) { Write-Warning ' * Found empty snippet text, skipping *'; continue; } if ($snippet.Length -eq 2) { if ($null -eq $snippet[1]) { Write-Warning " * Foreground col is null, for snippet: '$($snippet[0])', skipping * "; continue; } # Foreground colour specified # Write-Host $snippet[0] -NoNewline -ForegroundColor $snippet[1]; } else { # Foreground and background colours specified # Write-Host $snippet[0] -NoNewline -ForegroundColor $snippet[1] -BackgroundColor $snippet[2]; if ($snippet.Length -gt 3) { Write-Warning " * Excess entries found for snippet: '$($snippet[0])' * "; } } } if (-not ($NoNewLine.ToBool())) { Write-Host ''; } } function Write-RawPairsInColour { <# .NAME Write-RawPairsInColour .SYNOPSIS .DESCRIPTION !! DEPRECATED, use Scribbler/Krayon instead. The snippets passed in as element of $Pairs are in the same format as those passed into Write-InColour as TextSnippets. The only difference is that each snippet can only have 2 entries, the first being the key and the second being the value. .PARAMETER Pairs A 3 dimensional array representing a sequence of key/value pairs where each key and value are in themselves a sub-sequence of 2 or 3 items representing text, foreground colour & background colours. Eg: $PairsToWriteInColour = @( @(@("Sport", "Red"), @("Tennis", "Blue", "Yellow")), @(@("Star", "Green"), @("Martina Hingis", "Cyan")) ); .PARAMETER Format A string containing a placeholder for the Key and the Value. It represents how the whole key/value pair should be represented. It must contain the KEY-PLACE-HOLDER and VALUE-PLACE-HOLDER strings .PARAMETER KeyPlaceHolder The place holder that identifies the Key in the FORMAT string .PARAMETER ValuePlaceHolder The place holder that identifies the Value in the FORMAT string. .PARAMETER KeyColours Array of 1 or 2 items only, the first is the foreground colour and the optional second value is the background colour, that specifies how Keys are displayed. .PARAMETER ValueColours The same as KEY-COLOURS but it applies to Values. .PARAMETER Open Specifies the leading wrapper around the whole key/value pair collection, typically '('. .PARAMETER Close Specifies the tail wrapper around the whole key/value pair collection typically ')'. .PARAMETER Separator Specifies a sequence of characters that separates the Key/Vale pairs, typically ','. .PARAMETER MetaColours This is the colour specifier for any character that is not the key or the value. Eg: if the format is defined as ['<%KEY%>'='<%VALUE%>'], then [' '=' '] will be written in this colour. As with other write in clour functionality, the user can specify just a single colour (in a single item array), which would represent the foreground colour, or 2 colours can be specified, representing the foreground and background colours in that order inside the 2 element array (a pair). These meta colours will also apply to the Open, Close and Separator tokens. .PARAMETER MessageColours An optional message that appears preceding the Key/Value pair collection and this array describes the colours used to write that message. .PARAMETER MessageSuffix Specifies a sequence of characters that separates the MESSAGE (if present) from the Key/Value pair collection. #> # This function is supposed to write to the host, because the output is in colour. # Using Write-Host is Krayola's raison d'etre! # [Alias('Write-RawPairsInColor')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [string[][][]] $Pairs, [Parameter(Mandatory = $false)] [string] $Format = '("<%KEY%>"="<%VALUE%>")', [Parameter(Mandatory = $false)] [string] $KeyPlaceHolder = '<%KEY%>', [Parameter(Mandatory = $false)] [string] $ValuePlaceHolder = '<%VALUE%>', [Parameter(Mandatory = $false)] [AllowEmptyString()] $Open = '=== [', [AllowEmptyString()] [Parameter(Mandatory = $false)] $Close = '] ===', [AllowEmptyString()] [Parameter(Mandatory = $false)] $Separator = ', ', [Parameter(Mandatory = $false)] [string[]] $MetaColours = @('White'), [Parameter(Mandatory = $false)] [string] $Message, [Parameter(Mandatory = $false)] [string[]] $MessageColours = @('White'), [AllowEmptyString()] [Parameter(Mandatory = $false)] [string] $MessageSuffix = ' // ', [Switch]$NoNewLine ) if ($Pairs.Length -eq 0) { return; } if (($MetaColours.Length -lt 1) -or ($MetaColours.Length -gt 2)) { Write-Error -Message "Bad meta colours spec, aborting (No of colours specified: $($MetaColours.Length))"; } # Write the leading message # if (-not([String]::IsNullOrEmpty($Message))) { [string[]]$messageSnippet = @($Message) + $MessageColours; [string[][]]$wrapper = @(, $messageSnippet); Write-InColour -TextSnippets $wrapper -NoNewLine; if (-not([String]::IsNullOrEmpty($MessageSuffix))) { [string[]]$suffixSnippet = @($MessageSuffix) + $MessageColours; [string[][]]$wrapper = @(, $suffixSnippet); Write-InColour -TextSnippets $wrapper -NoNewLine; } } # Add the Open snippet # if (-not([String]::IsNullOrEmpty($Open))) { [string[]]$openSnippet = @($Open) + $MetaColours; [string[][]]$wrapper = @(, $openSnippet); Write-InColour -TextSnippets $wrapper -NoNewLine; } [int]$fieldCounter = 0; foreach ($field in $Pairs) { [string[][]]$displayField = @(); # Each element of a pair is an instance of a snippet that is compatible with Write-InColour # which we can defer to. We need to create the 5 snippets that represents the field pair. # if ($field.Length -ge 2) { # Get the key and value # [string[]]$keySnippet = $field[0]; $keyText, $keyColours = $keySnippet; [string[]]$valueSnippet = $field[1]; $valueText, $valueColours = $valueSnippet; [string[]]$constituents = Split-KeyValuePairFormatter -Format $Format ` -KeyConstituent $keyText -ValueConstituent $valueText ` -KeyPlaceHolder $KeyPlaceHolder -ValuePlaceHolder $ValuePlaceHolder; [string[]]$constituentSnippet = @(); # Now create the 5 snippets (header, key/value, mid, value/key, tail) # # header # $constituentSnippet = @($constituents[0]) + $MetaColours; $displayField += , $constituentSnippet; # key # $constituentSnippet = @($constituents[1]) + $keyColours; $displayField += , $constituentSnippet; # mid # $constituentSnippet = @($constituents[2]) + $MetaColours; $displayField += , $constituentSnippet; # value # $constituentSnippet = @($constituents[3]) + $valueColours; $displayField += , $constituentSnippet; # tail # $constituentSnippet = @($constituents[4]) + $MetaColours; $displayField += , $constituentSnippet; Write-InColour -TextSnippets $displayField -NoNewLine; if ($field.Length -gt 2) { Write-Warning ' * Ignoring excess snippets *'; } } else { Write-Warning ' * Insufficient snippet pair, 2 required, skipping *'; } # Field Separator snippet # if (($fieldCounter -lt ($Pairs.Length - 1)) -and (-not([String]::IsNullOrEmpty($Separator)))) { [string[]]$separatorSnippet = @($Separator) + $MetaColours; Write-InColour -TextSnippets @(, $separatorSnippet) -NoNewLine; } $fieldCounter++; } # Add the Close snippet # if (-not([String]::IsNullOrEmpty($Close))) { [string[]]$closeSnippet = @($Close) + $MetaColours; [string[][]]$wrapper = @(, $closeSnippet); Write-InColour -TextSnippets $wrapper -NoNewLine; } if (-not($NoNewLine.ToBool())) { Write-Host ''; } } function Write-ThemedPairsInColour { <# .NAME Write-ThemedPairsInColour .SYNOPSIS Writes a collection of key/value pairs in colour according to a specified Theme. .DESCRIPTION !! DEPRECATED, use Scribbler/Krayon instead. The Pairs defined here are colour-less, instead colours coming from the KEY-COLOURS and VALUE-COLOURS in the theme. The implications of this are firstly, the Pairs are simpler to specify. However, the colour representation is more restricted, because all Keys displayed must be of the same colour and the same goes for the values. When using Write-RawPairsInColour directly, the user has to specify each element of a pair with 2 or 3 items; text, foreground colour & background colour, eg: @(@("Sport", "Red"), @("Tennis", "Blue", "Yellow")) Now, each pair is specified as a simply a pair of strings: @("Sport", "Tennis") The purpose of this function is to generate a single call to Write-RawPairsInColour in the form: $PairsToWriteInColour = @( @(@("Sport", "Red"), @("Tennis", "Blue", "Yellow")), @(@("Star", "Green"), @("Martina Hingis", "Cyan")) ); Write-RawPairsInColour -Message ">>> Greetings" -MessageColours @("Magenta") ` -Pairs $PairsToWriteInColour -Format "'<%KEY%>'<--->'<%VALUE%>'" ` -MetaColours @(,"Blue") -Open " ••• <<" -Close ">> •••" A value can be highlighted by specifying a boolean affirmation value after the key/value pair. So the 'value' of a pair, eg 'Tennis' of @("Sport", "Tennis") can be highlighted by the addition of a boolean value: @("Sport", "Tennis", $true), will result in 'Tennis' being highlighted; written with a different colour value. This colour value is taken from the 'AFFIRM-COLOURS' entry in the theme. If the affirmation value is false, eg @("Sport", "Tennis", $false), then the value 'Tennis' will be written as per-normal using the 'VALUE-COLOURS' entry. You can create your own theme, using this template for assistance: $YourTheme = @{ "FORMAT" = "'<%KEY%>' = '<%VALUE%>'"; "KEY-PLACE-HOLDER" = "<%KEY%>"; "VALUE-PLACE-HOLDER" = "<%VALUE%>"; "KEY-COLOURS" = @("Red"); "VALUE-COLOURS" = @("Magenta"); "AFFIRM-COLOURS" = @("White"); "OPEN" = "("; "CLOSE" = ")"; "SEPARATOR" = ", "; "META-COLOURS" = @("Blue"); "MESSAGE-COLOURS" = @("Green"); "MESSAGE-SUFFIX" = " // " } .PARAMETER Pairs A 2 dimensional array representing the key/value pairs to be rendered. .PARAMETER Theme Hash-table that must contain all the following fields FORMAT KEY-PLACE-HOLDER VALUE-PLACE-HOLDER KEY-COLOURS VALUE-COLOURS OPEN CLOSE SEPARATOR META-COLOURS MESSAGE-COLOURS MESSAGE-SUFFIX .PARAMETER Message An optional message that precedes the display of the Key/Value sequence. #> [Alias('Write-ThemedPairsInColor')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseBOMForUnicodeEncodedFile', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [string[][]] $Pairs, [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $Theme, [Parameter(Mandatory = $false)] [string] $Message ) if (0 -eq $Pairs.Length) { return; } [boolean]$inEmergency = $false; function isThemeValid { param( [System.Collections.Hashtable]$themeToValidate ) [int]$minimumNoOfThemeEntries = 11; return ($themeToValidate -and ($themeToValidate.Count -ge $minimumNoOfThemeEntries)) } if (-not(isThemeValid($Theme))) { $Theme = $KrayolaThemes['EMERGENCY-THEME']; # In case the user has compromised the EMERGENCY theme, which should be modify-able (because we # can't be sure that the emergency theme we have defined is suitable for their console), # we'll use this internal emergency theme ... # if (-not(isThemeValid($Theme))) { $Theme = @{ 'FORMAT' = '{{<%KEY%>}}={{<%VALUE%>}}'; 'KEY-PLACE-HOLDER' = '<%KEY%>'; 'VALUE-PLACE-HOLDER' = '<%VALUE%>'; 'KEY-COLOURS' = @('White'); 'VALUE-COLOURS' = @('DarkGray'); 'OPEN' = '{'; 'CLOSE' = '}'; 'SEPARATOR' = '; '; 'META-COLOURS' = @('Black'); 'MESSAGE-COLOURS' = @('Gray'); 'MESSAGE-SUFFIX' = ' ֎ ' } } $inEmergency = $true; } [string[][][]] $pairsToWriteInColour = @(); # Construct the pairs # [string[]]$keyColours = $Theme['KEY-COLOURS']; [string[]]$valueColours = $Theme['VALUE-COLOURS']; foreach ($pair in $Pairs) { if (1 -ge $pair.Length) { [string[]]$transformedKey = @('!INVALID!') + $keyColours; [string[]]$transformedValue = @('---') + $valueColours; Write-Error "Found pair that does not contain 2 items (pair: $($pair)) [!!! Reminder: you need to use the comma op for a single item array]"; } else { [string[]]$transformedKey = @($pair[0]) + $keyColours; [string[]]$transformedValue = @($pair[1]) + $valueColours; # Apply affirmation # if ((3 -eq $pair.Length)) { if (($pair[2] -ieq 'true')) { if ($Theme.ContainsKey('AFFIRM-COLOURS')) { $transformedValue = @($pair[1]) + $Theme['AFFIRM-COLOURS']; } else { # Since the affirmation colour is missing, use another way of highlighting the value # ie, surround in asterisks # $transformedValue = @("*{0}*" -f $pair[1]) + $valueColours; } } elseif (-not($pair[2] -ieq 'false')) { Write-Error "Invalid affirm value found; not boolean value, found: $($pair[2]) [!!! Reminder: you need to use the comma op for a single item array]" } } elseif (3 -lt $pair.Length) { Write-Error "Found pair with excess items (pair: $($pair)) [!!! Reminder: you need to use the comma op for a single item array]" } } $transformedPair = , @($transformedKey, $transformedValue); $pairsToWriteInColour += $transformedPair; } [System.Collections.Hashtable]$parameters = @{ 'Pairs' = $pairsToWriteInColour; 'Format' = $Theme['FORMAT']; 'KeyPlaceHolder' = $Theme['KEY-PLACE-HOLDER']; 'ValuePlaceHolder' = $Theme['VALUE-PLACE-HOLDER']; 'Open' = $Theme['OPEN']; 'Close' = $Theme['CLOSE']; 'Separator' = $Theme['SEPARATOR']; 'MetaColours' = $Theme['META-COLOURS']; } if ([String]::IsNullOrEmpty($Message)) { if ($inEmergency) { $Message = 'ϞϞϞ '; } } else { if ($inEmergency) { $Message = 'ϞϞϞ ' + $Message; } } if (-not([String]::IsNullOrEmpty($Message))) { $parameters['Message'] = $Message; $parameters['MessageColours'] = $Theme['MESSAGE-COLOURS']; $parameters['MessageSuffix'] = $Theme['MESSAGE-SUFFIX']; } & 'Write-RawPairsInColour' @parameters; } function get-RawHostUiColours { # This function only really required to aid unit testing # [OutputType([array])] param() return (Get-Host).ui.rawUI.ForegroundColor, (Get-Host).ui.rawUI.BackgroundColor; } function Split-KeyValuePairFormatter { <# .NAME Split-KeyValuePairFormatter .SYNOPSIS Splits an input string which should conform to the format string containing <%KEY%> and <%VALUE%> constituents. .DESCRIPTION The format string should contain key and value token place holders. This function, will split the input returning an array of 5 strings representing the constituents. .PARAMETER Format Format specifier for each key/value pair encountered. The string must contain the tokens whatever is defined in KeyPlaceHolder and ValuePlaceHolder. .PARAMETER KeyConstituent The value of the Key. .PARAMETER ValueConstituent The value of the Value! .PARAMETER KeyPlaceHolder The place holder that identifies the Key in the Format parameter. .PARAMETER ValuePlaceHolder The place holder that identifies the Value in the Format parameter. #> [OutputType([string[]])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Format, [string] $KeyConstituent, [string] $ValueConstituent, [string] $KeyPlaceHolder = '<%KEY%>', [string] $ValuePlaceHolder = '<%VALUE%>' ) [string[]]$constituents = @(); [int]$keyPosition = $Format.IndexOf($KeyPlaceHolder); [int]$valuePosition = $Format.IndexOf($ValuePlaceHolder); if ($keyPosition -eq -1) { Write-Error -Message "Invalid formatter: '$($Format)', key: '$({$KeyPlaceHolder})' not found"; } if ($valuePosition -eq -1) { Write-Error -Message "Invalid formatter: '$($Format)', value: '$({$ValuePlaceHolder})' not found"; } # Need this check just in case the user wants Value=Key!!!, or perhaps something # exotic like [Value(Key)], ie; it's bad to make the assumption that the key comes # before the value in the format sring. # if ($keyPosition -lt $valuePosition) { [string]$header = ''; if ($keyPosition -ne 0) { # Insert everything up to the KeyFormat (the header) # $header = $Format.Substring(0, $keyPosition); } $constituents += $header; # Insert the KeyFormat # $constituents += $KeyConstituent; # Insert everything in between the key and value formats, typically the # equal sign (key=value), but it could be anything eg --> (key-->value) # [int]$midStart = $header.Length + $KeyPlaceHolder.Length; [int]$midLength = $valuePosition - $midStart; if ($midLength -lt 0) { Write-Error -Message "Internal error, couldn't get the middle of the formatter: '$Format'"; } [string]$middle = $Format.Substring($midStart, $midLength); $constituents += $middle; # Insert the value # $constituents += $ValueConstituent; # Insert everything after the ValueFormat (the tail) # 0 1 2 # 012345678901234567890 # [<%KEY%>=<%VALUE%>] # [int]$tailStart = $valuePosition + $ValuePlaceHolder.Length; # 9 + 9 [int]$tailEnd = $Format.Length - $tailStart; # 19 -18 [string]$tail = $Format.Substring($tailStart, $tailEnd); $constituents += $tail; } else { [string]$header = ''; if ($valuePosition -ne 0) { # Insert everything up to the ValueFormat (the header) # $header = $Format.Substring(0, $valuePosition); } $constituents += $header; # Insert the ValueFormat # $constituents += $ValueConstituent; # Insert everything in between the value and key formats, typically the # equal sign (value=key), but it could be anything eg --> (value-->key) # [int]$midStart = $header.Length + $ValuePlaceHolder.Length; [int]$midLength = $keyPosition - $midStart; if ($midLength -lt 0) { Write-Error -Message "Internal error, couldnt get the middle of the formatter: '$Format'"; } [string]$middle = $Format.Substring($midStart, $midLength); $constituents += $middle; # Insert the key # $constituents += $KeyConstituent; # Insert everything after the KeyFormat (the tail) # [int]$tailStart = $keyPosition + $KeyPlaceHolder.Length; [int]$tailEnd = $Format.Length - $tailStart; [string]$tail = $Format.Substring($tailStart, $tailEnd); $constituents += $tail; } return [string[]]$constituents; } <# .NAME couplet #> class couplet { [string]$Key; [string]$Value; [boolean]$Affirm; couplet () { } couplet([string[]]$props) { $this.Key = $props[0].Replace('\,', ',').Replace('\;', ';'); $this.Value = $props[1].Replace('\,', ',').Replace('\;', ';'); $this.Affirm = $props.Length -gt 2 ? [boolean]$props[2] : $false; } couplet ([string]$key, [string]$value, [boolean]$affirm) { $this.Key = $key.Replace('\,', ',').Replace('\;', ';'); $this.Value = $value.Replace('\,', ',').Replace('\;', ';'); $this.Affirm = $affirm; } couplet ([string]$key, [string]$value) { $this.Key = $key.Replace('\,', ',').Replace('\;', ';'); $this.Value = $value.Replace('\,', ',').Replace('\;', ';'); $this.Affirm = $false; } couplet([PSCustomObject]$custom) { $this.Key = $custom.Key; $this.Value = $custom.Value; $this.Affirm = $custom.psobject.properties.match('Affirm') -and $custom.Affirm; } [boolean] equal ([couplet]$other) { return ($this.Key -eq $other.Key) ` -and ($this.Value -eq $other.Value) ` -and ($this.Affirm -eq $other.Affirm); } [boolean] cequal ([couplet]$other) { return ($this.Key -ceq $other.Key) ` -and ($this.Value -ceq $other.Value) ` -and ($this.Affirm -ceq $other.Affirm); } [string] ToString() { return "[Key: '$($this.Key)', Value: '$($this.Value)', Affirm: '$($this.Affirm)']"; } } # couplet <# .NAME line #> class line { [couplet[]]$Line; [string]$Message; line() { } line([couplet[]]$couplets) { $this.Line = $couplets.Clone(); } line([string]$message, [couplet[]]$couplets) { $this.Message = $message; $this.Line = $couplets.Clone(); } line([line]$line) { $this.Line = $line.Line.Clone(); } [line] append([couplet]$couplet) { $this.Line += $couplet; return $this; } [line] append([couplet[]]$couplet) { $this.Line += $couplet; return $this; } [line] append([line]$other) { $this.Line += $other.Line; return $this; } [boolean] equal ([line]$other) { [boolean]$result = $true; if ($this.Line.Length -eq $other.Line.Length) { for ($index = 0; ($index -lt $this.Line.Length -and $result); $index++) { $result = $this.Line[$index].equal($other.line[$index]); } } else { $result = $false; } return $result; } [boolean] cequal ([line]$other) { [boolean]$result = $true; if ($this.Line.Length -eq $other.Line.Length) { for ($index = 0; ($index -lt $this.Line.Length -and $result); $index++) { $result = $this.Line[$index].cequal($other.line[$index]); } } else { $result = $false; } return $result; } [string] ToString() { return $($this.Line -join '; '); } } # line <# .NAME Krayon #> class Krayon { static [array]$ThemeColours = @('affirm', 'key', 'message', 'meta', 'value'); # Logically public properties # [string]$ApiFormatWithArg; [string]$ApiFormat; [hashtable]$Theme; # Logically private properties # hidden [string]$_fgc; hidden [string]$_bgc; hidden [string]$_defaultFgc; hidden [string]$_defaultBgc; hidden [array]$_affirmColours; hidden [array]$_keyColours; hidden [array]$_messageColours; hidden [array]$_metaColours; hidden [array]$_valueColours; hidden [string]$_format; hidden [string]$_keyPlaceHolder; hidden [string]$_valuePlaceHolder; hidden [string]$_open; hidden [string]$_close; hidden [string]$_separator; hidden [string]$_messageSuffix; hidden [string]$_messageSuffixFiller; hidden [regex]$_expression; hidden [regex]$_nativeExpression; Krayon([hashtable]$theme, [regex]$expression, [string]$FormatWithArg, [string]$Format, [regex]$NativeExpression) { $this.Theme = $theme; $this._defaultFgc, $this._defaultBgc = Get-DefaultHostUiColours $this._fgc = $this._defaultFgc; $this._bgc = $this._defaultBgc; $this._affirmColours = $this._initThemeColours('AFFIRM-COLOURS'); $this._keyColours = $this._initThemeColours('KEY-COLOURS'); $this._messageColours = $this._initThemeColours('MESSAGE-COLOURS'); $this._metaColours = $this._initThemeColours('META-COLOURS'); $this._valueColours = $this._initThemeColours('VALUE-COLOURS'); $this._format = $theme['FORMAT']; $this._keyPlaceHolder = $theme['KEY-PLACE-HOLDER']; $this._valuePlaceHolder = $theme['VALUE-PLACE-HOLDER']; $this._open = $theme['OPEN']; $this._close = $theme['CLOSE']; $this._separator = $theme['SEPARATOR']; $this._messageSuffix = $theme['MESSAGE-SUFFIX']; $this._messageSuffixFiller = [string]::new(' ', $this._messageSuffix.Length); $this._expression = $expression; $this._nativeExpression = $NativeExpression; $this.ApiFormatWithArg = $FormatWithArg; $this.ApiFormat = $Format; } # +Scribblers+ -------------------------------------------------------------- # [Krayon] Scribble([string]$source) { if (-not([string]::IsNullOrEmpty($source))) { [PSCustomObject []]$operations = $this._parse($source); if ($operations.Count -gt 0) { foreach ($op in $operations) { if ($op.psobject.properties.match('Arg') -and $op.Arg) { $null = $this.($op.Method)($op.Arg); } else { $null = $this.($op.Method)(); } } } } return $this; } [Krayon] ScribbleLn([string]$source) { return $this.Scribble($source).Ln(); } # +Text+ -------------------------------------------------------------------- # [Krayon] Text([string]$value) { $this._print($value); return $this; } [Krayon] TextLn([string]$value) { return $this.Text($value).Ln(); } # +Pair+ (Couplet) ---------------------------------------------------------- # [Krayon] Pair([couplet]$couplet) { $this._couplet($couplet); return $this; } [Krayon] PairLn([couplet]$couplet) { return $this.Pair($couplet).Ln(); } [Krayon] Pair([PSCustomObject]$couplet) { $this._couplet([couplet]::new($couplet)); return $this; } [Krayon] PairLn([PSCustomObject]$couplet) { return $this.Pair([couplet]::new($couplet)).Ln(); } # +Line+ -------------------------------------------------------------------- # [Krayon] Line([line]$line) { $null = $this.fore($this._metaColours[0]).back($this._metaColours[1]).Text($this._open); $this._coreLine($line); $null = $this.fore($this._metaColours[0]).back($this._metaColours[1]).Text($this._close); return $this.Ln(); } [Krayon] NakedLine([line]$nakedLine) { $null = $this.fore($this._metaColours[0]).back($this._metaColours[1]).Text( [string]::new(' ', $this._open.Length) ); $this._coreLine($nakedLine); $null = $this.fore($this._metaColours[0]).back($this._metaColours[1]).Text( [string]::new(' ', $this._open.Length) ); return $this.Ln(); } [void] _coreLine([line]$line) { [int]$count = 0; foreach ($couplet in $line.Line) { $null = $this.Pair($couplet); $count++; if ($count -lt $line.Line.Count) { $null = $this.fore($this._metaColours[0]).back($this._metaColours[1]).Text($this._separator); } } } [Krayon] Line([string]$message, [line]$line) { $this._lineWithMessage($message, $line); return $this.Line($line); } [Krayon] NakedLine([string]$message, [line]$line) { $this._lineWithMessage($message, $line); return $this.NakedLine($line); } [void] _lineWithMessage([string]$message, [line]$line) { $null = $this.fore($this._messageColours[0]).back($this._messageColours[1]).Text($message); $null = $this.fore($this._messageColours[0]).back($this._messageColours[1]).Text( [string]::IsNullOrEmpty($message.Trim()) ? $this._messageSuffixFiller : $this._messageSuffix ); } # +Message+ ----------------------------------------------------------------- # [Krayon] Message([string]$message) { $null = $this.ThemeColour('message'); return $this.Text($message).Text($this._messageSuffix); } [Krayon] MessageLn([string]$message) { return $this.Message($message).Ln(); } [Krayon] MessageNoSuffix([string]$message) { $null = $this.ThemeColour('message'); return $this.Text($message).Text($this._messageSuffixFiller); } [Krayon] MessageNoSuffixLn([string]$message) { return $this.MessageNoSuffix($message).Ln(); } # +Dynamic+ ----------------------------------------------------------------- # [Krayon] fore([string]$colour) { $this._fgc = $colour; return $this; } [Krayon] back([string]$colour) { $this._bgc = $colour; return $this; } [Krayon] defaultFore([string]$colour) { $this._defaultFgc = $colour; return $this; } [Krayon] defaultBack([string]$colour) { $this._defaultBgc = $colour; return $this; } [string] getDefaultFore() { return $this._defaultFgc; } [string] getDefaultBack() { return $this._defaultBgc; } # +Control+ ----------------------------------------------------------------- # [void] End() {} [Krayon] Ln() { # Write a non-breaking space (0xA0) # https://en.wikipedia.org/wiki/Non-breaking_space # (This is required because of a con-host bug in windows => # https://github.com/microsoft/terminal/issues/1040) # Write-Host ([char]0xA0); return $this; } [Krayon] Reset() { $this._fgc = $this._defaultFgc; $this._bgc = $this._defaultBgc; return $this; } # +Theme+ ------------------------------------------------------------------- # [Krayon] ThemeColour([string]$val) { [string]$trimmedValue = $val.Trim(); if ([Krayon]::ThemeColours -contains $trimmedValue) { [array]$cols = $this.Theme[$($trimmedValue.ToUpper() + '-COLOURS')]; $this._fgc = $cols[0]; $this._bgc = $cols.Length -eq 2 ? $cols[1] : $this._defaultBgc; } else { Write-Debug "Krayon.ThemeColour: ignoring invalid theme colour: '$trimmedValue'" } return $this; } # +Static Foreground Colours+ ----------------------------------------------- # [Krayon] black() { $this._fgc = 'black'; return $this; } [Krayon] darkBlue() { $this._fgc = 'darkBlue'; return $this; } [Krayon] darkGreen() { $this._fgc = 'darkGreen'; return $this; } [Krayon] darkCyan() { $this._fgc = 'darkCyan'; return $this; } [Krayon] darkRed() { $this._fgc = 'darkRed'; return $this; } [Krayon] darkMagenta() { $this._fgc = 'darkMagenta'; return $this; } [Krayon] darkYellow() { $this._fgc = 'darkYellow'; return $this; } [Krayon] gray() { $this._fgc = 'gray'; return $this; } [Krayon] darkGray() { $this._fgc = 'darkGray'; return $this; } [Krayon] blue() { $this._fgc = 'blue'; return $this; } [Krayon] green() { $this._fgc = 'green'; return $this; } [Krayon] cyan() { $this._fgc = 'cyan'; return $this; } [Krayon] red() { $this._fgc = 'red'; return $this; } [Krayon] magenta() { $this._fgc = 'magenta'; return $this; } [Krayon] yellow() { $this._fgc = 'yellow'; return $this; } [Krayon] white() { $this._fgc = 'white'; return $this; } # +Background Colours+ ------------------------------------------------------ # [Krayon] bgBlack() { $this._bgc = 'Black'; return $this; } [Krayon] bgDarkBlue() { $this._bgc = 'DarkBlue'; return $this; } [Krayon] bgDarkGreen() { $this._bgc = 'DarkGreen'; return $this; } [Krayon] bgDarkCyan() { $this._bgc = 'DarkCyan'; return $this; } [Krayon] bgDarkRed() { $this._bgc = 'DarkRed'; return $this; } [Krayon] bgDarkMagenta() { $this._bgc = 'DarkMagenta'; return $this; } [Krayon] bgDarkYellow() { $this._bgc = 'DarkYellow'; return $this; } [Krayon] bgGray() { $this._bgc = 'Gray'; return $this; } [Krayon] bgDarkGray() { $this._bgc = 'DarkGray'; return $this; } [Krayon] bgBlue() { $this._bgc = 'Blue'; return $this; } [Krayon] bgGreen() { $this._bgc = 'Green'; return $this; } [Krayon] bgCyan() { $this._bgc = 'Cyan'; return $this; } [Krayon] bgRed () { $this._bgc = 'Red'; return $this; } [Krayon] bgMagenta() { $this._bgc = 'Magenta'; return $this; } [Krayon] bgYellow() { $this._bgc = 'Yellow'; return $this; } [Krayon] bgWhite() { $this._bgc = 'White'; return $this; } # +Compounders+ ------------------------------------------------------------- # [Krayon] Line([string]$semiColonSV) { return $this._lineFromSemiColonSV($semiColonSV, 'Line'); } [Krayon] NakedLine([string]$semiColonSV) { return $this._lineFromSemiColonSV($semiColonSV, 'NakedLine'); } hidden [line] _convertToLine([string[]]$constituents) { [couplet[]]$couplets = ($constituents | ForEach-Object { New-Pair $($_ -split '(?<!\\),', 0, 'RegexMatch'); }); [line]$line = New-Line $couplets; return $line; } hidden [Krayon] _lineFromSemiColonSV([string]$semiColonSV, [string]$op) { [string[]]$constituents = $semiColonSV -split '(?<!\\);', 0, 'RegexMatch'; [string]$message, [string[]]$remainder = $constituents; [string]$unescapedComma = '(?<!\\),'; if ($message -match $unescapedComma) { [line]$line = $this._convertToLine($constituents); $null = $this.$op($line); } else { [line]$line = $this._convertToLine($remainder); $null = $this.$op($message, $line); } return $this; } [Krayon] Pair([string]$csv) { [string[]]$constituents = $csv -split '(?<!\\),'; [couplet]$pair = New-Pair $constituents; $this._couplet($pair); return $this; } [Krayon] PairLn([string]$csv) { return $this.Pair($csv).Ln(); } # +Utility+ ----------------------------------------------------------------- # [string] Native([string]$structured) { return $this._nativeExpression.Replace($structured, ''); } # styles (don't exist yet; virtual terminal sequences?) # [Krayon] bold() { return $this; } [Krayon] italic() { return $this; } [Krayon] strike() { return $this; } [Krayon] under() { return $this; } # Logically private # hidden [void] _couplet([couplet]$couplet) { [string[]]$constituents = Split-KeyValuePairFormatter -Format $this._format ` -KeyConstituent $couplet.Key -ValueConstituent $couplet.Value ` -KeyPlaceHolder $this._keyPlaceHolder -ValuePlaceHolder $this._valuePlaceHolder; # header # $this._fgc = $this._metaColours[0]; $this._bgc = $this._metaColours[1]; $this._print($constituents[0]); # key # $this._fgc = $this._keyColours[0]; $this._bgc = $this._keyColours[1]; $this._print($constituents[1]); # mid # $this._fgc = $this._metaColours[0]; $this._bgc = $this._metaColours[1]; $this._print($constituents[2]); # value # $this._fgc = ($couplet.Affirm) ? $this._affirmColours[0] : $this._valueColours[0]; $this._bgc = ($couplet.Affirm) ? $this._affirmColours[1] : $this._valueColours[1]; $this._print($constituents[3]); # tail # $this._fgc = $this._metaColours[0]; $this._bgc = $this._metaColours[1]; $this._print($constituents[4]); $null = $this.Reset(); } # _couplet hidden [void] _print([string]$text) { Write-Host $text -ForegroundColor $this._fgc -BackgroundColor $this._bgc -NoNewline; } # _print hidden [void] _printLn([string]$text) { Write-Host $text -ForegroundColor $this._fgc -BackgroundColor $this._bgc; } # _printLn hidden [array] _initThemeColours([string]$coloursKey) { [array]$thc = $this.Theme[$coloursKey]; if ($thc.Length -eq 1) { $thc += $this._defaultBgc; } return $thc; } # _initThemeColours hidden [array] _parse ([string]$source) { [System.Text.RegularExpressions.Match]$previousMatch = $null; [PSCustomObject []]$operations = if ($this._expression.IsMatch($source)) { [System.Text.RegularExpressions.MatchCollection]$mc = $this._expression.Matches($source); [int]$count = 0; foreach ($m in $mc) { [string]$method = $m.Groups['method']; [string]$parm = $m.Groups['p']; if ($previousMatch) { [int]$snippetStart = $previousMatch.Index + $previousMatch.Length; [int]$snippetEnd = $m.Index; [int]$snippetSize = $snippetEnd - $snippetStart; [string]$snippet = $source.Substring($snippetStart, $snippetSize); # If we find a text snippet, it must be applied before the current method invoke # if (-not([string]::IsNullOrEmpty($snippet))) { [PSCustomObject] @{ PSTypeName = 'Krayola.Krayon.Operation'; # Method = 'Text'; Arg = $snippet; } } if (-not([string]::IsNullOrEmpty($parm))) { [PSCustomObject] @{ PSTypeName = 'Krayola.Krayon.Operation'; # Method = $method; Arg = $parm; } } else { [PSCustomObject] @{ PSTypeName = 'Krayola.Krayon.Operation'; # Method = $method; } } } else { [string]$snippet = if ($m.Index -eq 0) { [int]$snippetStart = -1; [int]$snippetEnd = -1; [string]::Empty } else { [int]$snippetStart = 0; [int]$snippetEnd = $m.Index; $source.Substring($snippetStart, $snippetEnd); } if (-not([string]::IsNullOrEmpty($snippet))) { [PSCustomObject] @{ PSTypeName = 'Krayola.Krayon.Operation'; # Method = 'Text'; Arg = $snippet; } } if (-not([string]::IsNullOrEmpty($parm))) { [PSCustomObject] @{ PSTypeName = 'Krayola.Krayon.Operation'; # Method = $method; Arg = $parm; } } else { [PSCustomObject] @{ PSTypeName = 'Krayola.Krayon.Operation'; # Method = $method; } } } $previousMatch = $m; $count++; if ($count -eq $mc.Count) { [int]$lastSnippetStart = $m.Index + $m.Length; [string]$snippet = $source.Substring($lastSnippetStart); if (-not([string]::IsNullOrEmpty($snippet))) { [PSCustomObject] @{ PSTypeName = 'Krayola.Krayon.Operation'; # Method = 'Text'; Arg = $snippet; } } } } # foreach $m } else { @([PSCustomObject] @{ PSTypeName = 'Krayola.Krayon.Operation'; # Method = 'Text'; Arg = $source; }); } return $operations; } # _parse } # Krayon <# .NAME Scribbler #> class Scribbler { [System.Text.StringBuilder]$Builder; [Krayon]$Krayon; hidden [System.Text.StringBuilder]$_session; Scribbler([System.Text.StringBuilder]$builder, [Krayon]$krayon, [System.Text.StringBuilder]$Session) { $this.Builder = $builder; $this.Krayon = $krayon; $this._session = $Session; } # +Scribblers+ -------------------------------------------------------------- # [void] Scribble([string]$structuredContent) { $null = $this.Builder.Append($structuredContent); } # +Text+ Accelerators ------------------------------------------------------- # [Scribbler] Text([string]$value) { $this.Scribble($value); return $this; } [Scribbler] TextLn([string]$value) { return $this.Text($value).Ln(); } # +Pair+ (Couplet) Accelerators --------------------------------------------- # [Scribbler] Pair([couplet]$couplet) { [string]$pairSnippet = $this.PairSnippet($couplet); $this.Scribble($pairSnippet); return $this; } [Scribbler] PairLn([couplet]$couplet) { return $this.Pair($couplet).Ln(); } [Scribbler] Pair([PSCustomObject]$coupletObj) { [couplet]$couplet = [couplet]::new($coupletObj); return $this.Pair($couplet); } [Scribbler] PairLn([PSCustomObject]$coupletObj) { return $this.Pair([couplet]::new($coupletObj)).Ln(); } # +Line+ Accelerators ------------------------------------------------------- # [Scribbler] Line([string]$message, [line]$line) { $this._coreScribbleLine($message, $line, 'Line'); return $this; } [Scribbler] Line([line]$line) { $this.Line([string]::Empty, $line); return $this; } [Scribbler] NakedLine([string]$message, [line]$nakedLine) { $this._coreScribbleLine($message, $nakedLine, 'NakedLine'); return $this; } [Scribbler] NakedLine([line]$line) { $this.NakedLine([string]::Empty, $line); return $this; } hidden [void] _coreScribbleLine([string]$message, [line]$line, [string]$lineType) { [string]$structuredLine = $(($Line.Line | ForEach-Object { "$($this._escape($_.Key)),$($this._escape($_.Value)),$($_.Affirm)" }) -join ';'); if (-not([string]::IsNullOrEmpty($message))) { $structuredLine = "$message;" + $structuredLine; } [string]$lineSnippet = $this.WithArgSnippet( $lineType, $structuredLine ) $this.Scribble("$($lineSnippet)"); } # _coreScribbleLine # +Message+ Accelerators ---------------------------------------------------- # [Scribbler] Message([string]$message) { [string]$snippet = $this.WithArgSnippet('Message', $message); $this.Scribble($snippet); return $this; } [Scribbler] MessageLn([string]$message) { return $this.Message($message).Ln(); } [Scribbler] MessageNoSuffix([string]$message) { [string]$snippet = $this.WithArgSnippet('MessageNoSuffix', $message); $this.Scribble($snippet); return $this; } [Scribbler] MessageNoSuffixLn([string]$message) { return $this.MessageNoSuffix($message).Ln(); } # +Dynamic+ Accelerators ---------------------------------------------------- # [Scribbler] fore([string]$colour) { [string]$snippet = $this.WithArgSnippet('fore', $colour); $this.Scribble($snippet); return $this; } [Scribbler] back([string]$colour) { [string]$snippet = $this.WithArgSnippet('back', $colour); $this.Scribble($snippet); return $this; } [Scribbler] defaultFore([string]$colour) { [string]$snippet = $this.WithArgSnippet('defaultFore', $colour); $this.Scribble($snippet); return $this; } [Scribbler] defaultBack([string]$colour) { [string]$snippet = $this.WithArgSnippet('defaultBack', $colour); $this.Scribble($snippet); return $this; } # +Control+ Accelerators ---------------------------------------------------- # [void] End() { } [void] Flush () { $this.Krayon.Scribble($this.Builder.ToString()); $this._clear(); } [Scribbler] Ln() { [string]$snippet = $this.Snippets('Ln'); $this.Scribble($snippet); return $this; } [Scribbler] Reset() { [string]$snippet = $this.Snippets('Reset'); $this.Scribble($snippet); return $this; } [void] Restart() { if ($this._session) { $this._session.Clear(); } $this.Builder.Clear(); $this.Krayon.Reset().End(); } [void] Save([string]$fullPath) { [string]$directoryPath = [System.IO.Path]::GetDirectoryName($fullPath); [string]$fileName = [System.IO.Path]::GetFileName($fullPath) + '.struct.txt'; [string]$writeFullPath = Join-Path -Path $directoryPath -ChildPath $fileName; if ($this._session) { if (-not(Test-Path -Path $writeFullPath)) { Set-Content -LiteralPath $writeFullPath -Value $this._session.ToString(); } else { Write-Warning -Message $( "Can't write session to '$writeFullPath'. (file already exists)." ); } } else { Write-Warning -Message $( "Can't write session to '$writeFullPath'. (session not set)." ); } } # +Theme+ Accelerators ------------------------------------------------------ # [Scribbler] ThemeColour([string]$val) { [string]$snippet = $this.WithArgSnippet('ThemeColour', $val); $this.Scribble($snippet); return $this; } # +Static Foreground Colours+ Accelerators ---------------------------------- # [Scribbler] black() { [string]$snippet = $this.Snippets('black'); $this.Scribble($snippet); return $this; } [Scribbler] darkBlue() { [string]$snippet = $this.Snippets('darkBlue'); $this.Scribble($snippet); return $this; } [Scribbler] darkGreen() { [string]$snippet = $this.Snippets('darkGreen'); $this.Scribble($snippet); return $this; } [Scribbler] darkCyan() { [string]$snippet = $this.Snippets('darkCyan'); $this.Scribble($snippet); return $this; } [Scribbler] darkRed() { [string]$snippet = $this.Snippets('darkRed'); $this.Scribble($snippet); return $this; } [Scribbler] darkMagenta() { [string]$snippet = $this.Snippets('darkMagenta'); $this.Scribble($snippet); return $this; } [Scribbler] darkYellow() { [string]$snippet = $this.Snippets('darkYellow'); $this.Scribble($snippet); return $this; } [Scribbler] gray() { [string]$snippet = $this.Snippets('gray'); $this.Scribble($snippet); return $this; } [Scribbler] darkGray() { [string]$snippet = $this.Snippets('darkGray'); $this.Scribble($snippet); return $this; } [Scribbler] blue() { [string]$snippet = $this.Snippets('blue'); $this.Scribble($snippet); return $this; } [Scribbler] green() { [string]$snippet = $this.Snippets('green'); $this.Scribble($snippet); return $this; } [Scribbler] cyan() { [string]$snippet = $this.Snippets('cyan'); $this.Scribble($snippet); return $this; } [Scribbler] red() { [string]$snippet = $this.Snippets('red'); $this.Scribble($snippet); return $this; } [Scribbler] magenta() { [string]$snippet = $this.Snippets('magenta'); $this.Scribble($snippet); return $this; } [Scribbler] yellow() { [string]$snippet = $this.Snippets('yellow'); $this.Scribble($snippet); return $this; } [Scribbler] white() { [string]$snippet = $this.Snippets('white'); $this.Scribble($snippet); return $this; } # +Background Colours+ Accelerators ----------------------------------------- # [Scribbler] bgBlack() { [string]$snippet = $this.Snippets('bgBlack'); $this.Scribble($snippet); return $this; } [Scribbler] bgDarkBlue() { [string]$snippet = $this.Snippets('bgDarkBlue'); $this.Scribble($snippet); return $this; } [Scribbler] bgDarkGreen() { [string]$snippet = $this.Snippets('bgDarkGreen'); $this.Scribble($snippet); return $this; } [Scribbler] bgDarkCyan() { [string]$snippet = $this.Snippets('bgDarkCyan'); $this.Scribble($snippet); return $this; } [Scribbler] bgDarkRed() { [string]$snippet = $this.Snippets('bgDarkRed'); $this.Scribble($snippet); return $this; } [Scribbler] bgDarkMagenta() { [string]$snippet = $this.Snippets('bgDarkMagenta'); $this.Scribble($snippet); return $this; } [Scribbler] bgDarkYellow() { [string]$snippet = $this.Snippets('bgDarkYellow'); $this.Scribble($snippet); return $this; } [Scribbler] bgGray() { [string]$snippet = $this.Snippets('bgGray'); $this.Scribble($snippet); return $this; } [Scribbler] bgDarkGray() { [string]$snippet = $this.Snippets('bgDarkGray'); $this.Scribble($snippet); return $this; } [Scribbler] bgBlue() { [string]$snippet = $this.Snippets('bgBlue'); $this.Scribble($snippet); return $this; } [Scribbler] bgGreen() { [string]$snippet = $this.Snippets('bgGreen'); $this.Scribble($snippet); return $this; } [Scribbler] bgCyan() { [string]$snippet = $this.Snippets('bgCyan'); $this.Scribble($snippet); return $this; } [Scribbler] bgRed() { [string]$snippet = $this.Snippets('bgRed'); $this.Scribble($snippet); return $this; } [Scribbler] bgMagenta() { [string]$snippet = $this.Snippets('bgMagenta'); $this.Scribble($snippet); return $this; } [Scribbler] bgYellow() { [string]$snippet = $this.Snippets('bgYellow'); $this.Scribble($snippet); return $this; } [Scribbler] bgWhite() { [string]$snippet = $this.Snippets('bgWhite'); $this.Scribble($snippet); return $this; } # +Utility+ ----------------------------------------------------------------- # [string] Snippets ([string[]]$items) { [string]$result = [string]::Empty; foreach ($i in $items) { $result += $($this.Krayon.ApiFormat -f $i); } return $result; } [string] WithArgSnippet([string]$method, [string]$arg) { return "$($this.Krayon.ApiFormatWithArg -f $method, $arg)"; } [string] PairSnippet([couplet]$pair) { [string]$key = $this._escape($pair.Key); [string]$value = $this._escape($pair.Value); [string]$csv = "$($key),$($value),$($pair.Affirm)"; [string]$pairSnippet = $this.WithArgSnippet( 'Pair', $csv ) return $pairSnippet; } [string] LineSnippet([line]$line) { [string]$structuredLine = $(($line.Line | ForEach-Object { "$($this._escape($_.Key)),$($this._escape($_.Value)),$($_.Affirm)" }) -join ';'); [string]$lineSnippet = $this.WithArgSnippet( 'Line', $structuredLine ) return $lineSnippet; } # Other internal # hidden [void] _clear() { if ($this._session) { $this._session.Append($this.Builder); } $this.Builder.Clear(); } hidden [string] _escape([string]$value) { return $value.Replace(';', '\;').Replace(',', '\,'); } } # Scribbler class QuietScribbler: Scribbler { QuietScribbler([System.Text.StringBuilder]$builder, [Krayon]$krayon, [System.Text.StringBuilder]$Session):base($builder, $krayon, $Session) { } [void] Flush () { $this._clear(); } } # QuietScribbler function New-Krayon { <# .NAME New-Krayon .SYNOPSIS Helper factory function that creates Krayon instance. .DESCRIPTION The client can specify a custom regular expression and corresponding formatters which together support the scribble functionality (the ability to invoke krayon functions via a 'structured' string as opposed to calling the methods explicitly). Normally, the client can accept the default expression and formatter arguments. However, depending on circumstance, a custom pattern can be supplied along with corresponding formatters. The formatters specified MUST correspond to the pattern and if they don't, then an exception is thrown. The default tokens used are as follows: * lead: 'µ' * open: '«' * close: '»' So this means that to invoke the 'red' function on the Krayon, the client should invoke the Scribble function with the following 'structured' string: 'µ«red»'. To invoke a command which requires a parameter eg 'Message', the client needs to specify a string like: 'µ«Message,Greetings Earthlings»'. (NB: instructions are case insensitive). (Please note, that custom regular expressions do not have to have 'lead', 'open' and 'close' tokens as illustrated here; these are just what are used by default. The client can define any expression with formatters as long as it able to capture method calls with a single optional parameter.) However, please do not specify a literal string like this. If scribble functionality is required, then the Scribbler object should be used. The scribbler contains helper functions 'Snippets' and 'WithArgSnippet'. 'Snippets', which when given an array of instructions will return the correct structured string. So to 'Reset', set the foreground colour to red and the background colour to black: $scribbler.Snippets(@('Reset', 'red', 'black')) which would return 'µ«Reset»µ«red»µ«black»'. And 'WithArgSnippet' for the above Message example, the client should use the following: [string]$snippet = $scribbler.WithArgSnippet('Message', 'Greetings Earthlings'); $scribbler.Scribble($snippet); This is so that if for any reason, the expression and corresponding formatters need to be changed, then no other client code would be affected. And for completeness, an invoke requiring compound param representation eg to invoke the 'Line' method would be defined as: 'one,Eve of Destruction;two,Bango' => this is a line with 2 couplets which would be invoked like so: [string]$snippet = $scribbler.WithArgSnippet('one,Eve of Destruction;two,Bango'); and to Invoke 'Line' with a message: 'Greetings Earthlings;one,Eve of Destruction;two,Bango' if you look at the first segment, you will see that it contains no comma. The scribbler will interpret the first segment as a message with subsequent segments containing valid comma separated values, split by semi-colons. (Be careful to construct this string properly; if a segment does not contain a comma (except for the first segment), then will likely be an error). And if the message required, includes a comma, then it should be escaped with a back slash '\': 'Greetings\, Earthlings;one,Eve of Destruction;two,Bango'. .PARAMETER Expression A custom regular expression pattern than capture a Krayon method method call and an optional parameter. The expression MUST contain the following 2 named capture groups: * 'method': string to represent a method call on the Krayon instance. * 'p': optional string to represent a parameter passed into the function denoted by 'method'. Instructions can either have 0 or 1 argument. When an argument is specified that must represent a compound value (multiple items), then a compound representation must be used, eg a couplet is represented by a comma separated string and a line is represented by a semi-colon separated value, where the value inside each semi-colon segment is a pair represented by a comma separated value. .PARAMETER NativeExpression A custom regular expression pattern that recognises the content interpreted by the Krayon Scribble method. The 'inverse' native expression parses a structured string and returns the core text stripped of any method tokens. .PARAMETER Theme A hashtable instance containing the Krayola theme. .PARAMETER WriterFormat Format string that represents a Krayon method method call without an argument. This format needs to conform to the regular expression pattern specified by Expression. .PARAMETER WriterFormatWithArg Format string that represents a Krayon method method call with an argument. This format needs to conform to the regular expression pattern specified by Expression. This format must accommodate a single parameter. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [OutputType([Krayon])] param( [Parameter()] [hashtable]$Theme = $(Get-KrayolaTheme), [Parameter()] # OLD: '&\[(?<method>[\w]+)(,(?<p>[^\]]+))?\]' [regex]$Expression = [regex]::new("µ«(?<method>[\w]+)(,(?<p>[^»]+))?»"), [Parameter()] # OLD: '&[{0},{1}]' [string]$WriterFormatWithArg = "µ«{0},{1}»", [Parameter()] # OLD: '&[{0}]' [string]$WriterFormat = "µ«{0}»", [Parameter()] # OLD: '&\[[\w\s\-_]+(?:,\s*[\w\s\-_]+)?\]' [string]$NativeExpression = [regex]::new("µ«[\w\s\-_]+(?:,\s*[\w\s\-_]+)?»") ) [string]$dummyWithArg = $WriterFormatWithArg -replace "\{\d{1,2}\}", 'dummy'; if (-not($Expression.IsMatch($dummyWithArg))) { throw "New-Krayon: invalid WriterFormatWithArg ('$WriterFormatWithArg'), does not match the provided Expression: '$($Expression.ToString())'"; } [string]$dummy = $WriterFormat -replace "\{\d{1,2}\}", 'dummy'; if (-not($Expression.IsMatch($dummy))) { throw "New-Krayon: invalid WriterFormat ('$WriterFormat'), does not match the provided Expression: '$($Expression.ToString())'"; } return [Krayon]::new($Theme, $Expression, $WriterFormatWithArg, $WriterFormat, $NativeExpression); } # New-Krayon function New-Line { <# .NAME New-Line .SYNOPSIS Helper factory function that creates Line instance. .DESCRIPTION A Line is a wrapper around a collection of couplets. .PARAMETER Krayon The underlying krayon instance that performs real writes to the host. .PARAMETER couplets Collection of couplets to create Line with. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [OutputType([line])] [Alias('kl')] param( [Parameter()] [couplet[]]$couplets = @() ) return [line]::new($couplets); } # New-Line <# .NAME New-Pair .SYNOPSIS Helper factory function that creates a couplet instance. .DESCRIPTION A couplet is logically 2 items, but can contain a 3rd element representing its 'affirmed' status. An couplet that is affirmed is one that can be highlighted according to the Krayola theme (AFFIRM-COLOURS). .PARAMETER couplet A 2 or 3 item array representing a key/value pair and optional affirm boolean. #> function New-Pair { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [OutputType([couplet])] [Alias('kp')] param( [Parameter()] [string[]]$couplet ) return ($couplet.Count -ge 3) ` ? [couplet]::new($couplet[0], $couplet[1], [System.Convert]::ToBoolean($couplet[2])) ` : [couplet]::new($couplet[0], $couplet[1]); } # New-Pair function New-Scribbler { <# .NAME New-Scribbler .SYNOPSIS Helper factory function that creates a Scribbler instance. .DESCRIPTION Creates a new Scribbler instance with the optional krayon provided. The scribbler acts like a wrapper around the Krayon so that control can be exerted over where the output is directed to. In an interactive environment, clearly, the user needs to see the output so the Scribbler will direct the Krayon to render output to the console. However, within the context of a unit test the output needs to be suppressed. This is achieved by creating a Quiet Scribbler achieved by setting the Test switch or forcing it via the Silent switch. .PARAMETER Krayon The underlying krayon instance that performs real writes to the host. .PARAMETER Test switch to indicate if this is being invoked from a test case, so that the output can be suppressed if appropriate. By default, the test cases should be quiet. During development and test stage, the user might want to see actual output in the test output. The presence of variable 'EliziumTest' in the environment will enable verbose tests. When invoked by an interactive user in production environment, the Test flag should not be set. Doing so will suppress the output depending on the presence 'EliziumTest'. ALL test cases should specify this Test flag. This also applies to third party users building tests for commands that use the Scribbler. .PARAMETER Save switch to indicate if the Scribbler should record all output which will be saved to file for future playback. .PARAMETER Silent switch to force the creation of a Quiet Scribbler. Can not be specified at the same time as Test (although not currently enforced). Silent overrides Test. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [OutputType([Scribbler])] param( [Parameter()] [Krayon]$Krayon = $(New-Krayon), [Parameter()] [switch]$Test, [Parameter()] [switch]$Save, [Parameter()] [switch]$Silent ) [System.text.StringBuilder]$builder = [System.text.StringBuilder]::new(); [System.text.StringBuilder]$session = $Save.IsPresent ? [System.text.StringBuilder]::new() : $null; [Scribbler]$scribbler = if ($Silent) { [QuietScribbler]::New($builder, $Krayon, $null); } elseif ($Test) { $($null -eq (Get-EnvironmentVariable 'EliziumTest')) ` ? [QuietScribbler]::New($builder, $Krayon, $session) ` : [Scribbler]::New($builder, $Krayon, $session); } else { [Scribbler]::New($builder, $Krayon, $session); } return $scribbler; } # New-Scribbler Export-ModuleMember -Variable KrayolaThemes Export-ModuleMember -Alias kl, Show-ConsoleColors, Write-InColor, Write-RawPairsInColor, Write-ThemedPairsInColor, kp, kl Export-ModuleMember -Function Get-DefaultHostUiColours, Get-EnvironmentVariable, Get-IsKrayolaLightTerminal, Get-KrayolaTheme, Get-Krayon, Show-ConsoleColours, Write-InColour, Write-RawPairsInColour, Write-ThemedPairsInColour, New-Krayon, New-Line, New-Pair, New-Scribbler |