PowershellMenu.ps1
<#PSScriptInfo .NAME PowershellMenu.ps1 .VERSION 1.5.0 .GUID 3cded6d4-7b7c-4bc8-a616-36d716bc58ac .AUTHOR mikeb .COMPANYNAME None .COPYRIGHT (c) 2020 Mike Branham. .TAGS .LICENSE https://www.gnu.org/licenses/gpl-3.0.en.html .PROJECTURI https://github.com/Mike-Branham/PowershellMenu .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES 06/03/2020 Initial release 06/10/2020 Fixed issue with word wrap exceeding bounds in printing help text #> <# .SYNOPSIS Copyright (c) 2020 Mike Branham. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Powershell menu that allows up / down arrows as well as an optional key press to select a menu item. Provision for help text on the highlighted item to display in a defined help area. .DESCRIPTION Powershell menu that allows up / down arrows as well as an optional key press to select a menu item. Features: Cursor Up / Down to select Optionally add a character to the menu item for selection Optionally add help text that word-wrap displays in a defined help area on the screen Separate color settings for title, headers, menu items, and selected menu items Tests console width and height to make sure menu and optional help will fit .NOTES Copyright (c) 2020 Mike Branham. All rights reserved. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #> # Constants for the help text area # If helpenabled is set to $False, then the help area is never cleared or rendered New-Variable -Name 'HElPSTARTROW' -Value 1 -Option Constant New-Variable -Name 'HELPSTARTCOL' -Value 60 -Option Constant New-Variable -Name 'HELPWIDTH' -Value 40 -Option Constant New-Variable -Name 'HELPMAXROWS' -Value 20 -Option Constant New-Variable -Name 'HELPENABLED' -Value $True -Option Constant New-Variable -Name 'HELPCOLOR' -Value white -Option Constant # Constants for the menu display # AllowKeys - if set, it will use the character in the prompt, and allow selection with a key press. New-Variable -Name 'ALLOWKEYS' -Value $True -Option Constant # Colors # Title color is for the menu title at the top of the screen # Header color is used for any menu items that start with a period. They are not selectable. # Prompt color is the basic menu item prompt text color # Prompt fg and bg colors define the colors for the selected item. New-Variable -Name 'TITLECOLOR' -Value white -Option Constant New-Variable -Name 'HEADERCOLOR' -Value blue -Option Constant New-Variable -Name 'PROMPTCOLOR' -Value cyan -Option Constant New-Variable -Name 'PROMPTFGCOLOR' -Value black -Option Constant New-Variable -Name 'PROMPTBGCOLOR' -Value cyan -Option Constant # MenuItem class # $Prompt is the menu item text # $Value is the returned value when selected # $Help is the optional help text to display in the help area Class MenuItem { [string] $Prompt = "" # What is displayed on the screen [string] $Value = "" # What is returned if this item is selected [string] $Key = "" # Key to use as a short-cut instead of arrows and enter [string] $Help = "" # Optional help text to display in the heip text area # MenuItem constructor MenuItem([string] $Prompt, [string] $Value, [string] $Key, [string] $Help) { $this.Prompt = $Prompt $this.Value = $Value $this.Key = $Key $this.Help = $Help } } # Menu class # This class contains an array of menu items for display # To put a blank separator in the menu, simply have blank "" string for the Prompt # To put a heading (non-selectable item) in the menu use a period as the first character. Class Menu { [string] $Title = "" # Menu title displayed on the screen [System.Collections.ArrayList]$MenuItems = @() # Array list of menu items # Constructor, sets the title Menu([string] $Title) { $this.Title = $Title } # Add item helper function AddMenuItem([string] $Prompt, [string] $Value, [string] $Key, [string] $Help) { $newMenuItem = [MenuItem]::new($Prompt, $Value, $Key, $Help) $this.MenuItems.Add($newMenuItem) } } # Check screen width and height Function Test-Screen([Menu] $menu) { $widthNeeded = 0 $heightNeeded = 0 # Get the current sindow size [int]$width = $Host.UI.RawUI.WindowSize.Width [int]$height = $Host.UI.RawUI.WindowSize.Height # Calculate size needed. # Using the help area? if($HELPENABLED) { # width needed is help start plus help width plus buffer $widthNeeded = $HELPSTARTCOL + $HELPWIDTH + 2 } else { # Get the width of the longest menu prompt, and count lines for height foreach($item in $menu) { if($item.Prompt.Length -gt $widthNeeded) { $widthNeeded = $item.Prompt.Length } } } # height is title line plus number of menu items plus cursor line $heightNeeded = $Menu.MenuItems.count + 2 # test to make sure console is appropriately sized if(($width -lt $widthNeeded) -Or ($height -lt $heightNeeded)) { Write-Host "Current screen size is w: $width, h: $height. You need to adjust to w: $widthNeeded, h: $heightNeeded" exit } } # Write out the help item. # Clears the text area first, then word-wraps the help text into the defined area Function Write-Help( $text) { # Don't bother if not enabled if(-Not $HELPENABLED) {return} # Split text on white space $words = $text -split " " $line = "" # Initial text is blank $curRow = $HELPSTARTROW # Set starting row # First clear the help area $blanks = " " * $HELPWIDTH for($i = 0; $i -lt $HELPMAXROWS + 1; $i++) { $Host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates $HELPSTARTCOL, ($curRow + $i) Write-Host $blanks } # Write out the help text foreach ( $word in $words ) { # See if adding the word makes the line longer than the width $checkLine = $line + " " + $word if($checkLine.Length -gt $HELPWIDTH) { # We are at the end of the width, so output the line $Host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates $HELPSTARTCOL, $curRow Write-Host -NoNewLine "$line" -ForegroundColor $HELPCOLOR # Move to the next line and start the next line text with the word that didn't fit this line. $curRow++ $line = "$word " } else { # Simply add the word to the line with a space $line += "$word " } # output last line if needed if($line.Length) { $Host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates $HELPSTARTCOL, $curRow Write-Host -NoNewLine "$line" -ForegroundColor $HELPCOLOR } } } # Display the menu and process user input Function New-Menu () { Param( [Parameter(Mandatory=$True)][Menu]$Menu ) # Variable initialization $MaxValue = $Menu.MenuItems.count-1 $Selection = -1 # We will pre-increment this so it will start at zero $PreviousSelection = -1 # Save previous, as above will get set appropriately after menu draws the first time $EnterPressed = $False # Initially clear the screen. # Cursor movement will only modify the current and previous selections # This eliminates the screen flicker of drawing the entire screen for each cursor move Clear-Host # Find and set the first selectable menu item do { $Selection += 1 $item = $Menu.MenuItems[$Selection] } while ($item.Prompt -eq "" -Or $item.Prompt.StartsWith(".")) # No previous selection to start with $PreviousSelection = $Selection # If we allow key entry create a hashtable of keys and values $keysHash = @{} if($ALLOWKEYS) { # go through the menu items and add the key / values to the hashtable foreach($item in $Menu.MenuItems) { $keysHash[$item.Key] = $item.Value } } # Draw menu title Write-Host "$($Menu.Title)" -ForegroundColor $TITLECOLOR # Draw each menu item For ($i=0; $i -le $MaxValue; $i++){ $item = $Menu.MenuItems[$i] $prompt = $item.Prompt $key = "" # Add key value to the menu if allowed if ($ALLOWKEYS) {$key = "[$($item.Key)]"} # If the prompt is blank, blank the key as well if ($prompt -eq "") {$key = ""} if ($i -eq $Selection){ # Highlight initial selectable item Write-Host -BackgroundColor $PROMPTBGCOLOR -ForegroundColor $PROMPTFGCOLOR " $key $prompt " } Else { # If the string starts with a period, remove it and render with the header color if($prompt.StartsWith(".")) { Write-Host $prompt.substring(1) -ForegroundColor $HEADERCOLOR } else { # Output the normal menu item Write-Host " $key $prompt " -ForegroundColor $PROMPTCOLOR } } } # Save the cursor position at the end of the menu # so we can return it after updating the screen. $cursorPos = $Host.UI.RawUI.CursorPosition # Assume cursor moved to print the initial help $cursorMoved = $True # Process the menu While($EnterPressed -eq $False) { $row = 0 For ($i=0; $i -le $MaxValue; $i++){ $row += 1 $item = $Menu.MenuItems[$i] $key = "" # Add key value to menu if allowed if ($ALLOWKEYS) {$key = "[$($item.Key)]"} $prompt = $item.Prompt If ($i -eq $Selection){ # Highlight the current selection $Host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates 0, $row Write-Host -BackgroundColor $PROMPTBGCOLOR -ForegroundColor $PROMPTFGCOLOR " $key $prompt " } Else { if($i -eq $PreviousSelection) { # Remove hightlight from the previous selection $Host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates 0, $row Write-Host " $key $prompt " -ForegroundColor $PROMPTCOLOR } } } # Write help if enabled $item = $Menu.MenuItems[$Selection] if($cursorMoved) { Write-Help $item.Help } # Reset the cursor $Host.UI.RawUI.CursorPosition = $cursorPos $RawKey = $host.ui.rawui.readkey("NoEcho,IncludeKeyDown") $KeyInput = $RawKey.virtualkeycode $KeyCode = $RawKey.Character Switch($KeyInput){ 13{ # enter $EnterPressed = $True Return $item.Value break } 38{ # up arrow $cursorMoved = $True $PreviousSelection = $Selection do { If ($Selection -eq 0){ $Selection = $MaxValue } Else { $Selection -= 1 } $item = $Menu.MenuItems[$Selection] } while ($item.Prompt -eq "" -Or $item.Prompt.StartsWith(".")) break } 40{ # down arrow $cursorMoved = $True $PreviousSelection = $Selection do { If ($Selection -eq $MaxValue){ $Selection = 0 } Else { $Selection +=1 } $item = $Menu.MenuItems[$Selection] } while ($item.Prompt -eq "" -Or $item.Prompt.StartsWith(".")) break } Default{ $cursorMoved = $False # If we allow keys if ($ALLOWKEYS) { # See if the key is in the hashtable and return the value if found foreach($key in $keysHash.keys) { if($key -eq $KeyCode) { $ret = $keysHash[$key] Return $ret } } } } } } } $Menu = [Menu]::new("================= Select option ==================") $Menu.AddMenuItem("", "", "", "") $Menu.AddMenuItem(".Information", "", "", "") $Menu.AddMenuItem("Information item 1.", "i1", "0", "This is the first item that is selectable in the menu. This help text should wrap on to a couple of lines.") $Menu.AddMenuItem("Information Item 2.", "i2", "1", "Help for Item 2.") $Menu.AddMenuItem("Information Item 3.", "i3", "2", "Help for Item 3.") $Menu.AddMenuItem("", "", "", "") $Menu.AddMenuItem(".Setup", "", "", "") $Menu.AddMenuItem("Setup somthing.", "s1", "3", "") $Menu.AddMenuItem("Setup something else.", "s2", "4", "") $Menu.AddMenuItem("", "", "", "") $Menu.AddMenuItem(".Backup and restore", "", "", "") $Menu.AddMenuItem("Backup some configuration.", "b1", "5", "") $Menu.AddMenuItem("Backup something else.", "b2", "6", "") $Menu.AddMenuItem("", "", "", "") $Menu.AddMenuItem(".Other", "", "", "") $Menu.AddMenuItem("Do some other thing.", "o1", "7", "") $Menu.AddMenuItem("Do this and that.", "o2", "8", "") $Menu.AddMenuItem("Test something.", "o3", "9", "") $Menu.AddMenuItem("", "", "", "") $Menu.AddMenuItem("Quit.", "q", "q", "") Test-Screen $Menu # In the real world the return value would be your validated value to use in a switch # or other method for executing the selected item's function. $val = New-Menu $Menu Clear-Host Write-Host $val |