lib/ui.ps1

# WinMole Interactive UI Components
# PwshSpectreConsole-based menus and prompts

# Requires: lib/core.ps1 already dot-sourced

# ── Hide/restore cursor (VT100 sequences) ────────────────────────────────────
$script:_ESC = [char]27
function Hide-Cursor { Write-Host "$($script:_ESC)[?25l" -NoNewline }
function Show-Cursor { Write-Host "$($script:_ESC)[?25h" -NoNewline }

# ── Read a single keypress (arrow-key aware) ──────────────────────────────────
# Returns: 'Up','Down','Left','Right','Enter','Space','Delete','Backspace',
# 'Home','End','PageUp','PageDown','Escape','Tab', or the char itself.
function Read-Key {
    $k = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
    switch ($k.VirtualKeyCode) {
        38 { return 'Up' }
        40 { return 'Down' }
        37 { return 'Left' }
        39 { return 'Right' }
        13 { return 'Enter' }
        32 { return 'Space' }
        46 { return 'Delete' }
        8  { return 'Backspace' }
        36 { return 'Home' }
        35 { return 'End' }
        33 { return 'PageUp' }
        34 { return 'PageDown' }
        27 { return 'Escape' }
        9  { return 'Tab' }
        default {
            $c = $k.Character
            switch ($c) {
                'j' { return 'Down' }
                'k' { return 'Up' }
                'h' { return 'Left' }
                'l' { return 'Right' }
                'q' { return 'Escape' }
                'Q' { return 'Escape' }
                default { return [string]$c }
            }
        }
    }
}

# ── Multi-select checklist menu ───────────────────────────────────────────────
# Items: array of pscustomobject with .Label, .Size (optional), .Tag (optional),
# .Selected (bool), .Disabled (bool, optional)
# Returns: the items array with updated .Selected values, or $null if cancelled.
function Show-ChecklistMenu {
    param(
        [string]$Title,
        [object[]]$Items,
        [int]$MaxVisible = 18
    )

    if ($Items.Count -eq 0) { return $Items }

    $labels = [System.Collections.Generic.List[string]]::new()
    $labelToItem = @{}

    foreach ($item in $Items) {
        $label = Esc $item.Label
        if ($item.Size) { $label += " $(Format-Bytes $item.Size)" }
        if ($item.Tag) { $label += " | $($item.Tag)" }
        $labels.Add($label)
        $labelToItem[$label] = $item
    }

    try {
        $selected = $labels | Read-SpectreMultiSelection -Title $Title -PageSize $MaxVisible
    } catch {
        return $null
    }

    if (-not $selected) { return $null }

    foreach ($item in $Items) { $item.Selected = $false }
    $selectedSet = [System.Collections.Generic.HashSet[string]]::new([string[]]@($selected))
    foreach ($label in $labels) {
        if ($selectedSet.Contains($label) -and $labelToItem.ContainsKey($label)) {
            $labelToItem[$label].Selected = $true
        }
    }

    return $Items
}

# ── Simple single-select menu ─────────────────────────────────────────────────
function Show-SelectMenu {
    param(
        [string]$Title,
        [string[]]$Options,
        [int]$MaxVisible = 20
    )

    try {
        $selected = $Options | Read-SpectreSelection -Title $Title -PageSize $MaxVisible
    } catch {
        return -1
    }

    if (-not $selected) { return -1 }
    return [Array]::IndexOf($Options, $selected)
}