PSWriteColorEX.psm1

#Requires -Version 5.1

<#
.SYNOPSIS
    PSWriteColorEX - colored and styled console output with ANSI and TrueColor support
 
.DESCRIPTION
    PSWriteColorEX writes colored and styled text through Write-Host:
 
    - TrueColor (24-bit RGB), ANSI 256-color and 16-color modes, and the console's own colors
    - Gradients across the characters of a line
    - Padding to a display width (AutoPad) that counts emoji and CJK characters as 2 cells
    - Style profiles: Error, Warning, Info, Success, Critical, Debug, and custom ones
    - Detection of the terminal's color support, falling back to the modes it has
    - Windows, Linux and macOS, in Windows PowerShell 5.1 and PowerShell 7
    - Logging to a file, with timestamps and levels
 
.NOTES
    Author: MarkusMcNugen
    License: MIT
    Requires: PowerShell 5.1 or later
    Compatible: PowerShell Desktop and Core editions
 
    Importing the module detects the terminal's color support once and keeps the result.
 
.LINK
    https://github.com/MarkusMcNugen/PSWriteColorEX
 
.EXAMPLE
    Import-Module PSWriteColorEX
    Write-ColorEX "Hello World" -Color Green -Bold
 
.EXAMPLE
    Write-ColorError "Operation failed"
    Write-ColorSuccess "Completed successfully"
 
.EXAMPLE
    Write-ColorEX "GRADIENT" -Gradient @('Red','Yellow','Green','Cyan','Blue','Magenta')
#>


$script:ModuleRoot = $PSScriptRoot
$script:DebugMode = $env:PSWRITECOLOREX_DEBUG -eq 'true'
$script:CachedANSISupport = $null   # The terminal's color support, detected at import
$script:SupportsBoldFonts = $false  # Whether the terminal draws bold as a bold font
$script:CachedColorTable = $null    # The color table, built on first use
$script:RGB6LevelLookup = $null     # Each 0-255 channel value's step in the 6-step RGB cube

# The ANSI4 foreground code of each console color; a background code is 10 more
$script:ConsoleColorSgr = @{
    Black = 30; DarkRed = 31; DarkGreen = 32; DarkYellow = 33
    DarkBlue = 34; DarkMagenta = 35; DarkCyan = 36; Gray = 37
    DarkGray = 90; Red = 91; Green = 92; Yellow = 93
    Blue = 94; Magenta = 95; Cyan = 96; White = 97
}

if ($script:DebugMode) {
    Write-Verbose "PSWriteColorEX Debug Mode Enabled" -Verbose
}

function Initialize-RGB6LevelLookup {
    if ($null -eq $script:RGB6LevelLookup) {
        $script:RGB6LevelLookup = [int[]]::new(256)
        for ($i = 0; $i -lt 256; $i++) {
            $script:RGB6LevelLookup[$i] = if ($i -lt 48) { 0 }
                elseif ($i -lt 115) { 1 }
                elseif ($i -lt 155) { 2 }
                elseif ($i -lt 195) { 3 }
                elseif ($i -lt 235) { 4 }
                else { 5 }
        }
    }
}

Initialize-RGB6LevelLookup

# Classes load before the functions that use them
$ClassFiles = @(
    [System.IO.Path]::Combine($PSScriptRoot, 'Classes', 'PSColorStyle.ps1')
)

foreach ($file in $ClassFiles) {
    if (Test-Path -LiteralPath $file) {
        try {
            . $file
            if ($script:DebugMode) {
                Write-Verbose "Loaded class file: $file" -Verbose
            }
        } catch {
            Write-Error "Failed to load class file: $file. Error: $_"
        }
    }
}

$PrivateFunctions = @(
    [System.IO.Path]::Combine($PSScriptRoot, 'Private', 'DisplayWidthTable.ps1')
    [System.IO.Path]::Combine($PSScriptRoot, 'Private', 'ColorHost.ps1')
    [System.IO.Path]::Combine($PSScriptRoot, 'Private', 'Get-ColorHelperParams.ps1')
    [System.IO.Path]::Combine($PSScriptRoot, 'Private', 'New-GradientColorArray.ps1')
)

foreach ($file in $PrivateFunctions) {
    if (Test-Path -LiteralPath $file) {
        try {
            . $file
            if ($script:DebugMode) {
                Write-Verbose "Loaded private function: $(Split-Path $file -Leaf)" -Verbose
            }
        } catch {
            Write-Error "Failed to load private function: $(Split-Path $file -Leaf). Error: $_"
        }
    } else {
        Write-Warning "Private function file not found: $file"
    }
}

$PublicFunctions = @(
    [System.IO.Path]::Combine($PSScriptRoot, 'Public', 'Test-AnsiSupport.ps1')
    [System.IO.Path]::Combine($PSScriptRoot, 'Public', 'Convert-ColorValue.ps1')
    [System.IO.Path]::Combine($PSScriptRoot, 'Public', 'Write-ColorEX.ps1')
    [System.IO.Path]::Combine($PSScriptRoot, 'Public', 'Write-ColorHelpers.ps1')
    [System.IO.Path]::Combine($PSScriptRoot, 'Public', 'Measure-DisplayWidth.ps1')
)

foreach ($file in $PublicFunctions) {
    if (Test-Path -LiteralPath $file) {
        try {
            . $file
            if ($script:DebugMode) {
                Write-Verbose "Loaded public function: $(Split-Path $file -Leaf)" -Verbose
            }
        } catch {
            Write-Error "Failed to load public function: $(Split-Path $file -Leaf). Error: $_"
        }
    } else {
        Write-Warning "Public function file not found: $file"
    }
}

try {
    [PSColorStyle]::InitializeDefaultProfiles()
    if ($script:DebugMode) {
        Write-Verbose "Initialized default color profiles" -Verbose
    }
} catch {
    Write-Warning "Could not initialize default color profiles: $_"
}

# A class defined in a module is not visible to the session that imports it, only to scripts
# with 'using module'. A type accelerator makes [PSColorStyle] resolve after Import-Module too,
# and is removed with the module.
$TypeAcceleratorsClass = [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
$ExportableTypes = @([PSColorStyle])
foreach ($Type in $ExportableTypes) {
    $TypeAcceleratorsClass::Add($Type.FullName, $Type)
}

$script:ModuleInitialized = $false

function Initialize-PSWriteColorEX {
    <#
    .SYNOPSIS
        Detects the terminal's color support once and keeps the result for Write-ColorEX
    #>

    [CmdletBinding()]
    param()

    if ($script:ModuleInitialized) {
        return
    }

    $ansiResult = Test-AnsiSupport -Silent
    $script:CachedANSISupport = $ansiResult.ColorSupport
    $script:SupportsBoldFonts = $ansiResult.SupportsBoldFonts

    if ($script:DebugMode) {
        Write-Verbose "Module initialized with ANSI support level: $script:CachedANSISupport" -Verbose
        Write-Verbose "Bold font support: $script:SupportsBoldFonts" -Verbose
    }

    $script:ModuleInitialized = $true
}

Initialize-PSWriteColorEX

$ExportParams = @{
    Function = @(
        'Write-ColorEX',
        'Write-ColorError',
        'Write-ColorWarning',
        'Write-ColorInfo',
        'Write-ColorSuccess',
        'Write-ColorCritical',
        'Write-ColorDebug',
        'Set-ColorDefault',
        'Get-ColorProfiles',
        'New-ColorStyle',
        'Test-AnsiSupport',
        'Convert-HexToRGB',
        'Convert-RGBToANSI8',
        'Convert-RGBToANSI4',
        'Get-ColorTableWithRGB',
        'Measure-DisplayWidth',
        'Get-LighterRGBColor',
        'Get-LighterColorName',
        'Get-LighterANSI8Color'
    )

    Alias = @(
        # Write-ColorEX aliases
        'Write-ColourEX', 'Write-Color', 'Write-Colour', 'WC', 'WCEX', 'wcolor', 'wcolour',
        # Write-ColorError aliases
        'WCE', 'Write-ErrorColor', 'Write-ErrorColour', 'Write-ColourError', 'WError', 'wcerror',
        # Write-ColorWarning aliases
        'WCW', 'Write-WarningColor', 'Write-WarningColour', 'Write-ColourWarning', 'WWarning', 'WCWarn', 'wcwarning',
        # Write-ColorInfo aliases
        'WCI', 'Write-InfoColor', 'Write-InfoColour', 'Write-ColourInfo', 'WInfo', 'wcinfo',
        # Write-ColorSuccess aliases
        'WCS', 'Write-SuccessColor', 'Write-SuccessColour', 'Write-ColourSuccess', 'WSuccess', 'wcok', 'wcsuccess',
        # Write-ColorCritical aliases
        'WCC', 'Write-CriticalColor', 'Write-CriticalColour', 'Write-ColourCritical', 'WCritical', 'wccritical',
        # Write-ColorDebug aliases
        'WCD', 'Write-DebugColor', 'Write-DebugColour', 'Write-ColourDebug', 'WDebug', 'wcdebug',
        # Set-ColorDefault aliases
        'SCD', 'Set-ColourDefault', 'Set-DefaultColor', 'Set-DefaultColour',
        # Get-ColorProfiles aliases
        'GCP', 'Get-ColourProfiles', 'Get-Profiles', 'gcprofiles',
        # New-ColorStyle aliases
        'NCS', 'New-ColourStyle', 'New-Style', 'ncstyle',
        # Test-AnsiSupport aliases
        'TAS', 'Test-ANSI',
        # Convert-HexToRGB aliases
        'CHR', 'Hex2RGB',
        # Convert-RGBToANSI8 aliases
        'CRA8', 'RGB2ANSI8',
        # Convert-RGBToANSI4 aliases
        'CRA4', 'RGB2ANSI4',
        # Get-ColorTableWithRGB aliases
        'GCT', 'Get-ColorTable', 'Get-ColourTable',
        # Measure-DisplayWidth aliases
        'MDW', 'Get-DisplayWidth',
        # Get-LighterRGBColor aliases
        'Lighten-RGBColor',
        # Get-LighterColorName aliases
        'Lighten-ColorName',
        # Get-LighterANSI8Color aliases
        'Lighten-ANSI8Color', 'LA8', 'Lighten-ANSI8'
    )

    Variable = @()
}

Export-ModuleMember @ExportParams

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    foreach ($Type in $ExportableTypes) {
        $null = $TypeAcceleratorsClass::Remove($Type.FullName)
    }
}.GetNewClosure()