InteractiveMenu.psm1
#Requires -Version 5 class InteractiveMultiMenuItem { [ValidateNotNullOrEmpty()][object]$ItemInfo [ValidateNotNullOrEmpty()][string]$Label [ValidateNotNullOrEmpty()][bool]$Selected [ValidateNotNullOrEmpty()][int]$Order [bool]$Readonly [string]$Info [string]$Url InteractiveMultiMenuItem([object]$itemInfo, [string]$label, [int]$order) { $this.Init($itemInfo, $label, $false, $order, $null, $null, $null); } InteractiveMultiMenuItem([object]$itemInfo, [string]$label, [bool]$selected, [int]$order) { $this.Init($itemInfo, $label, $selected, $order, $null, $null, $null); } InteractiveMultiMenuItem([object]$itemInfo, [string]$label, [bool]$selected, [int]$order, [bool]$readonly) { $this.Init($itemInfo, $label, $selected, $order, $readonly, $null, $null); } InteractiveMultiMenuItem([object]$itemInfo, [string]$label, [bool]$selected, [int]$order, [bool]$readonly, [string]$info) { $this.Init($itemInfo, $label, $selected, $order, $readonly, $info, $null); } InteractiveMultiMenuItem([object]$itemInfo, [string]$label, [bool]$selected, [int]$order, [bool]$readonly, [string]$info, [string]$url) { $this.Init($itemInfo, $label, $selected, $order, $readonly, $info, $url); } hidden Init([object]$itemInfo, [string]$label, [bool]$selected, [int]$order, [bool]$readonly, [string]$info, [string]$url) { $this.ItemInfo = $itemInfo $this.Label = $label $this.Selected = $selected $this.Order = $order $this.Readonly = $readonly $this.Info = $info $this.Url = $url } } class InteractiveMultiMenu { [ValidateNotNullOrEmpty()][string]$Header [ValidateNotNullOrEmpty()][InteractiveMultiMenuItem[]]$Items [ValidateNotNullOrEmpty()][ConsoleColor]$HeaderColor = [ConsoleColor]::Magenta [ValidateNotNullOrEmpty()][ConsoleColor]$HelpColor = [ConsoleColor]::Cyan [ValidateNotNullOrEmpty()][ConsoleColor]$CurrentItemColor = [ConsoleColor]::DarkGreen [ValidateNotNullOrEmpty()][ConsoleColor]$LinkColor = [ConsoleColor]::DarkCyan [ValidateNotNullOrEmpty()][ConsoleColor]$CurrentItemLinkColor = [ConsoleColor]::Black [ValidateNotNullOrEmpty()][string]$MenuDeselected = "[ ]" [ValidateNotNullOrEmpty()][string]$MenuSelected = "[x]" [ValidateNotNullOrEmpty()][string]$MenuCannotSelect = "[/]" [ValidateNotNullOrEmpty()][string]$MenuCannotDeselect = "[!]" [ValidateNotNullOrEmpty()][ConsoleColor]$MenuInfoColor = [ConsoleColor]::DarkYellow [ValidateNotNullOrEmpty()][ConsoleColor]$MenuErrorColor = [ConsoleColor]::DarkRed hidden [object[]]$SelectedItems = $null hidden [int]$CurrentIndex = 0 hidden [string]$Error = $null hidden [bool]$Help = $false hidden [bool]$Info = $false InteractiveMultiMenu([string]$header, [InteractiveMultiMenuItem[]]$items) { $this.Header = $header $this.Items = $items | Sort-Object { $_.Order } } [object[]] GetSelections() { Clear-Host $shouldContinue = $true do { [Console]::CursorLeft = 0 [Console]::CursorTop = 0 if ($this.Help) { Clear-Host $this.ShowHelp() [Console]::ReadKey("NoEcho,IncludeKeyDown") $this.Help = $false Clear-Host continue } if ($this.Info) { Clear-Host $this.ShowCurrentItemInfo() [Console]::ReadKey("NoEcho,IncludeKeyDown") $this.Info = $false Clear-Host continue } Write-Host "$($this.Header)`n" -ForegroundColor $this.HeaderColor $this.Draw() $this.ShowUsage() $this.ShowErrors() $keyPress = [Console]::ReadKey("NoEcho,IncludeKeyDown") $shouldContinue = $this.ProcessKey($keyPress) } while ($shouldContinue) return $this.SelectedItems } [void] SetOptions([hashtable]$options) { foreach ($option in $options.GetEnumerator()) { if ($null -eq $this.$($option.Key)) { Write-Host "Invalid option key: $($option.Key)" } else { $this.$($option.Key) = $option.Value } } } hidden Draw() { $defaultForegroundColor = (get-host).ui.rawui.ForegroundColor $defaultBackgroundColor = (get-host).ui.rawui.BackgroundColor $i = 0 $this.Items | ForEach-Object { $currentItem = $_ $color = $defaultForegroundColor $background = $defaultBackgroundColor $linkColor = $this.LinkColor $selectionStatus = $this.MenuDeselected if ($i -eq $this.CurrentIndex) { $background = $this.CurrentItemColor $linkColor = $this.CurrentItemLinkColor } if ($currentItem.Selected) { $selectionStatus = $this.MenuSelected } if ($currentItem.Readonly) { if ($currentItem.Selected) { $selectionStatus = $this.MenuCannotDeselect } else { $selectionStatus = $this.MenuCannotSelect } } Write-Host "$($selectionStatus) $($currentItem.Label)" -NoNewline -ForegroundColor $color -BackgroundColor $background if (-not [string]::IsNullOrEmpty($currentItem.Url)) { Write-Host " [$($currentItem.Url)]" -ForegroundColor $linkColor -BackgroundColor $background } else { Write-Host } $i++ } } hidden ShowCurrentItemInfo() { $selectedItem = $this.GetSelectedItem(); Write-Host "`n$($selectedItem.Info)" -ForegroundColor $this.MenuInfoColor Write-Host "`nPress any key to go back" -ForegroundColor $this.HeaderColor } hidden [InteractiveMultiMenuItem] GetSelectedItem() { return $this.Items[$this.CurrentIndex]; } hidden [bool] ProcessKey($keyPress) { switch ($keyPress.Key) { $([ConsoleKey]::DownArrow) { $this.CurrentIndex++ if ($this.CurrentIndex -ge $this.Items.Length) { $this.CurrentIndex = $this.Items.Length -1; } } $([ConsoleKey]::D2) { # this is only for powersession $this.CurrentIndex++ if ($this.CurrentIndex -ge $this.Items.Length) { $this.CurrentIndex = $this.Items.Length -1; } } $([ConsoleKey]::UpArrow) { $this.CurrentIndex-- if ($this.CurrentIndex -lt 0) { $this.CurrentIndex = 0; } } $([ConsoleKey]::D8) { # this is only for powersession $this.CurrentIndex-- if ($this.CurrentIndex -lt 0) { $this.CurrentIndex = 0; } } $([ConsoleKey]::Spacebar) { $selectedItem = $this.GetSelectedItem() if ($selectedItem.Readonly) { $this.Error = "$($selectedItem.Label) is marked as readonly and cannot be changed" } else { $selectedItem.Selected=!$selectedItem.Selected; } } $([ConsoleKey]::A) { $this.Items | ForEach-Object { if (-not $_.Readonly) { $_.Selected = $true } } } $([ConsoleKey]::N) { $this.Items | ForEach-Object { if (-not $_.Readonly) { $_.Selected = $false } } } $([ConsoleKey]::H) { $this.Help = $true } $([ConsoleKey]::O) { $this.OpenBrowser() } $([ConsoleKey]::I) { $selectedItem = $this.GetSelectedItem() if (-not [string]::IsNullOrEmpty($selectedItem.Info)) { $this.Info = $true } } $([ConsoleKey]::Enter) { $this.StoreState() return $false } $([ConsoleKey]::Escape) { return $false } Default { $this.Error = "Unkown key pressed: $_. Press 'h' for help." } } return $true } hidden OpenBrowser() { $selectedItem = $this.GetSelectedItem() if (-not [string]::IsNullOrEmpty($selectedItem.Url)) { Start-Process "$($selectedItem.Url)" } else { $this.Error = "$($selectedItem.Label) doesn't have an URL" } } hidden StoreState() { $this.SelectedItems = @() foreach ($item in $this.Items) { if ($item.Selected) { $this.SelectedItems += $item.ItemInfo } } } hidden ShowHelp() { Write-Host "Legend:" Write-Host -NoNewline $this.MenuDeselected -ForegroundColor $this.HelpColor Write-Host " - unchecked item" Write-Host -NoNewline $this.MenuSelected -ForegroundColor $this.HelpColor Write-Host " - checked item" Write-Host -NoNewline $this.MenuCannotDeselect -ForegroundColor $this.HelpColor Write-Host " - mandatory (cannot uncheck)" Write-Host -NoNewline $this.MenuCannotSelect -ForegroundColor $this.HelpColor Write-Host " - unavailable (cannot check)" Write-Host "Usage:" Write-Host -NoNewline "[SpaceBar]" -ForegroundColor $this.HelpColor Write-Host -NoNewline " check/uncheck, " Write-Host -NoNewline "[Enter]" -ForegroundColor $this.HelpColor Write-Host -NoNewline " continue, " Write-Host -NoNewline "[Esc]" -ForegroundColor $this.HelpColor Write-Host " exit" Write-Host -NoNewline "[Up] [Down]" -ForegroundColor $this.HelpColor Write-Host -NoNewline " navigate, " Write-Host -NoNewline "[A]" -ForegroundColor $this.HelpColor Write-Host -NoNewline " select all, " Write-Host -NoNewline "[N]" -ForegroundColor $this.HelpColor Write-Host " deselect all" Write-Host -NoNewline "[O]" -ForegroundColor $this.HelpColor Write-Host " open url (if any)" Write-Host -NoNewline "[I]" -ForegroundColor $this.HelpColor Write-Host " show information about selected item (if any)" Write-Host "`nPress any key to go back" -ForegroundColor $this.HeaderColor } hidden ShowUsage() { Write-Host "`nPress [h] for help." -ForegroundColor $this.HelpColor } hidden ShowErrors() { $bufferFill = " " if (-not [string]::IsNullOrEmpty($this.Error)) { Write-Host "$($this.Error)$bufferFill" -ForegroundColor $this.MenuErrorColor $this.Error = $null } else { Write-Host $bufferFill } } } class InteractiveMenuChooseMenuItem { [ValidateNotNullOrEmpty()][string]$Label [ValidateNotNullOrEmpty()][string]$Value [ValidateNotNullOrEmpty()][string]$Info InteractiveMenuChooseMenuItem([string]$label, [string]$value, [string]$info) { $this.Label = $label $this.Value = $value $this.Info = $Info } } class InteractiveMenuChooseMenu { [ValidateNotNullOrEmpty()][string]$Question [ValidateNotNullOrEmpty()][InteractiveMenuChooseMenuItem[]]$Options [ValidateNotNullOrEmpty()][ConsoleColor]$MenuInfoColor = [ConsoleColor]::DarkYellow [ValidateNotNullOrEmpty()][ConsoleColor]$QuestionColor = [ConsoleColor]::Magenta [ValidateNotNullOrEmpty()][ConsoleColor]$HelpColor = [ConsoleColor]::Cyan [ValidateNotNullOrEmpty()][ConsoleColor]$ErrorColor = [ConsoleColor]::DarkRed [ValidateNotNullOrEmpty()][ConsoleColor]$HighlightColor = [ConsoleColor]::DarkGreen [ValidateNotNullOrEmpty()][string]$OptionSeparator = " " hidden [int]$CurrentIndex = 0 hidden [InteractiveMenuChooseMenuItem]$SelectedOption = $null hidden [bool]$Help = $false hidden [string]$Error = $null InteractiveMenuChooseMenu([string]$question, [InteractiveMenuChooseMenuItem[]]$options) { $this.Question = $question $this.Options = $options } [string] GetAnswer() { $shouldContinue = $true do { Clear-Host if ($this.Help) { $this.ShowHelp() [Console]::ReadKey("NoEcho,IncludeKeyDown") continue } Write-Host "$($this.Question)`n" -ForegroundColor $this.QuestionColor $this.Draw() $this.ShowCurrentItemInfo() $this.ShowUsage() $this.ShowErrors() $keyPress = [Console]::ReadKey("NoEcho,IncludeKeyDown") $shouldContinue = $this.ProcessKey($keyPress) } while ($shouldContinue) if ($null -eq $this.SelectedOption) { return $null } return $this.SelectedOption.Value } [void] SetOptions([hashtable]$options) { foreach ($option in $options.GetEnumerator()) { if ($null -eq $this.$($option.Key)) { Write-Host "Invalid option key: $($option.Key)" } else { $this.$($option.Key) = $option.Value } } } hidden Draw() { $defaultForegroundColor = (get-host).ui.rawui.ForegroundColor $defaultBackgroundColor = (get-host).ui.rawui.BackgroundColor $i = 0 $this.options | ForEach-Object { $foregroundColor = $defaultForegroundColor $backgroundColor = $defaultBackgroundColor if ($i -eq $this.CurrentIndex) { $backgroundColor = $this.HighlightColor } # Fix if foreground or background color is not valid or set in IDE if (![Enum]::IsDefined([ConsoleColor], $foregroundColor)) { # If foreground color is not a valid color, set it to a default color. $foregroundColor = [ConsoleColor]::White } if (![Enum]::IsDefined([ConsoleColor], $backgroundColor)) { # If background color is not a valid color, set it to a default color. $backgroundColor = [ConsoleColor]::Black } Write-Host " $($_.Label) " -NoNewline -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor Write-Host $this.OptionSeparator -NoNewline $i++ } Write-Host } hidden ShowCurrentItemInfo() { $selectedItem = $this.Options[$this.CurrentIndex]; if (-not [string]::IsNullOrEmpty($selectedItem.Info)) { Write-Host "`n$($selectedItem.Info)" -ForegroundColor $this.MenuInfoColor } } hidden [bool] ProcessKey($keyPress) { switch ($keyPress.Key) { $([ConsoleKey]::RightArrow) { $this.CurrentIndex++ if ($this.CurrentIndex -ge $this.Options.Length) { $this.CurrentIndex = $this.Options.Length -1; } } $([ConsoleKey]::D6) { # this is only for powersession $this.CurrentIndex++ if ($this.CurrentIndex -ge $this.Options.Length) { $this.CurrentIndex = $this.Options.Length -1; } } $([ConsoleKey]::LeftArrow) { $this.CurrentIndex-- if ($this.CurrentIndex -lt 0) { $this.CurrentIndex = 0; } } $([ConsoleKey]::D4) { # this is only for powersession $this.CurrentIndex-- if ($this.CurrentIndex -lt 0) { $this.CurrentIndex = 0; } } $([ConsoleKey]::H) { $this.Help = $true } $([ConsoleKey]::Enter) { $this.StoreState() return $false } $([ConsoleKey]::Escape) { return $false } Default { $this.Error = "Unkown key pressed: $_. Press 'h' for help." } } return $true } hidden StoreState() { $this.SelectedOption = $this.Options[$this.CurrentIndex] } hidden ShowUsage() { Write-Host "`nPress [h] for help." -ForegroundColor $this.HelpColor } hidden ShowErrors() { $bufferFill = " " if (-not [string]::IsNullOrEmpty($this.Error)) { Write-Host "$($this.Error)$bufferFill" -ForegroundColor $this.ErrorColor $this.Error = $null } else { Write-Host $bufferFill } } hidden ShowHelp() { Write-Host "Usage:" Write-Host -NoNewline "[Enter]" -ForegroundColor $this.HelpColor Write-Host -NoNewline " continue, " Write-Host -NoNewline "[Esc]" -ForegroundColor $this.HelpColor Write-Host " exit" Write-Host -NoNewline "[Lef] [Right]" -ForegroundColor $this.HelpColor Write-Host " navigate, " Write-Host "`nPress any key to go back" -ForegroundColor $this.QuestionColor $this.Help = $false } } function Get-InteractiveMultiMenuOption { param( [Parameter(Mandatory)][object]$Item, [Parameter(Mandatory)][string]$Label, [Parameter(Mandatory)][int]$Order, [Parameter()][switch]$Selected, [Parameter()][switch]$Readonly, [Parameter()][string]$Info, [Parameter()][string]$Url ) [InteractiveMultiMenuItem]::New($Item, $Label, $Selected.IsPresent, $Order, $Readonly.IsPresent, $Info, $Url) } function Get-InteractiveMenuUserSelection { param( [Parameter(Mandatory)][string]$Header, [Parameter(Mandatory)][object[]]$Items, [Parameter()][hashtable]$Options ) $menu = [InteractiveMultiMenu]::New($Header, $Items) if ($null -ne $Options) { $menu.SetOptions($Options); } return $menu.GetSelections() } function Get-InteractiveChooseMenuOption() { param( [Parameter(Mandatory)][string]$Label, [Parameter(Mandatory)][string]$Value, [Parameter(Mandatory)][string]$Info ) [InteractiveMenuChooseMenuItem]::new($Label, $Value, $Info) } function Get-InteractiveMenuChooseUserSelection { param( [Parameter(Mandatory)][string]$Question, [Parameter(Mandatory)][object[]]$Answers, [Parameter()][hashtable]$Options ) $menu = [InteractiveMenuChooseMenu]::new($Question, $Answers) if ($null -ne $Options) { $menu.SetOptions($Options); } return $menu.GetAnswer() } Export-ModuleMember -Function Get-InteractiveMultiMenuOption,Get-InteractiveMenuUserSelection,Get-InteractiveChooseMenuOption,Get-InteractiveMenuChooseUserSelection |