PSColorText.psm1

#requires -Version 3

function Write-ColorText
{
    <#
            .SYNOPSIS
            Writes text to the console using tags in the string itself
            to indicate the output color of the text. Can replace
            Write-Host cmdlet.
 
            .DESCRIPTION
            This script allows you to use custom markup to more easily
            write multi-colored text to the console. Can replace
            Write-Host cmdlet.
 
            It uses the general format of:
         
            !(foreground,background)
       
            to define a color setting.
 
            both background and foreground can be omitted, but the comma
            is required if you specify a background color.
            The following are all valid:
 
            !(red)
            !(,red)
            !(blue,)
            !(yellow,black)
 
            If you don't specify a color it will continue using the current
            color. If you specify "*" as a color it will revert to the default
            color.
 
            You can escape the markup using an additional '!':
 
            !!(red)
 
            .PARAMETER String
            The string to write out.
 
            .PARAMETER NoColor
            Disable color output.
 
            .PARAMETER NoNewLine
            Do not append a newline after writing out text.
 
            .PARAMETER ForegroundColor
            Initial Foreground color.
 
            .PARAMETER BackgroundColor
            Initial Background color.
 
            .EXAMPLE
            PS C:\> Write-ColorText "This is a test !(gray)[ !(red)fail!(gray) ]"
 
            .EXAMPLE
            PS C:\> Write-ColorText "This is a test !(gray)[!(black,green) fail !(gray,*)]"
 
            .INPUTS
            System.String
 
            .OUTPUTS
            None
    #>


    [CmdletBinding()]
    param(
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [System.String]
        $String,

        [ConsoleColor]
        $ForegroundColor,

        [ConsoleColor]
        $BackgroundColor,

        [switch]
        $NoNewline,

        [switch]
        $NoColor
    )
    
    begin 
    {
        function Test-Values
        {
            $matches = @($script:regex.Matches($String))
      
            # do some validation
            foreach ($match in $matches)
            {
                $foreground = $match.Groups['foreground'].Value.ToLower()
                $background = $match.Groups['background'].Value.ToLower()

                $success = $script:colors.Contains($foreground) -or $foreground -eq '*' -or $foreground -eq [string]::Empty
                if (!$success)
                {
                    $errorString = "Unrecognised Color: '{0}' : char: {1}`n" -f $foreground, $match.Index
                    $errorString += $String + "`n"
                    $errorString += (' ' * $match.Index) + ('~' * $match.Length)
                    throw $errorString
                }

                $success = $script:colors.Contains($background) -or $background -eq '*' -or $background -eq [string]::Empty
                if (!$success)
                {
                    $errorString = "Unrecognised Color: '{0}' : char: {1}`n" -f $background, $match.Index
                    $errorString += $String + "`n"
                    $errorString += (' ' * $match.Index) + ('~' * $match.Length)
                    throw $errorString
                }
            }
        } # function Test-Values

        function Resolve-Color
        {
            param($value, $bg = $false)

            if ($value -eq '*')
            {
                if ($bg)
                {
                    return $script:initialBackground
                }
                else
                {
                    return $script:initialForeground
                }
            }
            elseif ($value -eq [string]::Empty)
            {
                if ($bg)
                {
                    return $script:currentBackground
                }
                else
                {
                    return $script:currentForeground
                }
            }
      
            return $value
        } # Resolve-Color

        try 
        {
            $script:regex = [regex] '(?im)(?<!!)!\((?<foreground>(?:\w*|\*))(?:,(?<background>(?:\w*|\*)))?\)'
            $script:initialForeground = $host.UI.RawUI.ForegroundColor
            $script:initialBackground = $host.UI.RawUI.BackgroundColor

            $script:colors = [ConsoleColor].GetEnumNames() | ForEach-Object -Process {
                $_.ToLower()
            }
        } 
        catch 
        {
            throw
        }
    }
    process 
    {
        try 
        {
            if ($ForegroundColor)
            {
                $script:currentForeground = $ForegroundColor
            }
            else
            {
                $script:currentForeground = $script:initialForeground
            }

            if ($BackgroundColor)
            {
                $script:currentBackground = $BackgroundColor
            }
            else
            {
                $script:currentBackground = $script:initialBackground
            }

            Test-Values

            $matches = @($script:regex.Matches($String))
            $lastPos = 0

            foreach ($match in $matches)
            {
                if ($NoColor -or ($script:currentForeground.ToString() -eq '-1' -and $script:currentBackground.ToString() -eq '-1'))
                {
                    Write-Host -Object $String.Substring($lastPos, $match.Index - $lastPos) -NoNewline
                }
                elseif ($script:currentForeground.ToString() -eq '-1')
                {
                    Write-Host -Object $String.Substring($lastPos, $match.Index - $lastPos) -NoNewline -BackgroundColor $script:currentBackground
                }
                elseif ($script:currentBackground.ToString() -eq '-1')
                {
                    Write-Host -Object $String.Substring($lastPos, $match.Index - $lastPos) -NoNewline -ForegroundColor $script:currentForeground
                }
                else
                {
                    Write-Host -Object $String.Substring($lastPos, $match.Index - $lastPos) -NoNewline -BackgroundColor $script:currentBackground -ForegroundColor $script:currentForeground
                }

                $lastPos = $match.Index + $match.Length 
                $script:currentForeground = Resolve-Color $match.Groups['foreground'].Value $false
                $script:currentBackground = Resolve-Color $match.Groups['background'].Value $true
            }

            if ($NoColor -or ($script:currentForeground.ToString() -eq '-1' -and $script:currentBackground.ToString() -eq '-1'))
            {
                Write-Host -Object $String.Substring($lastPos) -NoNewline
            }
            elseif ($script:currentForeground.ToString() -eq '-1')
            {
                Write-Host -Object $String.Substring($lastPos) -BackgroundColor $script:currentBackground -NoNewline
            }
            elseif ($script:currentBackground.ToString() -eq '-1')
            {
                Write-Host -Object $String.Substring($lastPos) -ForegroundColor $script:currentForeground -NoNewline
            }
            else
            {
                Write-Host -Object $String.Substring($lastPos) -BackgroundColor $script:currentBackground -ForegroundColor $script:currentForeground -NoNewline
            }

            if (!$NoNewline.IsPresent)
            {
                Write-Host -Object ''
            }
        } 
        catch 
        {
            throw
        }
    }
    end 
    {
        try 
        {
            $host.UI.RawUI.ForegroundColor = $script:initialForeground
            $host.UI.RawUI.BackgroundColor = $script:initialBackground
        } 
        catch 
        {
            throw
        }
    }
}

Function Write-ColorLine
{
    <#
    .SYNOPSIS
        Utility to assemble a single line of colored text
        from component items.
    .DESCRIPTION
        Long description
    .EXAMPLE
    .INPUTS
        ColorLine Item(s)
        cf. New-ColorLineItem
    .OUTPUTS
    .NOTES
        A "powerline compatible" font is required to really
        benefit from this function.
    #>

    Param(
        [Parameter(Mandatory=$true)]
        [psobject[]]$items,
        [ConsoleColor]$defaultBackgroundColor = 'black',
        [Switch]$NoNewLine
    )
        
    $sym = ''
    $back = $defaultBackgroundColor
        
    foreach ($item in $items)
    {
        $fore = $back
        $back = $item.bg
        Write-ColorText "!($fore,$back)$sym" -NoNewLine
        $fore = $item.fg
        $back = $item.bg
        Write-ColorText ("!($fore,$back)" + $item.text) -NoNewline
    }
    Write-ColorText "!($back,$defaultBackgroundColor)$sym" -NoNewLine:$NoNewLine
}

Function New-ColorLineItem
{
    <#
    .SYNOPSIS
        Utility to create a ColorLineItem custom object.
    .DESCRIPTION
        Easily create a color line obkect.
        These objects have the structure
        @{ bg, fg, text }
        where bg is the background color
        fg is the foreground color
        and text is the display text.
    .EXAMPLE
        C:\PS> <example usage>
        Explanation of what the example does
    .INPUTS
    .OUTPUTS
        PsCustomObject
    #>

    Param(
        [Parameter(Mandatory=$true)]
        [Alias('fg')]
        [ConsoleColor]$ForegroundColor,
            
        [Parameter(Mandatory=$true)]
        [Alias('bg')]
        [ConsoleColor]$BackgroundColor,
        
        [Parameter(Mandatory=$true)]
        [string]$Text
    )
        
    [PSCustomObject]@{fg = $ForegroundColor; bg = $BackgroundColor; text = $text};
}

Export-ModuleMember Write-ColorText, Write-ColorLine, New-ColorLineItem