PoshCode Archive  Artifact [e7439ea14e]

Artifact e7439ea14ed900d11a3c77e2aad1271d79d5bdb4a028c60b1193e192151123dc:

  • File ConsoleToys-psm1.ps1 — part of check-in [4fe8a658d7] at 2018-06-10 13:53:06 on branch trunk — Show-ConsoleMenu shows a vertical “menu” in the console and allows you to pick numeric items from it. (user: Joel Bennett size: 23986)

# encoding: ascii
# api: powershell
# title: ConsoleToys,psm1
# description: Show-ConsoleMenu shows a vertical “menu” in the console and allows you to pick numeric items from it.
# version: 3.1
# type: function
# author: Joel Bennett
# license: CC0
# function: Show-ConsoleMenu
# x-poshcode-id: 5297
# x-archived: 2015-04-24T02:03:46
# x-published: 2015-07-10T22:45:00
#
# Update 1: start numbering of items at 1
# Update 2: fix and put back the “buffer box” code so you can get a fancy menu ;-)
#
function Show-ConsoleMenu {
   <#
      .Synopsis
        Displays a menu in the console and returns the selection
      .Description
        Displays a numbered list in the console, accepts a typed number from the user, and returns it.
      .Example
        ls | Show-ConsoleMenu -Title "Please pick a file to delete:" -Passthru | rm -whatif
      
        Description
        -----------
        Creates a menu showing a line for every file, and outputs the selected file.
      .Example
        if(Test-Path $Profile) {
           switch(Show-ConsoleMenu "Profile exists:" "Delete it!","Rename it with 01","Abort") {
              1 { rm $Profile -whatif }
              2 { mv $Profile [IO.Path]::ChangeExtension($Profile,"01.ps1") }
              3 { return }
           }
        }
      
        Description
        -----------
        Shows how to use the return value without the Passthru switch.
        This example would check if you have a profile, and if you do, would offer you the choice of removing or renaming it.
   #>
   param(
      # The items to be chosen from
      [Parameter(ValueFromPipeline=$true,Position=2)]
      [Alias("InputObject")]
      [PSObject[]]$Choices,
      # A caption to display before the choices
      [Parameter(Position=1)]
      [Alias("Title")]
      [string]$Caption,
      # A scriptblock expression for formatting the Choices.
      [Parameter(Position=3)]
      [ScriptBlock]$Expression=({$_}),
      # A prompt to display after the choices
      [Alias("Footer")]
      [string]$Prompt,
      # How much to indent the "center" of the selection menu (Defaults to 8)
      [int]$indentLeft=4,
      # If set, Show-ConsoleMenu returns the selected value from $choices, otherwise it returns the index (which is usually easier to use in a switch statement)
      [Switch]$Passthru,
      # If set, this function works with my New-BufferBox script by using Out-Buffer ( http://poshcode.org/2899 )
      [Switch]$UseBufferBox,
      # If set, allows multiple selection (Press Enter to stop selecting more)
      [Switch]$MultiSelect
   )
   begin {
      $allchoices = New-Object System.Collections.Generic.List[PSObject]
   }
   process {
      if($choices) {
         $allchoices.AddRange($choices)
      }
   }
   end {
      $Format = "{0:D1}"; 
      $Digits = ($allchoices.Count + 1).ToString().Length
      $Format = "{0:D${Digits}}" 

      $numbersWarning = "Numbers Only Please"
      $rangeWarning   = "Only {0:D${Digits}}..{1:D${Digits}} Please" -f 1, ($allchoices.Count)

      # Make a hashtable with keys
      for($i=0; $i -lt $allchoices.Count; $i++) {
         $allchoices[$i] = Add-Member -Input $allchoices[$i] -Type NoteProperty -Name ConsoleMenuKey -Value $($format -f ($i+1)) -Passthru
      }

      Write-Debug "There are $($allChoices.Count) choices, so we'll use $Digits digits"
      # output the menu to the screen
      $width = [Math]::Max("$Prompt".Length, "$Caption".Length)
      $width = [Math]::Max($width, $numbersWarning.Length)

      $menu = $allchoices | Format-Table -HideTableHeader @{E="ConsoleMenuKey";A="Right";W=$indentLeft}, @{E=$Expression;A="Left"} -Force |
                            Out-String -Stream | ForEach { $line = $_.TrimEnd(); if($width -lt $line.Length) { $width = $line.Length }; $line }
      $menu = $menu[0..$($menu.Length-2)]

      if($UseBufferBox) {
         $OriginalCursorPosition = $Host.UI.RawUI.CursorPosition
         $Width += $(if($indentLeft -ge 4) { $indentLeft } else { $Width += 4 })
         $menu = $menu | Where { $_.length }
         $Height = $Menu.Length + $(if($Prompt){5}else{4})
         Reset-Buffer -Position $OriginalCursorPosition -Title $Caption -Height $Height -Width $Width -ShowInput
         $menu | Out-Buffer 
         Set-BufferInputLine
      } else {
         Write-Host ("`n" + (" " * ($indentLeft/2)) + $Caption + "`n")  -ForegroundColor $Host.PrivateData.VerboseForegroundColor  -BackgroundColor $Host.PrivateData.VerboseBackgroundColor
         Write-Host $menu
      }
      
      do {
         if($Prompt) {
            if($UseBufferBox) {
               Reset-CursorPosition $BufferPromptPosition -LinesOffset 2
               Out-Buffer $Prompt -ForegroundColor $Host.PrivateData.VerboseForegroundColor  -BackgroundColor $Host.PrivateData.VerboseBackgroundColor -FixInput
            } else {
               Write-Host $Prompt -ForegroundColor $Host.PrivateData.VerboseForegroundColor  -BackgroundColor $Host.PrivateData.VerboseBackgroundColor
            }
         }      
         # get a choice from the user
         [string]$PreviousKeys=""
         do { 
            $Key = $Host.UI.RawUI.ReadKey("IncludeKeyDown,NoEcho").Character
            try { 
               [int][string]$choice = "${PreviousKeys}${Key}"
               $index = $choice - 1
               $PreviousKeys = "${PreviousKeys}${Key}"
               if($UseBufferBox) {
                  Set-BufferInputLine $PreviousKeys
               } else {
                   Write-Host $Key -NoNewline
               }
            } catch { 
               ## The "?" causes us to re-show the menu. Useful for long multi-selects, which might scroll off.
               if(63 -eq [int][char]$Key) {
                  if($UseBufferBox) {
                     $menu | Out-Buffer 
                  } else {
                     Write-Host $menu
                  }
               } elseif(13,27,0 -notcontains [int][char]$Key) {
                  if($UseBufferBox) {
                     $menu | Out-Buffer 
                     Out-Buffer $numbersWarning -ForegroundColor $Host.PrivateData.WarningForegroundColor -BackgroundColor $Host.PrivateData.WarningBackgroundColor -FixInput
                  } else {
                     Write-Warning $numbersWarning
                  }
               }
               [int][string]$choice = "${PreviousKeys}"
            }
            if($choice -gt $allchoices.Count) {
               $PreviousKeys = ""
               if($UseBufferBox) {
                  $menu | Out-Buffer
                  Out-Buffer $rangeWarning -ForegroundColor $Host.PrivateData.WarningForegroundColor -BackgroundColor $Host.PrivateData.WarningBackgroundColor -FixInput
               } else {
                  Write-Warning $rangeWarning
               }
            }
         } while( $PreviousKeys.Length -lt $Digits -and (13,27 -notcontains [int][char]$Key))

         if($index -ge 0 -and $index -lt $allchoices.Count) {
            if($UseBufferBox) {
               Reset-CursorPosition $BufferPromptPosition -LinesOffset 2
            } else {
               Write-Host
            }
            if($Passthru) { 
               $allchoices[$index] 
            } else { 
               $choice
            }
         }
      } while($key -ne [char]13 -and $MultiSelect)
   }
}



####################################################################################################
## This script is just a demonstration of some of the things you can do with the buffer
## in the default PowerShell host... it serves as a reminder of how much work remains on
## PoshConsole, and as an inspiration to anyone who is thinking about trying to create
## "interactive" PowerShell applications.
##
## Try Test-DisplayBox and then Test-BufferBox (note it has tab completion and everything).
####################################################################################################
$global:BoxChars = new-object PSObject -Property @{
   'HorizontalDouble'            = ([char]9552).ToString()
   'VerticalDouble'              = ([char]9553).ToString()
   'TopLeftDouble'               = ([char]9556).ToString()
   'TopRightDouble'              = ([char]9559).ToString()
   'BottomLeftDouble'            = ([char]9562).ToString()
   'BottomRightDouble'           = ([char]9565).ToString()
   'HorizontalDoubleSingleDown'  = ([char]9572).ToString()
   'HorizontalDoubleSingleUp'    = ([char]9575).ToString()
   'Horizontal'                  = ([char]9472).ToString()
   'Vertical'                    = ([char]9474).ToString()
   'TopLeft'                     = ([char]9484).ToString()
   'TopRight'                    = ([char]9488).ToString()
   'BottomLeft'                  = ([char]9492).ToString()
   'BottomRight'                 = ([char]9496).ToString()
   'Cross'                       = ([char]9532).ToString()
   'VerticalDoubleRightSingle'   = ([char]9567).ToString()
   'VerticalDoubleLeftSingle'    = ([char]9570).ToString()
}
$global:RectType = "system.management.automation.host.rectangle"


function Reset-Buffer {
   param(
      $Position = $Host.UI.RawUI.WindowPosition,
      [int]$Height = $Host.UI.RawUI.WindowSize.Height,
      [int]$Width = $Host.UI.RawUI.WindowSize.Width,
      # Note: all edges are padded by 1 for the box edges, but we also pad each side by this ammount:
      [int]$Padding = 1,
      $ForegroundColor = $Host.UI.RawUI.ForegroundColor,
      $BackgroundColor = $Host.UI.RawUI.BackgroundColor,
      $BorderColor     = "Yellow",
      [switch]$NoBorder,
      [switch]$ShowInput,
      [string]$Title = ""
   )

   $global:BufferHeight          = $Height
   $global:BufferWidth           = $Width
   $global:BufferPadding         = $Padding
   $global:BufferForegroundColor = $ForegroundColor 
   $global:BufferBackgroundColor = $BackgroundColor 
   $global:BufferBorderColor     = $BorderColor     

   if($NoBorder) {
      $global:BufferBoxSides = 0
   } else {
      $global:BufferBoxSides = 2
   }
   if($ShowInput) {
      $global:BufferHeight -= 2
   }

   Write-Debug "RESET: Position: $Position Left: $Left Top: $Top Width: $Width Height: $Height Offset: $Offset"

   $Host.UI.RawUI.SetBufferContents($Position,(New-BufferBox $BufferHeight $BufferWidth -Title:$Title -NoBorder:$NoBorder -ShowInput:$ShowInput -Background $BufferBackgroundColor -Border $BufferBorderColor))
  
   
   $global:BufferPosition = $Position   
   $global:BufferPosition.X += $global:BufferPadding + ($global:BufferBoxSides/2)
   # this gets set to the BOTTOM line, because I assume text will flow in from the bottom.
   $global:BufferPosition.Y += $global:BufferHeight - 2
   # and this goes below that ...
   $global:BufferPromptPosition = $BufferPosition
   $global:BufferPromptPosition.Y += 2
   $global:BufferPromptPosition.X += 2 - $global:BufferPadding # Prompt = "> "
}

function New-BufferBox {
   param(
      [int]$Height = $global:BufferHeight, 
      [int]$Width = $global:BufferWidth,
      $Title = "",
      [switch]$NoBorder,
      [switch]$ShowInput,
      $BackgroundColor = $global:BufferBackgroundColor,
      $BorderColor = $global:BufferBorderColor 
   )
   $Width = $Width - $global:BufferBoxSides
   
   $LineTop =( $global:BoxChars.HorizontalDouble * 2) + $Title `
            + $($global:BoxChars.HorizontalDouble * ($Width - ($Title.Length+2)))
   
   $LineField = ' ' * $Width
   $LineBottom = $global:BoxChars.HorizontalDouble * $Width
   $LineSeparator = $global:BoxChars.Horizontal * $Width
   $LinePrompt = '> ' + ' ' * ($Width-2) # Prompt = "> "
   
   if(!$NoBorder) {
      $LineField = $global:BoxChars.VerticalDouble + $LineField + $global:BoxChars.VerticalDouble
      $LinePrompt = $global:BoxChars.VerticalDouble + $LinePrompt + $global:BoxChars.VerticalDouble
      $LineBottom = $global:BoxChars.BottomLeftDouble + $LineBottom + $global:BoxChars.BottomRightDouble
      $LineTop = $global:BoxChars.TopLeftDouble + $LineTop + $global:BoxChars.TopRightDouble
      $LineSeparator = $global:BoxChars.VerticalDoubleRightSingle + $LineSeparator + $global:BoxChars.VerticalDoubleLeftSingle
   }

   if($ShowInput) {
      $box = &{$LineTop;1..($Height - 2) |% {$LineField};$LineSeparator;$LinePrompt;$LineBottom}
   } else {
      $box = &{$LineTop;1..($Height - 2) |% {$LineField};$LineBottom}
   }
   $boxBuffer = $Host.UI.RawUI.NewBufferCellArray($box,$BorderColor,$BackgroundColor)
   return ,$boxBuffer
}

function Move-Buffer {
   param(
      $Position = $global:BufferPosition,
      [int]$Left = $($global:BufferBoxSides/2),
      [int]$Top = (2 - $global:BufferHeight),
      [int]$Width = $global:BufferWidth - $global:BufferBoxSides,
      [int]$Height = $global:BufferHeight,
      [int]$Offset = -1
   )
   Write-Debug "MOVE: Position: $Position Left: $Left Top: $Top Width: $Width Height: $Height Offset: $Offset"

   $Position.X += $Left
   $Position.Y += $Top
   $Rect = New-Object $RectType $Position.X, $Position.Y, ($Position.X + $width), ($Position.Y + $height -1)
   $Position.Y += $OffSet
   $Host.UI.RawUI.ScrollBufferContents($Rect, $Position, $Rect, (new-object System.Management.Automation.Host.BufferCell(' ',$global:BufferForegroundColor,$global:BufferBackgroundColor,'complete')))
}

function Out-Buffer {
   param(
      [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Position=0,Mandatory=$true)]
      $Message, 
      [Parameter(ValueFromPipelineByPropertyName=$true)]
      $ForegroundColor = $global:BufferForegroundColor, 
      [Parameter(ValueFromPipelineByPropertyName=$true)]
      $BackgroundColor = $global:BufferBackgroundColor, 
      
      [switch]$NoScroll,
      
      [Parameter(ValueFromPipelineByPropertyName=$true)]
      $Position = $global:BufferPosition,
      [Parameter(ValueFromPipelineByPropertyName=$true)]
      [int]$Left = 0,
      [Parameter(ValueFromPipelineByPropertyName=$true)]
      [int]$Top    = $(3 - $global:BufferHeight),  # Box Edge + New Lines
      [Parameter(ValueFromPipelineByPropertyName=$true)]
      [int]$Width  = ($global:BufferWidth - $global:BufferBoxSides), # Box Edge
      [Parameter(ValueFromPipelineByPropertyName=$true)]
      [int]$Height = ($global:BufferHeight - $global:BufferBoxSides), # Box Edge
      [Parameter(ValueFromPipelineByPropertyName=$true)]
      [int]$Offset = $( 0 - ("$Message".Split("`n").Count)),
      [Switch]$FixInput
   )
   process {
      $lineCount = $Message.Split("`n").Count

      $Width = $Width - ($global:BufferPadding * 2)
      if(!$NoScroll){ Move-Buffer $Position $Left $Top $Width $Height $Offset }
      
      $MessageBuffer = New-Object "System.Management.Automation.Host.BufferCell[,]" $lineCount, $width
      
      $index = 0
      foreach( $line in $Message.Split("`n") ) {
         $Buffer = $Host.UI.RawUI.NewBufferCellArray( @($line.Trim("`r").PadRight($Width)), $ForegroundColor, $BackgroundColor )
         for($i = 0; $i -lt $width; $i++) {
            $MessageBuffer[$index,$i] = $Buffer[0,$i]
         }
         $index++
      }
      
      $Y = $global:BufferPosition.Y
      $global:BufferPosition.Y -= $lineCount - 1
      $Host.UI.RawUI.SetBufferContents($global:BufferPosition,$MessageBuffer)
      $global:BufferPosition.Y = $Y

      if($FixInput) {
         Set-BufferInputLine
      }
   }
}

function Set-BufferInputLine {
   param([String]$Line = "")
   $PromptText = $line.PadRight(($global:BufferWidth - $global:BufferBoxSides - $line.Length - 3)) # Prompt = "> "
   
   $CursorPosition = $BufferPromptPosition
   $CursorPosition.X += $line.Length
   
   $Prompt = $Host.UI.RawUI.NewBufferCellArray( @($PromptText),$global:BufferForegroundColor, $global:BufferBackgroundColor)
   $Host.UI.RawUI.SetBufferContents( $BufferPromptPosition, $prompt )
   $Host.UI.RawUI.CursorPosition = $CursorPosition
}

function Reset-CursorPosition {
   param(
      $CursorPosition = $BufferPromptPosition,
      $LinesOffset = 2
   )
   $CursorPosition.Y += $LinesOffset
   $CursorPosition.X = 0
   $Host.UI.RawUI.CursorPosition = $CursorPosition
}

function Test-DisplayBox {
   $Position = $Host.UI.RawUI.WindowPosition
   $Position.X += 10
   $Position.Y += 3

   Reset-Buffer $Position 20 66 3 -ForegroundColor 'Gray' -BackgroundColor 'Black' -BorderColor 'Green'
   Out-Buffer 'Greetings!' 'Yellow' 'black'
   sleep -m 600
   Out-Buffer '' 'Gray' 'black'
   sleep -m 600
   Out-Buffer '- - - Thank you for running this simple demo script! - - -' 'Green' 'black'
   sleep -m 600
   Out-Buffer '' 'Gray' 'black'
   sleep -m 600
   Out-Buffer "We are demonstrating how the scroll buffer works: you can`nadd more than one line at a time, but you don't really`nneed to, since they add almost as fast one at a time." 'white' 'black'
   sleep -m 3000
   Out-Buffer '' 'Gray' 'black'
   Out-Buffer 'That is, as long as you don''t have any delay, you can just' 'white' 'black'
   Out-Buffer 'write out as many lines as you like, one-at-a-time, like' 'white' 'black'
   Out-Buffer 'this, and there is really no downside to doing that.' 'white' 'black'
   sleep -m 3000
   Out-Buffer '' 'Gray' 'black'
   Out-Buffer 'Right? '.PadRight(58,"-") 'Red' 'black'   
   Out-Buffer '' 'Gray' 'black'
   sleep -m 600
   Out-Buffer 'It''s clearly not as slick to just pop in multiple lines' 'white' 'black'
   sleep -m 1200
   Out-Buffer 'with absolutely no scroll delay, and it makes it a little ' 'white' 'black'
   sleep -m 1200
   Out-Buffer 'hard to tell what you have read already, but that''s ok.' 'white' 'black'
   sleep -m 1200
   Out-Buffer '' 'Gray' 'black'
   sleep -m 600
   Out-Buffer 'If you delay between paragraphs.' 'Red' 'black'   
   sleep -m 600
   Out-Buffer '' 'Gray' 'black'
   sleep -m 600
   Out-Buffer 'But: don''t scroll off-screen faster than I can read!' 'Yellow' 'black'   
   sleep -m 600
   Out-Buffer '' 'Gray' 'black'
}

## Test-BufferBox 3.1 - Now with Tab completion
####################################################################################################
## Imagine it's a chat window: you can type, but the whole time, the ongoing conversation in the 
## chat room you have joined is going on in the background.
##
## NOTE: because this is a "chat" demo, we treat your input as text, and to execute script in-line 
## you have to enclose it inside $() braces.
####################################################################################################
function Test-BufferBox {
   param(
      $title = "PowerShell Interactive Buffer Demo"
   )

   Reset-Buffer -ShowInput -Title $Title

   ###################################################################################################
   ##### We only need this for the purposes of the demo...
   ##### In a real script you would, presumably, be getting stuff from somewhere else to display
   $noise = $MyInvocation.MyCommand.Definition -split "`n"
   $index = 0; 
   $random = New-Object "Random"
   [regex]$chunker = @'
[^ \"']+|([\"'])[^\\1]*?\\1[^ \"']*|([\"'])[^\\1]*$| $
'@
   ##### Printing a "How to Exit" message
   Out-Buffer "  " -Fore DarkGray -Back Cyan
   Out-Buffer "     This is just a demo. " -Fore Black -Back Cyan
   Out-Buffer "     We will stream in the source code of this script ... " -Fore Black -Back Cyan
   Out-Buffer "     And you can type at the bottom while it's running. " -Fore Black -Back Cyan
   Out-Buffer "     Imagine this as an n-way chat application like IRC, except that " -Fore Black -Back Cyan
   Out-Buffer "  FOR THIS PERFORMANCE ONLY: " -Fore Black -Back Cyan
   Out-Buffer "         The part of your chatting friends is played by my source code. " -Fore DarkGray -Back Cyan
   Out-Buffer "  " -Fore DarkGray -Back Cyan
   Out-Buffer "     Press ESC to exit, or enter 'exit' and hit Enter" -Fore Black -Back Cyan
   Out-Buffer "  " -Fore DarkGray -Back Cyan
   ##### Setting the prompt
   Set-BufferInputLine
   ##### And initializing our two variables ...
   $line=""
   $exit = $false
    
   switch(Show-ConsoleMenu "What would you like to do now?" "Continue the demo","Stop the demo","Exit PowerShell" -UseBuffer) {
   1 { <# do nothing, will continue #> }
   2 { $exit = $true <#this script exits cleanly below#> }
   3 { exit <# stop abruptly #> }
   }

   while(!$exit){
      while(!$exit -and $Host.UI.RawUI.KeyAvailable) { 
         $char = $Host.UI.RawUI.ReadKey("IncludeKeyUp,IncludeKeyDown,NoEcho"); 
         # we don't want the key up events, but we do want to get rid of them
         if($char.KeyDown) {
         switch([int]$char.Character) {
            13 { # Enter
                  if($line.Trim() -eq "exit") { $exit = $true; break; }
   ###################################################################################################
   ###################################################################################################
   ############# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING #############
   ############# This executes the user input!                                           #############
   ############# Don't use this on, say, content you got from a web page or chat room    #############
               iex "Out-Buffer `"$line`" -Fore Red";                                     #############
   ###################################################################################################
   ###################################################################################################
               $line = "";
               Set-BufferInputLine
               break;
            }
            27 { # Esc
               $exit = $true; break;
            }
            9 { # Tab
               if($line.Length -gt 0) {
                  [Array]$words = $chunker.Matches($line)
                  if($words.Count -ge 1) {
                     Out-Buffer ($Words | Out-String) -Fore Black -Back White
                     $lastWord = $words[$($words.Count-1)].Value
                     $trim = $lastWord.TrimEnd("`r","`n")
                     ## This may return more than one ... in which case subsequent tabs should iterate through them
                     ## But for demo purposes, this is more than enough work...
                     $replacement = TabExpansion -Line $line -LastWord ($lastWord.Trim() -replace '"')
                     Out-Buffer ($replacement | Out-String) -Fore Black -Back White
                     $line = $line.SubString(0, $line.Length - $lastWord.Length) + @($replacement)[0]
                     Set-BufferInputLine $line
                  }
               }         
               break;
            }
               8 { # Backspace
               if($line.Length -gt 0) {
                  $line = $line.SubString(0,$($line.Length-1))
               }
               # $pos = $Host.UI.RawUI.CursorPosition
               Set-BufferInputLine $line
               break;
            }         
            0 { 
               # Not actually a key
               # Out-Buffer $($Char | Out-String)
               break;
            }
            default {
               $line += $char.Character
               Set-BufferInputLine $line
            }
         }
         }
      }
      # Simulate doing useful things 25% of the time
      if($random.NextDouble() -gt 0.75) {
         $noise[($index)..($index++)] | Out-Buffer
         if($index -ge $noise.Length){$index = 0}
      }
      sleep -milli 100
   }
   Reset-CursorPosition $BufferPromptPosition
}