diff --git a/Write-Menu.ps1 b/Write-Menu.ps1 index 45b0f01..f7e0042 100644 --- a/Write-Menu.ps1 +++ b/Write-Menu.ps1 @@ -1,3 +1,4 @@ +# https://raw.githubusercontent.com/QuietusPlus/Write-Menu/master/Write-Menu.ps1 <# The MIT License (MIT) @@ -22,591 +23,644 @@ SOFTWARE. #> -function Write-Menu { - <# - .SYNOPSIS - Outputs a command-line menu which can be navigated using the keyboard. - - .DESCRIPTION - Outputs a command-line menu which can be navigated using the keyboard. - - * Automatically creates multiple pages if the entries cannot fit on-screen. - * Supports nested menus using a combination of hashtables and arrays. - * No entry / page limitations (apart from device performance). - * Sort entries using the -Sort parameter. - * -MultiSelect: Use space to check a selected entry, all checked entries will be invoked / returned upon confirmation. - * Jump to the top / bottom of the page using the "Home" and "End" keys. - * "Scrolling" list effect by automatically switching pages when reaching the top/bottom. - * Nested menu indicator next to entries. - * Remembers parent menus: Opening three levels of nested menus means you have to press "Esc" three times. - - Controls Description - -------- ----------- - Up Previous entry - Down Next entry - Left / PageUp Previous page - Right / PageDown Next page - Home Jump to top - End Jump to bottom - Space Check selection (-MultiSelect only) - Enter Confirm selection - Esc / Backspace Exit / Previous menu - - .EXAMPLE - PS C:\>$menuReturn = Write-Menu -Title 'Menu Title' -Entries @('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4') - - Output: - - Menu Title - - Menu Option 1 - Menu Option 2 - Menu Option 3 - Menu Option 4 - - .EXAMPLE - PS C:\>$menuReturn = Write-Menu -Title 'AppxPackages' -Entries (Get-AppxPackage).Name -Sort - - This example uses Write-Menu to sort and list app packages (Windows Store/Modern Apps) that are installed for the current profile. - - .EXAMPLE - PS C:\>$menuReturn = Write-Menu -Title 'Advanced Menu' -Sort -Entries @{ - 'Command Entry' = '(Get-AppxPackage).Name' - 'Invoke Entry' = '@(Get-AppxPackage).Name' - 'Hashtable Entry' = @{ - 'Array Entry' = "@('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4')" - } - } - - This example includes all possible entry types: - - Command Entry Invoke without opening as nested menu (does not contain any prefixes) - Invoke Entry Invoke and open as nested menu (contains the "@" prefix) - Hashtable Entry Opened as a nested menu - Array Entry Opened as a nested menu - - .NOTES - Write-Menu by QuietusPlus (inspired by "Simple Textbased Powershell Menu" [Michael Albert]) - - .LINK - https://quietusplus.github.io/Write-Menu - - .LINK - https://github.com/QuietusPlus/Write-Menu - #> - - [CmdletBinding()] - - <# - Parameters - #> - - param( - # Array or hashtable containing the menu entries - [Parameter(Mandatory=$true, ValueFromPipeline = $true)] - [ValidateNotNullOrEmpty()] - [Alias('InputObject')] - $Entries, - - # Title shown at the top of the menu. - [Parameter(ValueFromPipelineByPropertyName = $true)] - [Alias('Name')] - [string] - $Title, - - # Sort entries before they are displayed. - [Parameter()] - [switch] - $Sort, - - # Select multiple menu entries using space, each selected entry will then get invoked (this will disable nested menu's). - [Parameter()] - [switch] - $MultiSelect - ) - - <# +FUNCTION Write-Menu { +<# + .SYNOPSIS + Outputs a command-line menu which can be navigated using the keyboard. + + .DESCRIPTION + Outputs a command-line menu which can be navigated using the keyboard. + + * Automatically creates multiple pages if the entries cannot fit on-screen. + * Supports nested menus using a combination of hashtables and arrays. + * No entry / page limitations (apart from device performance). + * Sort entries using the -Sort parameter. + * -MultiSelect: Use space to check a selected entry, all checked entries will be invoked / returned upon confirmation. + * Jump to the top / bottom of the page using the "Home" and "End" keys. + * "Scrolling" list effect by automatically switching pages when reaching the top/bottom. + * Nested menu indicator next to entries. + * Remembers parent menus: Opening three levels of nested menus means you have to press "Esc" three times. + + Controls Description + -------- ----------- + Up Previous entry + Down Next entry + Left / PageUp Previous page + Right / PageDown Next page + Home Jump to top + End Jump to bottom + Space Check selection (-MultiSelect only) + Enter Confirm selection + Esc / Backspace Exit / Previous menu + + .PARAMETER Entries + Array or hashtable containing the menu entries + + .PARAMETER Title + Title shown at the top of the menu. + + .PARAMETER Sort + Sort entries before they are displayed. + + .PARAMETER MultiSelect + Select multiple menu entries using space, each selected entry will then get invoked (this will disable nested menu's). + + .PARAMETER NameProperty + A description of the NameProperty parameter. + + .PARAMETER ReturnProperty + A description of the ReturnProperty parameter. + + .EXAMPLE + PS C:\>$menuReturn = Write-Menu -Title 'Menu Title' -Entries @('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4') + + Output: + + Menu Title + + Menu Option 1 + Menu Option 2 + Menu Option 3 + Menu Option 4 + + .EXAMPLE + PS C:\>$menuReturn = Write-Menu -Title 'AppxPackages' -Entries (Get-AppxPackage).Name -Sort + + This example uses Write-Menu to sort and list app packages (Windows Store/Modern Apps) that are installed for the current profile. + + .EXAMPLE + PS C:\>$menuReturn = Write-Menu -Title 'Advanced Menu' -Sort -Entries @{ + 'Command Entry' = '(Get-AppxPackage).Name' + 'Invoke Entry' = '@(Get-AppxPackage).Name' + 'Hashtable Entry' = @{ + 'Array Entry' = "@('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4')" + } + } + + This example includes all possible entry types: + + Command Entry Invoke without opening as nested menu (does not contain any prefixes) + Invoke Entry Invoke and open as nested menu (contains the "@" prefix) + Hashtable Entry Opened as a nested menu + Array Entry Opened as a nested menu + + .NOTES + Write-Menu by QuietusPlus (inspired by "Simple Textbased Powershell Menu" [Michael Albert]) + + .LINK + https://quietusplus.github.io/Write-Menu + + .LINK + https://github.com/QuietusPlus/Write-Menu +#> + + [CmdletBinding()] + PARAM + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [Alias('InputObject')] + $Entries, + [Parameter(ValueFromPipelineByPropertyName = $true)] + [Alias('Name')] + [string]$Title, + [switch]$Sort, + [switch]$MultiSelect, + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string]$NameProperty = 'Name', + [ValidateSet('Name', 'Value')] + [string]$ReturnProperty = 'Name' + ) + + BEGIN { + <# Configuration #> - - # Entry prefix, suffix and padding - $script:cfgPrefix = ' ' - $script:cfgPadding = 2 - $script:cfgSuffix = ' ' - $script:cfgNested = ' >' - - # Minimum page width - $script:cfgWidth = 30 - - # Hide cursor - [System.Console]::CursorVisible = $false - - # Save initial colours - $script:colorForeground = [System.Console]::ForegroundColor - $script:colorBackground = [System.Console]::BackgroundColor - + + $script:WriteMenuConfiguration = New-Object System.Management.Automation.PSObject -Property @{ + # Entry prefix, suffix and padding + Prefix = ' ' + Padding = 2 + Suffix = ' ' + Nested = ' >' + + # Minimum page width + Width = 30 + entryWidth = $null + pageWidth = $null + + # Save initial colours + ForegroundColor = [System.Console]::ForegroundColor + BackgroundColor = [System.Console]::BackgroundColor + + # Save initial window title + InitialWindowTitle = $host.UI.RawUI.WindowTitle + WindowTitle = $Title + # Set menu height + pageSize = ($host.UI.RawUI.WindowSize.Height - 5) + pageTotal = $null + + pageCurrent = 0 + menuEntries = $null + menuEntryTotal = $null + } + Set-Variable -Scope script -Name WriteMenuConfiguration -Visibility Private + + # Hide cursor + [System.Console]::CursorVisible = $false + + + FUNCTION Invoke-CleanUp { + [System.Console]::CursorVisible = $true + $host.UI.RawUI.WindowTitle = $script:WriteMenuConfiguration.InitialWindowTitle + } + <# Checks - #> - - # Check if entries has been passed - if ($Entries -like $null) { - Write-Error "Missing -Entries parameter!" - return - } - - # Check if host is console - if ($host.Name -ne 'ConsoleHost') { - Write-Error "[$($host.Name)] Cannot run inside current host, please use a console window instead!" - return - } - + #> + # Check if entries has been passed + IF ($null -eq $Entries) { + Invoke-CleanUp + THROW "Missing -Entries parameter!" + } + + # Check if host is console + IF ($host.Name -ne 'ConsoleHost') { + Invoke-CleanUp + THROW "[$($host.Name)] Cannot run inside current host, please use a console window instead!" + } + + <# Set-Color #> - - function Set-Color ([switch]$Inverted) { - switch ($Inverted) { - $true { - [System.Console]::ForegroundColor = $colorBackground - [System.Console]::BackgroundColor = $colorForeground - } - Default { - [System.Console]::ForegroundColor = $colorForeground - [System.Console]::BackgroundColor = $colorBackground - } - } - } - + + FUNCTION Set-Color ([switch]$Inverted) { + SWITCH ($Inverted) { + $true { + [System.Console]::ForegroundColor = $script:WriteMenuConfiguration.BackgroundColor + [System.Console]::BackgroundColor = $script:WriteMenuConfiguration.ForegroundColor + } + DEFAULT { + [System.Console]::ForegroundColor = $script:WriteMenuConfiguration.ForegroundColor + [System.Console]::BackgroundColor = $script:WriteMenuConfiguration.BackgroundColor + } + } + } + <# Get-Menu #> - - function Get-Menu ($script:inputEntries) { - # Clear console - Clear-Host - - # Check if -Title has been provided, if so set window title, otherwise set default. - if ($Title -notlike $null) { - $host.UI.RawUI.WindowTitle = $Title - $script:menuTitle = "$Title" - } else { - $script:menuTitle = 'Menu' - } - - # Set menu height - $script:pageSize = ($host.UI.RawUI.WindowSize.Height - 5) - - # Convert entries to object - $script:menuEntries = @() - switch ($inputEntries.GetType().Name) { - 'String' { - # Set total entries - $script:menuEntryTotal = 1 - # Create object - $script:menuEntries = New-Object PSObject -Property @{ - Command = '' - Name = $inputEntries - Selected = $false - onConfirm = 'Name' - }; break - } - 'Object[]' { - # Get total entries - $script:menuEntryTotal = $inputEntries.Length - # Loop through array - foreach ($i in 0..$($menuEntryTotal - 1)) { - # Create object - $script:menuEntries += New-Object PSObject -Property @{ - Command = '' - Name = $($inputEntries)[$i] - Selected = $false - onConfirm = 'Name' - }; $i++ - }; break - } - 'Hashtable' { - # Get total entries - $script:menuEntryTotal = $inputEntries.Count - # Loop through hashtable - foreach ($i in 0..($menuEntryTotal - 1)) { - # Check if hashtable contains a single entry, copy values directly if true - if ($menuEntryTotal -eq 1) { - $tempName = $($inputEntries.Keys) - $tempCommand = $($inputEntries.Values) - } else { - $tempName = $($inputEntries.Keys)[$i] - $tempCommand = $($inputEntries.Values)[$i] - } - - # Check if command contains nested menu - if ($tempCommand.GetType().Name -eq 'Hashtable') { - $tempAction = 'Hashtable' - } elseif ($tempCommand.Substring(0,1) -eq '@') { - $tempAction = 'Invoke' - } else { - $tempAction = 'Command' - } - - # Create object - $script:menuEntries += New-Object PSObject -Property @{ - Name = $tempName - Command = $tempCommand - Selected = $false - onConfirm = $tempAction - }; $i++ - }; break - } - Default { - Write-Error "Type `"$($inputEntries.GetType().Name)`" not supported, please use an array or hashtable." - exit - } - } - - # Sort entries - if ($Sort -eq $true) { - $script:menuEntries = $menuEntries | Sort-Object -Property Name - } - - # Get longest entry - $script:entryWidth = ($menuEntries.Name | Measure-Object -Maximum -Property Length).Maximum - # Widen if -MultiSelect is enabled - if ($MultiSelect) { $script:entryWidth += 4 } - # Set minimum entry width - if ($entryWidth -lt $cfgWidth) { $script:entryWidth = $cfgWidth } - # Set page width - $script:pageWidth = $cfgPrefix.Length + $cfgPadding + $entryWidth + $cfgPadding + $cfgSuffix.Length - - # Set current + total pages - $script:pageCurrent = 0 - $script:pageTotal = [math]::Ceiling((($menuEntryTotal - $pageSize) / $pageSize)) - - # Insert new line - [System.Console]::WriteLine("") - - # Save title line location + write title - $script:lineTitle = [System.Console]::CursorTop - [System.Console]::WriteLine(" $menuTitle" + "`n") - - # Save first entry line location - $script:lineTop = [System.Console]::CursorTop - } - + + FUNCTION Get-Menu ($script:inputEntries) { + # Clear console + Clear-Host + + # Check if -Title has been provided, if so set window title, otherwise set default. + IF ($Title -notlike $null) { + $script:WriteMenuConfiguration.WindowTitle = $Title + $host.UI.RawUI.WindowTitle = $script:WriteMenuConfiguration.WindowTitle + } ELSE { + $script:WriteMenuConfiguration.WindowTitle = 'Menu' + } + + # Convert entries to object + $script:WriteMenuConfiguration.menuEntries = @() + SWITCH ($inputEntries.GetType().Name) { + 'String' { + # Set total entries + $script:WriteMenuConfiguration.menuEntryTotal = 1 + $script:menuEntryTotal = 1 + # Create object + $script:WriteMenuConfiguration.menuEntries = New-Object PSObject -Property @{ + Command = '' + Name = $inputEntries + Value = $inputEntries + Selected = $false + onConfirm = 'Name' + }; BREAK + } + 'Object[]' { + # Get total entries + $script:WriteMenuConfiguration.menuEntryTotal = $inputEntries.Length + # Loop through array + FOREACH ($i IN 0 .. $($script:WriteMenuConfiguration.menuEntryTotal - 1)) { + # Create object + $script:WriteMenuConfiguration.menuEntries += New-Object PSObject -Property @{ + Command = '' + Name = $($inputEntries)[$i].($NameProperty) + Value = $($inputEntries)[$i] + Selected = $false + onConfirm = 'Name' + }; $i++ + }; BREAK + } + 'Hashtable' { + # Get total entries + $script:WriteMenuConfiguration.menuEntryTotal = $inputEntries.Count + # Loop through hashtable + FOREACH ($i IN 0 .. ($script:WriteMenuConfiguration.menuEntryTotal - 1)) { + # Check if hashtable contains a single entry, copy values directly if true + IF ($script:WriteMenuConfiguration.menuEntryTotal -eq 1) { + $tempName = $($inputEntries.Keys) + $tempCommand = $($inputEntries.Values) + } ELSE { + $tempName = $($inputEntries.Keys)[$i] + $tempCommand = $($inputEntries.Values)[$i] + } + + # Check if command contains nested menu + IF ($tempCommand.GetType().Name -eq 'Hashtable') { + $tempAction = 'Hashtable' + } ELSEIF ($tempCommand.Substring(0, 1) -eq '@') { + $tempAction = 'Invoke' + } ELSE { + $tempAction = 'Command' + } + + # Create object + $script:WriteMenuConfiguration.menuEntries += New-Object PSObject -Property @{ + Name = $tempName + Value = $tempName + Command = $tempCommand + Selected = $false + onConfirm = $tempAction + }; $i++ + }; BREAK + } + DEFAULT { + THROW "Type `"$($inputEntries.GetType().Name)`" not supported, please use an array or hashtable." + } + } + + # Sort entries + IF ($Sort -eq $true) { + $script:WriteMenuConfiguration.menuEntries = $script:WriteMenuConfiguration.menuEntries | Sort-Object -Property Name + } + + # Get longest entry + $script:WriteMenuConfiguration.entryWidth = ($script:WriteMenuConfiguration.menuEntries.Name | Measure-Object -Maximum -Property Length).Maximum + + # Widen if -MultiSelect is enabled + IF ($MultiSelect) { $script:WriteMenuConfiguration.entryWidth += 4 } + # Set minimum entry width + IF ($script:WriteMenuConfiguration.entryWidth -lt $script:WriteMenuConfiguration.Width) { $script:WriteMenuConfiguration.entryWidth = $script:WriteMenuConfiguration.Width } + # Set page width + $script:WriteMenuConfiguration.pageWidth = $script:WriteMenuConfiguration.Prefix.Length + $script:WriteMenuConfiguration.Padding + $script:WriteMenuConfiguration.entryWidth + $script:WriteMenuConfiguration.Padding + $script:WriteMenuConfiguration.Suffix.Length + + # Set current + total pages + $script:WriteMenuConfiguration.pageCurrent = 0 + $script:WriteMenuConfiguration.pageTotal = [math]::Ceiling((($script:WriteMenuConfiguration.menuEntryTotal - $script:WriteMenuConfiguration.pageSize) / $script:WriteMenuConfiguration.pageSize)) + + # Insert new line + [System.Console]::WriteLine("") + + # Save title line location + write title + $script:lineTitle = [System.Console]::CursorTop + [System.Console]::WriteLine(" $($script:WriteMenuConfiguration.WindowTitle)" + "`n") + + # Save first entry line location + $script:lineTop = [System.Console]::CursorTop + } + <# Get-Page #> - - function Get-Page { - # Update header if multiple pages - if ($pageTotal -ne 0) { Update-Header } - - # Clear entries - for ($i = 0; $i -le $pageSize; $i++) { - # Overwrite each entry with whitespace - [System.Console]::WriteLine("".PadRight($pageWidth) + ' ') - } - - # Move cursor to first entry - [System.Console]::CursorTop = $lineTop - - # Get index of first entry - $script:pageEntryFirst = ($pageSize * $pageCurrent) - - # Get amount of entries for last page + fully populated page - if ($pageCurrent -eq $pageTotal) { - $script:pageEntryTotal = ($menuEntryTotal - ($pageSize * $pageTotal)) - } else { - $script:pageEntryTotal = $pageSize - } - - # Set position within console - $script:lineSelected = 0 - - # Write all page entries - for ($i = 0; $i -le ($pageEntryTotal - 1); $i++) { - Write-Entry $i - } - } - + + FUNCTION Get-Page { + # Update header if multiple pages + IF ($script:WriteMenuConfiguration.pageTotal -ne 0) { Update-Header } + + # Clear entries + FOR ($i = 0; $i -le $script:WriteMenuConfiguration.pageSize; $i++) { + # Overwrite each entry with whitespace + [System.Console]::WriteLine("".PadRight($script:WriteMenuConfiguration.pageWidth) + ' ') + } + + # Move cursor to first entry + [System.Console]::CursorTop = $lineTop + + # Get index of first entry + $script:pageEntryFirst = ($script:WriteMenuConfiguration.pageSize * $script:WriteMenuConfiguration.pageCurrent) + + # Get amount of entries for last page + fully populated page + IF ($script:WriteMenuConfiguration.pageCurrent -eq $script:WriteMenuConfiguration.pageTotal) { + $script:pageEntryTotal = ($script:WriteMenuConfiguration.menuEntryTotal - ($script:WriteMenuConfiguration.pageSize * $script:WriteMenuConfiguration.pageTotal)) + } ELSE { + $script:pageEntryTotal = $script:WriteMenuConfiguration.pageSize + } + + # Set position within console + $script:lineSelected = 0 + + # Write all page entries + FOR ($i = 0; $i -le ($pageEntryTotal - 1); $i++) { + Write-Entry $i + } + } + <# Write-Entry #> - - function Write-Entry ([int16]$Index, [switch]$Update) { - # Check if entry should be highlighted - switch ($Update) { - $true { $lineHighlight = $false; break } - Default { $lineHighlight = ($Index -eq $lineSelected) } - } - - # Page entry name - $pageEntry = $menuEntries[($pageEntryFirst + $Index)].Name - - # Prefix checkbox if -MultiSelect is enabled - if ($MultiSelect) { - switch ($menuEntries[($pageEntryFirst + $Index)].Selected) { - $true { $pageEntry = "[X] $pageEntry"; break } - Default { $pageEntry = "[ ] $pageEntry" } - } - } - - # Full width highlight + Nested menu indicator - switch ($menuEntries[($pageEntryFirst + $Index)].onConfirm -in 'Hashtable', 'Invoke') { - $true { $pageEntry = "$pageEntry".PadRight($entryWidth) + "$cfgNested"; break } - Default { $pageEntry = "$pageEntry".PadRight($entryWidth + $cfgNested.Length) } - } - - # Write new line and add whitespace without inverted colours - [System.Console]::Write("`r" + $cfgPrefix) - # Invert colours if selected - if ($lineHighlight) { Set-Color -Inverted } - # Write page entry - [System.Console]::Write("".PadLeft($cfgPadding) + $pageEntry + "".PadRight($cfgPadding)) - # Restore colours if selected - if ($lineHighlight) { Set-Color } - # Entry suffix - [System.Console]::Write($cfgSuffix + "`n") - } - + + FUNCTION Write-Entry ([int16]$Index, [switch]$Update) { + # Check if entry should be highlighted + SWITCH ($Update) { + $true { $lineHighlight = $false; BREAK } + DEFAULT { $lineHighlight = ($Index -eq $lineSelected) } + } + + # Page entry name + $pageEntry = $script:WriteMenuConfiguration.menuEntries[($pageEntryFirst + $Index)].Name + + # Prefix checkbox if -MultiSelect is enabled + IF ($MultiSelect) { + SWITCH ($script:WriteMenuConfiguration.menuEntries[($pageEntryFirst + $Index)].Selected) { + $true { $pageEntry = "[X] $pageEntry"; BREAK } + DEFAULT { $pageEntry = "[ ] $pageEntry" } + } + } + + # Full width highlight + Nested menu indicator + SWITCH ($script:WriteMenuConfiguration.menuEntries[($pageEntryFirst + $Index)].onConfirm -in 'Hashtable', 'Invoke') { + $true { $pageEntry = "$pageEntry".PadRight($script:WriteMenuConfiguration.entryWidth) + "$($script:WriteMenuConfiguration.Nested)"; BREAK } + DEFAULT { $pageEntry = "$pageEntry".PadRight($script:WriteMenuConfiguration.entryWidth + $script:WriteMenuConfiguration.Nested.Length) } + } + + # Write new line and add whitespace without inverted colours + [System.Console]::Write("`r" + $script:WriteMenuConfiguration.Prefix) + # Invert colours if selected + IF ($lineHighlight) { Set-Color -Inverted } + # Write page entry + [System.Console]::Write("".PadLeft($script:WriteMenuConfiguration.Padding) + $pageEntry + "".PadRight($script:WriteMenuConfiguration.Padding)) + # Restore colours if selected + IF ($lineHighlight) { Set-Color } + # Entry suffix + [System.Console]::Write($script:WriteMenuConfiguration.Suffix + "`n") + } + <# Update-Entry #> - - function Update-Entry ([int16]$Index) { - # Reset current entry - [System.Console]::CursorTop = ($lineTop + $lineSelected) - Write-Entry $lineSelected -Update - - # Write updated entry - $script:lineSelected = $Index - [System.Console]::CursorTop = ($lineTop + $Index) - Write-Entry $lineSelected - - # Move cursor to first entry on page - [System.Console]::CursorTop = $lineTop - } - + + FUNCTION Update-Entry ([int16]$Index) { + # Reset current entry + [System.Console]::CursorTop = ($lineTop + $lineSelected) + Write-Entry $lineSelected -Update + + # Write updated entry + $script:lineSelected = $Index + [System.Console]::CursorTop = ($lineTop + $Index) + Write-Entry $lineSelected + + # Move cursor to first entry on page + [System.Console]::CursorTop = $lineTop + } + <# Update-Header #> - - function Update-Header { - # Set corrected page numbers - $pCurrent = ($pageCurrent + 1) - $pTotal = ($pageTotal + 1) - - # Calculate offset - $pOffset = ($pTotal.ToString()).Length - - # Build string, use offset and padding to right align current page number - $script:pageNumber = "{0,-$pOffset}{1,0}" -f "$("$pCurrent".PadLeft($pOffset))","/$pTotal" - - # Move cursor to title - [System.Console]::CursorTop = $lineTitle - # Move cursor to the right - [System.Console]::CursorLeft = ($pageWidth - ($pOffset * 2) - 1) - # Write page indicator - [System.Console]::WriteLine("$pageNumber") - } - - <# + + FUNCTION Update-Header { + # Set corrected page numbers + $pCurrent = ($script:WriteMenuConfiguration.pageCurrent + 1) + $pTotal = ($script:WriteMenuConfiguration.pageTotal + 1) + + # Calculate offset + $pOffset = ($pTotal.ToString()).Length + + # Build string, use offset and padding to right align current page number + $script:pageNumber = "{0,-$pOffset}{1,0}" -f "$("$pCurrent".PadLeft($pOffset))", "/$pTotal" + + # Move cursor to title + [System.Console]::CursorTop = $lineTitle + # Move cursor to the right + [System.Console]::CursorLeft = ($script:WriteMenuConfiguration.pageWidth - ($pOffset * 2) - 1) + # Write page indicator + [System.Console]::WriteLine("$pageNumber") + } + } + PROCESS { + <# Initialisation #> - - # Get menu - Get-Menu $Entries - - # Get page - Get-Page - - # Declare hashtable for nested entries - $menuNested = [ordered]@{} - + + # Get menu + Get-Menu $Entries + + # Get page + Get-Page + + # Declare hashtable for nested entries + $menuNested = [ordered]@{ } + <# User Input #> - - # Loop through user input until valid key has been pressed - do { $inputLoop = $true - - # Move cursor to first entry and beginning of line - [System.Console]::CursorTop = $lineTop - [System.Console]::Write("`r") - - # Get pressed key - $menuInput = [System.Console]::ReadKey($false) - - # Define selected entry - $entrySelected = $menuEntries[($pageEntryFirst + $lineSelected)] - - # Check if key has function attached to it - switch ($menuInput.Key) { - # Exit / Return - { $_ -in 'Escape', 'Backspace' } { - # Return to parent if current menu is nested - if ($menuNested.Count -ne 0) { - $pageCurrent = 0 - $Title = $($menuNested.GetEnumerator())[$menuNested.Count - 1].Name - Get-Menu $($menuNested.GetEnumerator())[$menuNested.Count - 1].Value - Get-Page - $menuNested.RemoveAt($menuNested.Count - 1) | Out-Null - # Otherwise exit and return $null - } else { - Clear-Host - $inputLoop = $false - [System.Console]::CursorVisible = $true - return $null - }; break - } - - # Next entry - 'DownArrow' { - if ($lineSelected -lt ($pageEntryTotal - 1)) { # Check if entry isn't last on page - Update-Entry ($lineSelected + 1) - } elseif ($pageCurrent -ne $pageTotal) { # Switch if not on last page - $pageCurrent++ - Get-Page - }; break - } - - # Previous entry - 'UpArrow' { - if ($lineSelected -gt 0) { # Check if entry isn't first on page - Update-Entry ($lineSelected - 1) - } elseif ($pageCurrent -ne 0) { # Switch if not on first page - $pageCurrent-- - Get-Page - Update-Entry ($pageEntryTotal - 1) - }; break - } - - # Select top entry - 'Home' { - if ($lineSelected -ne 0) { # Check if top entry isn't already selected - Update-Entry 0 - } elseif ($pageCurrent -ne 0) { # Switch if not on first page - $pageCurrent-- - Get-Page - Update-Entry ($pageEntryTotal - 1) - }; break - } - - # Select bottom entry - 'End' { - if ($lineSelected -ne ($pageEntryTotal - 1)) { # Check if bottom entry isn't already selected - Update-Entry ($pageEntryTotal - 1) - } elseif ($pageCurrent -ne $pageTotal) { # Switch if not on last page - $pageCurrent++ - Get-Page - }; break - } - - # Next page - { $_ -in 'RightArrow','PageDown' } { - if ($pageCurrent -lt $pageTotal) { # Check if already on last page - $pageCurrent++ - Get-Page - }; break - } - - # Previous page - { $_ -in 'LeftArrow','PageUp' } { # Check if already on first page - if ($pageCurrent -gt 0) { - $pageCurrent-- - Get-Page - }; break - } - - # Select/check entry if -MultiSelect is enabled - 'Spacebar' { - if ($MultiSelect) { - switch ($entrySelected.Selected) { - $true { $entrySelected.Selected = $false } - $false { $entrySelected.Selected = $true } - } - Update-Entry ($lineSelected) - }; break - } - - # Select all if -MultiSelect has been enabled - 'Insert' { - if ($MultiSelect) { - $menuEntries | ForEach-Object { - $_.Selected = $true - } - Get-Page - }; break - } - - # Select none if -MultiSelect has been enabled - 'Delete' { - if ($MultiSelect) { - $menuEntries | ForEach-Object { - $_.Selected = $false - } - Get-Page - }; break - } - - # Confirm selection - 'Enter' { - # Check if -MultiSelect has been enabled - if ($MultiSelect) { - Clear-Host - # Process checked/selected entries - $menuEntries | ForEach-Object { - # Entry contains command, invoke it - if (($_.Selected) -and ($_.Command -notlike $null) -and ($entrySelected.Command.GetType().Name -ne 'Hashtable')) { - Invoke-Expression -Command $_.Command - # Return name, entry does not contain command - } elseif ($_.Selected) { - return $_.Name - } - } - # Exit and re-enable cursor - $inputLoop = $false - [System.Console]::CursorVisible = $true - break - } - - # Use onConfirm to process entry - switch ($entrySelected.onConfirm) { - # Return hashtable as nested menu - 'Hashtable' { - $menuNested.$Title = $inputEntries - $Title = $entrySelected.Name - Get-Menu $entrySelected.Command - Get-Page - break - } - - # Invoke attached command and return as nested menu - 'Invoke' { - $menuNested.$Title = $inputEntries - $Title = $entrySelected.Name - Get-Menu $(Invoke-Expression -Command $entrySelected.Command.Substring(1)) - Get-Page - break - } - - # Invoke attached command and exit - 'Command' { - Clear-Host - Invoke-Expression -Command $entrySelected.Command - $inputLoop = $false - [System.Console]::CursorVisible = $true - break - } - - # Return name and exit - 'Name' { - Clear-Host - return $entrySelected.Name - $inputLoop = $false - [System.Console]::CursorVisible = $true - } - } - } - } - } while ($inputLoop) -} + + # Loop through user input until valid key has been pressed + TRY { + DO { + $inputLoop = $true + + # Move cursor to first entry and beginning of line + [System.Console]::CursorTop = $lineTop + [System.Console]::Write("`r") + + # Get pressed key + $menuInput = [System.Console]::ReadKey($false) + + # Define selected entry + $entrySelected = $script:WriteMenuConfiguration.menuEntries[($pageEntryFirst + $lineSelected)] + + # Check if key has function attached to it + SWITCH ($menuInput.Key) { + # Exit / Return + { $_ -in 'Escape', 'Backspace' } { + # Return to parent if current menu is nested + IF ($menuNested.Count -ne 0) { + $script:WriteMenuConfiguration.pageCurrent = 0 + $Title = $($menuNested.GetEnumerator())[$menuNested.Count - 1].Name + Get-Menu $($menuNested.GetEnumerator())[$menuNested.Count - 1].Value + Get-Page + $menuNested.RemoveAt($menuNested.Count - 1) | Out-Null + # Otherwise exit and return $null + } ELSE { + Clear-Host + $inputLoop = $false + Invoke-CleanUp + RETURN $null + }; BREAK + } + + # Next entry + 'DownArrow' { + IF ($lineSelected -lt ($pageEntryTotal - 1)) { + # Check if entry isn't last on page + Update-Entry ($lineSelected + 1) + } ELSEIF ($script:WriteMenuConfiguration.pageCurrent -ne $script:WriteMenuConfiguration.pageTotal) { + # Switch if not on last page + $script:WriteMenuConfiguration.pageCurrent++ + Get-Page + }; BREAK + } + + # Previous entry + 'UpArrow' { + IF ($lineSelected -gt 0) { + # Check if entry isn't first on page + Update-Entry ($lineSelected - 1) + } ELSEIF ($script:WriteMenuConfiguration.pageCurrent -ne 0) { + # Switch if not on first page + $script:WriteMenuConfiguration.pageCurrent-- + Get-Page + Update-Entry ($pageEntryTotal - 1) + }; BREAK + } + + # Select top entry + 'Home' { + IF ($lineSelected -ne 0) { + # Check if top entry isn't already selected + Update-Entry 0 + } ELSEIF ($script:WriteMenuConfiguration.pageCurrent -ne 0) { + # Switch if not on first page + $script:WriteMenuConfiguration.pageCurrent-- + Get-Page + Update-Entry ($pageEntryTotal - 1) + }; BREAK + } + + # Select bottom entry + 'End' { + IF ($lineSelected -ne ($pageEntryTotal - 1)) { + # Check if bottom entry isn't already selected + Update-Entry ($pageEntryTotal - 1) + } ELSEIF ($script:WriteMenuConfiguration.pageCurrent -ne $script:WriteMenuConfiguration.pageTotal) { + # Switch if not on last page + $script:WriteMenuConfiguration.pageCurrent++ + Get-Page + }; BREAK + } + + # Next page + { $_ -in 'RightArrow', 'PageDown' } { + IF ($script:WriteMenuConfiguration.pageCurrent -lt $script:WriteMenuConfiguration.pageTotal) { + # Check if already on last page + $script:WriteMenuConfiguration.pageCurrent++ + Get-Page + }; BREAK + } + + # Previous page + { $_ -in 'LeftArrow', 'PageUp' } { + # Check if already on first page + IF ($script:WriteMenuConfiguration.pageCurrent -gt 0) { + $script:WriteMenuConfiguration.pageCurrent-- + Get-Page + }; BREAK + } + + # Select/check entry if -MultiSelect is enabled + 'Spacebar' { + IF ($MultiSelect) { + SWITCH ($entrySelected.Selected) { + $true { $entrySelected.Selected = $false } + $false { $entrySelected.Selected = $true } + } + Update-Entry ($lineSelected) + }; BREAK + } + + # Select all if -MultiSelect has been enabled + 'Insert' { + IF ($MultiSelect) { + $script:WriteMenuConfiguration.menuEntries | ForEach-Object { + $_.Selected = $true + } + Get-Page + }; BREAK + } + + # Select none if -MultiSelect has been enabled + 'Delete' { + IF ($MultiSelect) { + $script:WriteMenuConfiguration.menuEntries | ForEach-Object { + $_.Selected = $false + } + Get-Page + }; BREAK + } + + # Confirm selection + 'Enter' { + # Check if -MultiSelect has been enabled + IF ($MultiSelect) { + Clear-Host + # Process checked/selected entries + $script:WriteMenuConfiguration.menuEntries | ForEach-Object { + # Entry contains command, invoke it + IF (($_.Selected) -and ($_.Command -notlike $null) -and ($entrySelected.Command.GetType().Name -ne 'Hashtable')) { + Invoke-Expression -Command $_.Command + # Return name, entry does not contain command + } ELSEIF ($_.Selected) { + Invoke-CleanUp + RETURN $_.($ReturnProperty) + } + } + # Exit and re-enable cursor + $inputLoop = $false + [System.Console]::CursorVisible = $true + BREAK + } + + # Use onConfirm to process entry + SWITCH ($entrySelected.onConfirm) { + # Return hashtable as nested menu + 'Hashtable' { + $menuNested.$Title = $inputEntries + $Title = $entrySelected.Name + Get-Menu $entrySelected.Command + Get-Page + BREAK + } + + # Invoke attached command and return as nested menu + 'Invoke' { + $menuNested.$Title = $inputEntries + $Title = $entrySelected.Name + Get-Menu $(Invoke-Expression -Command $entrySelected.Command.Substring(1)) + Get-Page + BREAK + } + + # Invoke attached command and exit + 'Command' { + Clear-Host + Invoke-Expression -Command $entrySelected.Command + $inputLoop = $false + BREAK + } + + # Return name and exit + 'Name' { + Clear-Host + $inputLoop = $false + Invoke-CleanUp + RETURN $entrySelected.($ReturnProperty) + } + } + } + } + } WHILE ($inputLoop) + } CATCH { + THROW $_ + } FINALLY { + Invoke-CleanUp + } + + } + END { + } +} \ No newline at end of file