├── .gitignore ├── Examples ├── AdvancedMenu.md ├── AdvancedMenu.ps1 ├── AppxPackages.gif ├── AppxPackages.md ├── AppxPackages.ps1 ├── CustomMenu.md ├── CustomMenu.ps1 └── MultiSelect.gif ├── LICENSE ├── README.md ├── Snippets └── vscode-powershell.json └── Write-Menu.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .history/ 2 | -------------------------------------------------------------------------------- /Examples/AdvancedMenu.md: -------------------------------------------------------------------------------- 1 | # Example: AdvancedMenu 2 | 3 | This example includes all possible entry types: 4 | 5 | ``` 6 | Command Entry Invoke without opening as nested menu (does not contain any prefixes) 7 | Invoke Entry Invoke and open as nested menu (contains the "@" prefix) 8 | Hashtable Entry Opened as a nested menu 9 | Array Entry Opened as a nested menu 10 | ``` 11 | 12 | ## Input 13 | 14 | ```powershell 15 | . ..\Write-Menu.ps1 16 | 17 | $menuReturn = Write-Menu -Title 'Advanced Menu' -Sort -Entries @{ 18 | 'Command Entry' = '(Get-AppxPackage).Name' 19 | 'Invoke Entry' = '@(Get-AppxPackage).Name' 20 | 'Hashtable Entry' = @{ 21 | 'Array Entry' = "@('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4')" 22 | } 23 | } 24 | 25 | Write-Output $menuReturn 26 | ``` 27 | 28 | ## Console Output 29 | 30 | ### Main Menu 31 | 32 | ``` 33 | 34 | Advanced Menu 35 | 36 | Command Entry 37 | Hashtable Entry > 38 | Invoke Entry > 39 | 40 | ``` 41 | 42 | ### Command 43 | 44 | ``` 45 | ... 46 | Microsoft.MicrosoftSolitaireCollection 47 | Microsoft.Advertising.Xaml 48 | Microsoft.BingFinance 49 | Microsoft.BingNews 50 | Microsoft.BingWeather 51 | Microsoft.WindowsMaps 52 | Microsoft.ZuneVideo 53 | Microsoft.WindowsCalculator 54 | Microsoft.XboxApp 55 | ... 56 | ``` 57 | 58 | ### Hashtable and Array 59 | 60 | ``` 61 | 62 | Hashtable Entry 63 | 64 | Array Entry > 65 | 66 | ``` 67 | 68 | ``` 69 | 70 | Array Entry 71 | 72 | Menu Option 1 73 | Menu Option 2 74 | Menu Option 3 75 | Menu Option 4 76 | 77 | ``` 78 | 79 | ### Invoke 80 | 81 | ``` 82 | 83 | Invoke Entry 2/6 84 | 85 | Microsoft.CommsPhone 86 | Microsoft.ConnectivityStore 87 | Microsoft.Getstarted 88 | Microsoft.LockApp 89 | Microsoft.Messaging 90 | Microsoft.MicrosoftEdge 91 | Microsoft.MicrosoftOfficeHub 92 | Microsoft.MicrosoftSolitaireCollection 93 | Microsoft.NET.Native.Framework.1.0 94 | Microsoft.NET.Native.Framework.1.0 95 | Microsoft.NET.Native.Framework.1.1 96 | Microsoft.NET.Native.Framework.1.1 97 | Microsoft.NET.Native.Framework.1.3 98 | 99 | ``` 100 | 101 | ## Function Output 102 | 103 | Returns the selected menu entry. -------------------------------------------------------------------------------- /Examples/AdvancedMenu.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Example: AdvancedMenu 3 | #> 4 | 5 | . ..\Write-Menu.ps1 6 | 7 | $menuReturn = Write-Menu -Title 'Advanced Menu' -Sort -Entries @{ 8 | 'Command Entry' = '(Get-AppxPackage).Name' 9 | 'Invoke Entry' = '@(Get-AppxPackage).Name' 10 | 'Hashtable Entry' = @{ 11 | 'Array Entry' = "@('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4')" 12 | } 13 | } 14 | 15 | Write-Output $menuReturn 16 | -------------------------------------------------------------------------------- /Examples/AppxPackages.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuietusPlus/Write-Menu/99940d97670f7507739ff9b8234c265c4800ee54/Examples/AppxPackages.gif -------------------------------------------------------------------------------- /Examples/AppxPackages.md: -------------------------------------------------------------------------------- 1 | # Example: AppxPackages 2 | 3 | This example uses Write-Menu to list the app packages (Windows Store/Modern Apps) that are installed for the current profile. 4 | 5 | ![AppxPackages](AppxPackages.gif) 6 | 7 | ##Input 8 | 9 | ```powershell 10 | # Include 11 | . ..\Write-Menu.ps1 12 | 13 | $menuReturn = Write-Menu -Title 'AppxPackages' -Entries (Get-AppxPackage).Name 14 | Write-Host $menuReturn 15 | ``` 16 | 17 | ##Console Output 18 | 19 | ```powershell 20 | AppxPackages 21 | 22 | Microsoft.NET.Native.Framework.1.1 23 | Microsoft.NET.Native.Framework.1.1 24 | Microsoft.NET.Native.Runtime.1.1 25 | Microsoft.Appconnector 26 | Microsoft.VCLibs.140.00 27 | Microsoft.WindowsCalculator 28 | Microsoft.NET.Native.Runtime.1.1 29 | Microsoft.WindowsStore 30 | windows.immersivecontrolpanel 31 | Microsoft.Windows.ShellExperienceHost 32 | Microsoft.Windows.Cortana 33 | Microsoft.AAD.BrokerPlugin 34 | Microsoft.AccountsControl 35 | Microsoft.BioEnrollment 36 | Microsoft.LockApp 37 | Microsoft.MicrosoftEdge 38 | 39 | Page 1 / 2 40 | ``` 41 | 42 | ##Function Output 43 | 44 | ```powershell 45 | AppxPackage.Name 46 | ``` 47 | -------------------------------------------------------------------------------- /Examples/AppxPackages.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Example: AppxPackages 3 | #> 4 | 5 | # Include 6 | . ..\Write-Menu.ps1 7 | 8 | $menuReturn = Write-Menu -Title 'AppxPackages' -Entries (Get-AppxPackage).Name 9 | Write-Host $menuReturn 10 | -------------------------------------------------------------------------------- /Examples/CustomMenu.md: -------------------------------------------------------------------------------- 1 | # Example: CustomMenu 2 | 3 | This example generates a custom menu by manually specifying each entry. 4 | 5 | ##Input 6 | 7 | #####Option 1 8 | 9 | ```powershell 10 | # Include 11 | . ..\Write-Menu.ps1 12 | 13 | $menuReturn = Write-Menu -Title 'Custom Menu' -Entries @('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4') 14 | Write-Host $menuReturn 15 | ``` 16 | 17 | #####Option 2 18 | 19 | ```powershell 20 | # Include 21 | . ..\Write-Menu.ps1 22 | 23 | $menuReturn = Write-Menu -Title 'Custom Menu' -Entries @( 24 | 'Menu Option 1' 25 | 'Menu Option 2' 26 | 'Menu Option 3' 27 | 'Menu Option 4' 28 | ) 29 | Write-Host $menuReturn 30 | ``` 31 | 32 | ##Console Output 33 | 34 | ``` 35 | Custom Menu 36 | 37 | Menu Option 1 38 | Menu Option 2 39 | Menu Option 3 40 | Menu Option 4 41 | 42 | Page 1 / 1 43 | ``` 44 | 45 | ##Function Output 46 | 47 | Returns the selected menu entry. For example: 48 | 49 | ```powershell 50 | Menu Option 3 51 | ``` 52 | -------------------------------------------------------------------------------- /Examples/CustomMenu.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Example: CustomMenu 3 | #> 4 | 5 | # Include 6 | . ..\Write-Menu.ps1 7 | 8 | $menuReturn = Write-Menu -Title 'Custom Menu' -Entries @( 9 | 'Menu Option 1' 10 | 'Menu Option 2' 11 | 'Menu Option 3' 12 | 'Menu Option 4' 13 | ) 14 | Write-Host $menuReturn 15 | -------------------------------------------------------------------------------- /Examples/MultiSelect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuietusPlus/Write-Menu/99940d97670f7507739ff9b8234c265c4800ee54/Examples/MultiSelect.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 QuietusPlus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Write-Menu 2 | 3 | ### -Title 'AppxPackages' -Sort -Entries (Get-AppxPackages).Name 4 | 5 | ![AppxPackages](Examples/AppxPackages.gif) 6 | 7 | ### -Title 'AppxPackages' -Sort -MultiSelect -Entries (Get-AppxPackages).Name 8 | 9 | ![AppxPackages](Examples/MultiSelect.gif) 10 | 11 | _NOTE: The menu has been updated multiple times since capturing these screen-grabs, so they are no longer accurate..._ 12 | 13 | ## Description 14 | 15 | Outputs a command-line menu which can be navigated using the keyboard. 16 | 17 | * Automatically creates multiple pages if the entries cannot fit on-screen. 18 | * Supports nested menus using a combination of hashtables and arrays. 19 | * No entry / page limitations (apart from device performance). 20 | * Sort entries using the -Sort parameter. 21 | * -MultiSelect: Use space to check a selected entry, all checked entries will be invoked / returned upon confirmation. 22 | * Jump to the top / bottom of the page using the Home and End keys. 23 | 24 | 25 | ## Parameters 26 | 27 | | | Parameter | Example | 28 | |:--|:--|:--| 29 | | Required | Entries (array) | `-Entries @('Entry 1', 'Entry 2', 'Entry 3')` | 30 | | | Entries (hashtable) | `-Entries @{'Entry 1' = 'Write-Host "Command 1"'; 'Entry 2' = 'Write-Host "Command 2"'; 'Entry 3' = 'Write-Host "Command 3'"}` | 31 | | Optional | Title | `-Title 'Example Title'` | 32 | | Optional | Sort | `-Sort` | 33 | | Optional | MultiSelect | `-MultiSelect` 34 | 35 | ## Examples 36 | 37 | | Example | Description | 38 | | :-- | :-- | 39 | | [AdvancedMenu](Examples/AdvancedMenu.md) | Demonstrates all supported entry types (regular command + methods of adding a nested menu). | 40 | | [AppxPackages](Examples/AppxPackages.md) | Uses Write-Menu to list app packages (Windows Store/Modern Apps) | 41 | | [CustomMenu](Examples/CustomMenu.md) | Generates a custom menu by manually specifying each entry | 42 | 43 | ## Controls 44 | 45 | | Key | Description | 46 | |:--|:--| 47 | | Up | Previous entry | 48 | | Down | Next entry | 49 | | Left / PageUp | Previous page| 50 | | Right / PageDown | Next page | 51 | | Home | Jump to top | 52 | | End | Jump to bottom | 53 | | Space | `-MultiSelect` Select current | 54 | | Insert | `-MultiSelect` Select all | 55 | | Delete | `-MultiSelect` Select none | 56 | | Enter | Confirm selection | 57 | | Esc / Backspace | Exit / Previous menu | 58 | 59 | -------------------------------------------------------------------------------- /Snippets/vscode-powershell.json: -------------------------------------------------------------------------------- 1 | { 2 | /* 3 | Custom: Write-Menu 4 | */ 5 | 6 | "Write-Menu Command": { 7 | "prefix": "Menu Commmand [Write-Menu]", 8 | "body": [ 9 | "Write-Menu -Title '${1:MenuTitle}' -Sort -Entries (Get-$2).${3:Name}$4" 10 | ], 11 | "description": "Write-Menu using the output from a command" 12 | }, 13 | 14 | "Write-Menu Array": { 15 | "prefix": "Menu Array [Write-Menu]", 16 | "body": [ 17 | "Write-Menu -Title '${1:MenuTitle}' -Sort -Entries $(", 18 | "\t'${2:MenuEntry}'$3", 19 | ")$4" 20 | ], 21 | "description": "Write-Menu using an array" 22 | }, 23 | 24 | "Write-Menu array -Multiselect": { 25 | "prefix": "Menu Array MultiSelect [Write-Menu]", 26 | "body": [ 27 | "Write-Menu -Title '${1:MenuTitle}' -Sort -Multiselect -Entries $(", 28 | "\t'${2:MenuEntry}'$3", 29 | ")$4" 30 | ], 31 | "description": "Write-Menu using an array with -MultiSelect enabled" 32 | }, 33 | 34 | "Write-Menu hashtable": { 35 | "prefix": "Menu Hashtable [Write-Menu]", 36 | "body": [ 37 | "Write-Menu -Title '${1:MenuTitle}' -Sort -Entries @{", 38 | "\t'${2:MenuEntry}' = '${3:Command}'$4", 39 | "}$5" 40 | ], 41 | "description": "Write-Menu using a hashtable" 42 | }, 43 | 44 | "Write-Menu hashtable -Multiselect": { 45 | "prefix": "Menu Hashtable MultiSelect [Write-Menu]", 46 | "body": [ 47 | "Write-Menu -Title '${1:MenuTitle}' -Multiselect -Sort -Entries @{", 48 | "\t'${2:MenuEntry}' = '${3:Command}'$4", 49 | "}$5" 50 | ], 51 | "description": "Write-Menu using a hashtable with -MultiSelect enabled" 52 | }, 53 | 54 | "Write-Menu Nested Array": { 55 | "prefix": "Menu Entry Nested Array [Write-Menu]", 56 | "body": [ 57 | "'${1:NestedMenuTitle}' = @(", 58 | "\t'${2:MenuEntry}'$3", 59 | ")$4" 60 | ], 61 | "description": "Write-Menu nested array entry" 62 | }, 63 | 64 | "Write-Menu Nested Hashtable": { 65 | "prefix": "Menu Entry Nested Hashtable [Write-Menu]", 66 | "body": [ 67 | "'${1:NestedMenuTitle}' = @{", 68 | "\t'${2:MenuEntry}' = '${3:Command}'$4", 69 | "}$5" 70 | ], 71 | "description": "Write-Menu nested hashtable entry" 72 | }, 73 | 74 | "Write-Menu Array Entry": { 75 | "prefix": "Menu Entry Array [Write-Menu]", 76 | "body": [ 77 | "'${2:MenuEntry}'$3" 78 | ], 79 | "description": "Write-Menu array entry" 80 | }, 81 | 82 | "Write-Menu Hashtable Entry": { 83 | "prefix": "Menu Entry Hashtable [Write-Menu]", 84 | "body": [ 85 | "'${2:MenuEntry}' = '${3:Command}'$4" 86 | ], 87 | "description": "Write-Menu hashtable entry" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Write-Menu.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 QuietusPlus 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | #> 24 | 25 | function Write-Menu { 26 | <# 27 | .SYNOPSIS 28 | Outputs a command-line menu which can be navigated using the keyboard. 29 | 30 | .DESCRIPTION 31 | Outputs a command-line menu which can be navigated using the keyboard. 32 | 33 | * Automatically creates multiple pages if the entries cannot fit on-screen. 34 | * Supports nested menus using a combination of hashtables and arrays. 35 | * No entry / page limitations (apart from device performance). 36 | * Sort entries using the -Sort parameter. 37 | * -MultiSelect: Use space to check a selected entry, all checked entries will be invoked / returned upon confirmation. 38 | * Jump to the top / bottom of the page using the "Home" and "End" keys. 39 | * "Scrolling" list effect by automatically switching pages when reaching the top/bottom. 40 | * Nested menu indicator next to entries. 41 | * Remembers parent menus: Opening three levels of nested menus means you have to press "Esc" three times. 42 | 43 | Controls Description 44 | -------- ----------- 45 | Up Previous entry 46 | Down Next entry 47 | Left / PageUp Previous page 48 | Right / PageDown Next page 49 | Home Jump to top 50 | End Jump to bottom 51 | Space Check selection (-MultiSelect only) 52 | Enter Confirm selection 53 | Esc / Backspace Exit / Previous menu 54 | 55 | .EXAMPLE 56 | PS C:\>$menuReturn = Write-Menu -Title 'Menu Title' -Entries @('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4') 57 | 58 | Output: 59 | 60 | Menu Title 61 | 62 | Menu Option 1 63 | Menu Option 2 64 | Menu Option 3 65 | Menu Option 4 66 | 67 | .EXAMPLE 68 | PS C:\>$menuReturn = Write-Menu -Title 'AppxPackages' -Entries (Get-AppxPackage).Name -Sort 69 | 70 | This example uses Write-Menu to sort and list app packages (Windows Store/Modern Apps) that are installed for the current profile. 71 | 72 | .EXAMPLE 73 | PS C:\>$menuReturn = Write-Menu -Title 'Advanced Menu' -Sort -Entries @{ 74 | 'Command Entry' = '(Get-AppxPackage).Name' 75 | 'Invoke Entry' = '@(Get-AppxPackage).Name' 76 | 'Hashtable Entry' = @{ 77 | 'Array Entry' = "@('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4')" 78 | } 79 | } 80 | 81 | This example includes all possible entry types: 82 | 83 | Command Entry Invoke without opening as nested menu (does not contain any prefixes) 84 | Invoke Entry Invoke and open as nested menu (contains the "@" prefix) 85 | Hashtable Entry Opened as a nested menu 86 | Array Entry Opened as a nested menu 87 | 88 | .NOTES 89 | Write-Menu by QuietusPlus (inspired by "Simple Textbased Powershell Menu" [Michael Albert]) 90 | 91 | .LINK 92 | https://quietusplus.github.io/Write-Menu 93 | 94 | .LINK 95 | https://github.com/QuietusPlus/Write-Menu 96 | #> 97 | 98 | [CmdletBinding()] 99 | 100 | <# 101 | Parameters 102 | #> 103 | 104 | param( 105 | # Array or hashtable containing the menu entries 106 | [Parameter(Mandatory=$true, ValueFromPipeline = $true)] 107 | [ValidateNotNullOrEmpty()] 108 | [Alias('InputObject')] 109 | $Entries, 110 | 111 | # Title shown at the top of the menu. 112 | [Parameter(ValueFromPipelineByPropertyName = $true)] 113 | [Alias('Name')] 114 | [string] 115 | $Title, 116 | 117 | # Sort entries before they are displayed. 118 | [Parameter()] 119 | [switch] 120 | $Sort, 121 | 122 | # Select multiple menu entries using space, each selected entry will then get invoked (this will disable nested menu's). 123 | [Parameter()] 124 | [switch] 125 | $MultiSelect 126 | ) 127 | 128 | <# 129 | Configuration 130 | #> 131 | 132 | # Entry prefix, suffix and padding 133 | $script:cfgPrefix = ' ' 134 | $script:cfgPadding = 2 135 | $script:cfgSuffix = ' ' 136 | $script:cfgNested = ' >' 137 | 138 | # Minimum page width 139 | $script:cfgWidth = 30 140 | 141 | # Hide cursor 142 | [System.Console]::CursorVisible = $false 143 | 144 | # Save initial colours 145 | $script:colorForeground = [System.Console]::ForegroundColor 146 | $script:colorBackground = [System.Console]::BackgroundColor 147 | 148 | <# 149 | Checks 150 | #> 151 | 152 | # Check if entries has been passed 153 | if ($Entries -like $null) { 154 | Write-Error "Missing -Entries parameter!" 155 | return 156 | } 157 | 158 | # Check if host is console 159 | if ($host.Name -ne 'ConsoleHost') { 160 | Write-Error "[$($host.Name)] Cannot run inside current host, please use a console window instead!" 161 | return 162 | } 163 | 164 | <# 165 | Set-Color 166 | #> 167 | 168 | function Set-Color ([switch]$Inverted) { 169 | switch ($Inverted) { 170 | $true { 171 | [System.Console]::ForegroundColor = $colorBackground 172 | [System.Console]::BackgroundColor = $colorForeground 173 | } 174 | Default { 175 | [System.Console]::ForegroundColor = $colorForeground 176 | [System.Console]::BackgroundColor = $colorBackground 177 | } 178 | } 179 | } 180 | 181 | <# 182 | Get-Menu 183 | #> 184 | 185 | function Get-Menu ($script:inputEntries) { 186 | # Clear console 187 | Clear-Host 188 | 189 | # Check if -Title has been provided, if so set window title, otherwise set default. 190 | if ($Title -notlike $null) { 191 | $host.UI.RawUI.WindowTitle = $Title 192 | $script:menuTitle = "$Title" 193 | } else { 194 | $script:menuTitle = 'Menu' 195 | } 196 | 197 | # Set menu height 198 | $script:pageSize = ($host.UI.RawUI.WindowSize.Height - 5) 199 | 200 | # Convert entries to object 201 | $script:menuEntries = @() 202 | switch ($inputEntries.GetType().Name) { 203 | 'String' { 204 | # Set total entries 205 | $script:menuEntryTotal = 1 206 | # Create object 207 | $script:menuEntries = New-Object PSObject -Property @{ 208 | Command = '' 209 | Name = $inputEntries 210 | Selected = $false 211 | onConfirm = 'Name' 212 | }; break 213 | } 214 | 'Object[]' { 215 | # Get total entries 216 | $script:menuEntryTotal = $inputEntries.Length 217 | # Loop through array 218 | foreach ($i in 0..$($menuEntryTotal - 1)) { 219 | # Create object 220 | $script:menuEntries += New-Object PSObject -Property @{ 221 | Command = '' 222 | Name = $($inputEntries)[$i] 223 | Selected = $false 224 | onConfirm = 'Name' 225 | }; $i++ 226 | }; break 227 | } 228 | 'Hashtable' { 229 | # Get total entries 230 | $script:menuEntryTotal = $inputEntries.Count 231 | # Loop through hashtable 232 | foreach ($i in 0..($menuEntryTotal - 1)) { 233 | # Check if hashtable contains a single entry, copy values directly if true 234 | if ($menuEntryTotal -eq 1) { 235 | $tempName = $($inputEntries.Keys) 236 | $tempCommand = $($inputEntries.Values) 237 | } else { 238 | $tempName = $($inputEntries.Keys)[$i] 239 | $tempCommand = $($inputEntries.Values)[$i] 240 | } 241 | 242 | # Check if command contains nested menu 243 | if ($tempCommand.GetType().Name -eq 'Hashtable') { 244 | $tempAction = 'Hashtable' 245 | } elseif ($tempCommand.Substring(0,1) -eq '@') { 246 | $tempAction = 'Invoke' 247 | } else { 248 | $tempAction = 'Command' 249 | } 250 | 251 | # Create object 252 | $script:menuEntries += New-Object PSObject -Property @{ 253 | Name = $tempName 254 | Command = $tempCommand 255 | Selected = $false 256 | onConfirm = $tempAction 257 | }; $i++ 258 | }; break 259 | } 260 | Default { 261 | Write-Error "Type `"$($inputEntries.GetType().Name)`" not supported, please use an array or hashtable." 262 | exit 263 | } 264 | } 265 | 266 | # Sort entries 267 | if ($Sort -eq $true) { 268 | $script:menuEntries = $menuEntries | Sort-Object -Property Name 269 | } 270 | 271 | # Get longest entry 272 | $script:entryWidth = ($menuEntries.Name | Measure-Object -Maximum -Property Length).Maximum 273 | # Widen if -MultiSelect is enabled 274 | if ($MultiSelect) { $script:entryWidth += 4 } 275 | # Set minimum entry width 276 | if ($entryWidth -lt $cfgWidth) { $script:entryWidth = $cfgWidth } 277 | # Set page width 278 | $script:pageWidth = $cfgPrefix.Length + $cfgPadding + $entryWidth + $cfgPadding + $cfgSuffix.Length 279 | 280 | # Set current + total pages 281 | $script:pageCurrent = 0 282 | $script:pageTotal = [math]::Ceiling((($menuEntryTotal - $pageSize) / $pageSize)) 283 | 284 | # Insert new line 285 | [System.Console]::WriteLine("") 286 | 287 | # Save title line location + write title 288 | $script:lineTitle = [System.Console]::CursorTop 289 | [System.Console]::WriteLine(" $menuTitle" + "`n") 290 | 291 | # Save first entry line location 292 | $script:lineTop = [System.Console]::CursorTop 293 | } 294 | 295 | <# 296 | Get-Page 297 | #> 298 | 299 | function Get-Page { 300 | # Update header if multiple pages 301 | if ($pageTotal -ne 0) { Update-Header } 302 | 303 | # Clear entries 304 | for ($i = 0; $i -le $pageSize; $i++) { 305 | # Overwrite each entry with whitespace 306 | [System.Console]::WriteLine("".PadRight($pageWidth) + ' ') 307 | } 308 | 309 | # Move cursor to first entry 310 | [System.Console]::CursorTop = $lineTop 311 | 312 | # Get index of first entry 313 | $script:pageEntryFirst = ($pageSize * $pageCurrent) 314 | 315 | # Get amount of entries for last page + fully populated page 316 | if ($pageCurrent -eq $pageTotal) { 317 | $script:pageEntryTotal = ($menuEntryTotal - ($pageSize * $pageTotal)) 318 | } else { 319 | $script:pageEntryTotal = $pageSize 320 | } 321 | 322 | # Set position within console 323 | $script:lineSelected = 0 324 | 325 | # Write all page entries 326 | for ($i = 0; $i -le ($pageEntryTotal - 1); $i++) { 327 | Write-Entry $i 328 | } 329 | } 330 | 331 | <# 332 | Write-Entry 333 | #> 334 | 335 | function Write-Entry ([int16]$Index, [switch]$Update) { 336 | # Check if entry should be highlighted 337 | switch ($Update) { 338 | $true { $lineHighlight = $false; break } 339 | Default { $lineHighlight = ($Index -eq $lineSelected) } 340 | } 341 | 342 | # Page entry name 343 | $pageEntry = $menuEntries[($pageEntryFirst + $Index)].Name 344 | 345 | # Prefix checkbox if -MultiSelect is enabled 346 | if ($MultiSelect) { 347 | switch ($menuEntries[($pageEntryFirst + $Index)].Selected) { 348 | $true { $pageEntry = "[X] $pageEntry"; break } 349 | Default { $pageEntry = "[ ] $pageEntry" } 350 | } 351 | } 352 | 353 | # Full width highlight + Nested menu indicator 354 | switch ($menuEntries[($pageEntryFirst + $Index)].onConfirm -in 'Hashtable', 'Invoke') { 355 | $true { $pageEntry = "$pageEntry".PadRight($entryWidth) + "$cfgNested"; break } 356 | Default { $pageEntry = "$pageEntry".PadRight($entryWidth + $cfgNested.Length) } 357 | } 358 | 359 | # Write new line and add whitespace without inverted colours 360 | [System.Console]::Write("`r" + $cfgPrefix) 361 | # Invert colours if selected 362 | if ($lineHighlight) { Set-Color -Inverted } 363 | # Write page entry 364 | [System.Console]::Write("".PadLeft($cfgPadding) + $pageEntry + "".PadRight($cfgPadding)) 365 | # Restore colours if selected 366 | if ($lineHighlight) { Set-Color } 367 | # Entry suffix 368 | [System.Console]::Write($cfgSuffix + "`n") 369 | } 370 | 371 | <# 372 | Update-Entry 373 | #> 374 | 375 | function Update-Entry ([int16]$Index) { 376 | # Reset current entry 377 | [System.Console]::CursorTop = ($lineTop + $lineSelected) 378 | Write-Entry $lineSelected -Update 379 | 380 | # Write updated entry 381 | $script:lineSelected = $Index 382 | [System.Console]::CursorTop = ($lineTop + $Index) 383 | Write-Entry $lineSelected 384 | 385 | # Move cursor to first entry on page 386 | [System.Console]::CursorTop = $lineTop 387 | } 388 | 389 | <# 390 | Update-Header 391 | #> 392 | 393 | function Update-Header { 394 | # Set corrected page numbers 395 | $pCurrent = ($pageCurrent + 1) 396 | $pTotal = ($pageTotal + 1) 397 | 398 | # Calculate offset 399 | $pOffset = ($pTotal.ToString()).Length 400 | 401 | # Build string, use offset and padding to right align current page number 402 | $script:pageNumber = "{0,-$pOffset}{1,0}" -f "$("$pCurrent".PadLeft($pOffset))","/$pTotal" 403 | 404 | # Move cursor to title 405 | [System.Console]::CursorTop = $lineTitle 406 | # Move cursor to the right 407 | [System.Console]::CursorLeft = ($pageWidth - ($pOffset * 2) - 1) 408 | # Write page indicator 409 | [System.Console]::WriteLine("$pageNumber") 410 | } 411 | 412 | <# 413 | Initialisation 414 | #> 415 | 416 | # Get menu 417 | Get-Menu $Entries 418 | 419 | # Get page 420 | Get-Page 421 | 422 | # Declare hashtable for nested entries 423 | $menuNested = [ordered]@{} 424 | 425 | <# 426 | User Input 427 | #> 428 | 429 | # Loop through user input until valid key has been pressed 430 | do { $inputLoop = $true 431 | 432 | # Move cursor to first entry and beginning of line 433 | [System.Console]::CursorTop = $lineTop 434 | [System.Console]::Write("`r") 435 | 436 | # Get pressed key 437 | $menuInput = [System.Console]::ReadKey($false) 438 | 439 | # Define selected entry 440 | $entrySelected = $menuEntries[($pageEntryFirst + $lineSelected)] 441 | 442 | # Check if key has function attached to it 443 | switch ($menuInput.Key) { 444 | # Exit / Return 445 | { $_ -in 'Escape', 'Backspace' } { 446 | # Return to parent if current menu is nested 447 | if ($menuNested.Count -ne 0) { 448 | $pageCurrent = 0 449 | $Title = $($menuNested.GetEnumerator())[$menuNested.Count - 1].Name 450 | Get-Menu $($menuNested.GetEnumerator())[$menuNested.Count - 1].Value 451 | Get-Page 452 | $menuNested.RemoveAt($menuNested.Count - 1) | Out-Null 453 | # Otherwise exit and return $null 454 | } else { 455 | Clear-Host 456 | $inputLoop = $false 457 | [System.Console]::CursorVisible = $true 458 | return $null 459 | }; break 460 | } 461 | 462 | # Next entry 463 | 'DownArrow' { 464 | if ($lineSelected -lt ($pageEntryTotal - 1)) { # Check if entry isn't last on page 465 | Update-Entry ($lineSelected + 1) 466 | } elseif ($pageCurrent -ne $pageTotal) { # Switch if not on last page 467 | $pageCurrent++ 468 | Get-Page 469 | }; break 470 | } 471 | 472 | # Previous entry 473 | 'UpArrow' { 474 | if ($lineSelected -gt 0) { # Check if entry isn't first on page 475 | Update-Entry ($lineSelected - 1) 476 | } elseif ($pageCurrent -ne 0) { # Switch if not on first page 477 | $pageCurrent-- 478 | Get-Page 479 | Update-Entry ($pageEntryTotal - 1) 480 | }; break 481 | } 482 | 483 | # Select top entry 484 | 'Home' { 485 | if ($lineSelected -ne 0) { # Check if top entry isn't already selected 486 | Update-Entry 0 487 | } elseif ($pageCurrent -ne 0) { # Switch if not on first page 488 | $pageCurrent-- 489 | Get-Page 490 | Update-Entry ($pageEntryTotal - 1) 491 | }; break 492 | } 493 | 494 | # Select bottom entry 495 | 'End' { 496 | if ($lineSelected -ne ($pageEntryTotal - 1)) { # Check if bottom entry isn't already selected 497 | Update-Entry ($pageEntryTotal - 1) 498 | } elseif ($pageCurrent -ne $pageTotal) { # Switch if not on last page 499 | $pageCurrent++ 500 | Get-Page 501 | }; break 502 | } 503 | 504 | # Next page 505 | { $_ -in 'RightArrow','PageDown' } { 506 | if ($pageCurrent -lt $pageTotal) { # Check if already on last page 507 | $pageCurrent++ 508 | Get-Page 509 | }; break 510 | } 511 | 512 | # Previous page 513 | { $_ -in 'LeftArrow','PageUp' } { # Check if already on first page 514 | if ($pageCurrent -gt 0) { 515 | $pageCurrent-- 516 | Get-Page 517 | }; break 518 | } 519 | 520 | # Select/check entry if -MultiSelect is enabled 521 | 'Spacebar' { 522 | if ($MultiSelect) { 523 | switch ($entrySelected.Selected) { 524 | $true { $entrySelected.Selected = $false } 525 | $false { $entrySelected.Selected = $true } 526 | } 527 | Update-Entry ($lineSelected) 528 | }; break 529 | } 530 | 531 | # Select all if -MultiSelect has been enabled 532 | 'Insert' { 533 | if ($MultiSelect) { 534 | $menuEntries | ForEach-Object { 535 | $_.Selected = $true 536 | } 537 | Get-Page 538 | }; break 539 | } 540 | 541 | # Select none if -MultiSelect has been enabled 542 | 'Delete' { 543 | if ($MultiSelect) { 544 | $menuEntries | ForEach-Object { 545 | $_.Selected = $false 546 | } 547 | Get-Page 548 | }; break 549 | } 550 | 551 | # Confirm selection 552 | 'Enter' { 553 | # Check if -MultiSelect has been enabled 554 | if ($MultiSelect) { 555 | Clear-Host 556 | # Process checked/selected entries 557 | $menuEntries | ForEach-Object { 558 | # Entry contains command, invoke it 559 | if (($_.Selected) -and ($_.Command -notlike $null) -and ($entrySelected.Command.GetType().Name -ne 'Hashtable')) { 560 | Invoke-Expression -Command $_.Command 561 | # Return name, entry does not contain command 562 | } elseif ($_.Selected) { 563 | return $_.Name 564 | } 565 | } 566 | # Exit and re-enable cursor 567 | $inputLoop = $false 568 | [System.Console]::CursorVisible = $true 569 | break 570 | } 571 | 572 | # Use onConfirm to process entry 573 | switch ($entrySelected.onConfirm) { 574 | # Return hashtable as nested menu 575 | 'Hashtable' { 576 | $menuNested.$Title = $inputEntries 577 | $Title = $entrySelected.Name 578 | Get-Menu $entrySelected.Command 579 | Get-Page 580 | break 581 | } 582 | 583 | # Invoke attached command and return as nested menu 584 | 'Invoke' { 585 | $menuNested.$Title = $inputEntries 586 | $Title = $entrySelected.Name 587 | Get-Menu $(Invoke-Expression -Command $entrySelected.Command.Substring(1)) 588 | Get-Page 589 | break 590 | } 591 | 592 | # Invoke attached command and exit 593 | 'Command' { 594 | Clear-Host 595 | Invoke-Expression -Command $entrySelected.Command 596 | $inputLoop = $false 597 | [System.Console]::CursorVisible = $true 598 | break 599 | } 600 | 601 | # Return name and exit 602 | 'Name' { 603 | Clear-Host 604 | return $entrySelected.Name 605 | $inputLoop = $false 606 | [System.Console]::CursorVisible = $true 607 | } 608 | } 609 | } 610 | } 611 | } while ($inputLoop) 612 | } 613 | --------------------------------------------------------------------------------