Private/UI/Show-PSMBMenuArrowSingle.ps1

function Show-PSMBMenuArrowSingle {
    <#
    .SYNOPSIS
        Arrow-key single-select menu for PS 7+ interactive terminals.
    .DESCRIPTION
        Renders a title and menu items inside a box, then enters a key loop
        where Up/Down moves the highlight, Enter selects, Escape returns
        Back or Quit. Supports viewport scrolling for long lists.
    #>

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

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

        [Parameter()]
        [switch]$IncludeBack,

        [Parameter()]
        [switch]$IncludeQuit
    )

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

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

    $titlePadded = (' {0}' -f $Title).PadRight($innerWidth)
    Write-Host (' {0}{1}{2}{3}{4}{5}{6}' -f $palette.Surface, $border, $palette.Amber, $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

    # Viewport calculation
    $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 { }

        & $adjustViewport
        Render-PSMBMenuBox -Items $Options -SelectedIndex $selectedIndex -AnchorTop $anchorTop `
            -IncludeBack:$IncludeBack -IncludeQuit:$IncludeQuit `
            -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-PSMBMenuBox -Items $Options -SelectedIndex $selectedIndex -AnchorTop $anchorTop `
                        -IncludeBack:$IncludeBack -IncludeQuit:$IncludeQuit `
                        -ViewportOffset $viewportOffset -ViewportSize $viewportSize
                }
                40 {
                    # Down arrow
                    if ($selectedIndex -lt ($itemCount - 1)) { $selectedIndex++ }
                    else { $selectedIndex = 0 }
                    & $adjustViewport
                    Render-PSMBMenuBox -Items $Options -SelectedIndex $selectedIndex -AnchorTop $anchorTop `
                        -IncludeBack:$IncludeBack -IncludeQuit:$IncludeQuit `
                        -ViewportOffset $viewportOffset -ViewportSize $viewportSize
                }
                13 { return $selectedIndex } # Enter
                27 {
                    # Escape
                    if ($IncludeBack) { return 'Back' }
                    if ($IncludeQuit) { return 'Quit' }
                }
            }
        }
    }
    finally {
        try { [Console]::CursorVisible = $true } catch { }
    }
}