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 }
            }
        }
    }
}