private/Menu.ps1
|
# FR-018: interactive checklist menu. Supports two equivalent ways to select an item - # a direct shortcut keypress (case-insensitive) or Up/Down arrow navigation + Enter. Renders # the banner (FR-022) above the list on every screen, not only once. function Clear-OctaScreen { <# Windows PowerShell 5.1's built-in Clear-Host manipulates the legacy console buffer in a way that modern ConPTY-hosted terminals (Windows Terminal, VS Code, and other terminals built on the same pseudoconsole layer - confirmed reproducible, not host-specific) translate into a full scroll-to-bottom on every redraw, instead of an in-place clear - this is what made every arrow-key press look like the terminal was "jumping". An ANSI clear+home sequence (`Esc[2J` + `Esc[H`) is a true in-place clear under VT processing, which Windows 11's terminals enable by default - no extra native-API call needed to turn it on, matching this project's own Windows-11-only scope. #> $esc = [char]27 Write-Host "$esc[2J$esc[H" -NoNewline } function Show-OctaMenu { <# $Items: array of @{ Key = '1'; Label = 'Telemetry'; Description = '...' } Returns the Key of the chosen item. #> [CmdletBinding()] param( [Parameter(Mandatory)][object[]]$Items, [Parameter(Mandatory)][string]$Header, [string]$NavHint = 'Up/Down to navigate - Enter to select - or press the number/letter directly' ) # ponytail: check interactivity up front, not just via the ReadKey try/catch below - some # non-interactive hosts (PS Direct/WinRM remoting) don't throw on ReadKey, they just hang # forever waiting for a keystroke that can never arrive. Test-OctaInteractiveConsole catches # that case; the try/catch remains as a second guard for hosts that do throw instead. if (-not (Test-OctaInteractiveConsole)) { throw "Octa's interactive menu needs a real console (Up/Down + Enter, or a direct keypress). This session has none - use the exported cmdlets directly instead (Invoke-OctaCategory, Invoke-OctaQuickClean, Show-OctaDashboard, etc.) for non-interactive/automated use." } $selectedIndex = 0 while ($true) { Clear-OctaScreen Show-OctaBanner Write-Host $Header Write-Host '' for ($i = 0; $i -lt $Items.Count; $i++) { $item = $Items[$i] $pointer = if ($i -eq $selectedIndex) { '>' } else { ' ' } Write-Host ("{0} {1} {2,-22}{3}" -f $pointer, $item.Key.ToString().ToUpperInvariant(), $item.Label, $item.Description) } Write-Host '' Write-Host $NavHint # ponytail: ReadKey throws in a non-interactive host (no console attached - a scheduled # task, a remote session, redirected output). Without this guard the caught exception was # a non-terminating error, so the loop kept redrawing forever instead of stopping - fail # fast with a clear message instead. try { $key = [Console]::ReadKey($true) } catch [System.InvalidOperationException] { throw "Octa's interactive menu needs a real console (Up/Down + Enter, or a direct keypress). This session has none - use the exported cmdlets directly instead (Invoke-OctaCategory, Invoke-OctaQuickClean, Show-OctaDashboard, etc.) for non-interactive/automated use." } switch ($key.Key) { 'UpArrow' { $selectedIndex = ($selectedIndex - 1 + $Items.Count) % $Items.Count } 'DownArrow' { $selectedIndex = ($selectedIndex + 1) % $Items.Count } 'Enter' { return $Items[$selectedIndex].Key } default { $typed = $key.KeyChar.ToString().ToLowerInvariant() $match = $Items | Where-Object { $_.Key.ToString().ToLowerInvariant() -eq $typed } if ($match) { return $match[0].Key } } } } } |