feat(tui): 读键归一化与 -InputScript 输入缝(第一轮第二块)
三层拆开,让唯一能无控制台测试的那层露出来:Get-BakNRetKeyName 纯映射(键码→键名)、New-BakNRetInputDriver 造驱动对象、Read-BakNRetKey 取下一个键。门禁把方向键/回车/空格/字母全钉住,只把"真的读到一个键"留给人工。 两条"不猜"的设计:序列用尽必须抛错(绝不退回去读真终端 —— 门禁挂起比变红糟得多);没有控制台也没有序列时不返回任何默认键(没人按键却继续执行,在备份工具里等于替你做了决定)。 这块连撞三个"看起来该有值、其实是空的"坑,全被"绿了才提交"拦在提交之外:① 键名少写冒号;② @($null) 造出含 $null 的单元素数组,"空序列"被当成"有一个键",且两版本表现不同(7 静默返回、5.1 抛错);③ **多行插入时换行被折成空格**,于是注释与 `Script = $scriptKeys` 一起落进同一行注释,那条赋值从未执行 —— 症状是"对象一个属性都没有"。教训:往文件里插多行代码不能靠拼接字符串,写文件就整份重写。 验收:断言 15 条(含驱动序列消费与两条必须报错);两版本手工输出一致;test.ps1 9/9 全绿。
This commit is contained in:
1 parent
8a0924330a
commit
f2959a4279
6 files changed
+145
No files matched your search
@@ -19,6 +19,9 @@
|
||||
CompatiblePSEditions = @('Desktop', 'Core')
|
||||
|
||||
FunctionsToExport = @(
|
||||
'Get-BakNRetKeyName',
|
||||
'New-BakNRetInputDriver',
|
||||
'Read-BakNRetKey',
|
||||
'Get-BakNRetCellWidth',
|
||||
'Write-BakNRetRunSummary',
|
||||
'Save-BakNRetItemRecord',
|
||||
|
||||
@@ -146,7 +146,14 @@
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetCellWidth.ps1')
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetKeyName.ps1')
|
||||
. (Join-Path $PSScriptRoot 'Public\New-BakNRetInputDriver.ps1')
|
||||
. (Join-Path $PSScriptRoot 'Public\Read-BakNRetKey.ps1')
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'Get-BakNRetKeyName',
|
||||
'New-BakNRetInputDriver',
|
||||
'Read-BakNRetKey',
|
||||
'Get-BakNRetCellWidth',
|
||||
'Write-BakNRetRunSummary',
|
||||
'Save-BakNRetItemRecord',
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
function Get-BakNRetKeyName {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
把一次按键(虚拟键码 + 字符)归一化成一个稳定的键名,供 TUI 的各个界面判断。
|
||||
|
||||
.DESCRIPTION
|
||||
为什么把它单独拆出来:**这是整个 TUI 里唯一能无控制台测试的部分**。读键本身必须有真终端,
|
||||
而映射不需要 —— 把纯逻辑与 I/O 分开,门禁里就能把"上/下/回车/空格/字母"这些路径钉住,
|
||||
只把"真的读到一个键"留给人工。
|
||||
|
||||
返回的键名:Up / Down / Left / Right / Enter / Esc / Space / Tab / Backspace /
|
||||
Char:<小写字符> / Unknown<键码>。
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][int]$VirtualKeyCode,
|
||||
[string]$Character
|
||||
)
|
||||
|
||||
switch ($VirtualKeyCode) {
|
||||
38 { return 'Up' }
|
||||
40 { return 'Down' }
|
||||
37 { return 'Left' }
|
||||
39 { return 'Right' }
|
||||
13 { return 'Enter' }
|
||||
27 { return 'Esc' }
|
||||
32 { return 'Space' }
|
||||
9 { return 'Tab' }
|
||||
8 { return 'Backspace' }
|
||||
}
|
||||
|
||||
# 可打印 ASCII 归一成 Char:<小写>,这样 -InputScript 里写 'Char:k' 就是按 k
|
||||
if ($Character -and $Character -match '^[ -~]$') { return 'Char:' + $Character.ToLowerInvariant() }
|
||||
return 'Unknown' + $VirtualKeyCode
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
function New-BakNRetInputDriver {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
造一个输入驱动:要么按给定的按键序列走(给门禁用),要么读真终端。
|
||||
|
||||
.DESCRIPTION
|
||||
为什么是"驱动对象"而不是模块级状态:这个仓库的原则是**状态属于调用方**(见 ADR-0011)。
|
||||
界面循环自己持有这个对象,读完就丢;模块只提供读的能力。
|
||||
|
||||
-Script 给的是键名序列(例如 @('Down', 'Enter')),它会**按顺序**被消费;
|
||||
序列用尽而界面还在等输入时必须**报错退出**,绝不能退回去读真终端 —— 那会让门禁挂起,
|
||||
而挂起比变红糟得多(红会告诉你哪里错了,挂起只会耗掉你的时间)。
|
||||
#>
|
||||
param(
|
||||
[string[]]$Script,
|
||||
[switch]$NonInteractive
|
||||
)
|
||||
|
||||
$hasConsole = -not [Console]::IsOutputRedirected
|
||||
if ($NonInteractive -and -not $Script -and $hasConsole) {
|
||||
# 明确要求非交互,却既没有脚本又坐在真终端前:不猜,直接报错
|
||||
throw '指定了 -NonInteractive 但没有给出 -Script:没有可用的输入来源。'
|
||||
}
|
||||
|
||||
# 先把数组算好,再放进哈希。
|
||||
# 两个都踩过的坑:① `if` 是**语句**不是表达式,写在哈希字面量的值位置上会得不到结果;
|
||||
# ② 不能写 @($Script) —— $Script 为 $null 时 @($null) 会造出**一个含 $null 的单元素数组**,
|
||||
# 于是"空序列"被当成"有一个键",读者静默返回 $null 而不报错(而且两个 PowerShell 版本表现
|
||||
# 还不一样:实测 7 静默返回、5.1 抛错)。所以显式分岔,让"没有序列"就是 0 个元素。
|
||||
$scriptKeys = if ($Script) { @($Script) } else { @() }
|
||||
|
||||
return [pscustomobject]@{
|
||||
Script = $scriptKeys
|
||||
Index = 0
|
||||
HasConsole = $hasConsole
|
||||
NonInteractive = [bool]$NonInteractive
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
function Read-BakNRetKey {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
从驱动里取下一个键名。
|
||||
|
||||
.DESCRIPTION
|
||||
三条来源,按优先级:① 驱动里的脚本序列(门禁用)② 真终端的 RawUI.ReadKey(人用)
|
||||
③ 都没有 → 报错。**绝不静默返回一个默认键**:那样界面会自己往下走,而"没人按键却继续
|
||||
执行"在备份工具里等于替你做了决定。
|
||||
|
||||
ReadKey 必须带 NoEcho:否则你按的键会回显到界面上,把刚画好的菜单弄花。
|
||||
#>
|
||||
param([Parameter(Mandatory = $true)]$Driver)
|
||||
|
||||
if ($Driver.Script.Count -gt 0) {
|
||||
if ($Driver.Index -ge $Driver.Script.Count) {
|
||||
throw ("按键序列已用尽(共 {0} 个),但界面仍在等待输入。门禁里这种情况必须失败而不是挂起;" -f $Driver.Script.Count)
|
||||
}
|
||||
$key = $Driver.Script[$Driver.Index]
|
||||
$Driver.Index = $Driver.Index + 1
|
||||
return $key
|
||||
}
|
||||
|
||||
if (-not $Driver.HasConsole) {
|
||||
throw '没有可用的控制台,也没有给出按键序列:无法读取输入。'
|
||||
}
|
||||
|
||||
$pressed = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
|
||||
return (Get-BakNRetKeyName -VirtualKeyCode $pressed.VirtualKeyCode -Character $pressed.Character)
|
||||
}
|
||||
@@ -1432,6 +1432,39 @@ Test-Case '控制台列宽:中文 2 列、半角片假名 1 列、emoji 按一
|
||||
Assert-Equal 0 (Get-BakNRetCellWidth -Text '') '空串 = 0 列'
|
||||
}
|
||||
|
||||
Test-Case '按键归一化与输入缝:映射可无控制台测,序列用尽必须报错而不是挂起' {
|
||||
# 纯映射:整个 TUI 里唯一能无控制台测的部分,所以每条路径都钉住。
|
||||
Assert-Equal 'Up' (Get-BakNRetKeyName -VirtualKeyCode 38) '上'
|
||||
Assert-Equal 'Down' (Get-BakNRetKeyName -VirtualKeyCode 40) '下'
|
||||
Assert-Equal 'Left' (Get-BakNRetKeyName -VirtualKeyCode 37) '左'
|
||||
Assert-Equal 'Right' (Get-BakNRetKeyName -VirtualKeyCode 39) '右'
|
||||
Assert-Equal 'Enter' (Get-BakNRetKeyName -VirtualKeyCode 13) '回车'
|
||||
Assert-Equal 'Esc' (Get-BakNRetKeyName -VirtualKeyCode 27) 'Esc'
|
||||
Assert-Equal 'Space' (Get-BakNRetKeyName -VirtualKeyCode 32) '空格'
|
||||
Assert-Equal 'Char:k' (Get-BakNRetKeyName -VirtualKeyCode 75 -Character 'K') '字母统一成小写,且带冒号前缀'
|
||||
Assert-Equal 'Unknown255' (Get-BakNRetKeyName -VirtualKeyCode 255) '未知键也要有稳定名字,不能返回空'
|
||||
|
||||
# 驱动:按序列顺序消费(这一条曾经因为 Script 被折进注释而静默变成"没有序列")
|
||||
$driver = New-BakNRetInputDriver -Script @('Down', 'Enter')
|
||||
Assert-Equal 2 $driver.Script.Count '驱动里应当有两个键'
|
||||
Assert-Equal 'Down' (Read-BakNRetKey -Driver $driver) '第一个键'
|
||||
Assert-Equal 'Enter' (Read-BakNRetKey -Driver $driver) '第二个键'
|
||||
|
||||
# 关键一条:序列用尽必须失败。门禁里挂起比变红糟得多,而"退回去读真终端"正是挂起的成因。
|
||||
$threw = $false
|
||||
try { Read-BakNRetKey -Driver $driver | Out-Null } catch { $threw = $true }
|
||||
Assert-True $threw '按键序列用尽后必须抛错'
|
||||
|
||||
# 另一个"不猜"的路径:没有控制台也没有序列时,不能静默返回一个默认键
|
||||
$threw2 = $false
|
||||
try {
|
||||
$driver2 = New-BakNRetInputDriver -NonInteractive
|
||||
Assert-Equal 0 $driver2.Script.Count '空序列必须是 0 个元素(不能是含 $null 的单元素数组)'
|
||||
Read-BakNRetKey -Driver $driver2 | Out-Null
|
||||
} catch { $threw2 = $true }
|
||||
Assert-True $threw2 '没有任何输入来源时必须抛错'
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Reference in new issue
Block a user