形态是行内(ADR-0010):菜单按普通行往下打,之后每次按键只把那几行原地重画(记下起始行再定位回去)。不做全屏、不切备用缓冲区 —— 崩了的时候滚屏里还留着上文,而"终端被 TUI 弄乱"是这类代码最难排查的故障。 没有控制台时只打一次、不重画、不抛错,靠 -Driver 里的 -InputScript 驱动 —— 同一个循环在真终端与门禁里走**同一条代码路径**,断言看的是返回值(Action/Index/Chosen/Checked),不需要捕获控制台输出。 两处刻意的不猜:空菜单直接返回 cancel 并说明原因(没有可选项时不该等按键 —— 第一次使用时归档目录就是空的,在计划任务里等按键等于挂起);绝不 exit、绝不碰退出码(TUI 的异常不该污染备份结果)。 这块的断言按**数量分档**写(0 项 / 1 项 / 3 项)——上一块的单键 Bug 正是藏在"1 项"这个档位里,而当时只跑了 3 项的序列。 验收:断言 10 条;test.ps1 9/9 全绿(5.1 与 7);真实清单只读冒烟 4/4。
93 lines
4.1 KiB
PowerShell
93 lines
4.1 KiB
PowerShell
function Invoke-BakNRetMenu {
|
|
<#
|
|
.SYNOPSIS
|
|
画一个行内菜单并把按键循环跑到底,返回用户选了什么。
|
|
|
|
.DESCRIPTION
|
|
形态是**行内**(ADR-0010):菜单按普通行往下打,之后每次按键只把那几行**原地重画**
|
|
(记下起始行,再 SetCursorPosition 回去)。不做全屏、不切备用缓冲区 —— 崩了的时候滚屏里
|
|
还留着上文,而"终端被 TUI 弄乱"是这类代码最难排查的故障。
|
|
|
|
没有控制台(重定向 / 计划任务 / 门禁)时:只打一次菜单、不重画、不抛错,靠 -Driver 里的
|
|
`-InputScript` 把按键喂进来。这样同一个循环在真终端和门禁里是**同一条代码路径**。
|
|
|
|
**绝不 exit、绝不写退出码**:TUI 的异常不该污染备份结果(ADR-0010)。这个函数只返回值,
|
|
退出码永远属于 `$failed` 语义。
|
|
|
|
空菜单直接返回 cancel 并说明原因 —— 没有可选项时不该等按键(第一次使用时归档目录就是空的,
|
|
等按键在计划任务里等于挂起)。
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Title,
|
|
[Parameter(Mandatory = $true)]$Items,
|
|
[Parameter(Mandatory = $true)]$Driver,
|
|
[bool]$MultiSelect = $true,
|
|
[int]$InitialIndex = 0,
|
|
[string[]]$CheckedKeys = @(),
|
|
[string]$Hint = '↑↓ 选择 空格 勾选 回车 确认 Esc 返回'
|
|
)
|
|
|
|
$itemList = @($Items | Where-Object { $_ })
|
|
|
|
if ($itemList.Count -eq 0) {
|
|
Write-BakNRetAt -X 0 -Y 0 -Text ("{0}:没有可选项。" -f $Title) -ForegroundColor Yellow
|
|
return [pscustomobject]@{ Action = 'cancel'; Index = 0; Checked = @{}; Chosen = $null; Reason = 'empty' }
|
|
}
|
|
|
|
# 有控制台才重画;没有就只打一次(重定向下重画毫无意义,还会把日志弄花)
|
|
$canRedraw = -not [Console]::IsOutputRedirected
|
|
$blockRow = 0
|
|
|
|
$checked = @{}
|
|
foreach ($k in @($CheckedKeys)) { if ($k) { $checked[$k] = $true } }
|
|
$state = [pscustomobject]@{ Index = $InitialIndex; Checked = $checked; Action = 'none' }
|
|
|
|
$consoleWidth = 80
|
|
if ($canRedraw) { try { $consoleWidth = [Console]::WindowWidth } catch { } }
|
|
|
|
while ($true) {
|
|
if ($canRedraw) {
|
|
try {
|
|
if ($blockRow -gt 0) { [Console]::SetCursorPosition(0, $blockRow) }
|
|
}
|
|
catch {
|
|
# 定位失败就退化成"每次往下打一遍",难看好过失败
|
|
$canRedraw = $false
|
|
}
|
|
}
|
|
|
|
Write-BakNRetAt -X 0 -Y 0 -Text $Title -ForegroundColor Cyan
|
|
if ($canRedraw -and $blockRow -eq 0) { $blockRow = [Console]::CursorTop }
|
|
|
|
for ($i = 0; $i -lt $itemList.Count; $i++) {
|
|
$item = $itemList[$i]
|
|
$cursor = if ($i -eq $state.Index) { '>' } else { ' ' }
|
|
$box = ''
|
|
if ($MultiSelect) {
|
|
$box = if ($state.Checked.ContainsKey([string]$item.Key)) { '[x] ' } else { '[ ] ' }
|
|
}
|
|
$line = ' {0} {1}{2}' -f $cursor, $box, $item.Text
|
|
# 按列宽裁剪:超长条目不能把边框/后续内容挤走,也不能把汉字劈成半个
|
|
$line = Format-BakNRetPaddedText -Text $line -Width ([Math]::Max(10, $consoleWidth - 1))
|
|
if ($i -eq $state.Index) { Write-BakNRetAt -X 0 -Y 0 -Text $line -ForegroundColor White }
|
|
else { Write-BakNRetAt -X 0 -Y 0 -Text $line }
|
|
}
|
|
Write-BakNRetAt -X 0 -Y 0 -Text $Hint -ForegroundColor DarkGray
|
|
|
|
$key = Read-BakNRetKey -Driver $Driver
|
|
$next = Update-BakNRetMenuState -Items $itemList -Index $state.Index -Checked $state.Checked -MultiSelect $MultiSelect -Key $key
|
|
$state = [pscustomobject]@{ Index = $next.Index; Checked = $next.Checked; Action = $next.Action }
|
|
|
|
if ($state.Action -ne 'none') { break }
|
|
}
|
|
|
|
$chosen = if ($state.Action -eq 'confirm') { $itemList[$state.Index] } else { $null }
|
|
return [pscustomobject]@{
|
|
Action = $state.Action
|
|
Index = $state.Index
|
|
Checked = $state.Checked
|
|
Chosen = $chosen
|
|
Reason = ''
|
|
}
|
|
}
|