Functions/Public/Write-Menu.ps1
|
<#
================================================================================ ORION DESIGN - POWERSHELL UI FRAMEWORK | Write-Menu Function ================================================================================ Author: Sune Alexandersen Narud Date: January 30, 2026 Module: OrionDesign v2.1.0 Category: Interactive Elements Dependencies: OrionDesign Theme System FUNCTION PURPOSE: Creates interactive selection menus with keyboard navigation and styling. Core interactive component providing user choice interfaces with consistent visual design and intuitive navigation patterns. Includes built-in Exit option with 'X' key for consistent user experience across all menus. HLD INTEGRATION: ┌─ INTERACTIVE ─┐ ┌─ USER INPUT ─┐ ┌─ OUTPUT ─┐ │ Write-Menu │◄──►│ Keyboard │───►│ Selection│ │ • Options │ │ Navigation │ │ Result │ │ • Styles │ │ Enter/Escape │ │ Index │ │ • Navigation │ │ Arrow Keys │ │ Value │ └───────────────┘ └──────────────┘ └──────────┘ ================================================================================ #> <# .SYNOPSIS Creates styled interactive menus for user selection with built-in Exit option. .DESCRIPTION The Write-Menu function displays a formatted menu with options that users can select from. Supports different styles and keyboard navigation. Every menu automatically includes an Exit option (selectable with 'X') as the last item for consistent user experience. .PARAMETER Title The title of the menu. .PARAMETER Options Array of menu options to display. An Exit option is automatically appended. .PARAMETER Style The visual style of the menu. Available styles: • Simple: Clean numbered list without icons, minimal formatting and compact layout • Modern: Stylish menu with icons, accent colors and enhanced visual appeal • Boxed: Menu enclosed in decorative border with frame and padding • Compact: Space-efficient single-line style for limited display areas Valid values: Simple, Modern, Boxed, Compact .PARAMETER AllowEscape Allow users to press Escape to exit without selection. .PARAMETER DefaultSelection Default option number (1-based) to highlight. .PARAMETER MultiSelect Allow users to select multiple options using comma-separated numbers (e.g., "1,3,5"). .PARAMETER ExitLabel Customize the text for the Exit option. Default is 'Exit'. .EXAMPLE Write-Menu -Title "Main Menu" -Options @("Deploy","Test","Rollback") Creates a menu with three options plus an automatic Exit option (X). .EXAMPLE Write-Menu -Title "Environment Selection" -Options @("Development","Testing","Production") -Style Modern -DefaultSelection 1 Creates a modern styled menu with default selection and Exit option. .EXAMPLE Write-Menu -Title "Select Features" -Options @("Logging","Caching","Monitoring","Alerts") -MultiSelect Creates a menu allowing multiple selections via comma-separated input (e.g., "1,2,4"), with X to exit. .EXAMPLE $result = Write-Menu -Title "Actions" -Options @("Save","Load") -ExitLabel "Cancel" if ($result.Exit) { Write-Host "User cancelled" } Creates a menu with custom Exit label and checks if user chose to exit. .OUTPUTS Hashtable with the following keys: - Index: Zero-based index of selection (-1 for Exit) - Value: The selected option text (or ExitLabel for Exit) - Number: The selection number (or 'X' for Exit) - Exit: $true if user selected Exit, not present otherwise For MultiSelect: - Indices: Array of zero-based indices - Values: Array of selected option texts - Numbers: Array of selection numbers #> function Write-Menu { [CmdletBinding(DefaultParameterSetName = 'Default')] param( [Parameter(Mandatory, ParameterSetName = 'Default', Position = 0)][string]$Title, [Parameter(Mandatory, ParameterSetName = 'Default', Position = 1)][array]$Options, [ValidateSet('Simple', 'Modern', 'Boxed', 'Compact')] [string]$Style = 'Modern', [switch]$AllowEscape, [int]$DefaultSelection = 1, [switch]$MultiSelect, [string]$ExitLabel = 'Exit', [Parameter(Mandatory, ParameterSetName = 'Demo')] [switch]$Demo ) if ($Demo) { $renderCodeBlock = { param([string[]]$Lines) $innerWidth = ($Lines | Measure-Object -Property Length -Maximum).Maximum + 4 $bar = '─' * $innerWidth Write-Host ' # Code' -ForegroundColor DarkGray Write-Host " ┌$bar┐" -ForegroundColor DarkGray foreach ($line in $Lines) { $padded = (" $line").PadRight($innerWidth) Write-Host " │" -ForegroundColor DarkGray -NoNewline Write-Host $padded -ForegroundColor Green -NoNewline Write-Host '│' -ForegroundColor DarkGray } Write-Host " └$bar┘" -ForegroundColor DarkGray Write-Host '' } $demoOptions = @('Deploy to Production', 'Run Tests', 'Rollback Changes', 'View Logs') $demoTitle = 'Deployment Menu' Write-Host '' Write-Host ' Write-Menu Demo' -ForegroundColor Cyan Write-Host ' ===============' -ForegroundColor DarkGray Write-Host ' (Static preview - actual function accepts keyboard input)' -ForegroundColor DarkGray Write-Host '' foreach ($style in @('Simple', 'Modern', 'Boxed', 'Compact')) { Write-Host " [Style: $style]" -ForegroundColor Yellow Write-Host '' & $renderCodeBlock @( "`$options = @('Deploy to Production', 'Run Tests', 'Rollback Changes', 'View Logs')", "Write-Menu -Title '$demoTitle' -Options `$options -Style $style" ) # Static preview header if (Get-Command Write-Separator -ErrorAction SilentlyContinue) { try { Write-Separator $demoTitle -Style Thick } catch { Write-Host "=== $demoTitle ===" } } else { Write-Host "=== $demoTitle ===" } Write-Host '' for ($i = 0; $i -lt $demoOptions.Count; $i++) { Write-Host " $($i+1). $($demoOptions[$i])" } Write-Host ' X. Exit' -ForegroundColor DarkGray Write-Host '' } return } # Default theme if (-not $script:Theme) { $script:Theme = @{ Accent = 'Cyan' Success = 'Green' Warning = 'Yellow' Error = 'Red' Text = 'White' Muted = 'DarkGray' Divider = '─' UseAnsi = $true } if ($psISE) { $script:Theme.UseAnsi = $false } } Write-Host # Display title switch ($Style) { 'Simple' { Write-Host $Title -ForegroundColor $script:Theme.Accent Write-Host ("─" * $Title.Length) -ForegroundColor $script:Theme.Accent } 'Boxed' { $titleLine = "┌─ $Title " + ("─" * (50 - $Title.Length - 4)) + "┐" Write-Host $titleLine -ForegroundColor $script:Theme.Accent } default { Write-Host "📋 $Title" -ForegroundColor $script:Theme.Accent Write-Host ("═" * ($Title.Length + 3)) -ForegroundColor $script:Theme.Accent } } if ($Style -ne 'Simple') { Write-Host } # Display options for ($i = 0; $i -lt $Options.Count; $i++) { $optionNumber = $i + 1 $option = $Options[$i] switch ($Style) { 'Simple' { Write-Host " $optionNumber. $option" -ForegroundColor $script:Theme.Text } 'Modern' { $icon = if ($optionNumber -eq $DefaultSelection) { "▶️" } else { " " } Write-Host "$icon $optionNumber. " -ForegroundColor $script:Theme.Accent -NoNewline Write-Host $option -ForegroundColor $script:Theme.Text } 'Boxed' { Write-Host "│ $optionNumber. $option" -ForegroundColor $script:Theme.Text } 'Compact' { Write-Host "[$optionNumber] $option " -ForegroundColor $script:Theme.Text -NoNewline } } } # Display Exit option with X switch ($Style) { 'Simple' { Write-Host " X. $ExitLabel" -ForegroundColor $script:Theme.Muted } 'Modern' { Write-Host " X. " -ForegroundColor $script:Theme.Muted -NoNewline Write-Host $ExitLabel -ForegroundColor $script:Theme.Muted } 'Boxed' { Write-Host "│ X. $ExitLabel" -ForegroundColor $script:Theme.Muted } 'Compact' { Write-Host "[X] $ExitLabel " -ForegroundColor $script:Theme.Muted -NoNewline } } if ($Style -eq 'Boxed') { Write-Host "└" + ("─" * 49) + "┘" -ForegroundColor $script:Theme.Accent } elseif ($Style -eq 'Compact') { Write-Host } Write-Host # Get user selection do { if ($MultiSelect) { Write-Host "Select options (1-$($Options.Count), comma-separated, or X to exit): " -ForegroundColor $script:Theme.Muted -NoNewline } elseif ($AllowEscape) { Write-Host "Select option (1-$($Options.Count), X to exit) or press Esc to cancel: " -ForegroundColor $script:Theme.Muted -NoNewline } else { Write-Host "Select option (1-$($Options.Count), or X to exit): " -ForegroundColor $script:Theme.Muted -NoNewline } $selection = Read-Host if ([string]::IsNullOrWhiteSpace($selection) -and $DefaultSelection -and -not $MultiSelect) { $selection = $DefaultSelection } # Check for Exit selection if ($selection -eq 'X' -or $selection -eq 'x') { Write-Host "👋 $ExitLabel" -ForegroundColor $script:Theme.Muted Write-Host return @{ Index = -1 Value = $ExitLabel Number = 'X' Exit = $true } } if ($MultiSelect) { # Parse comma-separated selections $selectedNumbers = @() $validSelection = $true $parts = $selection -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } if ($parts.Count -eq 0) { $validSelection = $false } else { foreach ($part in $parts) { $num = 0 if ([int]::TryParse($part, [ref]$num) -and $num -ge 1 -and $num -le $Options.Count) { if ($num -notin $selectedNumbers) { $selectedNumbers += $num } } else { $validSelection = $false break } } } if ($validSelection -and $selectedNumbers.Count -gt 0) { Write-Host "✅ Selected: " -ForegroundColor $script:Theme.Success -NoNewline $selectedValues = $selectedNumbers | ForEach-Object { $Options[$_ - 1] } Write-Host ($selectedValues -join ', ') -ForegroundColor $script:Theme.Text Write-Host return @{ Indices = $selectedNumbers | ForEach-Object { $_ - 1 } Values = $selectedValues Numbers = $selectedNumbers } } else { Write-Host "❌ Invalid selection. Please enter numbers between 1 and $($Options.Count) (comma-separated), or X to exit" -ForegroundColor $script:Theme.Error } } else { # Single selection $selectedNumber = 0 if ([int]::TryParse($selection, [ref]$selectedNumber) -and $selectedNumber -ge 1 -and $selectedNumber -le $Options.Count) { Write-Host "✅ Selected: " -ForegroundColor $script:Theme.Success -NoNewline Write-Host $Options[$selectedNumber - 1] -ForegroundColor $script:Theme.Text Write-Host return @{ Index = $selectedNumber - 1 Value = $Options[$selectedNumber - 1] Number = $selectedNumber } } else { Write-Host "❌ Invalid selection. Please enter a number between 1 and $($Options.Count), or X to exit" -ForegroundColor $script:Theme.Error } } } while ($true) } |