From 5df40d3341ae99a1004653cd694e7d26a9bde88d Mon Sep 17 00:00:00 2001 From: Shuery <2463253700@qq.com> Date: Sun, 27 Sep 2026 16:40:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(tui):=20=E6=B8=85=E5=8D=95=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E5=BE=AA=E7=8E=AF=EF=BC=88=E7=AC=AC=E4=B8=80?= =?UTF-8?q?=E8=BD=AE=E7=AC=AC=E4=BA=94=E5=9D=97=C2=B7=E8=A3=85=E9=85=8D?= =?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 装配:选行 → 选新方向 → 逐行校验 → 校验后原子保存 + 时间戳备份。输入走 -Driver,所以门禁里能用 -InputScript @('Enter','Down','Enter') 走完整流程,断言看返回值与**文件内容**,不需要捕获控制台输出。 四条"不猜":取消时什么都不写也不产生备份;方向菜单**预选当前值**(不预选的话"连按两次回车"会把字段改成菜单第一项 —— 不是没改,是改错了);方向没变直接返回"没有变化";保存前逐行校验(每行都要能被 ConvertFrom-BackupListLine 解析)—— 这正是 TUI 编辑器比手改安全的地方。 这个函数我试了两次才落地,两次都栽在同一件事上:**用脚本做多行字符串替换时,换行被折成空格**,于是注释与赋值落进同一行、被整段注释掉(症状是"对象没有属性"/"流程莫名走取消分支")。所以这条现在当硬约束执行:多行代码一律整份写文件,不做插入+删除式的拼接。 判据 12 条,覆盖保存 / 取消 / 无变化三条路径,并逐条断言注释行与未编辑行一字不动、备份里存原文、取消时不新增备份。 验收:test.ps1 9/9 全绿(5.1 与 7)。 --- BakNRet/BakNRet.psd1 | 1 + BakNRet/BakNRet.psm1 | 3 + .../Public/Invoke-BakNRetBackupListEditor.ps1 | 105 ++++++++++++++++++ tests/Run-Tests.ps1 | 38 +++++++ 4 files changed, 147 insertions(+) create mode 100644 BakNRet/Public/Invoke-BakNRetBackupListEditor.ps1 diff --git a/BakNRet/BakNRet.psd1 b/BakNRet/BakNRet.psd1 index f5a824f..b39d02f 100644 --- a/BakNRet/BakNRet.psd1 +++ b/BakNRet/BakNRet.psd1 @@ -19,6 +19,7 @@ CompatiblePSEditions = @('Desktop', 'Core') FunctionsToExport = @( + 'Invoke-BakNRetBackupListEditor', 'Set-BakNRetBackupListDirection', 'Save-BakNRetConfigFile', 'Get-BakNRetBackupListRow', diff --git a/BakNRet/BakNRet.psm1 b/BakNRet/BakNRet.psm1 index fbcf18e..7437a9e 100644 --- a/BakNRet/BakNRet.psm1 +++ b/BakNRet/BakNRet.psm1 @@ -164,7 +164,10 @@ . (Join-Path $PSScriptRoot 'Public\Set-BakNRetBackupListDirection.ps1') +. (Join-Path $PSScriptRoot 'Public\Invoke-BakNRetBackupListEditor.ps1') + Export-ModuleMember -Function @( + 'Invoke-BakNRetBackupListEditor', 'Set-BakNRetBackupListDirection', 'Save-BakNRetConfigFile', 'Get-BakNRetBackupListRow', diff --git a/BakNRet/Public/Invoke-BakNRetBackupListEditor.ps1 b/BakNRet/Public/Invoke-BakNRetBackupListEditor.ps1 new file mode 100644 index 0000000..5befb97 --- /dev/null +++ b/BakNRet/Public/Invoke-BakNRetBackupListEditor.ps1 @@ -0,0 +1,105 @@ +function Invoke-BakNRetBackupListEditor { + <# + .SYNOPSIS + 清单的行编辑器:选一行 → 选新方向 → 逐行校验 → 校验后原子保存(并留时间戳备份)。 + + .DESCRIPTION + 把前面几块装配起来:行表(Get-BakNRetBackupListRow)、单行替换(Set-BakNRetBackupListRow)、 + 改方向(Set-BakNRetBackupListDirection)、校验后原子保存(Save-BakNRetConfigFile)。 + + 因为输入走 -Driver,整个流程在门禁里可以被 `-InputScript @('Enter','Down','Enter')` 驱动, + 断言看的是**返回值**与**文件内容**,不需要捕获控制台输出 —— 与菜单循环同一个套路。 + + 四条"不猜": + * 选行或选方向时按 Esc → 什么都不写,也不产生备份; + * **方向菜单预选当前值** —— 不预选的话,"连按两次回车"会把字段改成菜单的第一项, + 那不是"没改",是改错了(实测踩到); + * 方向没变 → 直接返回"没有要保存的内容",不做无意义的写盘与备份; + * 保存前**逐行校验**(每行都要能被 ConvertFrom-BackupListLine 解析)——这正是 TUI 编辑器 + 比手改安全的地方:手改没有校验。 + #> + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)]$Driver, + [switch]$WhatIf + ) + + if (-not (Test-Path -LiteralPath $Path)) { + return [pscustomobject]@{ Saved = $false; Changed = $false; Error = "找不到清单文件:$Path"; BackupPath = $null } + } + + $text = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 + $rows = @(Get-BakNRetBackupListRow -Text $text) + $entryRows = @($rows | Where-Object { $_.Record }) + + if ($entryRows.Count -eq 0) { + return [pscustomobject]@{ Saved = $false; Changed = $false; Error = '清单里没有可编辑的条目。'; BackupPath = $null } + } + + $items = @(foreach ($row in $entryRows) { + [pscustomobject]@{ + Key = [string]$row.Line + Text = ('第 {0,3} 行 {1,-7} {2}' -f $row.Line, $row.Record.Direction, $row.Record.Path) + } + }) + + $picked = Invoke-BakNRetMenu -Title ('清单:{0}' -f $Path) -Items $items -Driver $Driver -MultiSelect $false -Hint '↑↓ 选行 回车 编辑 Esc 取消' + if ($picked.Action -ne 'confirm') { + return [pscustomobject]@{ Saved = $false; Changed = $false; Error = ''; BackupPath = $null } + } + + $lineNumber = [int]$picked.Chosen.Key + $target = $rows | Where-Object { $_.Line -eq $lineNumber } | Select-Object -First 1 + + $directionItems = @( + [pscustomobject]@{ Key = 'both'; Text = 'both  两个方向都处理(不带标记)' }, + [pscustomobject]@{ Key = 'backup'; Text = 'backup 只备份(行首 +)' }, + [pscustomobject]@{ Key = 'restore'; Text = 'restore 只恢复(行首 -)' } + ) + + # 预选当前取值:菜单停在字段现在的值上,用户要改才按方向键。 + $initialIndex = 0 + for ($scan = 0; $scan -lt $directionItems.Count; $scan++) { + if ($directionItems[$scan].Key -eq [string]$target.Record.Direction) { + $initialIndex = $scan + break + } + } + + $chosen = Invoke-BakNRetMenu -Title '改成哪个方向?' -Items $directionItems -Driver $Driver -MultiSelect $false -InitialIndex $initialIndex -Hint '↑↓ 选方向 回车 应用 Esc 取消' + if ($chosen.Action -ne 'confirm') { + return [pscustomobject]@{ Saved = $false; Changed = $false; Error = ''; BackupPath = $null } + } + + $newLine = Set-BakNRetBackupListDirection -Raw $target.Raw -Direction $chosen.Chosen.Key + if ($newLine -eq $target.Raw) { + return [pscustomobject]@{ Saved = $false; Changed = $false; Error = '方向没有变化,没有要保存的内容。'; BackupPath = $null } + } + + $newText = Set-BakNRetBackupListRow -Text $text -Line $lineNumber -NewText $newLine + + $validate = { + param($candidate) + $bad = @() + $number = 0 + foreach ($one in ($candidate -split "`r?`n")) { + $number++ + $trimmed = $one.Trim() + if (-not $trimmed -or $trimmed.StartsWith('#')) { continue } + if (-not (ConvertFrom-BackupListLine -Line $one)) { + $bad += ('第 {0} 行解析不了:{1}' -f $number, $trimmed) + } + } + return $bad + } + + $saved = Save-BakNRetConfigFile -Path $Path -Text $newText -Validate $validate ` + -BackupDirectory (Join-Path (Split-Path -Parent $Path) 'logs\config-backups') -WhatIf:$WhatIf + + return [pscustomobject]@{ + Saved = $saved.Saved + Changed = $true + Error = $saved.Error + BackupPath = $saved.BackupPath + } +} diff --git a/tests/Run-Tests.ps1 b/tests/Run-Tests.ps1 index 62f9bce..49ba7cc 100644 --- a/tests/Run-Tests.ps1 +++ b/tests/Run-Tests.ps1 @@ -1647,6 +1647,44 @@ Test-Case '改方向:只动行首标记,其余逐字节保持,且保留用 Assert-True $threw '注释行改方向必须抛错' } +Test-Case '清单编辑器:脚本驱动走完"选行→改方向→保存",只动一行且留下备份' { + $dir = Join-Path $env:TEMP ("bnr-listed-" + [guid]::NewGuid().ToString('N').Substring(0, 8)) + New-Item -ItemType Directory -Path $dir -Force | Out-Null + $file = Join-Path $dir 'BackupList.txt' + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + $original = "# 注释一`nEdge`n+WindowsTerminal :encrypt`n" + [System.IO.File]::WriteAllText($file, $original, $utf8Bom) + $backupDir = Join-Path $dir 'logs\config-backups' + try { + # 1) 选第一行(回车)、方向菜单预选当前值 both、下移到 backup、回车应用 + $r = Invoke-BakNRetBackupListEditor -Path $file -Driver (New-BakNRetInputDriver -Script @('Enter', 'Down', 'Enter')) + Assert-Equal $true $r.Saved '应当保存' + $after = @((Get-Content -LiteralPath $file -Raw -Encoding UTF8) -split "`r?`n") + Assert-Equal '# 注释一' $after[0] '注释行一字不动' + Assert-Equal '+Edge' $after[1] '第一行改成了 +Edge' + Assert-Equal '+WindowsTerminal :encrypt' $after[2] '第三行一字不动' + Assert-True (Test-Path $r.BackupPath) '应当留下时间戳备份' + Assert-Equal $original ([System.IO.File]::ReadAllText($r.BackupPath)) '备份里是原文' + + # 2) 取消(Esc):什么都不写,也不产生新备份 + $backupsBefore = @(Get-ChildItem $backupDir -File | Measure-Object).Count + $contentBefore = [System.IO.File]::ReadAllText($file) + $c = Invoke-BakNRetBackupListEditor -Path $file -Driver (New-BakNRetInputDriver -Script @('Esc')) + Assert-Equal $false $c.Saved '取消不该保存' + Assert-Equal $false $c.Changed '取消不该标记为已改' + Assert-Equal $contentBefore ([System.IO.File]::ReadAllText($file)) '取消后文件内容不变' + Assert-Equal $backupsBefore @(Get-ChildItem $backupDir -File | Measure-Object).Count '取消不该新增备份' + + # 3) 方向没变:菜单预选当前值 backup,直接回车 → 说明"没有变化",不白写一次盘 + $same = Invoke-BakNRetBackupListEditor -Path $file -Driver (New-BakNRetInputDriver -Script @('Enter', 'Enter')) + Assert-Equal $false $same.Saved '方向没变不该保存' + Assert-True ($same.Error -like '*没有变化*') '应当说明原因' + Assert-Equal $contentBefore ([System.IO.File]::ReadAllText($file)) '文件内容仍然不变' + } finally { + Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue + } +} + # ============================================================================ Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue