Private/Interactive/Show-TBMenuArrowMultiSelect.ps1

function Show-TBMenuArrowMultiSelect {
    <#
    .SYNOPSIS
        Arrow-key multi-select menu for PS 7+ interactive terminals.
    .DESCRIPTION
        Renders menu items with checkboxes inside a box. Arrow keys move the
        highlight, Space toggles selection, A toggles all, Enter confirms the
        checked items, and Escape returns Back. Supports viewport scrolling
        for lists that exceed the available terminal height.
    .PARAMETER Title
        The menu title displayed above the items.
    .PARAMETER Options
        Array of option display strings.
    .PARAMETER IncludeBack
        If specified, Escape returns 'Back'.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Title,

        [Parameter(Mandatory = $true)]
        [string[]]$Options,

        [Parameter()]
        [switch]$IncludeBack
    )

    $palette = Get-TBColorPalette
    $esc = [char]27
    $reset = "${esc}[0m"

    $innerWidth = Get-TBConsoleInnerWidth
    $border = ([char]0x2502)

    # Title inside box continuation
    $titlePadded = (' {0}' -f $Title).PadRight($innerWidth)
    Write-Host (' {0}{1}{2}{3}{4}{5}{6}' -f $palette.Surface, $border, $palette.Teal, $palette.Bold, $titlePadded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset))

    $titleUnderline = (' {0}' -f ('-' * $Title.Length)).PadRight($innerWidth)
    Write-Host (' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $palette.Dim, $titleUnderline, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset))

    $anchorTop = [Console]::CursorTop

    $selectedIndex = 0
    $itemCount = $Options.Count
    $checked = [bool[]]::new($itemCount)

    # Viewport: chrome rows = top-empty + bottom-empty + separator + hint + bottom-border
    $chromeRows = 5
    $maxVisible = [Math]::Max(3, [Console]::WindowHeight - $anchorTop - $chromeRows)
    $viewportSize = if ($itemCount -gt $maxVisible) { $maxVisible } else { 0 }
    $viewportOffset = 0

    $adjustViewport = {
        if ($viewportSize -le 0) { return }
        $hasAbove = ($viewportOffset -gt 0)
        $hasBelow = (($viewportOffset + $viewportSize) -lt $itemCount)
        $visibleFirst = $viewportOffset + $(if ($hasAbove) { 1 } else { 0 })
        $visibleLast  = $viewportOffset + $viewportSize - 1 - $(if ($hasBelow) { 1 } else { 0 })
        $newOffset = $viewportOffset
        if ($selectedIndex -lt $visibleFirst) {
            $newOffset = [Math]::Max(0, $selectedIndex - 1)
            if ($selectedIndex -eq 0) { $newOffset = 0 }
        }
        elseif ($selectedIndex -gt $visibleLast) {
            $newOffset = [Math]::Min($itemCount - $viewportSize, $selectedIndex - $viewportSize + 2)
            if ($selectedIndex -eq ($itemCount - 1)) { $newOffset = $itemCount - $viewportSize }
        }
        Set-Variable -Name viewportOffset -Value $newOffset -Scope 1
    }

    try {
        try { [Console]::CursorVisible = $false } catch { }

        # Initial render
        & $adjustViewport
        Render-TBMenuBox -Items $Options -SelectedIndex $selectedIndex -AnchorTop $anchorTop `
            -Checked $checked -IncludeBack:$IncludeBack -MultiSelect `
            -ViewportOffset $viewportOffset -ViewportSize $viewportSize

        while ($true) {
            $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')

            switch ($key.VirtualKeyCode) {
                38 { # Up arrow
                    if ($selectedIndex -gt 0) {
                        $selectedIndex--
                    }
                    else {
                        $selectedIndex = $itemCount - 1
                    }
                    & $adjustViewport
                    Render-TBMenuBox -Items $Options -SelectedIndex $selectedIndex -AnchorTop $anchorTop `
                        -Checked $checked -IncludeBack:$IncludeBack -MultiSelect `
                        -ViewportOffset $viewportOffset -ViewportSize $viewportSize
                }
                40 { # Down arrow
                    if ($selectedIndex -lt ($itemCount - 1)) {
                        $selectedIndex++
                    }
                    else {
                        $selectedIndex = 0
                    }
                    & $adjustViewport
                    Render-TBMenuBox -Items $Options -SelectedIndex $selectedIndex -AnchorTop $anchorTop `
                        -Checked $checked -IncludeBack:$IncludeBack -MultiSelect `
                        -ViewportOffset $viewportOffset -ViewportSize $viewportSize
                }
                32 { # Space - toggle checkbox
                    $checked[$selectedIndex] = -not $checked[$selectedIndex]
                    Render-TBMenuBox -Items $Options -SelectedIndex $selectedIndex -AnchorTop $anchorTop `
                        -Checked $checked -IncludeBack:$IncludeBack -MultiSelect `
                        -ViewportOffset $viewportOffset -ViewportSize $viewportSize
                }
                13 { # Enter - confirm
                    $result = @()
                    for ($i = 0; $i -lt $itemCount; $i++) {
                        if ($checked[$i]) {
                            $result += $i
                        }
                    }
                    if ($result.Count -gt 0) {
                        return $result
                    }
                    # If nothing checked, do nothing (require at least one selection)
                }
                27 { # Escape
                    if ($IncludeBack) { return 'Back' }
                }
                65 { # A key - toggle all
                    $allChecked = $true
                    for ($i = 0; $i -lt $itemCount; $i++) {
                        if (-not $checked[$i]) {
                            $allChecked = $false
                            break
                        }
                    }
                    $newState = -not $allChecked
                    for ($i = 0; $i -lt $itemCount; $i++) {
                        $checked[$i] = $newState
                    }
                    Render-TBMenuBox -Items $Options -SelectedIndex $selectedIndex -AnchorTop $anchorTop `
                        -Checked $checked -IncludeBack:$IncludeBack -MultiSelect `
                        -ViewportOffset $viewportOffset -ViewportSize $viewportSize
                }
            }
        }
    }
    finally {
        try { [Console]::CursorVisible = $true } catch { }
    }
}