src/UI/01-Input.ps1
|
# 01-Input.ps1 — key reading with resize + background-activity wake-ups. # Is this key the universal hard-quit chord (Ctrl+Q / Ctrl+X)? function script:Test-PSMMHardQuitKey { param([Parameter(Mandatory)] $KeyInfo) ((($KeyInfo.Modifiers -band [ConsoleModifiers]::Control) -ne 0) -and ($KeyInfo.Key -in [ConsoleKey]::Q, [ConsoleKey]::X)) } # Is this the "go home" alias? Ctrl+H where the terminal reports it # distinctly (Windows Terminal/conhost deliver full key records; over VT # paths Ctrl+H IS backspace). The primary route home is the g goto overlay # ('g' then 'h', 03-Goto.ps1) - plain 'g' is handled by each screen loop, # so this test answers immediately without a second key read. function script:Test-PSMMHomeKey { param([Parameter(Mandatory)] $KeyInfo) ((($KeyInfo.Modifiers -band [ConsoleModifiers]::Control) -ne 0) -and ($KeyInfo.Key -eq [ConsoleKey]::H)) } # Key read that also notices terminal resizes and background-task activity: # returns $null when the window size changed OR a background job/task changed # state or produced output (caller should re-render, typically via continue). # Falls back to a plain blocking read where KeyAvailable is unsupported. function script:Read-PSMMKeyResize { $w0 = Get-PSMMWinSize $fp0 = Get-PSMMTaskFingerprint $tick = 0 while ($true) { try { if ([Console]::KeyAvailable) { return [Console]::ReadKey($true) } } catch { return [Console]::ReadKey($true) } Start-Sleep -Milliseconds 100 $w = Get-PSMMWinSize if ($w.Width -ne $w0.Width -or $w.Height -ne $w0.Height) { return $null } # every 500 ms: wake when background activity changed, so the grid's # overlay and job lines update by themselves (#25) $tick++ if ($tick % 5 -eq 0) { $fp = Get-PSMMTaskFingerprint if ($fp -ne $fp0) { return $null } } } } # Consistent "press a key" pause that honours Ctrl+Q / Ctrl+X (hard quit). # Returns $false when hard-quitting so callers can bail out immediately. function script:Wait-PSMMKey { param([string]$Message = 'press any key to continue') Write-PSMMLine (Get-PSMMHint -Pairs @("any key=$Message", '^q=quit')) $k = [Console]::ReadKey($true) if (Test-PSMMHardQuitKey $k) { $script:PSMM_UI.HardQuit = $true return $false } return $true } # left/right = back out / drill in, the same on EVERY screen (gh#7). Returns # 'out', 'in' or ''. Screens with nothing to drill into answer 'in' with # Get-PSMMNoDrillStatus instead of silently doing nothing - a key that does # nothing on some screens is worse than no key. function script:Get-PSMMDrillKey { param([Parameter(Mandatory)] $KeyInfo) switch ($KeyInfo.Key) { ([ConsoleKey]::LeftArrow) { return 'out' } ([ConsoleKey]::RightArrow) { return 'in' } } '' } function script:Get-PSMMNoDrillStatus { "[$script:PSMM_ColMute]nothing to open on this row $([char]0x00B7) left backs out[/]" } # Typed-phrase confirmation for destructive, hard-to-undo actions (gh#13). # y/n and enter are one keystroke away from navigation; moving a whole module # tree must not be. The user has to type the phrase - anything else, including # empty input and esc, cancels. Comparison ignores case and extra whitespace, # not content. function script:Read-PSMMConfirmPhrase { param( [Parameter(Mandatory)][string]$Phrase, [string]$Warning ) if ($Warning) { Write-PSMMProse -Text $Warning -Colour $script:PSMM_ColWarn } Write-PSMMLine "[$script:PSMM_ColMute]this cannot be undone from psmm $([char]0x2014) type [/][$script:PSMM_ColAccent]$(ConvertTo-PSMMSafe $Phrase)[/][$script:PSMM_ColMute] to go ahead, anything else cancels[/]" $typed = Read-PSMMText -Message "confirm" -AllowEmpty if ($null -eq $typed) { return $false } $norm = ("$typed".Trim() -replace '\s+', ' ') [string]::Equals($norm, $Phrase, [System.StringComparison]::OrdinalIgnoreCase) } # Bounded number picker (gh#14): a small panel with a sensible default # preselected, up/dn to adjust, enter to accept, esc to cancel. $MaxReason is # shown verbatim - a bare "max 16" tells the user nothing about why. # Returns the chosen [int], or $null when cancelled. function script:Read-PSMMNumber { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Every parameter is used inside the live-display scriptblock; the rule cannot see into it.')] param( [Parameter(Mandatory)][string]$Title, [string]$Message, [int]$Default = 1, [int]$Min = 1, [int]$Max = 8, [string]$MaxReason, [string]$Unit = '' ) $state = @{ Value = [Math]::Max($Min, [Math]::Min($Max, $Default)); Cancelled = $false } Invoke-PSMMLive -Body { param($ctx) while ($true) { $items = [System.Collections.Generic.List[Spectre.Console.Rendering.IRenderable]]::new() $items.Add([Spectre.Console.Markup]::new("[$script:PSMM_ColAccent]$(ConvertTo-PSMMSafe $Title)[/]")) if ($Message) { foreach ($l in (Get-PSMMProseMarkup -Text $Message)) { $items.Add([Spectre.Console.Markup]::new($l)) } } $bar = ('#' * ($state.Value - $Min + 1)).PadRight([Math]::Max(1, $Max - $Min + 1), '.') $items.Add([Spectre.Console.Markup]::new( "[$script:PSMM_ColOk]$($state.Value)[/][$script:PSMM_ColMute]$(if ($Unit) { " $(ConvertTo-PSMMSafe $Unit)" })[/] [$script:PSMM_ColDim]$bar (min $Min, max $Max)[/]")) if ($MaxReason) { foreach ($l in (Get-PSMMProseMarkup -Text $MaxReason -Colour $script:PSMM_ColDim)) { $items.Add([Spectre.Console.Markup]::new($l)) } } $items.Add([Spectre.Console.Markup]::new((Get-PSMMHint -Pairs @('up/dn=change', 'enter=go', 'esc=cancel') -NoLegend))) $panel = [Spectre.Console.Panel]::new([Spectre.Console.Rows]::new($items)) $panel.Border = [Spectre.Console.BoxBorder]::Rounded $panel.BorderStyle = [Spectre.Console.Style]::Parse($script:PSMM_ColAccent) $ctx.UpdateTarget($panel) $ctx.Refresh() $k = [Console]::ReadKey($true) if (Test-PSMMHardQuitKey $k) { $script:PSMM_UI.HardQuit = $true; $state.Cancelled = $true; return } switch ($k.Key) { ([ConsoleKey]::UpArrow) { $state.Value = [Math]::Min($Max, $state.Value + 1) } ([ConsoleKey]::RightArrow) { $state.Value = [Math]::Min($Max, $state.Value + 1) } ([ConsoleKey]::DownArrow) { $state.Value = [Math]::Max($Min, $state.Value - 1) } ([ConsoleKey]::LeftArrow) { $state.Value = [Math]::Max($Min, $state.Value - 1) } ([ConsoleKey]::Home) { $state.Value = $Min } ([ConsoleKey]::End) { $state.Value = $Max } ([ConsoleKey]::Enter) { return } ([ConsoleKey]::Escape) { $state.Cancelled = $true; return } } } } if ($state.Cancelled) { return $null } $state.Value } |