整个 TUI 的地基:位置、对齐、边框都靠它。而它算错了**不会报错** —— 表现只是菜单右边框歪一点、光标偏一列,所以每条边界都在断言里钉住。 为什么必须自己算:.Length 是 UTF-16 code unit 的个数(汉字 1 个 char、2 列);而 $Host.UI.RawUI.LengthInBufferCells 在 PowerShell 7 上对、在 5.1 上错(实测 中文 返回 2、字体边框字符 返回 6),且不抛错 —— 这种"一边对一边错"的 API 比两边都错更危险。 表里三个边界是实测踩过的:半角片假名 U+FF61–FF9F 是 1 列(不能划进全角区);全角拉丁 U+FF00–FF60 是 2 列;emoji 是代理对,按一个码点算。 验收:断言 8 条(含空串与控制字符);test.ps1 9/9 全绿(5.1 与 7)。
78 lines
5.5 KiB
PowerShell
78 lines
5.5 KiB
PowerShell
param([Parameter(Mandatory=$true)][string]$OutFile, [Parameter(Mandatory=$true)][string]$Work)
|
|
$lines = New-Object System.Collections.Generic.List[string]
|
|
function L($s) { $lines.Add($s); [System.IO.File]::WriteAllLines($OutFile, $lines, (New-Object System.Text.UTF8Encoding($false))) }
|
|
L ("PSVersion: " + $PSVersionTable.PSVersion + " IsOutputRedirected=" + [Console]::IsOutputRedirected)
|
|
$nstack = Join-Path $Work 'nstack111\lib\netstandard2.0\NStack.dll'
|
|
$tg = Join-Path $Work 'tg115\lib\net472\Terminal.Gui.dll'
|
|
$sc = Join-Path $Work 'sc049\lib\netstandard2.0\Spectre.Console.dll'
|
|
function Dump($asm, $tag) {
|
|
L (" [" + $tag + "] FullName = " + $asm.FullName)
|
|
try { L (" [$tag] Location = " + $asm.Location) } catch { L (" [$tag] Location: dynamic") }
|
|
try {
|
|
$ts = $asm.GetTypes()
|
|
L (" [$tag] GetTypes() = " + $ts.Count + " types")
|
|
$pub = $ts | Where-Object { $_.IsPublic } | Select-Object -First 4
|
|
L (" [$tag] sample public: " + (($pub | ForEach-Object { $_.FullName }) -join ' | '))
|
|
$app = $ts | Where-Object { $_.FullName -eq 'Terminal.Gui.Application' }
|
|
L (" [$tag] type named Terminal.Gui.Application in GetTypes(): " + $(if ($app) { 'YES -> ' + $app.AssemblyQualifiedName } else { 'NO' }))
|
|
} catch { L (" [$tag] GetTypes() THREW: " + $_.Exception.Message) }
|
|
}
|
|
L ""
|
|
L "=== STEP 1: Add-Type -Path (no prior load) ==="
|
|
Add-Type -Path $nstack
|
|
L " NStack Add-Type OK"
|
|
Add-Type -Path $tg
|
|
L " Terminal.Gui Add-Type OK"
|
|
$loaded = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -in @('NStack','Terminal.Gui') }
|
|
foreach ($a in $loaded) { Dump $a $a.GetName().Name }
|
|
L ""
|
|
L "=== STEP 2: type resolution methods ==="
|
|
$tgAsm = $loaded | Where-Object { $_.GetName().Name -eq 'Terminal.Gui' }
|
|
L (" asm.GetType('Terminal.Gui.Application') -> " + $(try { $r = $tgAsm.GetType('Terminal.Gui.Application'); if ($r) { 'FOUND ' + $r.FullName } else { 'null' } } catch { 'THREW ' + $_.Exception.Message }))
|
|
L (" [Type]::GetType('Terminal.Gui.Application, Terminal.Gui') -> " + $(try { $r = [Type]::GetType('Terminal.Gui.Application, Terminal.Gui'); if ($r) { 'FOUND ' + $r.FullName } else { 'null' } } catch { 'THREW ' + $_.Exception.Message }))
|
|
try {
|
|
$aq = $tgAsm.GetName().FullName
|
|
L (" asm fullname = " + $aq)
|
|
$r2 = [Type]::GetType('Terminal.Gui.Application, ' + $aq)
|
|
L (" [Type]::GetType(full qualified) -> " + $(if ($r2) { 'FOUND' } else { 'null' }))
|
|
} catch { L (" full-qualified Type.GetType THREW: " + $_.Exception.Message) }
|
|
L ""
|
|
L "=== STEP 3: Application static method overloads (real API) ==="
|
|
$appType = $tgAsm.GetType('Terminal.Gui.Application')
|
|
if ($appType) {
|
|
foreach ($m in ($appType.GetMethods([System.Reflection.BindingFlags]'Public,Static') | Where-Object { $_.Name -in @('Init','Shutdown','Run','Refresh','Begin','End') })) {
|
|
L (" " + $m.ReturnType.Name + " " + $m.Name + "(" + (($m.GetParameters() | ForEach-Object { $_.ParameterType.Name + ' ' + $_.Name }) -join ', ') + ")")
|
|
}
|
|
}
|
|
L ""
|
|
L "=== STEP 4: Spectre.Console type resolution ==="
|
|
Add-Type -Path $sc
|
|
L " Spectre.Console Add-Type OK"
|
|
$scAsm = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Spectre.Console' }
|
|
Dump $scAsm 'Spectre.Console'
|
|
$ansiT = $scAsm.GetType('Spectre.Console.AnsiConsole')
|
|
L (" scAsm.GetType('Spectre.Console.AnsiConsole') -> " + $(if ($ansiT) { 'FOUND' } else { 'null' }))
|
|
if ($ansiT) {
|
|
L (" static methods of AnsiConsole: " + (($ansiT.GetMethods([System.Reflection.BindingFlags]'Public,Static') | Select-Object -First 25 | ForEach-Object { $_.Name }) -join ', '))
|
|
}
|
|
L ""
|
|
L "=== STEP 5: is Table.AddRow an EXTENSION method on IGridColumn/Table? ==="
|
|
$tableT = $scAsm.GetType('Spectre.Console.Table')
|
|
L (" Table type: " + [bool]$tableT)
|
|
if ($tableT) {
|
|
L (" Table instance methods containing 'Add': " + (($tableT.GetMethods() | Where-Object { $_.Name -like '*Add*' } | ForEach-Object { $_.Name + '(' + (($_.GetParameters() | ForEach-Object { $_.ParameterType.Name }) -join ',') + ')' }) -join ' | '))
|
|
L (" Table properties: " + (($tableT.GetProperties() | ForEach-Object { $_.PropertyType.Name + ' ' + $_.Name }) -join ', '))
|
|
}
|
|
$extT = $scAsm.GetType('Spectre.Console.TableExtensions')
|
|
L (" TableExtensions type: " + [bool]$extT)
|
|
if ($extT) {
|
|
$em = $extT.GetMethods([System.Reflection.BindingFlags]'Public,Static') | Where-Object { $_.Name -like '*Row*' -or $_.Name -like '*Column*' }
|
|
foreach ($m in $em) { L (" ext: " + $m.ReturnType.Name + ' ' + $m.Name + '(' + (($m.GetParameters() | ForEach-Object { $_.ParameterType.Name + ' ' + $_.Name }) -join ', ') + ')') }
|
|
}
|
|
L ""
|
|
L "=== STEP 6: Spectre prompts / progress classes available in netstandard2.0 build ==="
|
|
foreach ($n in @('Spectre.Console.ConfirmationPrompt','Spectre.Console.SelectionPrompt`1','Spectre.Console.MultiSelectionPrompt`1','Spectre.Console.TextPrompt`1','Spectre.Console.Progress','Spectre.Console.Status','Spectre.Console.Live','Spectre.Console.Layout','Spectre.Console.Panel','Spectre.Console.Rule','Spectre.Console.Tree','Spectre.Console.BarChart','Spectre.Console.BreakdownChart','Spectre.Console.ProgressBar','Spectre.Console.ProgressColumn','Spectre.Console.SpinnerColumn','Spectre.Console.Columns','Spectre.Console.Rows','Spectre.Console.Grid','Spectre.Console.Padder','Spectre.Console.Align','Spectre.Console.Canvas','Spectre.Console.Json','Spectre.Console.Calendar','Spectre.Console.ExceptionFormats','Spectre.Console.AnsiConsoleSettings','Spectre.Console.AnsiConsoleFactory','Spectre.Console.IAnsiConsole')) {
|
|
$tt = $scAsm.GetType($n)
|
|
L (" " + $n.PadRight(44) + " -> " + $(if ($tt) { 'AVAILABLE' } else { 'MISSING' }))
|
|
}
|