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 }
    $cursor = 0
    $offset = 0
    $pageSize = [Math]::Max(5, $MaxVisible)

    Hide-Cursor
    try {
        try {
            while ([Console]::KeyAvailable) {
                [void]$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
            }
        } catch {}

        while ($true) {
            [Console]::Clear()
            Write-Host ""
            Write-SpectreHost " [bold deepskyblue1]$(Esc $Title)[/]"
            Write-Host ""

            $end = [Math]::Min($offset + $pageSize, $Items.Count)
            for ($i = $offset; $i -lt $end; $i++) {
                $item = $Items[$i]
                $selected = [bool](Get-OptionalPropertyValue -InputObject $item -Name 'Selected' -Default $false)
                $disabled = [bool](Get-OptionalPropertyValue -InputObject $item -Name 'Disabled' -Default $false)
                $size = Get-OptionalPropertyValue -InputObject $item -Name 'Size' -Default 0
                $tag = Get-OptionalPropertyValue -InputObject $item -Name 'Tag' -Default ''

                $pointer = if ($i -eq $cursor) { '[deepskyblue1]▶[/]' } else { ' ' }
                $check = if ($selected) { '[green][x][/]' } else { '[grey][ ][/]' }
                $line = " $pointer $check [white]$(Esc $item.Label)[/]"
                if ([long]$size -gt 0) { $line += " [gold1]$(Format-Bytes $size)[/]" }
                if ($tag) { $line += " [grey]| $(Esc $tag)[/]" }
                if ($disabled) { $line += " [grey](disabled)[/]" }
                Write-SpectreHost $line
            }

            Write-Host ""
            Write-SpectreHost " [grey]↑↓ Navigate Space Toggle Enter Confirm Esc Cancel[/]"

            $key = Read-Key
            switch ($key) {
                'Up' {
                    if ($cursor -gt 0) {
                        $cursor--
                        if ($cursor -lt $offset) { $offset = $cursor }
                    }
                }
                'Down' {
                    if ($cursor -lt ($Items.Count - 1)) {
                        $cursor++
                        if ($cursor -ge ($offset + $pageSize)) { $offset++ }
                    }
                }
                'Home' {
                    $cursor = 0
                    $offset = 0
                }
                'End' {
                    $cursor = $Items.Count - 1
                    $offset = [Math]::Max(0, $Items.Count - $pageSize)
                }
                'PageUp' {
                    $cursor = [Math]::Max(0, $cursor - $pageSize)
                    $offset = [Math]::Max(0, $offset - $pageSize)
                }
                'PageDown' {
                    $cursor = [Math]::Min($Items.Count - 1, $cursor + $pageSize)
                    $offset = [Math]::Min([Math]::Max(0, $Items.Count - $pageSize), $offset + $pageSize)
                }
                'Space' {
                    $item = $Items[$cursor]
                    $disabled = [bool](Get-OptionalPropertyValue -InputObject $item -Name 'Disabled' -Default $false)
                    if (-not $disabled) {
                        $item.Selected = -not [bool](Get-OptionalPropertyValue -InputObject $item -Name 'Selected' -Default $false)
                    }
                }
                'Enter' { return $Items }
                'Escape' { return $null }
            }
        }
    } finally {
        Show-Cursor
    }
}

# ── 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)
}