├── .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 | 
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 | 
6 |
7 | ### -Title 'AppxPackages' -Sort -MultiSelect -Entries (Get-AppxPackages).Name
8 |
9 | 
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 |
--------------------------------------------------------------------------------