Theme.PSReadLine.psm1

using namespace System.Collections.Generic
using namespace System.Management.Automation.Language
#Region '.\Private\ConvertToCssColor.ps1' -1

#using namespace System.Collections.Generic

function ConvertToCssColor {
    [CmdletBinding()]
    param(
        [Parameter(ParameterSetName="PListColorDictionary", Mandatory, Position = 0)]
        [Dictionary[string,object]]$colors,

        [Parameter(ParameterSetName="ColorValue", Mandatory, Position = 0)]
        [string]$color,

        [switch]$Background
    )
    end {
        if ($PSCmdlet.ParameterSetName -eq "PListColorDictionary") {
            [int]$r = 255 * $colors["Red Component"]
            [int]$g = 255 * $colors["Green Component"]
            [int]$b = 255 * $colors["Blue Component"]
            [PoshCode.Pansies.RgbColor]::new($r, $g, $b).ToVtEscapeSequence($Background)
        }
        if ($PSCmdlet.ParameterSetName -eq "ColorValue") {
            $color = $color -replace '^#[0-9a-f]{2}([0-9a-f]{6})$', '#$1'
            [PoshCode.Pansies.RgbColor]::new($color).ToVtEscapeSequence($Background)
        }
    }
}
#EndRegion '.\Private\ConvertToCssColor.ps1' 27
#Region '.\Private\FindVSCodeTheme.ps1' -1

function FindVsCodeTheme {
    [CmdletBinding(DefaultParameterSetName = "Specific")]
    param(
        [Parameter(ParameterSetName = "List")]
        [Parameter(ParameterSetName = "Specific", Mandatory, Position = 0)]
        [string]$Name,

        [Parameter(ParameterSetName = "List", Mandatory)]
        [switch]$List
    )

    $VSCodeExtensions = @(
        # VS Code themes are in one of two places: in the app, or in your profile folder:
        Convert-Path "~\.vscode*\extensions\"
        # If `code` is in your path, we can guess where that is...
        Get-Command Code-Insiders, Code -ErrorAction Ignore |
            Split-Path | Split-Path | Join-Path -ChildPath "resources\app\extensions\"
    )
    $Warnings = @()

    $Themes = @(
        # If they passed a file path that exists, use just that one file
        if (-not $List -and ($Specific = Test-Path -LiteralPath $Name)) {
            $File = Convert-Path $Name
            $(
                if ($File.EndsWith(".json")) {
                    try {
                        # Write-Debug "Parsing json file: $File"
                        ConvertFrom-Json (Get-Content -Path $File -Raw -Encoding utf8) -ErrorAction SilentlyContinue
                    } catch {
                        Write-Error "Couldn't parse '$File'. $(
                        if($PSVersionTable.PSVersion.Major -lt 6) {
                            'You could try again with PowerShell Core, the JSON parser there works much better!'
                        })"

                    }
                } else {
                    # Write-Debug "Parsing PList file: $File"
                    Import-PList -Path $File
                }
            ) | Select-Object @{ Name = "Name"
                                 Expr = {
                                    if ($_.name) {
                                        $_.name
                                    } else {
                                        [IO.Path]::GetFileNameWithoutExtension($File)
                                    }
                                }
                           }, @{ Name = "Path"
                                 Expr = {$File}
                           }
        } else {
            $VSCodeExtensions  = $VSCodeExtensions | Join-Path -ChildPath "\*\package.json" -Resolve
            foreach ($File in $VSCodeExtensions) {
                Write-Debug "Considering VSCode Extention $([IO.Path]::GetFileName([IO.Path]::GetDirectoryName($File)))"
                $JSON = Get-Content -Path $File -Raw -Encoding utf8
                try {
                    $Extension = ConvertFrom-Json $JSON -ErrorAction Stop
                    # if ($Extension.contributes.themes) {
                    # Write-Debug "Found $($Extension.contributes.themes.Count) themes"
                    # }
                    $Extension.contributes.themes |
                        Select-Object @{Name="Name" ; Expr={$_.label}},
                                      @{Name="Style"; Expr={$_.uiTheme}},
                                      @{Name="Path" ; Expr={Join-Path (Split-Path $File) $_.path -resolve}}
                } catch {
                    $Warning = "Couldn't parse some VSCode extensions."
                }
            }
        }
    )

    if ($Themes.Count -eq 0) {
        throw "Could not find any VSCode themes. Please use a full path."
    }

    if ($Specific -and $Themes.Count -eq 1) {
        $Themes
    }

    $Themes = $Themes | Sort-Object Name

    if ($List) {
        Write-Verbose "Found $($Themes.Count) Themes"
        $Themes
        return
    }

    # Make sure we're comparing the name to a name
    $Name = [IO.Path]::GetFileName(($Name -replace "\.json$|\.tmtheme$"))
    Write-Debug "Testing theme names for '$Name'"

    # increasingly fuzzy search: (eq -> like -> match)
    if (!($Theme = $Themes.Where{$_.name -eq $Name})) {
        if (!($Theme = $Themes.Where{$_.name -like $Name})) {
            if (!($Theme = $Themes.Where{$_.name -like "*$Name*"})) {
                foreach($Warning in $Warnings) {
                    Write-Warning $Warning
                }
                Write-Error "Couldn't find the theme '$Name', please try another: $(($Themes.name | Select-Object -Unique) -join ', ')"
            }
        }
    }
    if (@($Theme).Count -gt 1) {
        $Dupes = $(if (@($Theme.Name | Sort-Object -Unique).Count -gt 1) {$Theme.Name} else {$Theme.Path}) -join ", "
        Write-Warning "Found more than one theme for '$Name'. Using '$(@($Theme)[0].Path)', but you could try again for one of: $Dupes)"
    }

    @($Theme)[0]
}
#EndRegion '.\Private\FindVSCodeTheme.ps1' 110
#Region '.\Private\GetColorProperty.ps1' -1

function GetColorProperty{
    <#
        .SYNOPSIS
            Search the colors for a matching theme color name and returns the foreground
    #>

    param(
        # The array of colors
        [Array]$colors,

        # An array of (partial) scope names in priority order
        # The foreground color of the first matching scope in the tokens will be returned
        [string[]]$name,

        [switch]$background
    )
    # Since we loaded the themes in order of prescedence, we take the first match that has a foreground color
    foreach ($pattern in $name) {
        if ($foreground = @($colors.$pattern).Where{ $_ }[0]) {
            if ($pattern -match "Background(Color)?") {
                $background = $true
            }
            ConvertToCssColor $foreground -Background:$background
            return
        }
        if ($key, $property = $pattern -split "\.") {
            if ($foreground = @($colors.$key.$property).Where{ $_ }[0]) {
                if ($property -match "Background(Color)?") {
                    $background = $true
                }
                ConvertToCssColor $foreground -Background:$background
                return
            }
        }
        # Normalize color
    }
}
#EndRegion '.\Private\GetColorProperty.ps1' 37
#Region '.\Private\GetColorScopeForeground.ps1' -1

function GetColorScopeForeground {
    <#
        .SYNOPSIS
            Search the tokens for a scope name with a foreground color
    #>

    param(
        # The array of tokens
        [Array]$tokens,

        # An array of (partial) scope names in priority order
        # The foreground color of the first matching scope in the tokens will be returned
        [string[]]$name
    )
    # Since we loaded the themes in order of prescedence, we take the first match that has a foreground color
    foreach ($pattern in $name) {
        foreach ($token in $tokens) {
            if (($token.scope -split "\s*,\s*" -match $pattern) -and $token.settings.foreground) {
                ConvertToCssColor $token.settings.foreground
                return
            }
        }
    }
}
#EndRegion '.\Private\GetColorScopeForeground.ps1' 24
#Region '.\Private\ImportJsonIncludeLast.ps1' -1

function ImportJsonIncludeLast {
    <#
        .SYNOPSIS
            Import VSCode json themes, including any included themes
    #>

    [CmdletBinding()]
    param([string[]]$Path)

    # take the first
    $themeFile, $Path = $Path
    $theme = Get-Content $themeFile | ConvertFrom-Json

    # Output all the colors or token colors
    if ($theme.colors) {
        $theme.colors
    }
    if ($theme.tokenColors) {
        $theme.tokenColors
    }

    # Recurse includes
    if ($theme.include) {
        $Path += $themeFile | Split-Path | Join-Path -Child $theme.include | convert-path
    }
    if ($Path) {
        ImportJsonIncludeLast $Path
    }
}
#EndRegion '.\Private\ImportJsonIncludeLast.ps1' 29
#Region '.\Private\WriteToken.ps1' -1

function WriteToken {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.Language.Token]
        $Token,

        [Parameter(Mandatory)]
        [Text.StringBuilder]
        $StringBuilder,

        $Theme
    )
    $null = switch ($token) {
        { $_ -is [StringExpandableToken] } {
            $startingOffset = $_.Extent.StartOffset
            $lastEndOffset = $startingOffset
            foreach ($nestedToken in $_.NestedTokens) {
                $StringBuilder.Append($Theme.StringColor)
                $StringBuilder.Append($_.Text, $lastEndOffset - $startingOffset, $nestedToken.Extent.StartOffset - $lastEndOffset)

                WriteToken -Token $nestedToken -StringBuilder $StringBuilder -Theme $Theme
                $lastEndOffset = $nestedToken.Extent.EndOffset
            }
            $StringBuilder.Append($Theme.StringColor)
            $StringBuilder.Append($_.Text, $lastEndOffset - $startingOffset, $_.Extent.EndOffset - $lastEndOffset)
            return
        }
        { $_ -is [StringToken] } {
            if ($_.TokenFlags.HasFlag([TokenFlags]::CommandName)) {
                $StringBuilder.Append($Theme.CommandColor)
                break
            }
            $StringBuilder.Append($Theme.StringColor)
            break
        }
        { $_ -is [NumberToken] } { $StringBuilder.Append($Theme.NumberColor); break }
        { $_ -is [ParameterToken] } { $StringBuilder.Append($Theme.ParameterColor); break }
        { $_ -is [VariableToken] } { $StringBuilder.Append($Theme.VariableColor); break }
        { $_.TokenFlags.HasFlag([TokenFlags]::BinaryOperator) } { $StringBuilder.Append($Theme.OperatorColor); break }
        { $_.TokenFlags.HasFlag([TokenFlags]::UnaryOperator) } { $StringBuilder.Append($Theme.OperatorColor); break }
        { $_.TokenFlags.HasFlag([TokenFlags]::CommandName) } { $StringBuilder.Append($Theme.CommandColor); break; }
        { $_.TokenFlags.HasFlag([TokenFlags]::MemberName) } { $StringBuilder.Append($Theme.MemberColor); break; }
        { $_.TokenFlags.HasFlag([TokenFlags]::TypeName) } { $StringBuilder.Append($Theme.TypeColor); break; }
        { $_.TokenFlags.HasFlag([TokenFlags]::Keyword) } { $StringBuilder.Append($Theme.KeywordColor); break; }
        { $_ -is [StringToken] } { $StringBuilder.Append($Theme.StringColor); break }
        default { $StringBuilder.Append($Theme.DefaultTokenColor); break }
    }
    $null = $StringBuilder.Append($token.Text + "`e[0m")
}
#EndRegion '.\Private\WriteToken.ps1' 51
#Region '.\Public\Get-PSReadLineTheme.ps1' -1

function Get-PSReadLineTheme {
    <#
        .SYNOPSIS
            Returns a hashtable of the _current_ values that can be splatted to Set-Theme
    #>

    [CmdletBinding()]
    param()
    Get-PSReadLineOption | Select-Object *Color
}
#EndRegion '.\Public\Get-PSReadLineTheme.ps1' 10
#Region '.\Public\Get-VSCodeTheme.ps1' -1

function Get-VSCodeTheme {
    <#
        .SYNOPSIS
            Get a PSReadLine theme from a VS Code Theme that you have installed locally.
        .DESCRIPTION
            Gets PSReadLine colors from a Visual Studio Code Theme. Only works with locally installed Themes, but includes tab-completion for theme names so you can Ctrl+Space to list the ones you have available.
 
            The default output will show a little preview of what PSReadLine will look like. Note that the PSReadLine theme will _not_ set the background color.
 
            You can pipe the output to Set-PSReadLineTheme to import the theme for the PSReadLine module.
 
            Note that you may want to use -Verbose to see details of the import. In some cases, Get-VSCodeTheme will not be able to determine values for all PSReadLine colors, and there is a verbose output showing the colors that get the default value.
        .Example
            Get-VSCodeTheme 'Light+ (default light)'
 
            Gets the default "Dark+" theme from Code and shows you a preview. Note that to use this theme effectively, you need to have your terminal background color set to a light color like the white in the preview.
        .Example
            Get-VSCodeTheme 'Dark+ (default dark)' | Set-PSReadLineTheme
 
            Imports the "Dark+" theme from Code and sets it as your PSReadLine color theme.
        .Link
            Set-PSReadLineTheme
            Get-PSReadLineTheme
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = "ByName")]
    param(
        # The name of (or full path to) a vscode json theme which you have installed
        # E.g. 'Dark+' or 'Monokai'
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            Get-VSCodeTheme -List | ForEach-Object {
                if ($_.Name -match "[\s'`"]") {
                    "'{0}'" -f ($_.Name -replace "'", "''")
                } else {
                    $_.Name
                }
            } | Where-Object { $_.StartsWith($wordToComplete) }
        })]
        [Alias("PSPath", "Name")]
        [Parameter(ParameterSetName = "ByName", ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
        [string]$Theme,

        # List the VSCode themes available
        [Parameter(ParameterSetName = "ListOnly", Mandatory)]
        [switch]$List
    )
    process {
        if ($List) {
            FindVsCodeTheme -List
            return
        } else {
            $VsCodeTheme = FindVsCodeTheme $Theme -ErrorAction Stop
        }

        if ($PSCmdlet.ShouldProcess($VsCodeTheme.Path, "Import PSReadLine colors from theme")) {
            # Load the theme file and split the output into colors and tokencolors
            if ($VsCodeTheme.Path.endswith(".json")) {
                $colors, $tokens = (ImportJsonIncludeLast $VsCodeTheme.Path).Where({!$_.scope}, 'Split', 2)
            } else {
                $colors, $tokens = (Import-PList $VsCodeTheme.Path).settings.Where({!$_.scope}, 'Split', 2)
                $colors = $colors.settings
            }

            $ThemeOutput = [Ordered]@{
                PSTypeName = 'Selected.Microsoft.PowerShell.PSConsoleReadLineOptions'
                # These should come from the base colors, rather than token scopes
                BackgroundColor         = GetColorProperty $colors 'editor.background', 'background', 'settings.background', 'terminal.background'
                DefaultTokenColor       = GetColorProperty $colors 'editor.foreground', 'foreground', 'settings.foreground', 'terminal.foreground'
                SelectionColor          = GetColorProperty $colors 'editor.selectionBackground', 'editor.selectionHighlightBackground', 'selection' -Background
                ErrorColor              = @(@(GetColorProperty $colors 'errorForeground', 'editorError.foreground') + @(GetColorScopeForeground $tokens 'invalid'))[0]
                # The rest of these come from token scopes
                CommandColor            = GetColorScopeForeground $tokens 'support.function'
                CommentColor            = GetColorScopeForeground $tokens 'comment'
                ContinuationPromptColor = GetColorScopeForeground $tokens 'constant.character'
                EmphasisColor           = GetColorScopeForeground $tokens 'markup.bold', 'markup.italic', 'emphasis', 'strong', 'constant.other.color', 'markup.heading'
                InlinePredictionColor   = GetColorScopeForeground $tokens 'markup.underline',
                KeywordColor            = GetColorScopeForeground $tokens '^keyword.control$', '^keyword$', 'keyword.control', 'keyword'
                MemberColor             = GetColorScopeForeground $tokens 'variable.other.object.property', 'member', 'type.property', 'support.function.any-method', 'entity.name.function'
                NumberColor             = GetColorScopeForeground $tokens 'constant.numeric', 'constant'
                OperatorColor           = GetColorScopeForeground $tokens 'keyword.operator$', 'keyword'
                ParameterColor          = GetColorScopeForeground $tokens 'parameter'
                StringColor             = GetColorScopeForeground $tokens '^string$'
                TypeColor               = GetColorScopeForeground $tokens '^storage.type$', '^support.class$', '^entity.name.type.class$', '^entity.name.type$'
                VariableColor           = GetColorScopeForeground $tokens '^variable$', '^entity.name.variable$', '^variable.other$'
            }

            <# ###### We *COULD* map some colors to other themable modules #####
            # If the VSCode Theme has terminal colors, and you had Theme.Terminal or Theme.WindowsTerminal or Theme.WindowsConsole
            if ($colors.'terminal.ansiBrightYellow') {
                Write-Verbose "Exporting Theme.Terminal"
                $ThemeOutput['Theme.Terminal'] = @(
                        GetColorProperty $colors "terminal.ansiBlack"
                        GetColorProperty $colors "terminal.ansiRed"
                        GetColorProperty $colors "terminal.ansiGreen"
                        GetColorProperty $colors "terminal.ansiYellow"
                        GetColorProperty $colors "terminal.ansiBlue"
                        GetColorProperty $colors "terminal.ansiMagenta"
                        GetColorProperty $colors "terminal.ansiCyan"
                        GetColorProperty $colors "terminal.ansiWhite"
                        GetColorProperty $colors "terminal.ansiBrightBlack"
                        GetColorProperty $colors "terminal.ansiBrightRed"
                        GetColorProperty $colors "terminal.ansiBrightGreen"
                        GetColorProperty $colors "terminal.ansiBrightYellow"
                        GetColorProperty $colors "terminal.ansiBrightBlue"
                        GetColorProperty $colors "terminal.ansiBrightMagenta"
                        GetColorProperty $colors "terminal.ansiBrightCyan"
                        GetColorProperty $colors "terminal.ansiBrightWhite"
                    )
                if ($colors."terminal.background") {
                    $ThemeOutput['Theme.Terminal']['background'] = GetColorProperty $colors "terminal.background"
                }
                if ($colors."terminal.foreground") {
                    $ThemeOutput['Theme.Terminal']['foreground'] = GetColorProperty $colors "terminal.foreground"
                }
            }
 
            # If the VSCode Theme has warning/info colors, and you had Theme.PowerShell
            if (GetColorProperty $colors 'editorWarning.foreground') {
                $ThemeOutput['Theme.PowerShell'] = @{
                    WarningForegroundColor = GetColorProperty $colors 'editorWarning.foreground'
                    ErrorForegroundColor = GetColorProperty $Colors 'editorError.foreground'
                    VerboseForegroundColor = GetColorProperty $Colors 'editorInfo.foreground'
                    ProgressForegroundColor = GetColorProperty $Colors 'notifications.foreground'
                    ProgressBackgroundColor = GetColorProperty $Colors 'notifications.background'
                }
            } #>


            if ($DebugPreference -in "Continue", "Inquire") {
                $global:colors = $colors
                $global:tokens = $tokens
                $global:Theme = $ThemeOutput
                ${function:global:Get-VSColorScope} = ${function:GetColorScopeForeground}
                ${function:global:Get-VSColor} = ${function:GetColorProperty}
                Write-Debug "For debugging, `$Theme, `$Colors, `$Tokens were copied to global variables, and Get-VSColor and Get-VSColorScope exported."
            }

            if ($ThemeOutput.Values -contains $null) {
                [string[]]$missing = @()
                foreach ($kv in @($ThemeOutput.GetEnumerator())) {
                    if ($null -eq $kv.Value) {
                        $missing += $kv.Key
                        $ThemeOutput[$kv.Key] = $ThemeOutput["DefaultToken"]
                    }
                }
                Write-Verbose "Used DefaultTokenColor for some colors: $($missing -join ', ')"
            }

            [PSCustomObject]$ThemeOutput
        }
    }
}
#EndRegion '.\Public\Get-VSCodeTheme.ps1' 152
#Region '.\Public\Set-PSReadLineTheme.ps1' -1

function Set-PSReadLineTheme {
    <#
        .SYNOPSIS
            Set the theme for PSReadLine as escape sequences.
        .DESCRIPTION
            Set the theme for PSReadLine. Supports setting all the properties that are formatting related.
            Takes colors and other formatting options (bold, underline, etc.) as escape sequences.
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipelineByPropertyName)]
        $CommandColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $CommentColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $ContinuationPromptColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $DefaultTokenColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $EmphasisColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $ErrorColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $KeywordColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $MemberColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $NumberColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $OperatorColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $ParameterColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $InlinePredictionColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $SelectionColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $StringColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $TypeColor,
        [Parameter(ValueFromPipelineByPropertyName)]
        $VariableColor
    )
    process {
        $ParameterNames = $PSBoundParameters.Keys.Where{
            $_ -notin [System.Management.Automation.PSCmdlet]::CommonParameters
        }

        $Colors = @{}
        foreach ($ParameterName in $ParameterNames) {
            if (($Value = Get-Variable -Scope Local -Name $ParameterName -ValueOnly)) {
                $ColorName = $ParameterName -replace "(token)?color"
                # This is only working when I use the AsEscapeSequence, but the input values are already escape sequences!
                $Colors[$ColorName] = [Microsoft.PowerShell.VTColorUtils]::AsEscapeSequence( $Value )
            }
        }
        Set-PSReadLineOption -Colors $Colors
    }
}
#EndRegion '.\Public\Set-PSReadLineTheme.ps1' 60
#Region '.\Public\Show-Code.ps1' -1

#using namespace System.Management.Automation.Language
function Show-Code {
    [CmdletBinding(DefaultParameterSetName = 'HistoryId')]
    param(
        # A script block or a path to a script file
        [Parameter(Mandatory, ParameterSetName = 'Script', Position = 0)]
        [string]$Script,

        # The history id of the script to show
        [Parameter(Mandatory, ParameterSetName = 'History', Position = 0)]
        [int]$HistoryId,

        # The PSReadLine theme to use. If not specified, the current theme is used.
        # Can be the output of Get-VSCodeTheme or Get-PSReadLineTheme
        $Theme = (Get-PSReadLineTheme)
    )
    if ($HistoryId) {
        $Script = (Get-History -Id $HistoryId).CommandLine
    }

    $ParseErrors = $null
    $Tokens = $null
    $null = if (Test-Path "$Script" -ErrorAction SilentlyContinue) {
        [System.Management.Automation.Language.Parser]::ParseFile((Convert-Path $Script), [ref]$Tokens, [ref]$ParseErrors)
    } else {
        [System.Management.Automation.Language.Parser]::ParseInput([String]$Script, [ref]$Tokens, [ref]$ParseErrors)
    }

    $lastEndOffset = 0
    $StringBuilder = [Text.StringBuilder]::new()

    foreach ($token in $tokens) {
        $null = $StringBuilder.Append([char]' ', ($token.Extent.StartOffset - $lastEndOffset))
        $lastEndOffset = $token.Extent.EndOffset
        $null = WriteToken -Token $token -StringBuilder $StringBuilder -Theme $Theme
    }
    # Reset the colors
    $null = $StringBuilder.Append("$([char]0x1b)[0m$([char]0x1b)[24m$([char]0x1b)[27m")
    $StringBuilder.ToString()
}
#EndRegion '.\Public\Show-Code.ps1' 41
#Region '.\postfix.ps1' -1

if (Get-Module EzTheme -ErrorAction SilentlyContinue) {
    Get-ModuleTheme | Set-PSReadLineTheme
}
#EndRegion '.\postfix.ps1' 4