Public/Show-Win32Toolkit.ps1

function Show-Win32Toolkit {
<#
.SYNOPSIS
    Launches the interactive, menu-driven text UI (TUI) for win32-toolkit.
.DESCRIPTION
    A fool-proof, first-line-friendly front-end over the whole pipeline: package an app from winget
    or a manual installer, test and update-test existing projects (Windows Sandbox or the Hyper-V
    VM), manage dependencies, package and publish to Intune, manage org templates, the test VM, and
    settings — all from guided menus. The first screen is a prerequisite health check that tells you
    exactly what is missing and how to fix it.
 
    Built on PwshSpectreConsole — offered for one-time install on first launch if absent. Requires
    PowerShell 7.2+ and an interactive console.
.PARAMETER BasePath
    Optional base folder override for this session. If omitted, the registry-saved value is used, or
    first-run setup prompts for it.
.EXAMPLE
    Show-Win32Toolkit
#>

    [CmdletBinding()]
    param([string]$BasePath)

    # ── Guards: PS7.2+ and an interactive console ─────────────────────────────
    if ($PSVersionTable.PSVersion -lt [version]'7.2') {
        Write-Warning 'Show-Win32Toolkit requires PowerShell 7.2 or later.'
        return
    }
    if ([System.Console]::IsInputRedirected -or [System.Console]::IsOutputRedirected) {
        Write-Warning 'Show-Win32Toolkit needs an interactive console window (not a redirected or embedded host).'
        return
    }

    # ── Ensure the UI component (plain prompt — Spectre may not be present yet) ─
    if (-not (Get-Module -ListAvailable PwshSpectreConsole)) {
        Write-Host 'The text UI needs the PwshSpectreConsole module (a one-time install).' -ForegroundColor Yellow
        if ((Read-Host 'Install it now for your user account? (Y/N)') -match '^[Yy]') {
            try { Install-Module PwshSpectreConsole -Scope CurrentUser -Force -AcceptLicense -ErrorAction Stop }
            catch { Write-Warning "Install failed: $($_.Exception.Message)"; return }
        }
        else {
            Write-Host 'Cannot start the UI without it. You can still use the commands: Get-Command -Module win32-toolkit' -ForegroundColor Yellow
            return
        }
    }
    Import-Module PwshSpectreConsole -ErrorAction Stop

    # ── Progress bars and a Spectre TUI cannot share a console ─────────────────
    # PowerShell's progress renderer owns its own screen region and paints straight over the
    # live-rendered UI. It does not repaint when Spectre redraws and it is not cleared by Clear-Host,
    # so a bar (classically Remove-Item's "Removed N of M files [MB/s]") is left stranded across the
    # menu until the console scrolls it away.
    #
    # Silencing it HERE — once, at the TUI root — is the whole fix: $ProgressPreference is DYNAMICALLY
    # scoped, so every command the menu launches inherits it (package / test / publish and their
    # helpers), including implicit cmdlet bars (Invoke-WebRequest, Expand-Archive, Copy-Item,
    # Remove-Item -Recurse, Install-Module) and the explicit Write-Progress countdowns in
    # New-TargetedDocumentation and Invoke-AzBlobUpload. Guarding each call site instead is
    # whack-a-mole: it is how a bar leaked back in after the first round of per-site fixes.
    #
    # The existing per-site guards stay — they matter for DIRECT, non-TUI calls, where progress is
    # useful and harmless — and they keep composing correctly: the "previous" value each one saves and
    # restores is simply this one.
    #
    # Restored in the finally so a scripted caller's own preference survives the TUI.
    $prevProgress = $ProgressPreference
    $ProgressPreference = 'SilentlyContinue'

    # ── UTF-8 output so Spectre renders in full, not the cramped OEM/code-page fallback ──────────
    # PwshSpectreConsole warns that a non-UTF-8 console "limits Spectre Console functionality": tables
    # and borders render in a degraded mode (mis-measured columns, mid-word wrapping). Set UTF-8 for the
    # life of the TUI and restore it in the finally so a scripted caller's console is left unchanged.
    # Guarded so a host that rejects the change never blocks launch.
    $prevOutputEncoding = $null
    try {
        $prevOutputEncoding = [System.Console]::OutputEncoding
        [System.Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
    }
    catch { }

    try {
        # ── Resolve the base folder (first-run setup via the UI if unset) ──────────
        $base = Get-Win32ToolkitBasePath -BasePath $BasePath -NonInteractive
        Clear-Host
        Write-SpectreFigletText -Text 'win32-toolkit' -Color Blue
        if (-not $base) { $base = Show-Win32ToolkitFirstRun }

        # ── Main loop ──────────────────────────────────────────────────────────────
        while ($true) {
            Clear-Host
            Write-SpectreFigletText -Text 'win32-toolkit' -Color Blue
            $w32tVersion = Get-Win32ToolkitVersion
            if ($w32tVersion) { Write-SpectreHost "[grey]v$w32tVersion[/]" }
            Show-Win32ToolkitHealth -BasePath $base
            Show-Win32ToolkitUpdateNotice

            switch (Show-Win32ToolkitMainMenu) {
                'winget'    { Invoke-Win32ToolkitWingetWizard -BasePath $base }
                'manual'    { Invoke-Win32ToolkitManualWizard -BasePath $base }
                'project'   { Show-Win32ToolkitProjectActions -BasePath $base }
                'browse'    { Show-Win32ToolkitBrowse   -BasePath $base }
                'templates' { Show-Win32ToolkitTemplates -BasePath $base }
                'intune'    { Show-Win32ToolkitIntuneConnection -BasePath $base }
                'settings'  { $base = Show-Win32ToolkitSettings -BasePath $base }
                'exit'      { Write-SpectreHost '[grey]Goodbye.[/]'; return }
                default     { return }
            }
        }
    }
    finally {
        $ProgressPreference = $prevProgress
        if ($prevOutputEncoding) { try { [System.Console]::OutputEncoding = $prevOutputEncoding } catch { } }
    }
}