src/UI/30-Commands.ps1

# 30-Commands.ps1 — command browser + tabbed per-command help.
# Search here now matches every other screen: '/' enters filter mode, typing
# outside filter mode does nothing (#19, #20, #21).

function script:Build-PSMMCommandListView {
    param(
        [Parameter(Mandatory)] $State,
        [Parameter(Mandatory)] $Commands,   # full list
        [Parameter(Mandatory)] $View,       # filtered list
        [Parameter(Mandatory)][string]$ModuleName
    )
    $n = $View.Count
    $win = Get-PSMMWinSize
    $vp = Get-PSMMViewport -State $State -Count $n -Rows ($win.Height - 9)
    $T = [Spectre.Console.Table]::new()
    $T.Border = [Spectre.Console.TableBorder]::Rounded
    foreach ($h in ' ', 'Command', 'Type') { [void][Spectre.Console.TableExtensions]::AddColumn($T, $h) }
    for ($i = $vp.First; $i -le $vp.Last; $i++) {
        $c = $View[$i]
        $nm = ConvertTo-PSMMSafe $c.Name
        if ($i -eq $State.Cursor) { $nm = "[$script:PSMM_ColAccent]$nm[/]" }
        [void][Spectre.Console.TableExtensions]::AddRow($T, [string[]]@(
                $(if ($i -eq $State.Cursor) { "[$script:PSMM_ColAccent]>[/]" } else { ' ' }), $nm, "$($c.CommandType)"))
    }
    $pos = Get-PSMMPositionMarkup -State $State -Count $n -Viewport $vp
    $of = if ($n -ne $Commands.Count) { " [$script:PSMM_ColMute](of $($Commands.Count))[/]" } else { '' }
    $items = [System.Collections.Generic.List[Spectre.Console.Rendering.IRenderable]]::new()
    $items.Add([Spectre.Console.Markup]::new("[$script:PSMM_ColAccent]Commands in $(ConvertTo-PSMMSafe $ModuleName)[/]$pos$of$(Get-PSMMFilterMarkup -State $State)"))
    $items.Add($T)
    $hint = if ($State.FilterMode) {
        Get-PSMMHint -Pairs @('type=filter', 'enter=apply', 'esc=clear & exit filter', 'up/dn=move')
    } else {
        Get-PSMMHint -Pairs @('up/dn=move', 'enter=details', '/=search', '?=help', 'esc=back', 'Ctrl+Q=quit')
    }
    $items.Add([Spectre.Console.Markup]::new($hint))
    [Spectre.Console.Rows]::new($items)
}

function script:Show-PSMMCommands {
    param([Parameter(Mandatory)] $Entry)
    $ui = $script:PSMM_UI
    $cmds = @()
    try { $cmds = @(Get-Command -Module $Entry.Name -ErrorAction Stop | Sort-Object CommandType, Name) } catch { }
    if (-not $cmds.Count) {
        Clear-PSMMScreen
        Write-PSMMLine "[$script:PSMM_ColAccent]Commands in $(ConvertTo-PSMMSafe $Entry.Name)[/]"
        Write-PSMMLine '[orange1]No commands available (install/import the module first).[/]'
        $null = Wait-PSMMKey -Message 'back'
        return
    }

    $st = New-PSMMListState
    $pick = @{ Name = $null }
    while ($true) {
        if ($ui.HardQuit) { return }
        $pick.Name = $null
        Clear-PSMMScreen
        Invoke-PSMMLive -Body {
            param($ctx)
            while ($true) {
                if ($script:PSMM_UI.HardQuit) { return }
                $view = if ($st.Filter) { @($cmds | Where-Object { $_.Name -like "*$($st.Filter)*" }) } else { $cmds }
                $ctx.UpdateTarget((Build-PSMMCommandListView -State $st -Commands $cmds -View $view -ModuleName $Entry.Name))
                $ctx.Refresh()
                $k = Read-PSMMKeyResize
                if ($null -eq $k) { continue }
                if (Test-PSMMHardQuitKey $k) { $script:PSMM_UI.HardQuit = $true; return }
                if ($st.FilterMode) {
                    $r = Invoke-PSMMFilterKey -State $st -KeyInfo $k
                    if ($r) { continue }
                    $null = Invoke-PSMMListNav -State $st -KeyInfo $k -Count $view.Count
                    continue
                }
                if (Invoke-PSMMListNav -State $st -KeyInfo $k -Count $view.Count) { continue }
                switch ($k.Key) {
                    ([ConsoleKey]::Escape) {
                        if ($st.Filter) { $st.Filter = ''; $st.Cursor = 0; continue }
                        return
                    }
                    ([ConsoleKey]::Enter) {
                        if ($view.Count) { $pick.Name = $view[$st.Cursor].Name; return }
                        continue
                    }
                    ([ConsoleKey]::RightArrow) {
                        # drill into the command's help (#24)
                        if ($view.Count) { $pick.Name = $view[$st.Cursor].Name; return }
                        continue
                    }
                    ([ConsoleKey]::LeftArrow) { if (-not $st.Filter) { return }; continue }   # back out (#24)
                    ([ConsoleKey]::Oem2) {
                        if ($k.KeyChar -ne '?') { $st.FilterMode = $true; continue }
                    }
                    default { if ($k.KeyChar -eq '/') { $st.FilterMode = $true } }
                }
                if ($k.KeyChar -eq '?') { Show-PSMMHelpScreen -Topic 'commands'; Clear-PSMMScreen }
            }
        }
        if ($ui.HardQuit) { return }
        if ($pick.Name) { Show-PSMMCommandDetail -Name $pick.Name }   # runs its own live display AFTER this one ended
        else { return }                                               # esc with no filter -> back
    }
}

# Build one frame of the tabbed command help. Handles small terminals (#10):
# the tab bar is its own short row (never crops), and the body panel height
# adapts to the window with a hard minimum.
function script:Build-PSMMCommandDetailView {
    param(
        [Parameter(Mandatory)] $State,      # @{ Tab; Scroll }
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)] $Content,    # @{ Overview; Parameters; Examples }
        [Parameter(Mandatory)][string[]]$Tabs
    )
    $lines = @($Content[$Tabs[$State.Tab]] -split "`r?`n")
    $win = Get-PSMMWinSize
    $page = [Math]::Max(3, $win.Height - 8)
    $State.Scroll = [Math]::Max(0, [Math]::Min($State.Scroll, [Math]::Max(0, $lines.Count - $page)))
    $body = ($lines | Select-Object -Skip $State.Scroll -First $page | ForEach-Object { ConvertTo-PSMMSafe $_ }) -join "`n"
    $tabBar = (0..($Tabs.Count - 1) | ForEach-Object {
        if ($_ -eq $State.Tab) { "[$script:PSMM_ColAccent underline]$($Tabs[$_])[/]" } else { "[$script:PSMM_ColMute]$($Tabs[$_])[/]" }
    }) -join ' '
    $pos = if ($lines.Count -gt $page) { " [$script:PSMM_ColMute]lines $($State.Scroll + 1)-$([Math]::Min($lines.Count, $State.Scroll + $page))/$($lines.Count)[/]" } else { '' }
    $items = [System.Collections.Generic.List[Spectre.Console.Rendering.IRenderable]]::new()
    # name and tab bar on SEPARATE short rows: one long markup row collapses
    # to '...' on narrow terminals
    $items.Add([Spectre.Console.Markup]::new("[$script:PSMM_ColAccent]$(ConvertTo-PSMMSafe $Name)[/]$pos"))
    $items.Add([Spectre.Console.Markup]::new($tabBar))
    $panel = [Spectre.Console.Panel]::new([Spectre.Console.Markup]::new($body))
    $panel.Border = [Spectre.Console.BoxBorder]::Rounded
    $panel.BorderStyle = [Spectre.Console.Style]::Parse($script:PSMM_ColMute)
    $items.Add($panel)
    $items.Add([Spectre.Console.Markup]::new((Get-PSMMHint -Pairs @('left/right=switch tab', 'up/dn=scroll', 'esc=back', 'Ctrl+Q=quit'))))
    [Spectre.Console.Rows]::new($items)
}

# Tabbed, scrollable help for one command: <- -> switch tab, up/dn scroll.
function script:Show-PSMMCommandDetail {
    param([Parameter(Mandatory)][string]$Name)
    $tabs = @('Overview', 'Parameters', 'Examples')
    Clear-PSMMScreen
    Write-PSMMLine "[$script:PSMM_ColAccent]loading help for $(ConvertTo-PSMMSafe $Name)...[/]"
    $content = @{
        Overview   = ((Get-Help -Name $Name -ErrorAction SilentlyContinue | Out-String).Trim())
        Parameters = ((Get-Help -Name $Name -Parameter * -ErrorAction SilentlyContinue | Out-String).Trim())
        Examples   = ((Get-Help -Name $Name -Examples -ErrorAction SilentlyContinue | Out-String).Trim())
    }
    foreach ($t in $tabs) { if (-not $content[$t]) { $content[$t] = '(no content - module may need importing for full help)' } }

    $st = @{ Tab = 0; Scroll = 0 }
    Clear-PSMMScreen
    Invoke-PSMMLive -Body {
        param($ctx)
        while ($true) {
            if ($script:PSMM_UI.HardQuit) { return }
            $ctx.UpdateTarget((Build-PSMMCommandDetailView -State $st -Name $Name -Content $content -Tabs $tabs))
            $ctx.Refresh()
            $k = Read-PSMMKeyResize
            if ($null -eq $k) { continue }
            if (Test-PSMMHardQuitKey $k) { $script:PSMM_UI.HardQuit = $true; return }
            switch ($k.Key) {
                ([ConsoleKey]::LeftArrow)  { $st.Tab = ($st.Tab + $tabs.Count - 1) % $tabs.Count; $st.Scroll = 0 }
                ([ConsoleKey]::RightArrow) { $st.Tab = ($st.Tab + 1) % $tabs.Count; $st.Scroll = 0 }
                ([ConsoleKey]::Escape)     { return }
                default {
                    $null = Invoke-PSMMPagerNav -State $st -KeyInfo $k
                }
            }
        }
    }
    Clear-PSMMScreen
}