feat(tui): 定位写与按列对齐(第一轮第三块)
Format-BakNRetPaddedText:按**列宽**补齐或裁剪,且绝不切开宽字符 —— 剩余宽度差 1 列而下一个字符要占 2 列时,停下用空格补那 1 列。返回的字符串保证恰好 N 列,这条不变式是画边框的基础(按 .Length 补会让边框歪,按 .Length 裁会把汉字劈成半个显示成乱码)。 Write-BakNRetAt:第一道闸门是 [Console]::IsOutputRedirected,**不是** $Host.UI.SupportsVirtualTerminal(实测后者无控制台时仍返回 True,而 SetCursorPosition 会抛异常)。没有控制台就完全不定位、只把文本写出去;坐标越界时跳过定位但仍写出文本 —— 计划任务与窄窗口不该让 TUI 崩掉,只是画得难看一点。颜色走 Write-Host -ForegroundColor(零 VT 依赖)。 验收:断言 10 条(含"裁剪后恰好 5 列且不是 4 个字符"这条关键判据);两版手工输出一致;test.ps1 9/9 全绿。
This commit is contained in:
1 parent
f2959a4279
commit
d9d7967e64
5 files changed
+126
No files matched your search
@@ -19,6 +19,8 @@
|
|||||||
CompatiblePSEditions = @('Desktop', 'Core')
|
CompatiblePSEditions = @('Desktop', 'Core')
|
||||||
|
|
||||||
FunctionsToExport = @(
|
FunctionsToExport = @(
|
||||||
|
'Format-BakNRetPaddedText',
|
||||||
|
'Write-BakNRetAt',
|
||||||
'Get-BakNRetKeyName',
|
'Get-BakNRetKeyName',
|
||||||
'New-BakNRetInputDriver',
|
'New-BakNRetInputDriver',
|
||||||
'Read-BakNRetKey',
|
'Read-BakNRetKey',
|
||||||
|
|||||||
@@ -150,7 +150,12 @@
|
|||||||
. (Join-Path $PSScriptRoot 'Public\New-BakNRetInputDriver.ps1')
|
. (Join-Path $PSScriptRoot 'Public\New-BakNRetInputDriver.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'Public\Read-BakNRetKey.ps1')
|
. (Join-Path $PSScriptRoot 'Public\Read-BakNRetKey.ps1')
|
||||||
|
|
||||||
|
. (Join-Path $PSScriptRoot 'Public\Format-BakNRetPaddedText.ps1')
|
||||||
|
. (Join-Path $PSScriptRoot 'Public\Write-BakNRetAt.ps1')
|
||||||
|
|
||||||
Export-ModuleMember -Function @(
|
Export-ModuleMember -Function @(
|
||||||
|
'Format-BakNRetPaddedText',
|
||||||
|
'Write-BakNRetAt',
|
||||||
'Get-BakNRetKeyName',
|
'Get-BakNRetKeyName',
|
||||||
'New-BakNRetInputDriver',
|
'New-BakNRetInputDriver',
|
||||||
'Read-BakNRetKey',
|
'Read-BakNRetKey',
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
function Format-BakNRetPaddedText {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
把文本按**列宽**补齐或裁剪到指定宽度。
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
为什么不能用 `.PadRight()` / `.Substring()`:中文 1 个 char 占 2 列。按字符数补,边框会歪;
|
||||||
|
按字符数裁,会把一个汉字劈成半个、显示成乱码。
|
||||||
|
|
||||||
|
这里一律按 Get-BakNRetCellWidth 算,并且**绝不切开一个宽字符** —— 当剩余宽度差 1 列而下一个
|
||||||
|
字符要占 2 列时,就停下并用空格补上那 1 列。返回的字符串保证**恰好 Width 列**(Width ≤ 0 时
|
||||||
|
返回空串),这条不变式是画边框与对齐的基础。
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Text,
|
||||||
|
[Parameter(Mandatory = $true)][int]$Width
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($Width -le 0) { return '' }
|
||||||
|
|
||||||
|
$textWidth = Get-BakNRetCellWidth -Text $Text
|
||||||
|
if ($textWidth -le $Width) {
|
||||||
|
return $Text + (' ' * ($Width - $textWidth))
|
||||||
|
}
|
||||||
|
|
||||||
|
# 超宽:按列裁剪,绝不切开代理对或宽字符
|
||||||
|
$kept = ''
|
||||||
|
$used = 0
|
||||||
|
$index = 0
|
||||||
|
while ($index -lt $Text.Length) {
|
||||||
|
$chunk = [string]$Text[$index]
|
||||||
|
$high = [int]$Text[$index]
|
||||||
|
if ($high -ge 0xD800 -and $high -le 0xDBFF -and ($index + 1) -lt $Text.Length) {
|
||||||
|
$low = [int]$Text[$index + 1]
|
||||||
|
if ($low -ge 0xDC00 -and $low -le 0xDFFF) {
|
||||||
|
$chunk = $Text.Substring($index, 2)
|
||||||
|
$index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$index++
|
||||||
|
|
||||||
|
$chunkWidth = Get-BakNRetCellWidth -Text $chunk
|
||||||
|
if (($used + $chunkWidth) -gt $Width) { break }
|
||||||
|
$kept += $chunk
|
||||||
|
$used += $chunkWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
return $kept + (' ' * ($Width - $used))
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
function Write-BakNRetAt {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
在指定坐标写一段文本;没有控制台时退化成普通输出。
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
第一道闸门是 `[Console]::IsOutputRedirected`,**不是** `$Host.UI.SupportsVirtualTerminal` ——
|
||||||
|
实测后者在没有控制台时仍返回 `True`(撒谎),而 `[Console]::SetCursorPosition` 会直接抛异常。
|
||||||
|
所以:没有控制台就完全不定位、只把文本打出来,让重定向 / 日志 / CI 场景拿到可读输出而不是异常。
|
||||||
|
|
||||||
|
坐标会被夹到当前窗口内:越界时跳过定位但**仍然写出文本**。计划任务与窄窗口都不该让 TUI 崩掉,
|
||||||
|
只是画得难看一点。
|
||||||
|
|
||||||
|
颜色走 `Write-Host -ForegroundColor`(零 VT 依赖,conhost 与 Windows Terminal 都行),
|
||||||
|
这样即使某个环境的 VT 没开,界面仍能有眼色而不是一屏转义序列。
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][int]$X,
|
||||||
|
[Parameter(Mandatory = $true)][int]$Y,
|
||||||
|
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Text,
|
||||||
|
[string]$ForegroundColor,
|
||||||
|
[switch]$NoNewline
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([Console]::IsOutputRedirected) {
|
||||||
|
# 没有控制台:不定位、不抛错,只把文本写出去
|
||||||
|
if ($ForegroundColor) { Write-Host $Text -ForegroundColor $ForegroundColor -NoNewline:$NoNewline }
|
||||||
|
else { Write-Host $Text -NoNewline:$NoNewline }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$windowWidth = [Console]::WindowWidth
|
||||||
|
$windowHeight = [Console]::WindowHeight
|
||||||
|
if ($X -ge 0 -and $X -lt $windowWidth -and $Y -ge 0 -and $Y -lt $windowHeight) {
|
||||||
|
[Console]::SetCursorPosition($X, $Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# 定位失败不影响把文本写出去:画得难看,好过整个运行失败
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ForegroundColor) { Write-Host $Text -ForegroundColor $ForegroundColor -NoNewline:$NoNewline }
|
||||||
|
else { Write-Host $Text -NoNewline:$NoNewline }
|
||||||
|
}
|
||||||
@@ -1465,6 +1465,31 @@ Test-Case '按键归一化与输入缝:映射可无控制台测,序列用尽
|
|||||||
Assert-True $threw2 '没有任何输入来源时必须抛错'
|
Assert-True $threw2 '没有任何输入来源时必须抛错'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Test-Case 'TUI 文本对齐:按列宽补齐/裁剪,绝不切开宽字符,无控制台下不抛错' {
|
||||||
|
# 补齐:按列宽而不是字符数
|
||||||
|
$padded = Format-BakNRetPaddedText -Text '中文' -Width 10
|
||||||
|
Assert-Equal 10 (Get-BakNRetCellWidth -Text $padded) '补齐后应当恰好 10 列'
|
||||||
|
Assert-Equal 8 $padded.Length '10 列 = 8 个字符(两个汉字各占 2 列)'
|
||||||
|
Assert-Equal 'AB ' (Format-BakNRetPaddedText -Text 'AB' -Width 5) 'ASCII 补齐'
|
||||||
|
|
||||||
|
# 裁剪:'中文字' 是 6 列,裁到 5 列必须停在 '中文'(4 列)+ 1 个空格,
|
||||||
|
# 而不是把第三个汉字劈成半个 —— 那会显示成乱码。
|
||||||
|
$clipped = Format-BakNRetPaddedText -Text '中文字' -Width 5
|
||||||
|
Assert-Equal 5 (Get-BakNRetCellWidth -Text $clipped) '裁剪后应当恰好 5 列'
|
||||||
|
Assert-Equal 3 $clipped.Length '3 个字符(中文 + 1 空格),不是 4'
|
||||||
|
Assert-Equal '中' $clipped.Substring(0, 1) '前两个汉字保留'
|
||||||
|
Assert-Equal ' ' $clipped.Substring(2, 1) '剩下的 1 列用空格补'
|
||||||
|
|
||||||
|
# 边界:宽度 0 / 负数返回空串;空文本也能补齐
|
||||||
|
Assert-Equal 0 (Format-BakNRetPaddedText -Text 'abc' -Width 0).Length '宽度 0 返回空串'
|
||||||
|
Assert-Equal 4 (Format-BakNRetPaddedText -Text '' -Width 4).Length '空文本补成纯空格'
|
||||||
|
|
||||||
|
# 无控制台(本进程的真实环境就是重定向)+ 越界坐标:不能抛错
|
||||||
|
$threw = $false
|
||||||
|
try { Write-BakNRetAt -X 9999 -Y 9999 -Text 'x' } catch { $threw = $true }
|
||||||
|
Assert-True (-not $threw) '没有控制台且坐标越界时也不能抛错(TUI 画得难看,好过运行失败)'
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|||||||
Reference in new issue
Block a user