Public/ColorProfiles.ps1

function Export-ColorProfile {
    <#
    .SYNOPSIS
    Saves style profiles and registered color names to a file.
 
    .DESCRIPTION
    Export-ColorProfile writes the style profiles in [PSColorStyle]::Profiles, or those named,
    and every color name Register-ColorName added, to a JSON file that Import-ColorProfile reads
    back. The file also names the default style when it is one of the profiles saved.
 
    A profile holds the properties that differ from a new PSColorStyle's, and its colors as
    given. The file is UTF-8 without a byte order mark, with LF line ends, and the same in Windows
    PowerShell 5.1 and PowerShell 7. A file at the path is replaced.
 
    .PARAMETER Path
    The file to write.
 
    .PARAMETER Name
    The profiles to save. Wildcards are allowed.
 
    Default: every profile
 
    .EXAMPLE
    Export-ColorProfile -Path ~/colors.json
 
    .EXAMPLE
    Export-ColorProfile -Path ./build-colors.json -Name Build*
 
    Saves the profiles whose names start with Build.
 
    .INPUTS
    None
 
    .OUTPUTS
    None
 
    .NOTES
    Author: Mark Newton
    License: MIT
    Requires: PowerShell 5.1 or later
 
    .LINK
    https://github.com/MarkusMcNugen/PSWriteColorEX
 
    .LINK
    Import-ColorProfile
    #>

    [CmdletBinding()]
    [Alias('Export-ColourProfile')]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string] $Path,

        [SupportsWildcards()]
        [string[]] $Name = '*'
    )

    # The profiles named, in the order of their names in upper case, ordinal, the same everywhere
    $profileNames = [string[]]@([PSColorStyle]::Profiles.Keys | Where-Object {
        $key = $_
        @($Name | Where-Object { $key -like $_ }).Count -gt 0
    })
    $keys = [string[]]@(foreach ($profileName in $profileNames) { $profileName.ToUpperInvariant() })
    [Array]::Sort($keys, $profileNames, [System.StringComparer]::Ordinal)
    $blank = [PSColorStyle]::new()

    $document = [ordered]@{}
    $colors = [ordered]@{}
    foreach ($colorName in $script:CustomColors.Keys) {
        $rgb = $script:CustomColors[$colorName][4]
        $colors[$colorName] = '#{0:X2}{1:X2}{2:X2}' -f $rgb[0], $rgb[1], $rgb[2]
    }
    $document['Colors'] = $colors

    $default = $null
    $profiles = [System.Collections.Generic.List[object]]::new()
    foreach ($profileName in $profileNames) {
        $style = [PSColorStyle]::Profiles[$profileName]
        if ([object]::ReferenceEquals($style, [PSColorStyle]::Default)) {
            $default = $style.Name
        }
        $entry = [ordered]@{ Name = $style.Name }
        foreach ($property in $script:ColorProfileProperties) {
            $value = $style.$property
            if ($property -eq 'ForegroundColor' -or $property -eq 'BackgroundColor') {
                $entry[$property] = $value
                continue
            }
            $isDefault = if ($value -is [array]) { $value.Count -eq 0 } else { $value -eq $blank.$property }
            if ($null -ne $value -and -not $isDefault) {
                $entry[$property] = if ($value -is [char]) { [string]$value } else { $value }
            }
        }
        $profiles.Add($entry)
    }
    if ($null -ne $default) {
        $document['Default'] = $default
    }
    $document['Profiles'] = $profiles

    $file = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
    $text = (ConvertTo-ColorJson -Value $document) + "`n"
    [System.IO.File]::WriteAllText($file, $text, [System.Text.UTF8Encoding]::new($false))
}

function Import-ColorProfile {
    <#
    .SYNOPSIS
    Reads style profiles and color names from a file Export-ColorProfile wrote, or from a theme
    the module ships.
 
    .DESCRIPTION
    Import-ColorProfile registers the file's color names, replacing names in use, then adds its
    profiles to [PSColorStyle]::Profiles, replacing profiles of the same name, and makes the
    style the file names the default style.
 
    -Theme reads one of the module's themes instead:
 
    - A color theme registers its colors under names that start with the theme's name, such as
      DraculaPurple or Nord8 (SolarizedDark and SolarizedLight both use names that start with
      Solarized), and restyles the profiles Default, Error, Warning, Info, Success, Critical and
      Debug in its colors, with Default the default style.
    - CSS adds the CSS color names the color table lacks, such as Tomato, SlateGray and
      RebeccaPurple. A name already in the table keeps its color, and Grey, DarkGrey and LightGrey
      take the colors of Gray, DarkGray and LightGray.
    - PowerShell restyles Error, Warning, Info and Debug as PowerShell's own error, warning,
      verbose and debug output, and in PowerShell 7.2 and later Critical and Default as its error
      and formatting accents, with Default the default style. Each takes the text color,
      background and styles of PowerShell's setting and nothing else; a console color in it is
      written as the terminal's own color, as PowerShell writes it.
 
    .PARAMETER Path
    The file to read.
 
    .PARAMETER Theme
    The theme to read: CatppuccinFrappe, CatppuccinLatte, CatppuccinMacchiato, CatppuccinMocha,
    CSS, Dracula, GruvboxDark, GruvboxLight, Nord, OneDark, PowerShell, SolarizedDark,
    SolarizedLight or TokyoNight.
 
    .PARAMETER PassThru
    Writes the profiles read to the pipeline.
 
    .EXAMPLE
    Import-ColorProfile -Path ~/colors.json
    Write-ColorEX -Text 'Deploying' -StyleProfile ([PSColorStyle]::Profiles['Build'])
 
    .EXAMPLE
    Import-ColorProfile -Theme Dracula
    Write-ColorError 'Build failed'
    Write-ColorEX -Text 'In Dracula purple' -Color DraculaPurple
 
    Restyles the profiles in Dracula's colors and registers its palette.
 
    .EXAMPLE
    Import-ColorProfile -Theme PowerShell
 
    Makes Write-ColorError, Write-ColorWarning, Write-ColorInfo and Write-ColorDebug look like
    PowerShell's own error, warning, verbose and debug output.
 
    .EXAMPLE
    Import-ColorProfile -Theme CSS
    Write-ColorEX -Text 'Tomato' -Color Tomato
 
    .EXAMPLE
    # In a PowerShell profile script, so the styles are there in every session
    Import-ColorProfile -Path (Join-Path (Split-Path $PROFILE) 'colors.json')
 
    .INPUTS
    None
 
    .OUTPUTS
    None, or PSColorStyle with -PassThru
 
    .NOTES
    Author: Mark Newton
    License: MIT
    Requires: PowerShell 5.1 or later
 
    .LINK
    https://github.com/MarkusMcNugen/PSWriteColorEX
 
    .LINK
    Export-ColorProfile
    #>

    [CmdletBinding(DefaultParameterSetName = 'Path')]
    [Alias('Import-ColourProfile')]
    [OutputType([PSColorStyle])]
    param(
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'Path')]
        [string] $Path,

        [Parameter(Mandatory, ParameterSetName = 'Theme')]
        [ValidateSet('CatppuccinFrappe', 'CatppuccinLatte', 'CatppuccinMacchiato', 'CatppuccinMocha', 'CSS',
            'Dracula', 'GruvboxDark', 'GruvboxLight', 'Nord', 'OneDark', 'PowerShell', 'SolarizedDark',
            'SolarizedLight', 'TokyoNight')]
        [string] $Theme,

        [switch] $PassThru
    )

    if ($Theme -eq 'PowerShell') {
        $settings = Get-PowerShellColorSetting
        foreach ($style in (Get-PowerShellThemeStyle -PSStyleValue $settings.PSStyle -PrivateData $settings.PrivateData)) {
            $style.AddToProfiles()
            if ($style.Name -eq 'Default') {
                $style.SetAsDefault()
            }
            if ($PassThru) {
                $style
            }
        }
        return
    }

    $source = $Path
    if ($PSCmdlet.ParameterSetName -eq 'Theme') {
        # The theme's file, its name compared without case as -Theme compares it
        $themeFiles = @(Get-ChildItem -LiteralPath (Join-Path $script:ModuleRoot 'Themes') -Filter '*.json' -File -ErrorAction SilentlyContinue |
            Where-Object { $_.BaseName -eq $Theme })
        if ($themeFiles.Count -eq 0) {
            $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new(
                [System.IO.FileNotFoundException]::new("The module has no file for the theme '$Theme'."),
                'ThemeFileNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $Theme))
        }
        $file = $themeFiles[0].FullName
        $source = $file
    } else {
        $file = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
    }
    try {
        $document = [System.IO.File]::ReadAllText($file) | ConvertFrom-Json -ErrorAction Stop
    } catch {
        $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new(
            [System.IO.InvalidDataException]::new("Cannot read the profiles in '$source'. $($_.Exception.Message)"),
            'InvalidProfileFile', [System.Management.Automation.ErrorCategory]::InvalidData, $source))
    }

    # The CSS theme adds only the names the color table lacks
    $namesInUse = $null
    if ($Theme -eq 'CSS') {
        $namesInUse = Get-ColorTableWithRGB
    }
    if ($document.Colors) {
        foreach ($property in $document.Colors.PSObject.Properties) {
            if ($null -ne $namesInUse -and $namesInUse.ContainsKey($property.Name)) {
                continue
            }
            Register-ColorName -Name $property.Name -Color (ConvertFrom-ColorJsonValue -Value $property.Value) -Force
        }
    }

    foreach ($entry in @($document.Profiles)) {
        if ($null -eq $entry -or [string]::IsNullOrEmpty($entry.Name)) {
            continue
        }
        $style = [PSColorStyle]::new([string]$entry.Name, $null, $null)
        foreach ($property in $script:ColorProfileProperties) {
            $member = $entry.PSObject.Properties[$property]
            if ($null -ne $member) {
                $style.$property = ConvertFrom-ColorJsonValue -Value $member.Value
            }
        }
        $style.AddToProfiles()
        if ($document.Default -and $document.Default -eq $style.Name) {
            $style.SetAsDefault()
        }
        if ($PassThru) {
            $style
        }
    }
}

function Remove-ColorProfile {
    <#
    .SYNOPSIS
    Removes style profiles from [PSColorStyle]::Profiles.
 
    .DESCRIPTION
    A removed built-in profile, such as Error, leaves its helper, Write-ColorError, with the
    colors and styles it starts with. A name with no profile gives an error.
 
    .PARAMETER Name
    The profiles to remove.
 
    .EXAMPLE
    Remove-ColorProfile -Name Build
 
    .INPUTS
    System.String[]
    Names piped in are removed.
 
    .OUTPUTS
    None
 
    .NOTES
    Author: Mark Newton
    License: MIT
    Requires: PowerShell 5.1 or later
 
    .LINK
    https://github.com/MarkusMcNugen/PSWriteColorEX
 
    .LINK
    Get-ColorProfiles
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [Alias('Remove-ColourProfile')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline = $true)]
        [string[]] $Name
    )

    process {
        foreach ($item in $Name) {
            if (-not [PSColorStyle]::Profiles.ContainsKey($item)) {
                $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
                    [System.ArgumentException]::new("No profile named '$item'."),
                    'ProfileNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $item))
                continue
            }
            if ($PSCmdlet.ShouldProcess($item, 'Remove the style profile')) {
                [PSColorStyle]::Profiles.Remove($item)
            }
        }
    }
}