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;
}
Export-ModuleMember -Alias cfic, Make-WtSchemesIC

Export-ModuleMember -Function ConvertFrom-ItermColors