Ask.psm1

### Built from file: src\preamble.ps1
<#
 
Ask
Powershell Prompting made Pretty
 
Bringing back the good old days of valid input with a TUI
To report issues and be part of the discussion visit:
 
https://github.com/natebarkei/Ask
 
#>

### Built from file: src\ansi\ansi_codes.ps1

# ANSI Codes
$Script:ESC          = [char]27
$Script:a_U_ON       = "$Script:ESC[4m"
$Script:a_U_OFF      = "$Script:ESC[24m"
$Script:a_B_ON       = "$Script:ESC[1m"
$Script:a_B_OFF      = "$Script:ESC[21m"
$Script:a_CLLN       = "$Script:ESC[K"
$script:a_FgBlack    = "$Script:ESC[30m"
$script:a_FgRed      = "$Script:ESC[31m"
$script:a_FgGreen    = "$Script:ESC[32m"
$script:a_FgYellow   = "$Script:ESC[33m"
$script:a_FgBlue     = "$Script:ESC[34m"
$script:a_FgMagenta  = "$Script:ESC[35m"
$script:a_FgCyan     = "$Script:ESC[36m"
$script:a_FgWhite    = "$Script:ESC[37m"
$script:a_FgBBlack   = "$Script:ESC[90m"
$script:a_FgBRed     = "$Script:ESC[91m"
$script:a_FgBGreen   = "$Script:ESC[92m"
$script:a_FgBYellow  = "$Script:ESC[93m"
$script:a_FgBBlue    = "$Script:ESC[94m"
$script:a_FgBMagenta = "$Script:ESC[95m"
$script:a_FgBCyan    = "$Script:ESC[96m"
$script:a_FgBWhite   = "$Script:ESC[97m"

$Script:AnsiLookup = @{
    Black    = "$Script:ESC[30m"
    Red      = "$Script:ESC[31m"
    Green    = "$Script:ESC[32m"
    Yellow   = "$Script:ESC[33m"
    Blue     = "$Script:ESC[34m"
    Magenta  = "$Script:ESC[35m"
    Cyan     = "$Script:ESC[36m"
    White    = "$Script:ESC[37m"
    BBlack   = "$Script:ESC[90m"
    BRed     = "$Script:ESC[91m"
    BGreen   = "$Script:ESC[92m"
    BYellow  = "$Script:ESC[93m"
    BBlue    = "$Script:ESC[94m"
    BMagenta = "$Script:ESC[95m"
    BCyan    = "$Script:ESC[96m"
    BWhite   = "$Script:ESC[97m"
    crlf     = "`r`n"
    r        = "`r"
    normal   = "$Script:ESC[m"
    cl       = "$Script:ESC[2K"

}
### Built from file: src\ansi\nbCursorControl.ps1
function nbCursorHide(){
    $host.ui.Write("$Script:ESC[?25l")
}

function nbCursorShow() {
    $host.ui.Write("$Script:ESC[?25h")
}
### Built from file: src\ansi\nbMoveTo.ps1
function nbMoveTo([int32]$x,[int32]$y){
    $host.ui.Write("$Script:ESC[$($y+1);${x}H")  
    #$host.ui.RawUI.CursorPosition = [System.Management.Automation.Host.Coordinates]::new($x,$y)
}
### Built from file: src\ansi\nbScreenRecover.ps1
function nbScreenRecover(){
    #$AtNow = $host.UI.RawUI.CursorPosition
    $host.ui.Write("$Script:ESC[?1049l")
    $host.UI.RawUI.CursorPosition=$Script:curInitialPos
}
### Built from file: src\ansi\nbScreenSwitch.ps1
$Script:curInitialPos=$null

function nbScreenSwitch(){
    #need to adjust positioning based on buffer size of the console
    #is the cursor beyond the window size, ie have we scrolled down?
    if ($host.UI.RawUI.CursorPosition.Y -gt $host.UI.RawUI.WindowSize.Height) {
        $top = $host.ui.RawUI.WindowPosition.Y
    }
    else {
        $top = 0
    }
    $r = [System.Management.Automation.Host.Rectangle]::new(0, $top, $host.ui.rawui.windowsize.width,$top+$host.ui.RawUI.WindowSize.height)
 
    # Capture the screen buffer and current cursor position
    $b=$host.ui.RawUI.GetBufferContents($r)
    $Script:curInitialPos = $host.ui.RawUI.CursorPosition
    
    # Switch into the secondary screen
    $host.ui.Write("${ESC}[?1049h")

    # Write the buffer we captured into the alternate screen.
    $cWrite = [System.Management.Automation.Host.Coordinates]::new(0,0)
    $host.ui.RawUI.SetBufferContents($cWrite,$b)

    # Move the cursor to the exact location it was in on the primary screen
    $host.ui.Write("$Script:ESC[$($Script:curInitialPos.Y+1);0H")  
}
### Built from file: src\ansi\nbShowVersion.ps1
function nbShowVersion {
    param([string]$Version=$Script:Version)
    $ConsoleWidth = $host.ui.RawUI.WindowSize.Width
    $outVersion = $Script:Version
    if($outVersion.Length -lt $ConsoleWidth) {$outVersion=$outVersion + $(' ' * ($ConsoleWidth- $outVersion.Length))}
    Write-Host $outVersion -BackgroundColor Blue -ForegroundColor White
}
### Built from file: src\ansi\nbWriteAnsi.ps1
$script:noColorMode = $false

function nbColorEnable()  {$script:noColorMode=$true}
function nbColorDisable() {$script:noColorMode=$false}


function nbWriteAnsi {
    [CmdletBinding()]
    param(
        [string]$Text
    )

    $text.Split( [char]'<',[char]'>') | foreach-Object {$i = 0} {
        if($i % 2 -eq 0) {
            $host.ui.Write($_)
        }
        else {
            #Figure out our internal codes and translate to ansi
            if($Script:AnsiLookup.ContainsKey($_)) {
                $Ansi = $Script:AnsiLookup.Item($_)
            }
            if(-not [string]::IsNullOrEmpty($ansi)) {$host.ui.write($Ansi)}
        }
        $i++
    }
}
### Built from file: src\iconography.ps1
## Iconography
$Script:curInitialPos=$null
$script:Question = '?'
$script:Check='√'
$script:Bullet='•'
$script:Selector='>'
$script:Bad = 'X'
$script:Prompt = '›'
$script:UpArrow = '↑'
$script:DnArrow = '↓'

##
## Overwrite Specific Iconography with Emoji's if our terminal supports it.
##
if($env:WT_SESSION -or $env:TERM_PROGRAM -eq 'vscode') {
    $script:Check='✔'
    $script:Selector='❯'
    $script:Bad = '✘'
}
### Built from file: src\strings.ps1
## Core strings
$Script:Version = "Ask, Copyright 2015-2022 by Nate Barkei."
$Script:str_UnhandledError = 'An uncaught error happened in Invoke-Ask. The command that called it has been terminated because there is no way to validate if a proper input was entered.'
$Script:str_ConfirmYESno = '(Y/n)'
$script:str_ConfirmNOyes = '(N/y)'
$Script:str_FilterPrompt = " Filter: "
$Script:str_FilterPlaceholder = "<BBlack>Type to filter"
### Built from file: src\coreinput.ps1

function nbWaitSilentKeyPress([array]$AllowedKeys=$null) {
    $AllowExit = $false
    $KeyPressOptions= 7 # 1=AllowCtrlC, 4=IncludeKeyDown, 2=NoEcho
    while(-not $AllowExit) {
        
        [System.Management.Automation.Host.KeyInfo]$KeyPress=$host.ui.RawUI.ReadKey($KeyPressOptions)
        if($null -eq $AllowedKeys) {
            $AllowExit=$true
        }
        else {
            if($AllowedKeys -contains $KeyPress.VirtualKeyCode) { $AllowExit = $true}
            Write-Verbose "Key pressed was: $($keypress.VirtualKeyCode)"
        }
    }
    return $KeyPress
}
### Built from file: src\functions\confirm.ps1
$script:f_confirm = {

    if([string]::IsNullOrWhiteSpace($Message)){ $Message='Are you sure?' }
    $YN=$script:str_ConfirmNOyes
    if($DefaultTrue){$YN=$Script:str_ConfirmYESno}

    $promptString = "<BBlue>$script:Question <BWhite>$Message <BBlack>$yn $script:Prompt "
    
    nbWriteAnsi $promptString
           
    $keyPress = nbWaitSilentKeyPress -AllowedKeys 27,89,78,13,81
    switch($keyPress.VirtualKeyCode) {
        27 { $exitValue=$false } # ESC
        81 { $exitValue=$false } # q
        13 { if($DefaultTrue) {$exitValue=$true} else {$exitValue=$false} } # Enter
        89 { $exitValue=$true}   # Y
        78 { $exitValue=$false}  # N
    }
  
    $ClosingString = "<BGreen>$script:Check <BWhite>$message <BBlack>$script:Bullet <BGreen>$exitValue"
    
}
### Built from file: src\functions\select.ps1
$script:f_select = {
    if([string]::IsNullOrWhiteSpace($Message)){$Message='Select an option?'}
    $promptString = "<cl><BBlue>$script:Question <BWhite>$Message <BBlack>$script:Prompt "

    

    # If we have more items than the max height of window right now -1 (for the prompt)
    # then we have to get clever
    $ReservedSpace=1
    if(-not [string]::IsNullOrWhiteSpace($Filter) -or $ShowFilter) { $ReservedSpace = 2 } # Space for Filter prompt


    # This is where we get tricky with filters
    $ApplyFilter = {
        if([string]::IsNullOrWhiteSpace($filter)) {
            $FilteredItems = $Items
        }
        else {
            $FilteredItems = @($Items| ? {$_ -like "*$filter*"})
        }

        $ViewItems = $Items.Length
        if($ViewItems -gt $host.ui.RawUI.WindowSize.height-($ReservedSpace+1)) { $ViewItems = $host.UI.RawUI.WindowSize.height - $ReservedSpace }
        if($FullScreen) { $ViewItems = $host.UI.RawUI.WindowSize.height - $ReservedSpace }
    
        $VisibleTop = 0
        if($Selected -gt $filteredItems.Length-1) {$selected=0}
    }
    
    
    $PrintFilter = {
        if($ReservedSpace -gt 1) {
            nbWriteAnsi "<crlf>$Script:a_CLLN<r><white>$Script:str_FilterPrompt"
            if([string]::IsNullOrWhiteSpace($filter)) {
                nbWriteAnsi $Script:str_FilterPlaceholder
            }
            else {
                nbWriteAnsi $Filter
            }
        }
    }

    
    $PrintCurrentPage = {
        #if($filterRefresh) {
        #$host.ui.write("ViewItems $($ViewItems) VisibleTop=$($visibleTop) FilteredItemsLength=$($FilteredItems.Length)")
        #}
        for($i = 0; $i -lt $ViewItems; $i++){
            $currentIndex = $VisibleTop+$i
            
            if($currentIndex -gt $FilteredItems.Length-1) {
                nbWriteAnsi "<crlf>1<cl><r>" # Clear the line
                #if($filterRefresh) {$host.ui.write('-'); $host.ui.RawUI.ReadKey(7)}
            }
            else {
                nbWriteAnsi ("<crlf>$Script:a_CLLN<r> <BBlack>" + $FilteredItems[$currentIndex])
                #if($filterRefresh) {$host.ui.write('+'); $host.ui.RawUI.ReadKey(7)}
            }
            
        }
    }

    $PrintCurrentSelected = {
        
        nbMoveTo -x 0 -y ($FirstListItemY + ($Selected - $VisibleTop))
        if($filteredItems.count -eq 0) {
            nbwriteAnsi " <BRed>$script:Bad No items match filter<r>"
            return
        }
        nbWriteAnsi ' <BGreen>'
        $host.ui.write($script:Selector)
        nbwriteAnsi " <BWhite>"
        $host.ui.write("$($FilteredItems[$selected])`r")
    }


    # Initial draw before loop
    nbCursorHide
    . $ApplyFilter # This needs to be called before using $FilteredItems below
    nbWriteAnsi $promptString
    nbWriteAnsi "$($FilteredItems[$selected])"
    . $PrintFilter
    . $PrintCurrentPage


    # Now for some clever crap. Figure out where the cursor is and subtract ViewItems to get
    # the start location of the list
    $FirstListItemY = $host.UI.RawUI.CursorPosition.Y - $viewItems+1
    if($FirstListItemY -lt 0) {$FirstListItemY = 1}
    

    $continueLoop = $true
    while ($continueLoop) {
        . $printCurrentSelected        
        # Show scroll arrows if they are required
        if($VisibleTop -gt 0) {} # Show Up
        if($visibleTop+$ViewItems -lt $FilteredItems.Count) {} # Show Dn

        <#
        # Our initial timeout goes here
        while(-not [console]::KeyAvailable) {
             
            MoveTo -x 0 -y ($FirstListItemY + ($Selected - $VisibleTop))
        }
        #>


        # Blocking Keypress
        $keyPress=nbWaitSilentKeyPress #-AllowedKeys 27,40,38,13,33,34,35,36

        nbWriteAnsi " <BBlack> $($FilteredItems[$selected])" # Erases the current selector
        $FilterRefresh=$false
        $screenRefresh=$false
        switch ($keyPress.VirtualKeyCode) {
            27 {$continueLoop = $false; $exitValue = -1; break}
            40 {if($Selected -lt $FilteredItems.Count-1) { $selected ++}; break} #down
            38 {if($Selected -gt 0) {$selected --}; break} #up
            33 {$selected-=$viewItems; if($selected -lt 0){$selected=0}; break} #PageUp
            34 {$selected+=$ViewItems; if($selected -gt $FilteredItems.count-1){$selected=$FilteredItems.Count-1}; break} #PageDN
            35 {$selected = $FilteredItems.Count-1; break} # End
            36 {$selected = 0; break} # Home
            13 {
                    if($FilteredItems.Length -eq 0) {break} # Don't allow bad filtering
                    $continueLoop=$false;
                    if($Index) {
                        [int]$exitValue = $items.IndexOf($FilteredItems[$selected])
                    }
                    else {
                        $exitValue = $FilteredItems[$selected]
                    }
                    break 
                }
            8  {if($filter.Length -gt 0) {$filter= $filter.Substring(0,$filter.Length-1)}
                $FilterRefresh=$true
                break} #Backspace
            46 {$filter=''; $FilterRefresh=$true; break}# Delete key clears filter
            
            default {
                # We must be adding or removing the filter text
                if($keypress.Character -ne 0) {
                    $filter += $keyPress.Character;
                    $FilterRefresh=$true
                }
            }
        }
        if($selected -gt ($VisibleTop + $ViewItems -1)) {
            # Shift viewport down
            if($selected -eq $FilteredItems.Length-1) {
                $visibletop = $selected-$viewitems+1
                if($VisibleTop -lt 0) {$VisibleTop = 0}
            }
            else {
                $visibletop = $Selected
            }
            $screenRefresh=$true
        }
        if($selected -lt $VisibleTop) {
            # Shift viewport up
            $visibleTop = $selected - $viewItems+1
            if($VisibleTop -lt 0) {$VisibleTop = 0}
            $screenRefresh=$true
        }
        if ($FilterRefresh) {
            . $ApplyFilter
            $screenRefresh=$true
            
        }
        if($screenRefresh) {
            
            nbmoveto -x 0 -y ($FirstListItemY-1) # -1 required because we start with a crlf
            . $printCurrentPage
            
        }
        nbmoveto -x 0 -Y ($FirstListItemY-$ReservedSpace)
        nbWriteAnsi $promptString 
        nbWriteAnsi "$($FilteredItems[$selected])"
        . $PrintFilter

    }
    nbCursorShow

    $ClosingString = "<BGreen>$script:Check <BWhite>$message <BBlack>$script:Bullet <BGreen>$($FilteredItems[$selected])"
    
}
### Built from file: src\Invoke-Ask.ps1
<#
.SYNOPSIS
Ask pretty questions on the command line.
 
 
#>

function Invoke-Ask {
    [Alias('Ask')]
    [CmdletBinding(DefaultParameterSetName='help')]
    param(
        [Parameter(Mandatory=$true, ParameterSetName='confirm')][Alias('c')][switch]$Confirm,
        #[Parameter(Mandatory=$true, ParameterSetName='getinput')][switch]$GetInput,
        #[Parameter(Mandatory=$true, ParameterSetName='secret')][switch]$Secret,
        [Parameter(Mandatory=$true, ParameterSetName='select')][switch]$Select,
        #[Parameter(Mandatory=$true, ParameterSetName='multiselect')][switch]$MultiSelect,
        [Parameter(ParameterSetName='version')][switch]$Version,
        [Parameter(ParameterSetName='help')][switch]$Help,

        [Parameter(ParameterSetName='confirm')][switch]$DefaultTrue,
        #[Parameter(ParameterSetName='confirm')][Int]$Timeout,


        #[Parameter(ParameterSetName='getinput')][Parameter(ParameterSetName='secret')][switch]$AllowEmpty,

        #[Parameter(ParameterSetName='getinput')][string]$Default,
        
        #[Parameter(ParameterSetName='secret')][string]$Confirmation,

        #[Parameter(ParameterSetName='secret')][string]$MismatchErrorMessage,

        [Parameter(ParameterSetName='select')][int]$Selected=0,

        #[Parameter(ParameterSetName='multiselect')][int[]]$SelectedItems,

        [Parameter(Mandatory=$true, ParameterSetName='select')]
        [array]$Items,

        [Parameter(ParameterSetName='select')][switch]$Index,
        [Parameter(ParameterSetName='select')][switch]$ShowFilter,
        [Parameter(ParameterSetName='select')][string]$Filter=$null,

        [Parameter(HelpMessage='Message for the Prompt')]
        [Parameter(ParameterSetName='confirm')][Parameter(ParameterSetName='getinput')]
        [Parameter(ParameterSetName='secret')][Parameter(ParameterSetName='select')]
        [Parameter(ParameterSetName='multiselect')]
        [string]$Message,


        [switch]$NoColor,

        [Parameter(ParameterSetName='select')]
        [switch]$FullScreen
        
              
    )
    
    process {

        # IF we actually display this to the end-user then our logic has failed and they should know.
        $ClosingString = 'An internal error has occured inside of Invoke-Ask.'

        if ($NoColor) { nbColorDisable } else { nbColorEnable }

        if($PSCmdlet.ParameterSetName -eq 'help'){
            nbShowVersion
            Get-Help -Name Invoke-Ask
            return
        }

        trap {
            # This is our generic something went very bad so revert out trap
            nbScreenRecover
            nbCursorShow
            Write-Warning $Script:str_UnhandledError
            Write-Warning $_
            break TerminateAskCallingFunction
        }


        if($PSCmdlet.ParameterSetName -eq 'version') {
            nbShowVersion
            return
        }
        
        
        # Switch into the Alt Screen Buffer
        nbScreenSwitch

        ##
        ## CONFIRM
        ##
        if($Confirm) { . $script:f_confirm }

        ##
        ## SELECT
        ##
        if($Select) { . $script:f_select }

        # Restore the screen and write whatever we're supposed to and return our exitvalue
        nbScreenRecover
        nbWriteAnsi $ClosingString
        nbWriteAnsi '<normal><crlf>'
        return $exitValue
    }

}