Create-Menu.ps1

Function New-SelectionMenu {
    <#
        .SYNOPSIS
            Shows strings as a table to be selectable by navigating with arrow keys
     
        .DESCRIPTION
            Author: Nabil Redmann (BananaAcid)
            License: ISC
     
        .INPUTS
            array of strings
     
        .PARAMETER MenuOptions
            Value: <String[]>
             
            Takes an array with selections (must be more then one)
        .PARAMETER Title
            Value: <Null|ScriptBlock|String>
 
            Takes a string or a scriptblock, use $global:varname to link to Title, Footer or SelectionCallback (available vars: $Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $esc, $CL, $global:*)
        .PARAMETER Selected
            Value: <Null|Integer>
 
            Initial string to select
        .PARAMETER Footer
            Value: <Null|ScriptBlock|String>
 
            Takes a string or a scriptblock (available vars: $Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $esc, $CL, $global:*)
        .PARAMETER SelectionCallback
            Value: <Null|ScriptBlock>
 
            If you want to trigger something on selection or a key, or change the $MenuOptions/$Selection, return $False to exit
     
            # params in: $Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $KeyInput, $esc (char 27, ansi seq. start), $CL (ansicode to clear to end of line)
            # return: $False to end, or array of new options and triggers re-calculation of the menu
            # Modifiable: $Selection to change the selection, $MenuOptions for new array of options (like returning an array, BUT DOES NOT re-calculate the menu)
     
        .PARAMETER Columns
            Value: <"Auto"|Integer>
 
            Define how many columns should be shown (default: "Auto")
        .PARAMETER MaximumColumnWidth
            Value: <"Auto"|Integer>
 
            The maximum amount of chars in a cell should be displayed, if to large, '...' will be appended
            (default: "Auto" if Columns is a number, results in "20" if Columns is "Auto")
        .PARAMETER ShowCurrentSelection
            Value: <Boolean>
            Shows the current selection text in full length in the console title (default: $False)
        .PARAMETER PassThrou
            Without, will output the index of the selection, otherwise the selected string (default: $False)
        .PARAMETER ReturnObject
            Returns Selection index, SelectionValue string, MenuOptions string[] of maybe modified items, MenuOptionsInput string[] of input strings, Items object of {"Name" maybe modified,"Index","Input" originial string} -- has a higher priority then PassThrou
        .PARAMETER ForegroundColor
            Value: <[Console]::ForegroundColor|ConsoleColor>
            Color for the selection (default: Black)
        .PARAMETER ForegroundColorSelection
            Value: <Black|ConsoleColor>
 
            Color for the selection (default: Black)
        .PARAMETER BackgroundColorSelection
            Value: <Cyan|ConsoleColor>
 
            Color for the selection (default: Cyan)
        .PARAMETER ForegroundColorTitle
            Value: <Cyan|ConsoleColor>
 
            Color for the title (default: Cyan)
        .PARAMETER ForegroundColorFooter
            Value: <Black|ConsoleColor>
 
            Color for the footer (default: Black)
     
        .PARAMETER ClearHost
            Value: <Boolean>
 
            Will clear the screen on start and after selecting from the terminal (default: False)
     
        .PARAMETER CleanHost
            Value: <Boolean>
 
            Will clear the menu after selecting from the terminal (default: False)
     
        .PARAMETER FilterCallback
            Value: <Null|ScriptBlock>
 
            Will allows to modify the list of strings before they are shown
     
            # params in: $CellValue, $Current, $CurrentValue, $Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $esc, $CL, $Row, $Column
            # $esc = char 27, ansi seq. start
            # $CL = ansi seq to clear to end of line
            # $CellValue = the cell content, the $SelectedValue with spaces around it
            # return: new value for $CellValue (if nothing/$false/$null is returned, it will not be changed)
            # Modifiable: $MenuOptions,$Selection,$CellValue
     
        .OUTPUTS
            Default is the index of the selected string, using -PassThrou it will be the selected string, using -ReturnObject it will be an object with items
     
            ReturnObject = {
                Selection = index of the selection
                SelectionValue = string of the selection
     
                MenuOptions = string[] of maybe modified items (was used for display)
                MenuOptionsInput = string[] of input strings
     
                Items = {
                    Name = maybe modified (was used for display)
                    Index = index of the item within the MenuOptions (Not MenuOptionsInput)
                    Input = originial string
                }
            }
     
        .EXAMPLE
            import-module ./create-menu.ps1
            ls | Create-Menu
                show all files as a selection and return its index upon selecting
        .EXAMPLE
            New-Module -Name "Create Menu" -ScriptBlock ([Scriptblock]::Create((New-Object System.Net.WebClient).DownloadString("https://gist.githubusercontent.com/BananaAcid/b8efca90cc6ca873fa22a7f9b98d918a/raw/Create-Menu.ps1"))) | Out-Null
            ls | Create-Menu
                show all files as a selection and return its index upon selecting. Loading from remote location.
        .EXAMPLE
            $check = Create-Menu no,yes "Want it?" 1
                Shortest version. "no" first, becuase index 0 is equal to false. the "1" selects index 1 initially
     
                Outputs on single line: `Want it? no yes `
        .EXAMPLE
            $check = Create-Menu @("no","yes") -Title "Want it?" -Selected 1
                Longer version
        .EXAMPLE
             ls | create-menu -passthrou -T "Show Content:`n" |% {cat $_}
                Outputs a filename's contents after selecting
        .EXAMPLE
            echo "Select a letter"; $sel = @("a","b","c") | Create-Menu -PassThrou; echo "Selected: $sel"
                Usage for -MenuOptions by piping in
        .EXAMPLE
            ls ../ | Create-Menu -Title "abc`n-----"
                A simple string as title
        .EXAMPLE
            ls ../ | Create-Menu -Title {"SEL: $SelectionValue`n-----`n"}
                A scriptblock with an internal variable
        .EXAMPLE
            ls ../ | Create-Menu -Title {"SEL: $SelectionValue`n-----`n"} -ReturnObject
                A scriptblock with an internal variable, returning the selection opens
        .EXAMPLE
            ls ../ | Create-Menu -Title {Write-Host Green "SEL: $SelectionValue`n-----`n"}
                A scriptblock with colored title
        .EXAMPLE
            Create-Menu a,b -SelectionCallback {if ($KeyInput -eq 27) { return $False } }
                ESC to cancel input
        .EXAMPLE
            Create-Menu 0,1,2,3 -t {"SEL: $global:ki`n-----"} -SelectionCallback { $global:ki = $KeyInput }
                Show code of pressed key
        .EXAMPLE
            $YesNoCB = { If ($KeyInput -eq 89) { $Selection = 0 ; Return $True } If ($KeyInput -eq 78) { $Selection = 1 ; Return $True } }
            Create-Menu Y,n -SelectionCallback $YesNoCb
                Show "Y" and "n", pressing y (89) or n (78) will select the specific index, exit and show the selected
     
        .EXAMPLE
            $SpacePressed = {
                If ($KeyInput -eq 32) { # space-key
                    if ($SelectionValue -like '`* *') { # check if item has a '* ' prefix = is selected already
                        $MenuOptions[$Selection] = $MenuOptionsInput[$Selection] # remove prefix by setting it to the original string
                    } Else {
                        $MenuOptions[$Selection] = '* ' + $SelectionValue # add prefix to displayed item
                    }
                }
            }
            $ret = ls ~/ | Create-Menu -t {"Full Name: $SelectionValue`n-----`n"} -SelectionCallback $SpacePressed -MaximumColumnWidth 40 -ReturnObject
            $selected = $ret.Items |? { $_.Name -like '`* *' } |% Input # check all items, if they had been marked (would also work: .Name -ne .Input)
            Write-Host "File Paths: ", $selected
     
                Allows to select multiple files with the space key (Keycode 32), then gets their names
     
     
        .EXAMPLE
            $names = "Tom", "Tim", "John", "Alice", "Bob", "Eve", "Adam", "Sarah", "Michael", "Jessica", "William", "Oliver", "Benjamin", "Hannah", "Kevin", "Lily", "David", "Emily", "Matthew", "Ashley", "Joseph", "James", "Laura", "Robert", "Richard", "Patricia", "Christopher", "Nicolas", "Sam", "Jennifer", "Lisa", "Brian", "Heather", "Katherine", "Julia", "Steven", "Amanda", "Rebecca", "Linda", "Daniela", "Elizabeth", "Andrew", "Stephanie", "Anthony", "Rachel", "Michelle", "Joshua", "Samantha", "Emi", "Alex", "Steven", "Amanda", "Rebecca", "Linda", "Daniel", "Elizabeth", "André", "Stephanie", "Anthony", "Rachel", "Michelle", "Joshua", "Sammy", "Amy", "Alexander", "Sammy"
            [Collections.ArrayList]$Global:Selected = @() # collect indexes of selected names
            Create-Menu $names `
                -Title { "Selected Idx: $Global:selected`n-----`n" } `
                -Footer { "Selected Names: $( $Global:selected |% { $MenuOptionsInput[$_] } )" } `
                -SelectionCallback {
                    if ($KeyInput -eq 27) { return $False } # exit with ESC-key
     
                    If ($KeyInput -eq 32) { # select with space-key
                        if ($Global:selected -contains $Selection) { # the index is already selected (in the list)
                            $Global:selected.Remove($Selection) # remove the idx from the list
                        }
                        Else {$Global:selected.Add($Selection) } # remember the idx (NOT THE NAME, but the unique idx!)
                        return $true # add and remove return false - we need to overwrite the return
                    }
                } `
                -Filter {
                    if ($Global:selected -contains $Current) {
                        $CellValue -Replace $CurrentValue,"$esc[4m$CurrentValue$esc[24m" # highlight selected with ansi sq for underline, BUT preserve cell value spaces
                    }
                } | Out-Null # we only want the $global:Selected, we do not care about the default output
            Write-Host "Names: ", ( $Global:selected |% { $names[$_] } )
     
                Allows to select multiple files with the space key, then gets their names. ESC will exit
                It uses an external var to keep track of selecting and unselecting, and uses the -Filter to modify the items
     
        .EXAMPLE
            $num = Create-Menu (1..10) `
                -Title "Select a number:" `
                -Filter { $CellValue -Replace $CurrentValue,"<$CurrentValue>" } `
                -Clean `
                -PassThrou
            Write-Host "You selected: ", $num
     
                Show a selection of numbers and reformat them. After selection, the menu will vanish and at its place show the selected number
                 
                How?
                -Clean = Menu will vanish after selection
                -PassThrou = return the selected value, not index
     
        .NOTES
            Based on: https://community.spiceworks.com/scripts/show/4785-create-menu-2-0-arrow-key-driven-powershell-menu-for-scripts
    #>

    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline=$True ,Mandatory=$True )][Alias("Options")][String[]]$MenuOptions,
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][Object]$Title = $Null,
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("Index")][int]$Selected = $Null,

        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][Object]$Footer = $Null,
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("CB", "CallbackSelection")][ScriptBlock]$SelectionCallback = $Null,
                                            
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][String]$Columns = "Auto",
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][String]$MaximumColumnWidth = "Auto",
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][bool]$ShowCurrentSelection = $False,
        
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][switch]$PassThrou = $False,
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][switch]$ReturnObject = $False,
        
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("c")][ConsoleColor]$ForegroundColor = [Console]::ForegroundColor,
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("cs")][ConsoleColor]$ForegroundColorSelection = [ConsoleColor]::Black,
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("csb")][ConsoleColor]$BackgroundColorSelection = [ConsoleColor]::Cyan,
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("ct")][ConsoleColor]$ForegroundColorTitle = [ConsoleColor]::Yellow,
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("cf")][ConsoleColor]$ForegroundColorFooter = [ConsoleColor]::Green,
        
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][switch]$CleanHost = $False,
        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][switch]$ClearHost = $False,

        [Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias('Filter')][Object]$FilterCallback = $Null
    )

    # in case items were pipelined
    $all = @($Input)
    If ($all) {
        $MenuOptions = [array]$all
    }

    $MenuOptionsInput = $MenuOptions.psobject.copy() # | ConvertTo-Json -depth 100 | ConvertFrom-Json
    $MaxValue = $MenuOptions.count-1
    If ($Selected) { $Selection = $Selected } else { $Selection = 0 }
    $EnterPressed = $False
    $WindowTitleBackup = $Host.UI.RawUI.WindowTitle
    $RowQty = 0

    $outputBuffer = "" # contains output

    If ($Columns -eq "Auto") {
        If ($MaximumColumnWidth -eq "Auto") {
            $MaximumColumnWidth = 20
        }

        $WindowWidth = $Host.UI.RawUI.BufferSize.Width
        $Columns = [Math]::Floor($WindowWidth / ([int]$MaximumColumnWidth +2))
    }
    Else {
        If ($MaximumColumnWidth -eq "Auto") {
            $MaximumColumnWidth = [Math]::Floor(($Host.UI.RawUI.BufferSize.Width - [int]$Columns) / [int]$Columns)
        }
    }
        
    $MenuListing = @()
    Function New-MenuListing($MenuOptions) {
        If ([int]$Columns -gt $MenuOptions.count) {
            $Columns = $MenuOptions.count
        }

        $RowQty = ([Math]::Ceiling(($MaxValue +1) / [int]$Columns))

        # This loop is used to format the menu listing into a
        # two-dimensional array, where each row is a column in the
        # final menu listing. The outer loop iterates over the columns
        # while the inner loop iterates over the rows of the column.
        # The inner loop fetches the menu options for each column and
        # then formats them to fit within the maximum column width.
        # The formatted menu options are stored in the $MenuListing array.
        For ($i=0; $i -lt $Columns; $i++) {
                
            $ScratchArray = @()

            For ($j=($RowQty*$i); $j -lt ($RowQty*($i+1)); $j++) {

                $ScratchArray += $MenuOptions[$j]
            }

            $ColWidth = ($ScratchArray |Measure-Object -Maximum -Property length).Maximum

            If ($ColWidth -gt [int]$MaximumColumnWidth) {
                $ColWidth = [int]$MaximumColumnWidth-1
            }

            For ($j=0; $j -lt $ScratchArray.count; $j++) {
                
                If (($ScratchArray[$j]).length -gt $([int]$MaximumColumnWidth -2)) {
                    $ScratchArray[$j] = $($ScratchArray[$j]).Substring(0,$([int]$MaximumColumnWidth-2))
                    $ScratchArray[$j] = "$($ScratchArray[$j])…"
                }
                Else {
                    For ($k=$ScratchArray[$j].length; $k -lt $ColWidth; $k++) {
                        $ScratchArray[$j] = "$($ScratchArray[$j]) "
                    }
                }
                
                $ScratchArray[$j] = " $($ScratchArray[$j]) "
            }
            $MenuListing += $ScratchArray
        }
        return $MenuListing, $Columns, $RowQty
    }

    Function New-TextBlock ($Block, $BlockName, $ForegroundColor = -1, $Selection, $MenuOptions, $MenuOptionsInput, $esc, $CL) {
        $retBuffer = ""
        if ($Block) {
            if ($Block -is [String]) {
                $retBuffer += ConvertTo-CreateMenuAnsiColorString $Block -ForegroundColor $ForegroundColor
            }
            else {
                $retBuffer += Invoke-Command -ScriptBlock { param($Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $esc, $CL) # supply usable variables

                    # in module fix
                    try { $sbStr = Get-Variable -Name "Block" -Scope 1 -ValueOnly ; $Block = [scriptblock]::Create($sbStr) } 
                    catch { }
                    
                    Invoke-Command -ScriptBlock $Block *>&1 -OutVariable Lines | Out-Null
                    $retBuffer += ConvertTo-CreateMenuAnsiColorString $Lines -ForegroundColor $ForegroundColor  # manual color

                    return $retBuffer
                } -ArgumentList $Selection, $MenuOptions[$Selection], $MenuOptions, $MenuOptionsInput, $esc, $CL
            }
        }
        return $retBuffer | ForEach-Object { "$CL$_" }   #! ALWAYS clean the line before using
    }

    Function CleanHost {
        $Host.UI.RawUI.CursorPosition = @{x=0; y=$Host.UI.RawUI.CursorPosition.Y - $bufferHeight}
        Write-Host ("$CL`n" *( $bufferHeight)).Trim()
        $Host.UI.RawUI.CursorPosition = @{x=0; y=$Host.UI.RawUI.CursorPosition.Y - $bufferHeight}
    }
    
    
    
    
    
    $MenuListing, $Columns, $RowQty = New-MenuListing $MenuOptions
    
    [Console]::CursorVisible = $False


    $bufferHeight = 0
    $bufferHeightBackup = 0

    $esc = [char]27
    $CL = "$esc[0;0m$esc[0J"

    $topBufferHeight = -1

    
    if ($ClearHost) {
        [Console]::Clear()
    }
    
    While ($True) {
        $outputBuffer = ""
        $bufferHeightBackup = $bufferHeight
        
        If ($ShowCurrentSelection) {
            $Host.UI.RawUI.WindowTitle = "CURRENT SELECTION: $($MenuOptions[$Selection])"
        }

        # set cursor back to beginning of output (top of screen has to be processed at least once)
        if ($topBufferHeight -ge 0 -and ($DebugPreference -ne 'Continue') ) {
            $Host.UI.RawUI.CursorPosition = @{x=0; y=$Host.UI.RawUI.CursorPosition.Y  - $topBufferHeight}
        }


        # generate output

        $outputBuffer += New-TextBlock $Title "title-block" $ForegroundColorTitle  $Selection $MenuOptions $MenuOptionsInput $esc $CL

        # output selections
        For ($i=0; $i -lt $RowQty; $i++) {

            For ($j=0; $j -le (($Columns-1)*$RowQty);$j+=$RowQty) {

                $value = $MenuListing[$i+$j]

                if ($null -ne $FilterCallback) {
                    $MenuOptions,$Selection,$CellValue = Invoke-Command -ScriptBlock { param($CellValue, $Current, $CurrentValue, $Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $esc, $CL, $Row, $Column)
                        
                        # in module fix
                        try { $sbStr = Get-Variable -Name "FilterCallback" -Scope 1 -ValueOnly ; $FilterCallback = [scriptblock]::Create($sbStr) } 
                        catch { }

                        $CellValue = . $FilterCallback
                        
                       Return $MenuOptions, $Selection, $CellValue
                    } -ArgumentList $value, ($i+$j), $MenuOptions[$i+$j] , $Selection, $MenuOptions[$Selection], $MenuOptions, $MenuOptionsInput, $esc, $CL, $i, $j

                    $value = if ($CellValue) { $CellValue } else { $value }
                }

                # is end of line
                If ($j -eq (($Columns-1)*$RowQty)) {
                    If (($i+$j) -eq $Selection){
                        $outputBuffer += ConvertTo-CreateMenuAnsiColorString "$($value)$CL`n" -BackgroundColor $BackgroundColorSelection -ForegroundColor $ForegroundColorSelection
                    } Else {
                       $outputBuffer += ConvertTo-CreateMenuAnsiColorString "$($value)`n" -ForegroundColor $ForegroundColor
                    }
                # is in row
                } Else {
                    If (($i+$j) -eq $Selection) {
                        $outputBuffer += ConvertTo-CreateMenuAnsiColorString "$($value)" -BackgroundColor $BackgroundColorSelection -ForegroundColor $ForegroundColorSelection
                    } Else {
                        $outputBuffer += ConvertTo-CreateMenuAnsiColorString "$($value)" -ForegroundColor $ForegroundColor
                    }
                }
                
            }
        }

        $outputBuffer += New-TextBlock $Footer "footer-block" $ForegroundColorFooter   $Selection $MenuOptions $MenuOptionsInput $esc $CL


        $outputBuffer += " " # prevents line jumping

        # do positioning

        $bufferHeight = ($outputBuffer | Measure-Object -Line).lines
        $currentBufferHeight = $Host.UI.RawUI.BufferSize.Height

        If ($bufferHeight -gt $currentBufferHeight) {
            throw "Menu too tall to fit in buffer. Increase buffer height. (Window height)"
        }

        Write-Host $outputBuffer

        $topBufferHeight = $bufferHeight  # back to start line
        if ($bufferHeight -lt $bufferHeightBackup) { # rows changed inbetween
            Write-Host ("$CL`n" *( $bufferHeightBackup - $bufferHeight)).Trim()
            $topBufferHeight = $bufferHeightBackup
        }

        # $Host.UI.RawUI.CursorPosition = @{x=0; y=$Host.UI.RawUI.CursorPosition.Y - $topBufferHeight}


        if ($EnterPressed) {
            If ($ClearHost) {
                [Console]::Clear()
            }

            Break
        }

        $KeyInput = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown").virtualkeycode

        Switch ($KeyInput) {
            13 { #Enter
                # ignore empty fields
                If ($MenuOptions[$Selection]) {
                    $EnterPressed = $True

                    # set title to before menu
                    If ($ShowCurrentSelection) {
                        $Host.UI.RawUI.WindowTitle = $WindowTitleBackup
                    }

                    If ($ClearHost) {
                        [Console]::Clear()
                    }

                    if ($CleanHost) {
                        CleanHost
                    }

                    $returnContent = $null
                    
                    If ($ReturnObject) {

                        $items = @()

                        For ($i=0; $i -lt $MenuOptions.length; $i++) {
                            $items += @{
                                "Name" = $MenuOptions[$i];
                                "Index" = $i;
                                "Input" = $MenuOptionsInput[$i];
                            }
                        }

                        $returnContent = @{
                            "Selection" = $Selection;
                            "SelectionValue" = $MenuOptions[$Selection];
                            "MenuOptions" = $MenuOptions;
                            "MenuOptionsInput" = $MenuOptionsInput;
                            "Items" = $items;
                        }
                    }
                    ElseIf ($PassThrou) {
                        $returnContent = $MenuOptions[$Selection]
                    }
                    Else {
                        $returnContent = $Selection
                    }
                    # $Host.UI.RawUI.CursorPosition = @{x=0; y=$Host.UI.RawUI.CursorPosition.Y + $BufferHeight}
                    [Console]::CursorVisible = $true

                    Return $returnContent
                }

                Break
            }

            37 { #Left
                If ($Selection -ge $RowQty){
                    $Selection -= $RowQty
                } Else {
                    $Selection += ($Columns-1)*$RowQty
                }
                Break
            }

            38 { #Up
                If ((($Selection+$RowQty)%$RowQty) -eq 0) {
                    # If the selection is at the start of a row, move to the last item in the row
                    $Selection += $RowQty - 1
                } Else {
                    $Selection -= 1
                }
                Break
            }

            39{ #Right
                # If the selection is at the end of a row, move to the previous row
                # by subtracting the number of columns minus one multiplied by the row quantity
                # Otherwise, move to the next item in the row
                If ([Math]::Ceiling($Selection/$RowQty) -eq $Columns -or ($Selection/$RowQty)+1 -eq $Columns){
                    $Selection -= ($Columns-1)*$RowQty
                } Else {
                    $Selection += $RowQty
                }
                Break
            }

            40 { #Down
                If ((($Selection+1)%$RowQty) -eq 0 -or $Selection -eq $MaxValue){
                    $Selection = ([Math]::Floor(($Selection)/$RowQty))*$RowQty
                } Else {
                    $Selection += 1
                }
                Break
            }

            Default {
            }
        }

        if ($SelectionCallback -ne $Null) {
            # return new MenuOptions, if you want
            $ret,$sel,$res = Invoke-Command -ScriptBlock { param($Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $KeyInput, $esc, $CL)
                
                # in module fix, replace wiht localized script block
                try { $sbStr = Get-Variable -Name "SelectionCallback" -Scope 1 -ValueOnly ; $SelectionCallback = [scriptblock]::Create($sbStr) } 
                catch { }
                
                $res = . $SelectionCallback

                Return $MenuOptions, $Selection, $res
            } -ArgumentList $Selection, $MenuOptions[$Selection], $MenuOptions, $MenuOptionsInput, $KeyInput, $esc, $CL

            If ($sel -is [Int]) {
                $Selection = $sel
            }

            If ($res -eq $False) {
                $EnterPressed = $True

                # set title to before menu
                If ($ShowCurrentSelection) {
                    $Host.UI.RawUI.WindowTitle = $WindowTitleBackup
                }
            }

            ElseIf ($ret -is [Array]) {
                $MenuOptions = $ret
                $MenuListing, $Columns, $RowQty = New-MenuListing $MenuOptions
            }
        }

    }
}






Function ConvertTo-CreateMenuAnsiColorString {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true, Position=0)]
        [string]$InputString,
        [Parameter(Mandatory=$false)]
        [string]$ForegroundColor,
        [Parameter(Mandatory=$false)]
        [string]$BackgroundColor,
        [Parameter(Mandatory=$false)]
        [string[]]$Styles
    )
    
    $esc = [char]27 # using char 27 instead of string "`e" because not using `e makeis it PS 5.1 compatible

    $ansiColors = @{
        # Powershell defined ones, 0-15
        Black = 0; DarkBlue = 4; DarkGreen = 2; DarkCyan = 6; DarkRed = 1; DarkMagenta = 5; DarkYellow = 3; Gray = 7; DarkGray = 8; Blue = 12; Green = 10; Cyan = 14; Red = 9; Magenta = 13; Yellow = 11; White = 15;
    }
    $fgColor = $ansiColors[$ForegroundColor]
    $bgColor = $ansiColors[$BackgroundColor]

    $ansiStyles = @{ Normal = 0; Bold = 1; Dim = 2; Italic = 3; Underline = 4; Blink = 5; RapidBlink = 6; Reverse = 7; Hidden = 8; }
    if ($Styles) {
        $styleSequence = $Styles | ForEach-Object { "$esc[{0}m" -f $ansiStyles[$_] }
    }

    if ("$fgColor" -and "$bgColor") {
        return "$styleSequence$esc[38;5;{0};48;5;{1}m{2}$esc[0m" -f $fgColor, $bgColor, $InputString
    } elseif ("$fgColor") {
        return "$styleSequence$esc[38;5;{0}m{1}$esc[0m" -f $fgColor, $InputString
    } elseif ("$bgColor") {
        return "$styleSequence$esc[48;5;{0}m{1}$esc[0m" -f $bgColor, $InputString
    } elseif ($Styles) {
        return "$styleSequence$InputString$esc[0m"
    } else {
        return $InputString
    }
}