src/user-interface.ps1

function Invoke-Input {
    <#
    .SYNOPSIS
    A fancy Read-Host replacement meant to be used to make CLI applications.
    .PARAMETER Secret
    Displayed characters are replaced with asterisks
    .PARAMETER Number
    Switch to designate input is numerical
    .EXAMPLE
    $fullname = input 'Full Name?'
    $username = input 'Username?' -MaxLength 10 -Indent 4
    $age = input 'Age?' -Number -MaxLength 2 -Indent 4
    $pass = input 'Password?' -Secret -Indent 4
    .EXAMPLE
    $word = input 'Favorite Saiya-jin?' -Indent 4 -Autocomplete -Choices `
    @(
        'Goku'
        'Gohan'
        'Goten'
        'Vegeta'
        'Trunks'
    )
    Autocomplete will make suggestions. Press tab once to select suggestion, press tab again to cycle through matches.
    .EXAMPLE
    # Leverage autocomplete to input a folder name
    Invoke-Input 'Folder name?' -Autocomplete -Choices (Get-ChildItem -Directory | Select-Object -ExpandProperty Name)
    .EXAMPLE
    # Input labels can be customized with mustache color helpers
    $name = input 'What is your {{#blue name}}?'
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', 'global:PreviousRegularExpression')]
    [CmdletBinding()]
    [Alias('input')]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $True)]
        [String] $LabelText = 'input:',
        [Switch] $Secret,
        [Switch] $Number,
        [Switch] $Autocomplete,
        [Array] $Choices,
        [Int] $Indent,
        [Int] $MaxLength = 0
    )
    Write-Label -Text $LabelText -Indent $Indent
    $Global:PreviousRegularExpression = $Null
    $Result = ''
    $CurrentIndex = 0
    $AutocompleteMatches = @()
    $StartPosition = [Console]::CursorLeft
    function Format-Output {
        Param(
            [Parameter(Mandatory = $True, Position = 0)]
            [String] $Value
        )
        if ($Secret) {
            '*' * $Value.Length
        } else {
            $Value
        }
    }
    function Invoke-OutputDraw {
        Param(
            [Parameter(Mandatory = $True, Position = 0)]
            [String] $Output,
            [Int] $Left = 0
        )
        [Console]::SetCursorPosition($StartPosition, [Console]::CursorTop)
        if ($MaxLength -gt 0 -and $Output.Length -gt $MaxLength) {
            Write-Color $Output.Substring(0, $MaxLength) -NoNewLine
            Write-Color $Output.Substring($MaxLength, $Output.Length - $MaxLength) -NoNewLine -Red
        } else {
            Write-Color $Output -NoNewLine
            if ($Autocomplete) {
                Update-Autocomplete -Output $Output
            }
        }
        [Console]::SetCursorPosition($Left + 1, [Console]::CursorTop)
    }
    function Update-Autocomplete {
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', 'global:PreviousRegularExpression')]
        Param(
            [AllowEmptyString()]
            [String] $Output
        )
        $Global:PreviousRegularExpression = "^${Output}"
        $AutocompleteMatches = $Choices | Where-Object { $_ -match $Global:PreviousRegularExpression }
        if ($Null -eq $AutocompleteMatches -or $Output.Length -eq 0) {
            $Left = [Console]::CursorLeft
            [Console]::SetCursorPosition($Left, [Console]::CursorTop)
            Write-Color (' ' * 30) -NoNewLine
            [Console]::SetCursorPosition($Left, [Console]::CursorTop)
        } else {
            if ($AutocompleteMatches -is [String]) {
                $BestMatch = $AutocompleteMatches
            } else {
                $BestMatch = $AutocompleteMatches[0]
            }
            $Left = [Console]::CursorLeft
            [Console]::SetCursorPosition($StartPosition + $Output.Length, [Console]::CursorTop)
            Write-Color $BestMatch.Substring($Output.Length) -NoNewLine -Green
            Write-Color (' ' * 30) -NoNewLine
            [Console]::SetCursorPosition($Left, [Console]::CursorTop)
        }
    }
    do {
        $KeyInfo = [Console]::ReadKey($True)
        $KeyChar = $KeyInfo.KeyChar
        switch ($KeyInfo.Key) {
            'Backspace' {
                if (-not $Secret) {
                    $Left = [Console]::CursorLeft
                    if ($Left -gt $StartPosition) {
                        [Console]::SetCursorPosition($StartPosition, [Console]::CursorTop)
                        $Updated = $Result | Remove-Character -At ($Left - $StartPosition - 1)
                        $Result = $Updated
                        if ($MaxLength -eq 0) {
                            Write-Color $Updated -NoNewLine
                            if ($Autocomplete) {
                                Update-Autocomplete -Output $Updated
                            } else {
                                Write-Color ' ' -NoNewLine
                            }
                        } else {
                            [Console]::SetCursorPosition($StartPosition, [Console]::CursorTop)
                            if ($Result.Length -le $MaxLength) {
                                Write-Color "$Updated " -NoNewLine
                            } else {
                                Write-Color $Updated.Substring(0, $MaxLength) -NoNewLine
                                Write-Color ($Updated.Substring($MaxLength, $Updated.Length - $MaxLength) + ' ') -NoNewLine -Red
                            }
                        }
                        [Console]::SetCursorPosition([Math]::Max(0, $Left - 1), [Console]::CursorTop)
                    }
                }
            }
            'Delete' {
                if (-not $Secret) {
                    $Left = [Console]::CursorLeft
                    [Console]::SetCursorPosition($StartPosition, [Console]::CursorTop)
                    $Updated = $Result | Remove-Character -At ($Left - $StartPosition)
                    $Result = $Updated
                    if ($MaxLength -eq 0) {
                        Write-Color "$Updated " -NoNewLine
                    } else {
                        [Console]::SetCursorPosition($StartPosition, [Console]::CursorTop)
                        if ($Result.Length -le $MaxLength) {
                            Write-Color "$Updated " -NoNewLine
                        } else {
                            Write-Color $Updated.Substring(0, $MaxLength) -NoNewLine
                            Write-Color ($Updated.Substring($MaxLength, $Updated.Length - $MaxLength) + ' ') -NoNewLine -Red
                        }
                    }
                    if ($Autocomplete) {
                        Update-Autocomplete -Output $Updated
                    }
                    [Console]::SetCursorPosition([Math]::Max(0, $Left), [Console]::CursorTop)
                }
            }
            'DownArrow' {
                if ($Number) {
                    $Value = ($Result -as [Int]) - 1
                    if (($MaxLength -eq 0) -or ($MaxLength -gt 0 -and $Value -gt (-1 * [Math]::Pow(10, $MaxLength)))) {
                        $Left = [Console]::CursorLeft
                        $Result = "$Value"
                        [Console]::SetCursorPosition($StartPosition, [Console]::CursorTop)
                        Write-Color $Result -NoNewLine
                        [Console]::SetCursorPosition($Left, [Console]::CursorTop)
                    }
                }
            }
            'Enter' {
                # Do nothing
            }
            'LeftArrow' {
                if (-not $Secret) {
                    $Left = [Console]::CursorLeft
                    if ($Left -gt $StartPosition) {
                        [Console]::SetCursorPosition($Left - 1, [Console]::CursorTop)
                    }
                }
            }
            'RightArrow' {
                if (-not $Secret) {
                    $Left = [Console]::CursorLeft
                    if ($Left -lt ($StartPosition + $Result.Length)) {
                        [Console]::SetCursorPosition($Left + 1, [Console]::CursorTop)
                    }
                }
            }
            'Tab' {
                if ($Autocomplete -and $Result.Length -gt 0 -and -not ($Number -or $Secret) -and $Null -ne $AutocompleteMatches) {
                    $AutocompleteMatches = $Choices | Where-Object { $_ -match $Global:PreviousRegularExpression }
                    [Console]::SetCursorPosition($StartPosition, [Console]::CursorTop)
                    if ($AutocompleteMatches -is [String]) {
                        $Result = $AutocompleteMatches
                    } else {
                        $CurrentMatch = $AutocompleteMatches[$CurrentIndex]
                        if ($Result -eq $PreviousMatch) {
                            $Result = $PreviousSearch[$CurrentIndex]
                        } else {
                            $Result = $CurrentMatch
                            $PreviousMatch = $CurrentMatch
                            $PreviousSearch = $AutocompleteMatches
                        }
                        $CurrentIndex = ($CurrentIndex + 1) % $AutocompleteMatches.Length
                    }
                    Write-Color "$Result $(' ' * 30)" -NoNewLine -Green
                    [Console]::SetCursorPosition($StartPosition + $Result.Length, [Console]::CursorTop)
                }
            }
            'UpArrow' {
                if ($Number) {
                    $Value = ($Result -as [Int]) + 1
                    if (($MaxLength -eq 0) -or ($MaxLength -gt 0 -and $Value -lt [Math]::Pow(10, $MaxLength))) {
                        $Left = [Console]::CursorLeft
                        $Result = "$Value"
                        [Console]::SetCursorPosition($StartPosition, [Console]::CursorTop)
                        Write-Color "$Result " -NoNewLine
                        [Console]::SetCursorPosition($Left, [Console]::CursorTop)
                    }
                }
            }
            Default {
                $Left = [Console]::CursorLeft
                $OnlyNumbers = [Regex]'^-?[0-9]*$'
                if ($Left -eq $StartPosition) {
                    # prepend character
                    if ($Number) {
                        if ($KeyChar -match $OnlyNumbers) {
                            $Result = "${KeyChar}$Result"
                            Invoke-OutputDraw -Output (Format-Output $Result) -Left $Left
                        }
                    } else {
                        $Result = "${KeyChar}$Result"
                        Invoke-OutputDraw -Output (Format-Output $Result) -Left $Left
                    }
                } elseif ($Left -gt $StartPosition -and $Left -lt ($StartPosition + $Result.Length)) {
                    # insert character
                    if ($Number) {
                        if ($KeyChar -match $OnlyNumbers) {
                            $Result = $Result | Invoke-InsertString $KeyChar -At ($Left - $StartPosition)
                            Invoke-OutputDraw -Output $Result -Left $Left
                        }
                    } else {
                        $Result = $Result | Invoke-InsertString $KeyChar -At ($Left - $StartPosition)
                        Invoke-OutputDraw -Output $Result -Left $Left
                    }
                } else {
                    # append character
                    if ($Number) {
                        if ($KeyChar -match $OnlyNumbers) {
                            $Result += $KeyChar
                            $ShouldHighlight = ($MaxLength -gt 0) -and [Console]::CursorLeft -gt ($StartPosition + $MaxLength - 1)
                            Write-Color (Format-Output $KeyChar) -NoNewLine -Red:$ShouldHighlight
                            if ($Autocomplete) {
                                Update-Autocomplete -Output ($Result -as [String])
                            }
                        }
                    } else {
                        $Result += $KeyChar
                        $ShouldHighlight = ($MaxLength -gt 0) -and [Console]::CursorLeft -gt ($StartPosition + $MaxLength - 1)
                        Write-Color (Format-Output $KeyChar) -NoNewLine -Red:$ShouldHighlight
                        if ($Autocomplete) {
                            Update-Autocomplete -Output ($Result -as [String])
                        }
                    }
                }
            }
        }
    } until ($KeyInfo.Key -eq 'Enter' -or $KeyInfo.Key -eq 'Escape')
    Write-Color ''
    if ($KeyInfo.Key -ne 'Escape') {
        if ($Number) {
            $Result -as [Int]
        } else {
            if ($MaxLength -gt 0) {
                $Result.Substring(0, [Math]::Min($Result.Length, $MaxLength))
            } else {
                $Result
            }
        }
    } else {
        $Null
    }
}
function Invoke-Menu {
    <#
    .SYNOPSIS
    Create interactive single, multi-select, or single-select list menu.
    Controls:
    - Select item with ENTER key
    - Move up with UP arrow key
    - Move DOWN with down arrow key or TAB key
    - Multi-select and single-select with SPACE key
    - Next page with RIGHT arrow key (see Limit help)
    - Previous page with LEFT arrow key (see Limit help)
    .PARAMETER ReturnIndex
    Return the index of the selected item within the array of items.
    Note: If ReturnIndex is used with pagination (see Limit help), the index within the visible items will be returned.
    .PARAMETER Limit
    Maximum number of items per page
    If Limit is greater than zero and less than the number of items, pagination will be activated with "Limit" number of items per page.
    Note: When Limit is larger than the number of menu items, the menu will behave as though no limit value was passed.
    .PARAMETER FolderContent
    Use this switch to populate the menu with folder contents of current directory (see examples)
    .EXAMPLE
    Invoke-Menu 'one','two','three'
    .EXAMPLE
    Invoke-Menu 'one','two','three' -HighlightColor Blue
    .EXAMPLE
    'one','two','three' | Invoke-Menu -MultiSelect -ReturnIndex | Sort-Object
    .EXAMPLE
    1,2,3,4,5 | menu
    .EXAMPLE
    # The SingleSelect switch allows for only one item to be selected at a time
    1..10 | menu -SingleSelect
    .EXAMPLE
    1..100 | menu -Limit 10
    .EXAMPLE
    # Open a folder via an interactive list menu populated with folder content
    Invoke-Menu -FolderContent | Invoke-Item
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'HighlightColor')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')]
    [CmdletBinding()]
    [Alias('menu')]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $True)]
        [Array] $Items,
        [Switch] $MultiSelect,
        [Switch] $SingleSelect,
        [String] $HighlightColor = 'Cyan',
        [Switch] $ReturnIndex = $False,
        [Switch] $FolderContent,
        [Int] $Limit = 0,
        [Int] $Indent = 0
    )
    Begin {
        function Invoke-MenuDraw {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function')]
            Param(
                [Array] $VisibleItems,
                [Int] $Position,
                [Int] $PageNumber,
                [Array] $Selection,
                [Switch] $MultiSelect,
                [Switch] $SingleSelect,
                [Switch] $ShowHeader,
                [Int] $Indent = 0
            )
            $Index = 0
            $LengthValues = $Items | ForEach-Object { $_.ToString().Length }
            $MaxLength = Get-Maximum $LengthValues
            $MinLength = Get-Minimum $LengthValues
            $Clear = ' ' | Invoke-Repeat -Times ($MaxLength - $MinLength) | Invoke-Reduce -Add
            $LeftPadding = ' ' | Invoke-Repeat -Times $Indent | Invoke-Reduce -Add
            if ($ShowHeader) {
                $TextLength = $TotalPages.ToString().Length
                $CurrentPage = ($PageNumber + 1).ToString().PadLeft($TextLength, '0')
                "${LeftPadding}<<prev {{#${HighlightColor} ${CurrentPage}}}/${TotalPages} next>>" | Write-Color -DarkGray
                $Clear | Write-Color -Cyan
            }
            foreach ($Item in $VisibleItems) {
                if ($Null -ne $Item) {
                    $ModularIndex = ($PageNumber * $Limit) + $Index
                    if ($MultiSelect) {
                        $Item = if ($Selection -contains $ModularIndex) { "[x] $Item$Clear" } else { "[ ] $Item$Clear" }
                    } else {
                        if ($SingleSelect) {
                            $Item = if ($Selection -contains $ModularIndex) { "(o) $Item$Clear" } else { "( ) $Item$Clear" }
                        }
                    }
                    $Parameters = if ($Index -eq $Position) { @{ Color = $HighlightColor } } else { @{} }
                    Write-Color "$LeftPadding $Item$Clear" @Parameters
                }
                $Index++
            }
        }
        function Update-MenuSelection {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'SingleSelect')]
            Param(
                [Int] $Position,
                [Int] $PageNumber,
                [Array] $Selection,
                [Switch] $MultiSelect,
                [Switch] $SingleSelect
            )
            $ModularPosition = (($PageNumber * $Limit) + $Position)
            if ($Selection -contains $ModularPosition) {
                $Result = $Selection | Where-Object { $_ -ne $ModularPosition }
            } else {
                if ($MultiSelect) {
                    $Selection += $ModularPosition
                } else {
                    $Selection = , $ModularPosition
                }
                $Result = $Selection
            }
            $Result
        }
        [Console]::CursorVisible = $False
        $Keycodes = @{
            enter = 13
            escape = 27
            left = 37
            right = 39
            space = 32
            tab = 9
            up = 38
            down = 40
        }
        $Keycode = 0
        $Position = 0
        $Selection = @()
    }
    End {
        if ($Input.Length -gt 0) {
            $Items = $Input
        }
        if ($FolderContent) {
            $Items = Get-ChildItem -Directory | Select-Object -ExpandProperty Name | ForEach-Object { "$_/" }
            $Items += (Get-ChildItem -File | Select-Object -ExpandProperty Name)
        }
        $PageNumber = 0
        $TotalPages = if ($Limit -eq 0) { 1 } else { [Math]::Ceiling($Items.Length / $Limit) }
        $ShouldPaginate = $Limit -in 1..($Items.Count - 1)
        if ($ShouldPaginate) {
            $ExtraItemCount = $Limit - ($Items.Count % $Limit)
            for ($Index = 0; $Index -lt $ExtraItemCount; $Index++) {
                $Items += '...'
            }
        }
        $VisibleItems = if ($ShouldPaginate) {
            $StartIndex = $PageNumber * $Limit
            $Items[$StartIndex..($StartIndex + $Limit - 1)]
        } else {
            $Items
        }
        [Console]::SetCursorPosition(0, [Console]::CursorTop)
        $Parameters = @{
            VisibleItems = $VisibleItems
            Position = $Position
            Selection = $Selection
            MultiSelect = $MultiSelect
            SingleSelect = $SingleSelect
            ShowHeader = $ShouldPaginate
            PageNumber = $PageNumber
            Indent = $Indent
        }
        Invoke-MenuDraw @Parameters
        while ($Keycode -notin $Keycodes.enter, $Keycodes.escape) {
            $Keycode = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').virtualkeycode
            switch ($Keycode) {
                $Keycodes.escape {
                    $Position = $Null
                }
                $Keycodes.space {
                    $Parameters = @{
                        Position = $Position
                        PageNumber = $PageNumber
                        Selection = $Selection
                        MultiSelect = $MultiSelect
                        SingleSelect = $SingleSelect
                    }
                    $Selection = Update-MenuSelection @Parameters
                }
                $Keycodes.tab {
                    $Position = ($Position + 1) % $VisibleItems.Length
                }
                $Keycodes.up {
                    if ($Limit -in 1..($Items.Count - 1) -and $TotalPages -gt 1) {
                        $StartIndex = $PageNumber * $Limit
                        $VisibleItems = $Items[$StartIndex..($StartIndex + $Limit - 1)]
                    }
                    $Position = (($Position - 1) + $VisibleItems.Length) % $VisibleItems.Length
                }
                $Keycodes.down {
                    if ($Limit -in 1..($Items.Count - 1) -and $TotalPages -gt 1) {
                        $StartIndex = $PageNumber * $Limit
                        $VisibleItems = $Items[$StartIndex..($StartIndex + $Limit - 1)]
                    }
                    $Position = ($Position + 1) % $VisibleItems.Length
                }
                $Keycodes.left {
                    if ($Limit -in 1..($Items.Count - 1) -and $TotalPages -gt 1) {
                        $PageNumber = (($PageNumber - 1) + $TotalPages) % $TotalPages
                        $StartIndex = $PageNumber * $Limit
                        $VisibleItems = $Items[$StartIndex..($StartIndex + $Limit - 1)]
                    }
                }
                $Keycodes.right {
                    if ($Limit -in 1..($Items.Count - 1) -and $TotalPages -gt 1) {
                        $PageNumber = ($PageNumber + 1) % $TotalPages
                        $StartIndex = $PageNumber * $Limit
                        $VisibleItems = $Items[$StartIndex..($StartIndex + $Limit - 1)]
                    }
                }
            }
            If ($Null -ne $Position) {
                $StartPosition = if ($ShouldPaginate) {
                    [Console]::CursorTop - $VisibleItems.Count - 2
                } else {
                    [Console]::CursorTop - $Items.Count
                }
                [Console]::SetCursorPosition(0, $StartPosition)
                $Parameters = @{
                    VisibleItems = $VisibleItems
                    Position = $Position
                    Selection = $Selection
                    MultiSelect = $MultiSelect
                    SingleSelect = $SingleSelect
                    ShowHeader = $ShouldPaginate
                    PageNumber = $PageNumber
                    Indent = $Indent
                }
                Invoke-MenuDraw @Parameters
            }
        }
        [Console]::CursorVisible = $True
        if ($ReturnIndex -eq $False -and $Null -ne $Position) {
            if ($MultiSelect -or $SingleSelect) {
                if ($Selection.Length -gt 0) {
                    return $VisibleItems[$Selection]
                } else {
                    return $Null
                }
            } else {
                return $VisibleItems[$Position]
            }
        } else {
            if ($MultiSelect -or $SingleSelect) {
                return $Selection
            } else {
                return $Position + ($PageNumber * $Limit)
            }
        }
    }
}
function Write-BarChart {
    <#
    .SYNOPSIS
    Function to create horizontal bar chart of passed data object
    .PARAMETER Width
    Maximum value used for data normization. Also corresponds to actual width of longest bar (in characters)
    .PARAMETER Alternate
    Alternate row color between light and dark.
    .PARAMETER ShowValues
    Whether or not to show data values to right of each bar
    .EXAMPLE
    @{red = 55; white = 30; blue = 200} | Write-BarChart -WithColor -ShowValues
    .EXAMPLE
    # Can be used with Write-Title to create goo looking reports in the terminal
    Write-Title 'Colors'
    @{red = 55; white = 30; blue = 200} | Write-BarChart -Alternate -ShowValues
    Write-Color ''
    .EXAMPLE
    # Easily display a bar chart of files using Invoke-Reduce
    Get-ChildItem -File | Invoke-Reduce -FileInfo | Write-BarChart -ShowValues -WithColor
    .EXAMPLE
    # Use a 2-column matrix as input (names must be numbers)
    1..8 | matrix 4,2 | Write-BarChart
    .EXAMPLE
    # Use an array of values as input - name, value, name, value, etc...
    'red', 55, 'white', 30, 'blue', 200 | Write-BarChart
 
    # or with zip
    @('red', 'white', 'blue'), @(55, 30, 200) | zip | Write-BarChart
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function')]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        $InputObject,
        [Int] $Width = 100,
        [Switch] $ShowValues,
        [Switch] $Alternate,
        [Switch] $WithColor
    )
    Begin {
        $Tee = ([Char]9508).ToString()
        $Marker = ([Char]9608).ToString()
        function Write-Bar {
            Param(
                [String] $Name,
                [Int] $Value,
                [Int] $Index,
                [String] $LongestName = '#',
                [Int] $LargestValue = 1
            )
            $NormalizedValue = ($Value / $LargestValue) * $Width
            $Bar = $Marker | Invoke-Repeat -Times $NormalizedValue | Invoke-Reduce -Add
            $ValueLabel = if ($ShowValues) { " $Value" } else { '' }
            $IsEven = ($Index % 2) -eq 0
            if ($WithColor) {
                $Color = @{
                    Cyan = $($IsEven -and $Alternate)
                    DarkCyan = $((-not $IsEven -and $Alternate) -or (-not $Alternate))
                }
            } else {
                $Color = @{
                    White = $($IsEven -and $Alternate)
                    Gray = $(-not $IsEven -and $Alternate)
                }
            }
            $PaddedName = $Name.PadLeft($LongestName.Length, ' ')
            " {{#white $PaddedName $Tee}}$Bar" | Write-Color @Color -NoNewLine
            $ValueLabel | Write-Color @Color
        }
    }
    End {
        $Index = 0
        if ($Input.Count -gt 1) {
            $Names, $Values = $Input | Invoke-Flatten | Invoke-Chunk -Size 2 | Invoke-Unzip
            $LongestName = $Names | Sort-Object { $_.ToString().Length } -Descending | Select-Object -First 1
            $LargestValue = $Values | Get-Maximum
            $Data = for ($Index = 0; $Index -lt $Names.Count; ++$Index) {
                @{
                    Name = $Names[$Index]
                    Value = $Values[$Index]
                }
            }
            $Data | Sort-Object { $_.Value } | ForEach-Object {
                $Name = $_.Name
                $Value = $_.Value
                Write-Bar -Name $Name -Value $Value -Index ($Index++) -LongestName $LongestName -LargestValue $LargestValue
            }
        } else {
            switch ($InputObject.GetType().Name) {
                'Matrix' {
                    $Columns = $InputObject.Columns
                    $LongestName = $Columns[0].Real | Sort-Object { $_.ToString().Length } -Descending | Select-Object -First 1
                    $LargestValue = $Columns[1].Real | Get-Maximum
                    $InputObject.Rows | Sort-Object { $_[1].Real } | ForEach-Object {
                        $Name, $Value = $_.Real
                        Write-Bar -Name $Name -Value $Value -Index ($Index++) -LongestName $LongestName -LargestValue $LargestValue
                    }
                }
                Default {
                    $Data = [PSCustomObject]$InputObject
                    $LongestName = $Data.PSObject.Properties.Name | Sort-Object { $_.Length } -Descending | Select-Object -First 1
                    $LargestValue = $Data.PSObject.Properties | Select-Object -ExpandProperty Value | Sort-Object -Descending | Select-Object -First 1
                    $Data.PSObject.Properties | Sort-Object { $_.Value } | ForEach-Object {
                        $Name = $_.Name
                        $Value = $_.Value
                        Write-Bar -Name $Name -Value $Value -Index ($Index++) -LongestName $LongestName -LargestValue $LargestValue
                    }
                }
            }
        }
    }
}
function Write-Color {
    <#
    .SYNOPSIS
    Basically Write-Host with the ability to color parts of the output by using template strings
    .PARAMETER Color
    Performs the function Write-Host's -ForegroundColor. Useful for programmatically setting text color.
    .EXAMPLE
    '{{#red this will be red}} and {{#blue this will be blue}}' | Write-Color
    .EXAMPLE
    'You can color entire string using switch parameters' | Write-Color -Green
    .EXAMPLE
    'You can color entire string using Color parameter' | Write-Color -Color Green
    .EXAMPLE
    '{{#green Hello}} {{#blue {{ name }}}}' | New-Template -Data @{ name = 'World' } | Write-Color
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidUsingWriteHost', '', Scope = 'Function')]
    [CmdletBinding()]
    [OutputType([Void])]
    [OutputType([String])]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
        [AllowEmptyString()]
        [String] $Text,
        [ValidateSet('White', 'Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow')]
        [String] $Color,
        [Switch] $NoNewLine,
        [Switch] $Black,
        [Switch] $Blue,
        [Switch] $DarkBlue,
        [Switch] $DarkGreen,
        [Switch] $DarkCyan,
        [Switch] $DarkGray,
        [Switch] $DarkRed,
        [Switch] $DarkMagenta,
        [Switch] $DarkYellow,
        [Switch] $Cyan,
        [Switch] $Gray,
        [Switch] $Green,
        [Switch] $Red,
        [Switch] $Magenta,
        [Switch] $Yellow,
        [Switch] $White,
        [Switch] $PassThru
    )
    if ($Text.Length -eq 0) {
        Write-Host '' -NoNewline:$NoNewLine
    } else {
        if (-not $Color) {
            $Color = Find-FirstTrueVariable 'White', 'Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow'
        }
        $Position = 0
        $Text | Select-String -Pattern '(?<HELPER>){{#((?!}}).)*}}' -AllMatches | ForEach-Object Matches | ForEach-Object {
            Write-Host $Text.Substring($Position, $_.Index - $Position) -ForegroundColor $Color -NoNewline
            $HelperTemplate = $Text.Substring($_.Index, $_.Length)
            $Arr = $HelperTemplate | ForEach-Object { $_ -replace '{{#', '' } | ForEach-Object { $_ -replace '}}', '' } | ForEach-Object { $_ -split ' ' }
            Write-Host ($Arr[1..$Arr.Length] -join ' ') -ForegroundColor $Arr[0] -NoNewline
            $Position = $_.Index + $_.Length
        }
        if ($Position -lt $Text.Length) {
            Write-Host $Text.Substring($Position, $Text.Length - $Position) -ForegroundColor $Color -NoNewline:$NoNewLine
        }
    }
    if ($PassThru) {
        $Text
    }
}
function Write-Label {
    <#
    .SYNOPSIS
    Meant to be used with Invoke-Input or Invoke-Menu
    .EXAMPLE
    Write-Label 'Favorite number?' -NewLine
    $choice = menu @('one'; 'two'; 'three')
    .EXAMPLE
    # Labels can be customized using mustache color helper templates
    '{{#red Message? }}' | Write-Label -NewLine
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $True)]
        [String] $Text = 'label',
        [ValidateSet('White', 'Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow')]
        [String] $Color = 'Cyan',
        [Int] $Indent = 0,
        [Switch] $NewLine
    )
    Write-Color (' ' * $Indent) -NoNewLine
    Write-Color "$Text " -Color $Color -NoNewLine:$(-not $NewLine)
}
function Write-Title {
    <#
    .SYNOPSIS
    Function to print text with a border. Useful for displaying section titles for CLI apps.
    .PARAMETER Template
    Tells Write-Title to expect mustache color templates (see Get-Help Write-Color -Examples)
    .PARAMETER Fallback
    Use "+" and "-" to draw title border
    .PARAMETER Indent
    Add spaces to left of title box to align with input elements
    .EXAMPLE
    'Hello World' | Write-Title
    .EXAMPLE
    # Easily change border and title text color
    'Hello World' | Write-Title -Green
    .EXAMPLE
    # Change only the color of title text with -TextColor
    'Hello World' | Write-Title -Width 20 -TextColor Red
    .EXAMPLE
    # Titles can have set widths
    'Hello World' | Write-Title -Width 20
    .EXAMPLE
    # If your terminal does not have the fancy characters needed for a proper border, fallback to "+" and "-"
    'Hello World' | Write-Title -Fallback
    .EXAMPLE
    # Write-Title accepts same input as Write-Color and can be used to customize title text.
    '{{#magenta Hello}} World' | Write-Title -Template
    #>

    [CmdletBinding()]
    [Alias('title')]
    [OutputType([Void])]
    [OutputType([String])]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        [String] $Text,
        [String] $TextColor,
        [String] $SubText = '',
        [Switch] $Template,
        [Switch] $Fallback,
        [Switch] $Blue,
        [Switch] $Cyan,
        [Switch] $DarkBlue,
        [Switch] $DarkCyan,
        [Switch] $DarkGreen,
        [Switch] $DarkGray,
        [Switch] $DarkRed,
        [Switch] $DarkMagenta,
        [Switch] $DarkYellow,
        [Switch] $Green,
        [Switch] $Magenta,
        [Switch] $Red,
        [Switch] $White,
        [Switch] $Yellow,
        [Int] $Width,
        [Int] $Indent = 0,
        [Switch] $PassThru
    )
    if ($Template) {
        $TextLength = ($Text -replace '{{#\w*\s', '' | ForEach-Object { $_ -replace '}}', '' }).Length
    } else {
        $TextLength = $Text.Length
    }
    if ($Width -lt $TextLength) {
        $Width = $TextLength + 4
    }
    $Space = ' '
    if ($Fallback) {
        $TopLeft = '+'
        $TopEdge = '-'
        $TopRight = '+'
        $LeftEdge = $RightEdge = '|'
        $BottomLeft = '+'
        $BottomEdge = $TopEdge
        $BottomRight = '+'
    } else {
        $TopLeft = [Char]9484
        $TopEdge = [Char]9472
        $TopRight = [Char]9488
        $LeftEdge = $RightEdge = [Char]9474
        $BottomLeft = [Char]9492
        $BottomEdge = $TopEdge
        $BottomRight = [Char]9496
    }
    $PaddingLength = [Math]::Floor(($Width - $TextLength - 2) / 2)
    $Padding = $Space | Invoke-Repeat -Times $PaddingLength | Invoke-Reduce -Add
    $WidthInside = (2 * $PaddingLength) + $TextLength
    $BorderColor = @{
        Cyan = $Cyan
        Red = $Red
        Blue = $Blue
        Green = $Green
        Yellow = $Yellow
        Magenta = $Magenta
        White = $White
        DarkBlue = $DarkBlue
        DarkGreen = $DarkGreen
        DarkGray = $DarkGray
        DarkCyan = $DarkCyan
        DarkRed = $DarkRed
        DarkMagenta = $DarkMagenta
        DarkYellow = $DarkYellow
    }
    $TitleIndent = $Space | Invoke-Repeat -Times $Indent | Invoke-Reduce -Add
    $TitleTopLine = "$TopEdge" | Invoke-Repeat -Times ($WidthInside + 2) | Invoke-Reduce -Add
    $TitleBottomLine = "$BottomEdge" | Invoke-Repeat -Times ($WidthInside - $SubText.Length + 2) | Invoke-Reduce -Add
    Write-Color "$TitleIndent$TopLeft$TitleTopLine$TopRight" @BorderColor
    if ($TextColor) {
        Write-Color "$TitleIndent$LeftEdge$Padding{{#$TextColor $Text}}$Padding$RightEdge" @BorderColor
    } else {
        Write-Color "$TitleIndent$LeftEdge$Padding$Text$Padding$RightEdge" @BorderColor
    }
    Write-Color "$TitleIndent$BottomLeft$TitleBottomLine$SubText$BottomRight" @BorderColor
    if ($PassThru) {
        $Text
    }
}