commands.ps1


<#
    .SYNOPSIS
        Retrieves the border symbols for a specified border type.
         
    .DESCRIPTION
        The Get-BorderSymbol function returns a hashtable containing the symbols used for the top-left, top-right, bottom-left, bottom-right corners,
        as well as the spacers for the top, bottom, left, and right sides of a border. The border type is specified using the BorderType enum.
         
    .PARAMETER BorderType
        Specifies the type of border for which to retrieve the symbols. The parameter is mandatory and must be a valid value from the BorderType enum.
         
    .OUTPUTS
        System.Collections.Hashtable
        A hashtable containing the border symbols.
         
    .EXAMPLE
        PS C:\> Get-BorderSymbol -BorderType Box
         
        Returns the border symbols for the "Box" border type.
         
    .EXAMPLE
        PS C:\> Get-BorderSymbol -BorderType Asterisk
         
        Returns the border symbols for the "Asterisk" border type.
         
    .EXAMPLE
        PS C:\> Get-BorderSymbol -BorderType DoubleBox
         
        Returns the border symbols for the "DoubleBox" border type.
         
    .NOTES
        The function uses the BorderType enum for input validation, ensuring only valid border types are accepted.
#>

function Get-BorderSymbol {
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory = $true)]
        [BorderType] # Use the enum for validation
        $BorderType
    )

    switch ($BorderType) {
        'Asterisk' { 
            return @{
                TopLeft = "*"; TopRight = "*"; BottomLeft = "*"; BottomRight = "*";
                TopSpacer = "*"; BottomSpacer = "*"; LeftSpacer = "*"; RightSpacer = "*"
            }
        }
        'Hash' { 
            return @{
                TopLeft = "#"; TopRight = "#"; BottomLeft = "#"; BottomRight = "#";
                TopSpacer = "#"; BottomSpacer = "#"; LeftSpacer = "#"; RightSpacer = "#"
            }
        }
        'Plus' { 
            return @{
                TopLeft = "+"; TopRight = "+"; BottomLeft = "+"; BottomRight = "+";
                TopSpacer = "+"; BottomSpacer = "+"; LeftSpacer = "+"; RightSpacer = "+"
            }
        }
        'Box' {
            return @{
                TopLeft = "┌"; TopRight = "┐"; BottomLeft = "└"; BottomRight = "┘";
                TopSpacer = "─"; BottomSpacer = "─"; LeftSpacer = "│"; RightSpacer = "│"
            }
        }
        'TwoLinesFrame' {
            return @{
                TopLeft = "/"; TopRight = "\"; BottomLeft = "\"; BottomRight = "/";
                TopSpacer = "="; BottomSpacer = "="; LeftSpacer = "||"; RightSpacer = "||"
            }
        }
        'DoubleBox' {
            return @{
                TopLeft = "╔"; TopRight = "╗"; BottomLeft = "╚"; BottomRight = "╝";
                TopSpacer = "═"; BottomSpacer = "═"; LeftSpacer = "║"; RightSpacer = "║"
            }
        }
        'DoubleCorners' {
            return @{
                TopLeft = "╔"; TopRight = "╗"; BottomLeft = "╚"; BottomRight = "╝";
                TopSpacer = "─"; BottomSpacer = "─"; LeftSpacer = "│"; RightSpacer = "│"
            }
        }
        'BubbleBorder' {
            return @{
                TopLeft = "(_)"; TopRight = "(_)"; BottomLeft = "(_)"; BottomRight = "(_)";
                TopSpacer = "(_)"; BottomSpacer = "(_)"; LeftSpacer = "(_)"; RightSpacer = "(_)"
            }
        }
        'BoxBorder' {
            return @{
                TopLeft = "|_|"; TopRight = "|_|"; BottomLeft = "|_|"; BottomRight = "|_|";
                TopSpacer = "|_|"; BottomSpacer = "|_|"; LeftSpacer = "|_|"; RightSpacer = "|_|"
            }
        }
        'Dots' { 
            return @{
                TopLeft = "."; TopRight = "."; BottomLeft = ":"; BottomRight = ":";
                TopSpacer = "."; BottomSpacer = "."; LeftSpacer = ":"; RightSpacer = ":"
            }
        }
        'None' { 
            return @{
                TopLeft = ""; TopRight = ""; BottomLeft = ""; BottomRight = "";
                TopSpacer = ""; BottomSpacer = ""; LeftSpacer = ""; RightSpacer = ""
            }
        }
    }
}


<#
    .SYNOPSIS
        Handle time measurement
         
    .DESCRIPTION
        Handle time measurement from when a cmdlet / function starts and ends
         
        Will write the output to the verbose stream (Write-PSFMessage -Level Verbose)
         
    .PARAMETER Start
        Switch to instruct the cmdlet that a start time registration needs to take place
         
    .PARAMETER End
        Switch to instruct the cmdlet that a time registration has come to its end and it needs to do the calculation
         
    .EXAMPLE
        PS C:\> Invoke-TimeSignal -Start
         
        This will start the time measurement for any given cmdlet / function
         
    .EXAMPLE
        PS C:\> Invoke-TimeSignal -End
         
        This will end the time measurement for any given cmdlet / function.
        The output will go into the verbose stream.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Invoke-TimeSignal {
    [CmdletBinding(DefaultParameterSetName = 'Start')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Start', Position = 1 )]
        [switch] $Start,
        
        [Parameter(Mandatory = $True, ParameterSetName = 'End', Position = 2 )]
        [switch] $End
    )

    $Time = (Get-Date)

    $Command = (Get-PSCallStack)[1].Command

    if ($Start) {
        if ($Script:TimeSignals.ContainsKey($Command)) {
            Write-PSFMessage -Level Verbose -Message "The command '$Command' was already taking part in time measurement. The entry has been update with current date and time."
            $Script:TimeSignals[$Command] = $Time
        }
        else {
            $Script:TimeSignals.Add($Command, $Time)
        }
    }
    else {
        if ($Script:TimeSignals.ContainsKey($Command)) {
            $TimeSpan = New-TimeSpan -End $Time -Start (($Script:TimeSignals)[$Command])

            Write-PSFMessage -Level Verbose -Message "Total time spent inside the function was $TimeSpan" -Target $TimeSpan -FunctionName $Command -Tag "TimeSignal"
            $null = $Script:TimeSignals.Remove($Command)
        }
        else {
            Write-PSFMessage -Level Verbose -Message "The command '$Command' was never started to take part in time measurement."
        }
    }
}


<#
    .SYNOPSIS
        Converts a given text to ASCII art using a specified font and optional border style with color support.
         
    .DESCRIPTION
        The Convert-FSCPSTextToAscii function takes a string input and converts it into ASCII art using the specified font.
        Optionally, a border style and colors for the text and border can be applied. The function supports various fonts
        and border styles, allowing for customization of the output. The text and border colors can also be specified
        to enhance the visual appearance of the ASCII art.
         
        This function is ideal for creating visually appealing text banners or decorations in scripts, logs, or console outputs.
         
    .PARAMETER Text
        The text to be converted into ASCII art. This parameter is mandatory.
         
    .PARAMETER Font
        The font to be used for generating the ASCII art. This parameter is mandatory.
         
    .PARAMETER BorderType
        The type of border to apply around the ASCII art. This parameter is optional and defaults to 'None'.
         
    .PARAMETER TextColor
        The color to use for the ASCII art text. This parameter is optional and defaults to 'White'.
         
    .PARAMETER BorderColor
        The color to use for the border. This parameter is optional and defaults to 'Gray'.
         
    .PARAMETER Timestamp
        Enables or disables detailed information (timestamp) in the output. Defaults to `$false`.
         
    .EXAMPLE
        PS C:\> Convert-FSCPSTextToAscii -Text "Hello" -Font "Standard" -BorderType Asterisk -TextColor Yellow -BorderColor Green -DetailedInfo $true
         
        Converts the text "Hello" into ASCII art using the "Standard" font, surrounds it with an asterisk border, and applies yellow text with a green border. Detailed information (timestamp and function name) is enabled.
    .NOTES
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Convert-FSCPSTextToAscii {
    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Text,

        [Parameter(Mandatory=$true)]
        [FontType]$Font,
    
        [Parameter(Mandatory=$false)]
        [BorderType]$BorderType = [BorderType]::None,

        [Parameter(Mandatory=$false)]
        [ValidateSet("Black", "DarkBlue", "DarkGreen", "DarkCyan", "DarkRed", "DarkMagenta", "DarkYellow", "Gray", "DarkGray", "Blue", "Green", "Cyan", "Red", "Magenta", "Yellow", "White")]
        [string]$TextColor = "White",

        [Parameter(Mandatory=$false)]
        [ValidateSet("Black", "DarkBlue", "DarkGreen", "DarkCyan", "DarkRed", "DarkMagenta", "DarkYellow", "Gray", "DarkGray", "Blue", "Green", "Cyan", "Red", "Magenta", "Yellow", "White")]
        [string]$BorderColor = "Gray",

        [Parameter(Mandatory=$false)]
        [switch]$Timestamp = $false
    )
    begin {
        Invoke-TimeSignal -Start
        # Save the current state of the PSFramework message style settings
        $originalTimestampSetting = (Get-PSFConfig -Module PSFramework -Name 'Message.Style.Timestamp').Value
        
        $originalFunctionNameSetting = (Get-PSFConfig -Module PSFramework -Name 'Message.Style.FunctionName').Value

        # Apply the detailed info setting
        if ($Timestamp) {
            Set-PSFConfig -Module PSFramework -Name 'Message.Style.Timestamp' -Value $true
            Set-PSFConfig -Module PSFramework -Name 'Message.Style.FunctionName' -Value $false
        } else {
            Set-PSFConfig -Module PSFramework -Name 'Message.Style.Timestamp' -Value $false
            Set-PSFConfig -Module PSFramework -Name 'Message.Style.FunctionName' -Value $false
        }

        $border = Get-BorderSymbol -BorderType $BorderType

        $fontDirectory = "$ModuleRoot\internal\misc\Fonts"
        $fontFilePath = Join-Path $fontDirectory "$Font.flf"
        
        # Load .flf file lines
        $flfLines = (Get-Content -Path $fontFilePath -Raw -ErrorAction Stop -Encoding UTF8) -split "`r?`n"
    
        # Parse metadata from the first line
        $headerParts = $flfLines[0].Split(' ')
        $charHeight  = [int]$headerParts[1]
        $commentLines= [int]$headerParts[5]
    
        # Skip header + comment lines
        $startIndex = 1 + $commentLines
    
        # Build a dictionary of ASCII art for each printable character
        $charMap = @{ }
        $asciiCode = 32  # Start from space (ASCII 32)
    }
    PROCESS {

        $linePos = $startIndex
        while ($linePos -lt $flfLines.Count) {
            $charLines = @()
            for ($i = 0; $i -lt $charHeight; $i++) {
                if ($linePos -ge $flfLines.Count) { break }
                $charLines += $flfLines[$linePos]
                $linePos++
            }
            $charMap[$asciiCode] = $charLines
            $asciiCode++
    
            # Stop if we've passed typical ASCII printable range
            if ($asciiCode -gt 126) { break }
        }
    
        # Generate ASCII art lines for input text
        $outputLines = New-Object System.Collections.Generic.List[string]
        for ($row = 0; $row -lt $charHeight; $row++) {
            $rowBuilder = " "
            foreach ($c in $Text.ToCharArray()) {
                $charCode = [int][char]$c
                if ($charMap.ContainsKey($charCode)) {
                    $rowText = $charMap[$charCode][$row]
                    # Logic to handle '@' replacements
                    if ($rowText -eq "@") {
                        $rowText = " "
                    } elseif ($rowText.EndsWith("@")) {
                        $rowText = $rowText.TrimEnd("@")
                    }
    
                    # Logic to handle '$' replacements
                    if ($rowText -eq "$") {
                        $rowText = " "
                    } elseif ($rowText.EndsWith("$")) {
                        $rowText = $rowText.TrimEnd("$")
                    }
                    $rowBuilder += $rowText
                }
                else {
                    $rowBuilder += "?"  # fallback if not in font map
                }
            }
            $rowBuilder += " "
            $outputLines.Add($rowBuilder)
        }
    
        if ($outputLines[-1].Replace(" ", "").Length -eq 0) {
            $outputLines.RemoveAt($outputLines.Count - 1)  # Remove last line of whitespace
        }
    
        # Determine max line length
        $maxLen = ($outputLines | ForEach-Object { $_.Length } | Measure-Object -Maximum).Maximum
        
        # Calculate the total width of the content including side borders
        $totalWidth = $maxLen
        
        if ($BorderType -ne [BorderType]::None) {
            # Repeat spacer patterns to match the required total width
            $topBorder = $border.TopSpacer * ([math]::Ceiling($totalWidth / $border.TopSpacer.Length))
            $topBorder = $topBorder.Substring(0, $topBorder.Length)  # Trim to exact length
            
            $bottomBorder = $border.BottomSpacer * [math]::Ceiling($totalWidth / $border.BottomSpacer.Length)
            $bottomBorder = $bottomBorder.Substring(0, $bottomBorder.Length)  # Trim to exact length
            
            # Draw top border
            $topBorderLine = $border.TopLeft + $topBorder + $border.TopRight
            Write-PSFMessage -Level Important -Message ('<c="'+$BorderColor.ToLower()+'">' + $topBorderLine + "</c>")
            
            # Draw lines, padding each to the max length
            foreach ($line in $outputLines) {
                $curLineLength = $line.Length + $border.LeftSpacer.Length + $border.RightSpacer.Length 
                $curAdvDifference = ($topBorderLine.Length - ($curLineLength))
                $padded = $line.PadRight($maxLen + $curAdvDifference)
                Write-PSFMessage -Level Important -Message ('<c="'+$BorderColor.ToLower()+'">' + "$($border.LeftSpacer)" + "</c>" + '<c="'+$TextColor.ToLower()+'">' + $padded +"</c>" + '<c="'+$BorderColor.ToLower()+'">' + "$($border.RightSpacer)" + "</c>")
            }
            
            # Draw bottom border
            Write-PSFMessage -Level Host -Message  ('<c="'+$BorderColor.ToLower()+'">' + $border.BottomLeft + $bottomBorder + $border.BottomRight  + "</c>")
        }
        else {
            # Draw lines without borders
            foreach ($line in $outputLines) {
                Write-PSFMessage -Level Host -Message  ('<c="'+$TextColor.ToLower()+'">' + $line + "</c>") 
            }
        }    
    }
    END {
        # Restore the original state of the PSFramework message style settings
        Set-PSFConfig -Module PSFramework -Name 'Message.Style.Timestamp' -Value $originalTimestampSetting
        Set-PSFConfig -Module PSFramework -Name 'Message.Style.FunctionName' -Value $originalFunctionNameSetting
        Invoke-TimeSignal -End
    }    
}