public/Invoke-Octa.ps1

function Wait-OctaKeypress {
    <#
        Shared "press any key to return to the menu" step after running a category/tool -
        factored out so every action site (categories, tools, Quick Clean, Undo, Help) uses the
        exact same prompt instead of repeating it.
    #>

    param([Parameter(Mandatory)]$Strings)
    Write-Host ''
    Write-Host -NoNewline $Strings.pressKeyToContinue
    [Console]::ReadKey($true) | Out-Null
}

function Invoke-Octa {
    <#
        .SYNOPSIS
        Interactive entry point: language menu (first run) -> banner + main menu -> dispatch,
        looping back to the menu after each action so the whole session (every category, the
        built-in tools, undo) happens from this one running terminal - the user never needs to
        re-invoke bin/octa.ps1 with a different flag. FR-018, FR-022, FR-023.

        009/010: categories and built-in tools are grouped (main menu -> group picker -> leaf
        picker) instead of one flat 32-item list, following real user feedback that a flat list
        got overwhelming as more categories/tools were added. Running a leaf item returns to the
        group screen it was picked from, not all the way back to the top-level main menu.

        010: every screen (a menu, or a running action's own output) is drawn through
        Show-OctaFrame, anchored to the same starting row captured once at the top of this
        function - real user feedback (with a screenshot of the redesigned menu confirming the
        cursor-position redraw fix does work on their terminal) asked for the whole session to
        feel like one fixed, updating screen instead of each action's output piling up in the
        terminal's ordinary scrollback history.

        ponytail: direct single-keypress shortcuts are guaranteed unique for items 1-9 within
        any one screen (main menu, a group picker, or a leaf picker never has more than 9 items
        at once); items 10+ are still always reachable via Up/Down + Enter, just without a
        guaranteed single-digit direct key.
    #>

    [CmdletBinding()]
    param()

    $strings = Get-OctaStrings
    # -SkipRiskScan: this menu never displays HighestRiskLevel (only DisplayName/Description),
    # so the ~20s it took to live-scan all 20 categories just to compute a badge nothing read
    # was pure waste - see Get-OctaCategory's own comment for how this was found.
    $categories = @(Get-OctaCategory -SkipRiskScan)
    $anchorRow = [Console]::CursorTop

    $categoryGroups = @(
        [pscustomobject]@{ Key = '1'; Label = $strings.groupPrivacyAi; Description = $strings.groupPrivacyAiDesc; Ids = @('telemetry', 'background-activity', 'copilot', 'recall', 'ai-features') }
        [pscustomobject]@{ Key = '2'; Label = $strings.groupBloatware; Description = $strings.groupBloatwareDesc; Ids = @('appx', 'oem-suites', 'onedrive', 'widgets') }
        [pscustomobject]@{ Key = '3'; Label = $strings.groupPerformance; Description = $strings.groupPerformanceDesc; Ids = @('performance', 'power', 'gpu', 'services') }
        [pscustomobject]@{ Key = '4'; Label = $strings.groupDesktop; Description = $strings.groupDesktopDesc; Ids = @('explorer-start', 'desktop', 'preferences', 'user-experience', 'gaming') }
        [pscustomobject]@{ Key = '5'; Label = $strings.groupAdvanced; Description = $strings.groupAdvancedDesc; Ids = @('windows-features', 'registry-orphans') }
    )

    $toolCatalog = @(
        [pscustomobject]@{ Id = 'disk-cleanup'; Label = $strings.menuDiskCleanup; Description = $strings.descDiskCleanup; Action = { Invoke-OctaDiskCleanup | Out-Null } }
        [pscustomobject]@{ Id = 'disk-usage'; Label = $strings.menuDiskUsage; Description = $strings.descDiskUsage; Action = { Show-OctaDiskUsage | Out-Null } }
        [pscustomobject]@{ Id = 'disk-health'; Label = $strings.menuDiskHealth; Description = $strings.descDiskHealth; Action = { Show-OctaDiskHealth | Out-Null } }
        [pscustomobject]@{ Id = 'uninstaller'; Label = $strings.menuUninstaller; Description = $strings.descUninstaller; Action = { Invoke-OctaUninstaller | Out-Null } }
        [pscustomobject]@{ Id = 'startup-manager'; Label = $strings.menuStartupManager; Description = $strings.descStartupManager; Action = { Invoke-OctaStartupManager | Out-Null } }
        [pscustomobject]@{ Id = 'tasks'; Label = $strings.menuTasks; Description = $strings.descTasks; Action = { Invoke-OctaTaskBrowser | Out-Null } }
        [pscustomobject]@{ Id = 'scheduler'; Label = $strings.menuScheduler; Description = $strings.descScheduler; Action = { Invoke-OctaScheduler | Out-Null } }
        [pscustomobject]@{ Id = 'software-updater'; Label = $strings.menuSoftwareUpdater; Description = $strings.descSoftwareUpdater; Action = { Invoke-OctaSoftwareUpdater | Out-Null } }
        [pscustomobject]@{ Id = 'dashboard'; Label = $strings.menuDashboard; Description = $strings.descDashboard; Action = { Show-OctaDashboard | Out-Null } }
        [pscustomobject]@{ Id = 'history'; Label = $strings.menuHistory; Description = $strings.descHistory; Action = { Show-OctaHistory | Out-Null } }
        [pscustomobject]@{ Id = 'network-cleanup'; Label = $strings.menuNetworkCleanup; Description = $strings.descNetworkCleanup; Action = { Invoke-OctaNetworkCleanup | Out-Null } }
        [pscustomobject]@{ Id = 'malware-scan'; Label = $strings.menuMalwareScan; Description = $strings.descMalwareScan; Action = { Invoke-OctaMalwareScan | Format-List } }
    )

    $toolGroups = @(
        [pscustomobject]@{ Key = '1'; Label = $strings.groupDiskStorage; Description = $strings.groupDiskStorageDesc; ToolIds = @('disk-cleanup', 'disk-usage', 'disk-health', 'uninstaller') }
        [pscustomobject]@{ Key = '2'; Label = $strings.groupSystemMgmt; Description = $strings.groupSystemMgmtDesc; ToolIds = @('startup-manager', 'tasks', 'scheduler', 'software-updater') }
        [pscustomobject]@{ Key = '3'; Label = $strings.groupInfoHistory; Description = $strings.groupInfoHistoryDesc; ToolIds = @('dashboard', 'history') }
        [pscustomobject]@{ Key = '4'; Label = $strings.groupNetworkSecurity; Description = $strings.groupNetworkSecurityDesc; ToolIds = @('network-cleanup', 'malware-scan') }
    )

    while ($true) {
        $mainItems = @(
            [pscustomobject]@{ Key = '1'; Label = $strings.menuCategories; Description = $strings.menuCategoriesDesc }
            [pscustomobject]@{ Key = '2'; Label = $strings.menuTools; Description = $strings.menuToolsDesc }
            [pscustomobject]@{ Key = 'q'; Label = $strings.menuQuickClean; Description = $strings.menuQuickCleanDesc }
            [pscustomobject]@{ Key = 'u'; Label = $strings.menuUndo; Description = $strings.menuUndoDesc }
            [pscustomobject]@{ Key = 'l'; Label = $strings.menuLanguage; Description = $strings.menuLanguageDesc }
            [pscustomobject]@{ Key = 'h'; Label = $strings.menuHelp; Description = $strings.menuHelpDesc }
            [pscustomobject]@{ Key = 'x'; Label = $strings.menuExit; Description = $strings.menuExitDesc }
        )

        $choice = Show-OctaFrame -AnchorRow $anchorRow -Draw {
            Show-OctaMenu -Items $mainItems -Header $strings.mainMenuHeader -NavHint $strings.menuNavHint
        }

        switch ($choice) {
            '1' {
                while ($true) {
                    $groupItems = @($categoryGroups | ForEach-Object { [pscustomobject]@{ Key = $_.Key; Label = $_.Label; Description = $_.Description } })
                    $groupItems += [pscustomobject]@{ Key = 'b'; Label = $strings.menuBack; Description = '' }
                    $groupChoice = Show-OctaFrame -AnchorRow $anchorRow -Draw {
                        Show-OctaMenu -Items $groupItems -Header $strings.categoryGroupHeader -NavHint $strings.menuNavHint
                    }
                    if ($groupChoice -eq 'b') { break }
                    $group = $categoryGroups | Where-Object { $_.Key -eq $groupChoice }

                    while ($true) {
                        $leafCategories = @($categories | Where-Object { $_.Id -in $group.Ids })
                        $leafItems = @()
                        for ($i = 0; $i -lt $leafCategories.Count; $i++) {
                            $leafItems += [pscustomobject]@{ Key = ($i + 1).ToString(); Label = $leafCategories[$i].DisplayName; Description = $leafCategories[$i].Description }
                        }
                        $leafItems += [pscustomobject]@{ Key = 'b'; Label = $strings.menuBack; Description = '' }

                        $leafChoice = Show-OctaFrame -AnchorRow $anchorRow -Draw {
                            Show-OctaMenu -Items $leafItems -Header $group.Label -NavHint $strings.menuNavHint
                        }
                        if ($leafChoice -eq 'b') { break }

                        $index = [int]$leafChoice - 1
                        if ($index -ge 0 -and $index -lt $leafCategories.Count) {
                            Show-OctaFrame -AnchorRow $anchorRow -Draw {
                                Show-OctaBanner
                                Write-Host ($strings.runningHeader -f $leafCategories[$index].DisplayName)
                                Invoke-OctaCategory -CategoryId $leafCategories[$index].Id -Apply | Format-List
                                Wait-OctaKeypress -Strings $strings
                            } | Out-Null
                        }
                    }
                }
            }
            '2' {
                while ($true) {
                    $groupItems = @($toolGroups | ForEach-Object { [pscustomobject]@{ Key = $_.Key; Label = $_.Label; Description = $_.Description } })
                    $groupItems += [pscustomobject]@{ Key = 'b'; Label = $strings.menuBack; Description = '' }
                    $groupChoice = Show-OctaFrame -AnchorRow $anchorRow -Draw {
                        Show-OctaMenu -Items $groupItems -Header $strings.toolGroupHeader -NavHint $strings.menuNavHint
                    }
                    if ($groupChoice -eq 'b') { break }
                    $group = $toolGroups | Where-Object { $_.Key -eq $groupChoice }

                    while ($true) {
                        $leafTools = @($toolCatalog | Where-Object { $_.Id -in $group.ToolIds })
                        $leafItems = @()
                        for ($i = 0; $i -lt $leafTools.Count; $i++) {
                            $leafItems += [pscustomobject]@{ Key = ($i + 1).ToString(); Label = $leafTools[$i].Label; Description = $leafTools[$i].Description }
                        }
                        $leafItems += [pscustomobject]@{ Key = 'b'; Label = $strings.menuBack; Description = '' }

                        $leafChoice = Show-OctaFrame -AnchorRow $anchorRow -Draw {
                            Show-OctaMenu -Items $leafItems -Header $group.Label -NavHint $strings.menuNavHint
                        }
                        if ($leafChoice -eq 'b') { break }

                        $index = [int]$leafChoice - 1
                        if ($index -ge 0 -and $index -lt $leafTools.Count) {
                            Show-OctaFrame -AnchorRow $anchorRow -Draw {
                                Show-OctaBanner
                                Write-Host ($strings.runningHeader -f $leafTools[$index].Label)
                                & $leafTools[$index].Action
                                Wait-OctaKeypress -Strings $strings
                            } | Out-Null
                        }
                    }
                }
            }
            'q' {
                Show-OctaFrame -AnchorRow $anchorRow -Draw {
                    Show-OctaBanner
                    Write-Host ($strings.runningHeader -f $strings.menuQuickClean)
                    Invoke-OctaQuickClean -Apply | Format-List
                    Wait-OctaKeypress -Strings $strings
                } | Out-Null
            }
            'u' {
                Show-OctaFrame -AnchorRow $anchorRow -Draw {
                    Show-OctaBanner
                    Write-Host ($strings.runningHeader -f $strings.menuUndo)
                    Undo-OctaRun -RunId 'latest' | Format-List
                    Wait-OctaKeypress -Strings $strings
                } | Out-Null
            }
            'l' {
                # Was not wrapped in Show-OctaFrame at all before - its own banner+prompt just
                # printed wherever the cursor happened to be after the previous menu, instead of
                # replacing it in place, which is exactly the "everything stacks up" symptom a
                # real user reported.
                $culture = Show-OctaFrame -AnchorRow $anchorRow -Draw {
                    Show-OctaLanguageMenu
                }
                Set-OctaLanguage -Culture $culture
                $strings = Get-OctaStrings
            }
            'h' {
                Show-OctaFrame -AnchorRow $anchorRow -Draw {
                    Show-OctaBanner
                    Write-Host 'https://github.com/jpiribe/octa (see README for full reference)'
                    Wait-OctaKeypress -Strings $strings
                } | Out-Null
            }
            'x' { return }
        }
    }
}