Elizium.TerminalBuddy.psm1
Set-StrictMode -Version 1.0 function ConvertFrom-ItermColors { <# .EXTERNALHELP Elizium.TerminalBuddy-help.xml .NAME ConvertFrom-ItermColors .SYNOPSIS Converts .itermcolor files into a format that can be used in Window Terminal Settings. Depending on the parameters provided, will either integrate generated schemes into the setting files, or generate a separate file from the existing settings file. Any schemes already present in the setting files will be preserved. .DESCRIPTION Since there is currently no settings UI in Windows Terminal Settings app and the format that is used to express colour schemes is vastly different to that used by iterm, it is not easy to leverage the work done by others in creating desirable terminal schemes. This function makes it easier to apply iterm colour schemes into Windows Terminal. There are multiple ways to use this function: 1) generate an Output file (denoted by $Out parameter), which will contain a JSON object containing the colour schemes converted from iterm to Windows Terminal format. 2) generate a new Dry Run file which is a copy of the current Windows Terminal Settings file with the converted schemes integrated into it. 3) make a backup, of the Settings file, then integrate the generated schemes into the current Settings file. (See caveats further down below). The function errs on the side of caution, and by default works in 'Dry Run' mode. Due to the caveats, this method is effectively the same as not using the $SaveTerminalSettings switch, using $Out instead, because in this scenario, the user would be expected to open up the generated file and copy the generated scheme objects into the current Settings file. This is the recommended way to use this command. If the user wants to integrate the generated schemes into the Settings file automatically, then the $Force switch should be specified. In this case, the current live Settings file is backed up and then over-written by the new content. Existing schemes are preserved. And the caveats ... 1) For some reason, Microsoft decided to include comments inside the JSON setting file (probably in lieu of there not being a proper settings UI, making configuring the settings easier). However, comments are not part of the current JSON schema (although they are permitted in the rarely and sparsely supported json5 spec), which means that this conversion process will not preserve the comments. There is an alternative api that supposedly supports non standard JSON features, newtonsoft.json.ConvertTo-JsonNewtonsoft/ConvertFrom-JsonNewtonsoft but using these functions yield unsatisfactory results. 2) ConvertFrom-Json/Converto-Json do not properly handle the profiles .PARAMETER Path The path containing the iterm scheme files. If this refers to a directory, then a Filter should be specified to identify the files. This Path can also just refer directly to an individual file, in which case, no Filter is required. .PARAMETER Filter When Path refers to a directory, use Filter to specify files. A * can be used as a wildcard. .PARAMETER Out When Path refers to a directory, use Filter to specify files. A * can be used as a wildcard. The output file written to with the JSON represented the converted iterm themes. This content is is just a fragment of the settings file, in fact it's a JSON object which contains a single member named 'schemes' (after the corresponding entry in the Windows Terminal Settings file.) which is set to an array of scheme objects. .PARAMETER SaveTerminalSettings Switch, to indicate that the converted schemes should be saved into a complete settings file. Which settings file depends on the presence of the Force parameter. If Force is present, the the LiveSettingsFile path is used, otherwise the DryRunFile path is used. .PARAMETER Force Switch to indicate whether live settings should be modified to include generated schemes. To avoid accidental invocation, needs to be used in addition to SaveTerminalSettings. .PARAMETER LiveSettingsFile Well known path to the current windows terminal settings file. This is assumed to of the well known path. This can be overridden by the user if so required (just in case it's located elsewhere). .PARAMETER DryRunFile When run in Dry Run mode (by default), this is the path of the file written to. It will contain a merge of the current Windows Terminal Settings file and newly generated schemes as converted from iterm files specified by the $Path. .PARAMETER BackupFile When not in Dry Run mode ($Force and $SaveTerminalSettings specified), this parameter specifies the path to backup the live Windows Terminal Settings file to. .PARAMETER ThemeName The name of a Krayola Theme, that has been configured inside the global $KrayolaThemes hashtable variable. If not present, then an internal theme is used. The Krayola Theme shapes how output of this command is generated to the console. .PARAMETER PseudoSettingsFile This file is only required because of certain caveats of the current implementation, owing to Microsoft's choice in not using standard JSON file. In the interests of safety, instead of integrating new schemes into the LiveSettingsFile, PseudoSettingsFile specifies the file used instead of overwriting LiveSettingsFile. This function will indicating that the schemes should be manually copied over at the end of the run. .EXAMPLE ConvertFrom-ItermColors -Path 'C:\shared\Themes\ITerm2\Schemes\Banana Blueberry.itermcolors' -Out ~/terminal-settings.single.output.json .EXAMPLE ConvertFrom-ItermColors -Path C:\shared\Themes\ITerm2\Schemes -Filter "B*.itermcolors" -Out ~/terminal-settings.output.json .EXAMPLE ConvertFrom-ItermColors -Path 'C:\shared\Themes\ITerm2\Schemes\Banana Blueberry.itermcolors' -SaveTerminalSettings .EXAMPLE ConvertFrom-ItermColors -Path C:\shared\Themes\ITerm2\Schemes\ -Filter 'B*.itermcolors' -SaveTerminalSettings #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseBOMForUnicodeEncodedFile', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [Alias('cfic', 'Make-WtSchemesIC')] param ( [Parameter(Mandatory = $true)] [ValidateScript( { return Test-Path $_ })] [string] $Path, [Parameter(Mandatory = $false)] [string]$Filter = '*', [Parameter(Mandatory = $false)] [AllowEmptyString()] [ValidateScript( { return ([string]::IsNullOrWhiteSpace($_) ) ` -or (-not(Test-Path $_ -PathType 'Leaf')) })] [string]$Out, [switch]$SaveTerminalSettings, [switch]$Force, [Parameter(Mandatory = $false)] [string]$DryRunFile = '~/Windows.Terminal.dry-run.settings.json', [Parameter(Mandatory = $false)] [ValidateScript( { return -not(Test-Path $_ -PathType 'Leaf') })] [string]$BackupFile = "~/Windows.Terminal.back-up.settings.json", [Parameter(Mandatory = $false)] [AllowEmptyString()] [string]$ThemeName, [Parameter(Mandatory = $false)] [string]$LiveSettingsFile = $(get-WindowsTerminalSettingsPath), [Parameter(Mandatory = $false)] [string]$PseudoSettingsFile = '~/Windows.Terminal.pseudo.settings.json' ) [scriptblock]$containsXML = { # Not making assumption about suffix of the specfied source file(s), since # the only requirement is that the content of the file is xml. # param ( [System.IO.FileSystemInfo]$underscore ) try { return ([xml]@(Get-Content -Path $underscore.Fullname)).ChildNodes.Count -gt 0; } catch { return $false; } } # $containsXML [System.Collections.Hashtable]$displayTheme = Get-KrayolaTheme -KrayolaThemeName $ThemeName; [System.Collections.Hashtable]$passThru = @{ 'BODY' = 'import-ItermColors'; 'MESSAGE' = 'Importing Terminal Scheme'; 'KRAYOLA-THEME' = $displayTheme; 'ITEM-LABEL' = 'Scheme filename'; 'PRODUCT-LABEL' = 'Scheme name'; } [scriptblock]$wrapper = { # This wrapper is required because you can't pass a function name as a variable # without PowerShell mistaking it for an invoke request. # param( $_underscore, $_index, $_passthru, $_trigger ) return write-HostItemDecorator -Underscore $_underscore ` -Index $_index ` -PassThru $_passthru ` -Trigger $_trigger; } $null = invoke-ForeachFile -Path $Path -Body $wrapper -PassThru $passThru ` -Condition $containsXML -Filter $Filter; # Now collate the accumulated results stored inside the passthru # if ($passThru.ContainsKey('ACCUMULATOR')) { [System.Collections.Hashtable]$accumulator = $passThru['ACCUMULATOR']; if ($accumulator) { [string]$outputContent = join-AllSchemas -Schemes $accumulator; [string]$copyFromOutputUserHint = [string]::Empty; if ($SaveTerminalSettings.ToBool()) { if ($Force.ToBool()) { # Backup file (NB, WhatIf is set because the force write is not going into effect) # Copy-Item -Path $(Resolve-Path -Path $LiveSettingsFile) -Destination $BackupFile -WhatIf; # This line should be using get-WindowsTerminalSettingsPath as the OutputPath, # but this is being avoided until (if ever) a reliable way of reading and writing # JSON comments is found. Until that happens, the recommended user scenario is to use # SaveTerminalSettings without the Force switch and then subsquently manually copy the # scehemes from the generated Dry Run file to the real Settings file. # $copyFromOutputUserHint = $PseudoSettingsFile; merge-SettingsContent -Content $outputContent -SettingsPath $LiveSettingsFile ` -OutputPath $PseudoSettingsFile; } else { $copyFromOutputUserHint = $DryRunFile; merge-SettingsContent -Content $outputContent -SettingsPath $LiveSettingsFile ` -OutputPath $DryRunFile; } } else { $copyFromOutputUserHint = $out; Set-Content -Path $Out -Value $outputContent; } if (-not([string]::IsNullOrWhiteSpace($copyFromOutputUserHint))) { [System.Collections.Hashtable]$userHintTheme = Get-KrayolaTheme; $userHintTheme['VALUE-COLOURS'] = @(, @('Green')); [string[][]]$notice = @(, @('generated file', $copyFromOutputUserHint)); Write-ThemedPairsInColour -Pairs $notice -Theme $userHintTheme ` -Message 'Manual intervention notice !!!, Please open'; [string[][]]$pasteSchemes = @(, @('Windows Terminal Settings file', $LiveSettingsFile)); Write-ThemedPairsInColour -Pairs $pasteSchemes -Theme $userHintTheme ` -Message 'Copy & paste "schemes" into'; } } } } # ConvertFrom-ItermColors function convertFrom-ColourComponents { <# .NAME convertFrom-ColourComponents .SYNOPSIS Convert colour components from raw real to numerics representation (ie the number value prior to hex conversion). .DESCRIPTION Given an ANSI colour (eg 'Ansi 1 Color') and a dictionary of colour definitions as real numbers, creates a hash table of the colour component name, to colour value. .PARAMETER ColourDictionary XML info representing the colour components for a single ansi colour. .OUTPUTS [Hastable] Maps from component colour name and converted colour value. #> [OutputType([System.Collections.Hashtable])] [CmdletBinding()] param ( [Parameter()] [Microsoft.PowerShell.Commands.SelectXmlInfo]$ColourDictionary ) [System.Collections.Hashtable]$colourComponents = @{}; $node = $ColourDictionary.Node.FirstChild; do { # Handle 2 items at a time, first is key, second is real colour value # [string]$key = $node.InnerText; $node = $node.NextSibling; [string]$val = $node.InnerText; $node = $node.NextSibling; [float]$numeric = 0; if ([float]::TryParse($val, [ref]$numeric)) { $colourComponents[$key] = [int][math]::Round($numeric * 255); } } while ($node); return $colourComponents; } # convertFrom-ColourComponents [System.Collections.Hashtable]$script:ComponentNamingScheme = @{ 'RED_C' = 'Red Component'; 'GREEN_C' = 'Green Component'; 'BLUE_C' = 'Blue Component'; } function ConvertTo-RGB { <# .NAME ConvertTo-RGB .SYNOPSIS Creates the colour specification in hex code form. .DESCRIPTION The Hex string generated represents the string value supported by Windows Terminal that allows rendering in that colour. .PARAMETER Components Hashtable containing colour component descriptor mapped to the real colour value. .PARAMETER NamingScheme Mapping scheme that decouples external colour component names from internal names (not of interest to end user). .OUTPUTS [string] Windows terminal compatible Hex string representation of the converted RGB values #> [OutputType([string])] param( [Parameter()] [System.Collections.Hashtable]$Components, [Parameter()] [System.Collections.Hashtable]$NamingScheme = $ComponentNamingScheme ) [int]$R = $Components[$NamingScheme['RED_C']]; [int]$G = $Components[$NamingScheme['GREEN_C']]; [int]$B = $Components[$NamingScheme['BLUE_C']]; # Terminal doesn't support Alpha values so let's ignore the Alpha component # return '#{0:X2}{1:X2}{2:X2}' -f $R, $G, $B; } # ConvertTo-RGB function get-SortedFilesNatural { <# .NAME get-SortedFilesNatural .SYNOPSIS Sort a collection of files from the pipeline in natural order. .DESCRIPTION Sorts filenames in an order that makes sense to humans; ie 1 is followed by 2 and not 10. .PARAMETER InputObject Collection of files from pipeline to be sorted. .EXAMPLE PS C:\> Get-SortedFolderNatural 'E:\Uni\audio' .EXAMPLE PS C:\> gci E:\Uni\audio | Get-SortedFilesNatural #> [Alias("SortFilesNatural")] param ( [parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [System.Object[]]$InputObject ) begin { $files = @() } process { foreach ($item in $InputObject) { $files += $item } } end { $files | Sort-Object { [regex]::Replace($_.Name, '\d+', { $args[0].Value.PadLeft(20) }) } } } function get-WindowsTerminalSettingsPath { <# .NAME get-WindowsTerminalSettingsPath .SYNOPSIS Gets the windows terminal settings path. .DESCRIPTION If Windows terminal is not installed, this file won't exist, so empty string is returned. OUTPUTS Resolved windows well known Windows Terminal settings file path. #> [OutputType([string])] param() [string]$relativePath = '~\AppData\Local\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json'; if (Test-Path -Path $relativePath) { return Resolve-Path -Path $relativePath; } else { return ''; } } function import-ItermColors { <# .NAME import-ItermColors .SYNOPSIS Imports XML data from iterm file and converts to JSON format. .DESCRIPTION This function behaves like a reducer, because it populates an Accumulator collection for each file it is presented with. .PARAMETER Underscore fileinfo object representing the .itermcolors file. .PARAMETER Index 0 based numeric index specifying the ordinal of the file in the batch. .PARAMETER PassThru The dictionary object containing additional parameters. Also used by this function to append it's result to an 'ACCUMULATOR' hash (indexed by scheme name), which ultimately allows all the schemes to be collated into the 'schemes' array field in the settings file. .PARAMETER Trigger Trigger. .OUTPUTS [PSCustomObject] The result of invoking the BODY script block. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [OutputType([PSCustomObject])] [CmdletBinding()] param ( [Parameter( Mandatory = $true )] [System.IO.FileSystemInfo]$Underscore, [Parameter( Mandatory = $true )] [int]$Index, [Parameter( Mandatory = $true )] [System.Collections.Hashtable]$PassThru, [Parameter( Mandatory = $false )] [boolean]$Trigger ) [PSCustomObject]$result = [PSCustomObject]@{} [System.Collections.Hashtable]$terminalThemes = @{}; if ($PassThru.ContainsKey('ACCUMULATOR')) { $terminalThemes = $PassThru['ACCUMULATOR']; } else { $PassThru['ACCUMULATOR'] = $terminalThemes; } [System.Xml.XmlDocument]$document = [xml]@(Get-Content -Path $Underscore.Fullname); if ($document) { [string]$terminalTheme = new-SchemeJsonFromDocument -XmlDocument $document; if (-not([string]::IsNullOrWhiteSpace($terminalTheme))) { $result | Add-Member -MemberType NoteProperty -Name 'Trigger' -Value $true; [string]$product = [System.IO.Path]::GetFileNameWithoutExtension($_.Name); $result | Add-Member -MemberType NoteProperty -Name 'Product' -Value $product; } $terminalThemes[$Underscore.Name] = $terminalTheme; $PassThru['ACCUMULATOR'] = $terminalThemes; } return $result } # import-ItermColors function invoke-ForeachFile { <# .NAME invoke-ForeachFile .SYNOPSIS Performs iteration over a collection of files which are children of the directory specified by the caller. .DESCRIPTION Invoke an operation for each file found from the Path. (This needs to be refactored/redesigned to use the pipeline via InputObject as this is the idiomatic way to do this in PowerShell). .PARAMETER Path The parent directory to iterate. .PARAMETER Body The implementation script block that is to be implemented for each child file. The script block can either return $null or a PSCustomObject with fields Message(string) giving an indication of what was implemented, Product (string) which represents the item in question (ie the processed item as appropriate) and Colour(string) which is the console colour applied to the Product. Also, the Trigger should be set to true, if an action has been taken for any of the files iterated. This is so because if we iterate a collection of files, but the operation doesn't do anything to any of the files, then the whole operation should be considered a no-op, so we can keep output to a minimum. .PARAMETER PassThru The dictionary object used to pass parameters to the $Body scriptblock provided. .PARAMETER Filter The filter to apply to Get-ChildItem. .PARAMETER OnSummary A scriptblock that is invoked at the end of processing all processed files. (This still needs review; ie what can this provide that can't be as a result of invoking after calling invoke-ForeachFile). .PARAMETER Condition The result of Get-ChildItem is piped to a where statement whose condition is specified by this parameter. The (optional) scriptblock specified must be a predicate. .PARAMETER Inclusion Value that needs to be passed in into Get-ChildItem to additionally specify files in the include list. .OUTPUTS The collection of files iterated over. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] param ( [Parameter( Mandatory = $true )] [string]$Path, [Parameter( Mandatory = $true )] [scriptblock]$Body, [Parameter( Mandatory = $false )] [System.Collections.Hashtable]$PassThru, [Parameter( Mandatory = $false )] [string]$Filter = '*', [Parameter( Mandatory = $false )] [scriptblock]$Condition = ( { return $true; }) ) [int]$index = 0; [boolean]$trigger = $false; [System.Collections.Hashtable]$parameters = @{ 'Filter' = $Filter; 'Path' = $Path; } $collection = & 'Get-ChildItem' @parameters | get-SortedFilesNatural | Where-Object { $Condition.Invoke($_); } | ForEach-Object { # Do the invoke # [PSCustomObject]$result = $Body.Invoke($_, $index, $PassThru, $trigger); # Handle the result # if ($result) { if (($result.psobject.properties['Trigger'] -and ($result.Trigger))) { $trigger = $true; } if (($result.psobject.properties['Break'] -and ($result.Break))) { break; } } $index++; } # ForEach-Object return $collection; } function join-AllSchemas { <# .NAME join-AllSchemas .SYNOPSIS Builds the json content representing all the schemes previously collated. .DESCRIPTION Used by ConvertFrom-ItermColors. .PARAMETER Schemes Hastable of scheme names to their JSON string representations. .OUTPUTS [string] JSON string reprentation of all built schemas as members of the schemes array property. #> [OutputType([string])] param( [Parameter()] [System.Collections.Hashtable]$Schemes ) [string]$outputContent = '{ "schemes": ['; [string]$close = '] }'; [System.Collections.IDictionaryEnumerator]$enumerator = $Schemes.GetEnumerator(); if ($Schemes.Count -gt 0) { while ($enumerator.MoveNext()) { [System.Collections.DictionaryEntry]$entry = $enumerator.Current; [string]$themeFragment = $entry.Value; $outputContent += ($themeFragment + ','); } [int]$last = $outputContent.LastIndexOf(','); $outputContent = $outputContent.Substring(0, $last); } $outputContent += $close; $outputContent = $outputContent | ConvertTo-Json | ConvertFrom-Json; return $outputContent; } function merge-SettingsContent { <# .NAME merge-SettingsContent .SYNOPSIS Combines the new Content just generated with the existing Settings file. .DESCRIPTION Used by ConvertFrom-ItermColors. .PARAMETER Content The new settings content to merge. .PARAMETER SettingsPath The path to the settings file. .PARAMETER OutputPath The path to write the result to. #> param( [Parameter()] [string]$Content, [Parameter()] [string]$SettingsPath, [Parameter()] [string]$OutputPath ) [string]$settingsContentRaw = Get-Content -Path $SettingsPath -Raw; [PSCustomObject]$settingsObject = [PSCustomObject] ($settingsContentRaw | ConvertFrom-Json); $settingsSchemes = $settingsObject.schemes; [PSCustomObject]$contentObject = [PSCustomObject] ($Content | ConvertFrom-Json) [System.Collections.ArrayList]$integratedSchemes = New-Object ` -TypeName System.Collections.ArrayList -ArgumentList @(, $settingsSchemes); [System.Collections.Hashtable]$integrationTheme = Get-KrayolaTheme; $integrationTheme['VALUE-COLOURS'] = @(, @('Blue')); [System.Collections.Hashtable]$skippingTheme = Get-KrayolaTheme; $skippingTheme['VALUE-COLOURS'] = @(, @('Red')); foreach ($sch in $contentObject.schemes) { [string[][]]$pairs = @(, @('Scheme name', $sch.name)); if (-not(test-DoesContainScheme -SchemeName $sch.name -Schemes $settingsSchemes)) { Write-ThemedPairsInColour -Pairs $pairs -Theme $integrationTheme ` -Message 'Integrating new theme'; $null = $integratedSchemes.Add($sch); } else { Write-ThemedPairsInColour -Pairs $pairs -Theme $skippingTheme ` -Message 'Skipping existing theme'; } } $settingsObject.schemes = ($integratedSchemes | Sort-Object -Property name); Set-Content -Path $OutputPath -Value $($settingsObject | ConvertTo-Json); } # combineContent [System.Collections.Hashtable]$script:ItermTerminalColourMap = @{ # As defined in https://en.wikipedia.org/wiki/ANSI_escape_code#Colors # 'Ansi 0 Color' = 'black'; 'Ansi 1 Color' = 'red'; 'Ansi 2 Color' = 'green'; 'Ansi 3 Color' = 'yellow'; 'Ansi 4 Color' = 'blue'; 'Ansi 5 Color' = 'purple'; # magenta 'Ansi 6 Color' = 'cyan'; 'Ansi 7 Color' = 'white'; 'Ansi 8 Color' = 'brightBlack'; 'Ansi 9 Color' = 'brightRed'; 'Ansi 10 Color' = 'brightGreen'; 'Ansi 11 Color' = 'brightYellow'; 'Ansi 12 Color' = 'brightBlue'; 'Ansi 13 Color' = 'brightPurple'; # bright magenta 'Ansi 14 Color' = 'brightCyan'; 'Ansi 15 Color' = 'brightWhite'; # https://docs.microsoft.com/en-gb/windows/terminal/customize-settings/color-schemes # 'Background Color' = 'background'; 'Foreground Color' = 'foreground'; 'Cursor Text Color' = 'cursorColor'; 'Selection Color' = 'selectionBackground'; # Iterm colours discovered but not not mapped (to be logged out in verbose mode) # # Bold Color # Link Color # Cursor Guide Color # Badge Color } function new-SchemeJsonFromDocument { <# .NAME new-SchemeJsonFromDocument .SYNOPSIS Builds the json content representing all the schemes previously collated. .DESCRIPTION Local function new-SchemeJsonFromDocument, processes an xml document for an iterm scheme. This format is not in a form particularly helpful for xpath expressions. The key and values are all present at the same level in the xml hierarchy, so there is no direct relationship between the key and the value. All we can do is make an assumption that consecutive items are bound together by the key/value relationship. So these are processed as a result of 2 xpath expressions, the first selecting the keys (/plist/dict/key) and the other selecting the values (/plist/dict/dict) and we just make the assumption that the length of both result sets are the same and that items in the same position in their result sets are bound as a key/value pair. Used by ConvertFrom-ItermColors. .PARAMETER XmlDocument The XML document. .OUTPUTS [string] The JSON string representation of the scheme generated from the iterm document. #> [OutputType([string])] [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangeingFunctions', '', Justification='Cant use verb "build" so used new instead', Scope='Function')] param( [Parameter()] [System.Xml.XmlDocument]$XmlDocument ) # Get the top level dictionary (/dict) # $colourKeys = Select-Xml -Xml $XmlDocument -XPath '/plist/dict/key'; $colourDict = Select-Xml -Xml $XmlDocument -XPath '/plist/dict/dict'; [int]$colourIndex = 0; if ($colourKeys.Count -eq $colourDict.Count) { [PSCustomObject]$colourScheme = [PSCustomObject]@{ name = [System.IO.Path]::GetFileNameWithoutExtension($Underscore.Name) } foreach ($k in $colourKeys) { $colourDetails = $colourDict[$colourIndex]; [string]$colourName = $k.Node.InnerText; [System.Collections.Hashtable]$kols = convertFrom-ColourComponents -ColourDictionary $colourDetails; [string]$colourHash = ConvertTo-RGB -Components $kols; $colourIndex++; if ($ItermTerminalColourMap.ContainsKey($colourName)) { $colourScheme | Add-Member -MemberType 'NoteProperty' ` -Name $ItermTerminalColourMap[$colourName] -Value "$colourHash"; } else { Write-Verbose "Skipping un-mapped colour: $colourName"; } } [string]$jsonColourScheme = ConvertTo-Json -InputObject $colourScheme; Write-Verbose "$jsonColourScheme"; return $jsonColourScheme; } } # new-SchemeJsonFromDocument function test-DoesContainScheme { <# .NAME test-DoesContainScheme .SYNOPSIS Predicate that returns true if SchemeName is present in the Schemes collection. .DESCRIPTION Used by ConvertFrom-ItermColors. .PARAMETER SchemeName Name of the scheme to search for. .PARAMETER Schemes 0 based numeric index specifying the ordinal of the iterated target. .OUTPUTS [boolean] true if Schemes contains SchemeName, false otherwise #> [OutputType([boolean])] param( [Parameter()] [string]$SchemeName, [Parameter()] [object[]]$Schemes ) # The assignment to $null because of a bug in PSScriptAnalyzer # https://github.com/PowerShell/PSScriptAnalyzer/issues/1472 # $null = $SchemeName; $found = $Schemes | Where-Object { $_.name -eq $SchemeName }; return ($null -ne $found); } # Eventually, this function should go into Krayola # function write-HostItemDecorator { <# .NAME write-HostItemDecorator .SYNOPSIS Performs iteration over a collection of files which are children of the directory specified by the caller. .DESCRIPTION The purpose of this function is a act as a decorator to a custom function on behalf of which any write-host operations are performed. This keeps any display functionality out of that function so that it may be used in scenarios where output is not required. .PARAMETER Underscore The iterated target item provided by the parent iterator function. .PARAMETER Index 0 based numeric index specifying the ordinal of the iterated target. .PARAMETER PassThru The dictionary object used to pass parameters to the decorated scriptblock (enclosed within the PassThru Hashtable). .PARAMETER Trigger Trigger. .OUTPUTS The result of invoking the BODY script block. #> [OutputType([PSCustomObject])] [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ( [Parameter( Mandatory = $true )] [System.IO.FileSystemInfo]$Underscore, [Parameter( Mandatory = $true )] [int]$Index, [Parameter( Mandatory = $true )] [ValidateScript( { return $_.ContainsKey('BODY') ` -and $_.ContainsKey('KRAYOLA-THEME') -and $_.ContainsKey('ITEM-LABEL') })] [System.Collections.Hashtable] $PassThru, [boolean]$Trigger ) [scriptblock]$decorator = { param ($_underscore, $_index, $_passthru, $_trigger) [string]$decoratee = $passthru['BODY']; [System.Collections.Hashtable]$parameters = @{ 'Underscore' = $_underscore; 'Index' = $_index; 'PassThru' = $_passthru; 'Trigger' = $_trigger; } return & $decoratee @parameters; } $invokeResult = $decorator.Invoke($Underscore, $Index, $PassThru, $Trigger); [string]$message = $PassThru['MESSAGE']; [string]$itemLabel = $PassThru['ITEM-LABEL'] [System.Collections.Hashtable]$parameters = @{} [string]$writerFn = ''; [string]$productLabel = ''; if ($invokeResult.Product) { $productLabel = 'Product'; if ($PassThru.ContainsKey('PRODUCT-LABEL')) { $productLabel = $PassThru['PRODUCT-LABEL']; } } # Write with a Krayola Theme # if ($PassThru.ContainsKey('KRAYOLA-THEME')) { [System.Collections.Hashtable]$krayolaTheme = $PassThru['KRAYOLA-THEME']; [string[][]]$themedPairs = @(@('No', $("{0,3}" -f ($Index + 1))), @($itemLabel, $Underscore.Name)); if (-not([string]::IsNullOrWhiteSpace($productLabel))) { $themedPairs = $themedPairs += , @($productLabel, $invokeResult.Product); } $parameters['Pairs'] = $themedPairs; $parameters['Theme'] = $krayolaTheme; $writerFn = 'Write-ThemedPairsInColour'; } if (-not([string]::IsNullOrWhiteSpace($message))) { $parameters['Message'] = $message; } if (-not([string]::IsNullOrWhiteSpace($writerFn))) { & $writerFn @parameters; } return $invokeResult; } |