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