docs: TUI 与入口重组的设计记录(4 条 ADR + 验收锚点)

grilling 三轮把设计树走完了,这里把它落成文件,避免决策只活在对话里。

ADR-0010 零依赖自研 TUI:三个候选库全部实测排除 —— ConsoleGuiTools 的 PSGallery 元数据声明最低 7.2(5.1 装不上);Spectre.Console 0.49.1 能在 5.1 加载但要塞 4 个第三方 DLL 且不支持鼠标;Terminal.Gui 1.15.0 能反射接线到真的渲染出窗口,但 Application.Shutdown() 在两端都抛 NRE。同时记下两条实测坑:无控制台时 $Host.UI.SupportsVirtualTerminal 会撒谎返回 True(第一道闸门必须是 [Console]::IsOutputRedirected),以及 5.1 的 RawUI.LengthInBufferCells 对中文返回 2、对边框字符返回 6(都不能用来排版)。

ADR-0011 进度回调这个例外:它是注入点不是状态,无头路径为 $null 时行为与今天完全一致。

ADR-0012 入口改名与垫片:这次留垫片而 Common.psm1 直接删,是因为前者是外部接口(断了是静默没用)后者是内部实现(断了当场报错)。并纠正了自己的一个伪前提 —— 本机实测没有注册任何 BakNRet 计划任务。

ADR-0013 TUI 写配置:外科式改写 + 校验通过才原子替换。硬事实是 SoftwareCatalog.psd1 根本不能被 Import-PowerShellDataFile 读入(动态表达式),任何"解析成对象再序列化"的方案都会毁掉表达式与注释。

.scratch/tui/spec.md 是这次改造的验收锚点:9 条已定决策 + 三轮的判据 + 风险表。

验收:Parse 两版仍绿。
This commit is contained in:
Shuery committed 2026-09-27 15:27:01 +08:00
1 parent 187549e2af
commit 60f2ae3e0f
25 files changed
+1708

No files matched your search

+200
View File
@@ -0,0 +1,200 @@
# ============================================================================
# ZERO-DEPENDENCY PowerShell TUI PROTOTYPE (works on 5.1 and 7.x)
# ASCII-only source so 5.1 (which reads non-BOM files as ANSI) parses it.
# ============================================================================
param([string]$OutFile, [switch]$Headless)
$script:Log = New-Object System.Collections.Generic.List[string]
function Note($s) { $script:Log.Add($s) }
# ---------------------------------------------------------------------------
# 1. CAPABILITY DETECTION -- never assume VT, never assume a console
# ---------------------------------------------------------------------------
$script:HasRealConsole = $false
try { $script:HasRealConsole = -not [Console]::IsOutputRedirected } catch { $script:HasRealConsole = $false }
$script:HasVT = $false
if ($script:HasRealConsole) {
try { $script:HasVT = [bool]$Host.UI.SupportsVirtualTerminal } catch { $script:HasVT = $false }
}
$script:Cols = 80; $script:Rows = 24
function Get-ConsoleSize {
$w = 0; $h = 0
try { $w = [Console]::WindowWidth; $h = [Console]::WindowHeight } catch { }
if ($w -le 0 -or $h -le 0) {
try { $s = $Host.UI.RawUI.WindowSize; $w = $s.Width; $h = $s.Height } catch { }
}
if ($w -le 0) { $w = 80 }
if ($h -le 0) { $h = 24 }
return @($w, $h)
}
# ---------------------------------------------------------------------------
# 2. DISPLAY WIDTH -- MUST be computed by hand. Measured facts:
# * ".Length" counts UTF-16 code units, NOT columns (CJK = 1 unit, 2 cols)
# * RawUI.LengthInBufferCells() is UNRELIABLE and INCONSISTENT:
# - 5.1: returns the char COUNT for CJK, and 2 for box-drawing chars
# - 7.x: returns 2 for CJK, 1 for box-drawing (correct)
# * Measure-Object -Character == .Length (useless for layout)
# So: own East-Asian-Wide table.
# ---------------------------------------------------------------------------
$script:WideRanges = @(
@(0x1100, 0x115F), @(0x2E80, 0x303E), @(0x3041, 0x33FF),
@(0x3400, 0x4DBF), @(0x4E00, 0x9FFF), @(0xA000, 0xA4CF),
@(0xAC00, 0xD7A3), @(0xF900, 0xFAFF), @(0xFE30, 0xFE6F),
@(0xFF00, 0xFF60), @(0xFFE0, 0xFFE6),
@(0x1F300, 0x1F64F), @(0x1F900, 0x1F9FF), @(0x20000, 0x2FFFD), @(0x30000, 0x3FFFD)
)
function Get-CellWidth {
param([string]$Text)
$w = 0
$i = 0
while ($i -lt $Text.Length) {
$cp = [int][char]$Text[$i]
if ($cp -ge 0xD800 -and $cp -le 0xDBFF -and ($i + 1) -lt $Text.Length) {
$lo = [int][char]$Text[$i + 1]
if ($lo -ge 0xDC00 -and $lo -le 0xDFFF) {
$cp = 0x10000 + (($cp - 0xD800) * 0x400) + ($lo - 0xDC00)
$i++
}
}
$cw = 1
foreach ($r in $script:WideRanges) { if ($cp -ge $r[0] -and $cp -le $r[1]) { $cw = 2; break } }
if ($cp -lt 0x20) { $cw = 0 }
if ($cp -eq 0x7F) { $cw = 0 }
$w += $cw
$i++
}
return $w
}
function Pad-ToWidth {
param([string]$Text, [int]$Width)
$cur = Get-CellWidth $Text
if ($cur -ge $Width) { return $Text }
return $Text + (' ' * ($Width - $cur))
}
# ---------------------------------------------------------------------------
# 3. LOW-LEVEL DRAW PRIMITIVES
# ---------------------------------------------------------------------------
function Move-To {
param([int]$X, [int]$Y)
if ($script:HasRealConsole) {
try { [Console]::SetCursorPosition($X, $Y); return } catch { }
}
if ($script:HasVT) { [Console]::Out.Write("$([char]27)[$($Y + 1);$($X + 1)H"); [Console]::Out.Flush() }
}
function Write-At {
param([int]$X, [int]$Y, [string]$Text, [string]$Color)
Move-To $X $Y
$useAnsi = ($script:HasVT) -and ((Get-CellWidth $Text) -eq 0 -or $Text -notmatch "[$([char]27)]")
if ($Color -and $useAnsi) {
$code = switch ($Color) { 'Red' { 91 } 'Green' { 92 } 'Yellow' { 93 } 'Cyan' { 96 } 'Gray' { 90 } default { 0 } }
if ($code) { Write-Host -NoNewline "$([char]27)[${code}m$Text$([char]27)[0m"; return }
}
if ($Color) { Write-Host -NoNewline -ForegroundColor $Color $Text } else { Write-Host -NoNewline $Text }
}
function New-Frame {
param([string]$Title, [int]$X, [int]$Y, [int]$W, [int]$H)
$tl = [char]0x250C; $tr = [char]0x2510; $bl = [char]0x2514; $br = [char]0x2518
$hz = [char]0x2500; $vt = [char]0x2502
Write-At $X $Y ($tl + ($hz.ToString() * ($W - 2)) + $tr) 'Cyan'
for ($r = 1; $r -lt ($H - 1); $r++) {
Write-At $X ($Y + $r) $vt 'Cyan'
Write-At ($X + $W - 1) ($Y + $r) $vt 'Cyan'
}
Write-At $X ($Y + $H - 1) ($bl + ($hz.ToString() * ($W - 2)) + $br) 'Cyan'
if ($Title) { Write-At ($X + 2) $Y (" $Title ") 'Yellow' }
}
# ---------------------------------------------------------------------------
# 4. MENU with multi-select: own key loop via RawUI.ReadKey
# ---------------------------------------------------------------------------
function Read-KeyRaw {
# Returns @{ Key; Char } -- 'NoEcho' keeps the console clean
try {
$k = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
return @{ Key = $k.VirtualKeyCode; Char = $k.Character }
} catch {
Note ("ReadKey unavailable: " + $_.Exception.Message)
return $null
}
}
function Show-Menu {
param([string[]]$Items, [string[]]$Checked, [string]$Title)
$sel = 0; $top = 0; $visible = 10
while ($true) {
# ---- render ----
Write-Host ""
New-Frame $Title 0 0 60 ($visible + 4)
for ($i = 0; $i -lt [Math]::Min($visible, $Items.Count); $i++) {
$idx = $top + $i
$mark = if ($Checked[$idx] -eq 'x') { '[x]' } else { '[ ]' }
$line = "$mark $($Items[$idx])"
$col = if ($idx -eq $sel) { 'Yellow' } else { 'Gray' }
Write-At 2 ($i + 2) (Pad-ToWidth $line (56)) $col
}
Write-At 2 ($visible + 2) (Pad-ToWidth 'Space=toggle Enter=confirm Esc=cancel Up/Down=move' 56) 'Gray'
$k = Read-KeyRaw
if (-not $k) { Note 'No raw key input available -> returning current state.'; break }
switch ($k.Key) {
38 { if ($sel -gt 0) { $sel--; if ($sel -lt $top) { $top-- } } } # Up
40 { if ($sel -lt $Items.Count - 1) { $sel++; if ($sel -ge ($top + $visible)) { $top++ } } } # Down
32 { if ($Checked[$sel] -eq 'x') { $Checked[$sel] = ' ' } else { $Checked[$sel] = 'x' } } # Space
13 { return $Checked } # Enter
27 { return $null } # Esc
}
}
return $Checked
}
# ---------------------------------------------------------------------------
# 5. PROGRESS -- two strategies, pick by capability
# ---------------------------------------------------------------------------
function Show-Progress {
param([int]$Percent, [string]$Label, [int]$Y)
$width = 40
$filled = [int]([Math]::Round($width * $Percent / 100.0))
$bar = ('#' * $filled) + ('.' * ($width - $filled))
Write-At 2 $Y (Pad-ToWidth ("$Label [$bar] $Percent%") 76) 'Cyan'
}
function Show-Spinner {
param([int]$Frame, [string]$Label, [int]$Y)
$s = @('|', '/', '-', '\')[$Frame % 4]
Write-At 2 $Y (Pad-ToWidth "$s $Label" 76) 'Yellow'
}
# ---------------------------------------------------------------------------
# RUN
# ---------------------------------------------------------------------------
Note ("PSVersion : " + $PSVersionTable.PSVersion)
Note ("PSEdition : " + $PSVersionTable.PSEdition)
Note ("HasRealConsole : " + $script:HasRealConsole)
Note ("HasVT : " + $script:HasVT)
$sz = Get-ConsoleSize
Note ("Console size : " + $sz[0] + "x" + $sz[1])
Note ("ReadKey class : " + [System.Management.Automation.Host.KeyInfo].FullName)
Note ("Measure width demo: 'AB' -> " + (Get-CellWidth 'AB') + " cells, 2 CJK chars -> " + (Get-CellWidth ([string]::Concat([char]0x4E2D, [char]0x6587))) + " cells")
Note ("Pad-ToWidth('中文', 10) length = " + (Pad-ToWidth ([string]::Concat([char]0x4E2D, [char]0x6587)) 10).Length + " chars / " + (Get-CellWidth (Pad-ToWidth ([string]::Concat([char]0x4E2D, [char]0x6587)) 10)) + " cells")
if ($Headless -or (-not $script:HasRealConsole)) {
Note 'HEADLESS MODE: skipping interactive menu + drawing (no console attached).'
Note 'This is exactly the branch a scheduled task must take.'
} else {
try { [Console]::CursorVisible = $false } catch { }
try { [Console]::Clear() } catch { }
Show-Progress 0 'Probe' 12
for ($p = 10; $p -le 100; $p += 30) { Show-Progress $p 'Probe' 12; Start-Sleep -Milliseconds 250 }
$items = @('alpha-backup', 'beta-backup', 'gamma-backup', 'delta-backup', 'epsilon-backup')
$checked = @('x', ' ', ' ', ' ', ' ')
$res = Show-Menu -Items $items -Checked $checked -Title 'Select backup sets'
if ($res) { Note ('USER SELECTED: ' + (($items | Where-Object { $res[$items.IndexOf($_)] -eq 'x' }) -join ', ')) }
else { Note 'USER CANCELLED' }
try { [Console]::Clear() } catch { }
try { [Console]::CursorVisible = $true } catch { }
}
if ($OutFile) { [System.IO.File]::WriteAllLines($OutFile, $script:Log, (New-Object System.Text.UTF8Encoding($false))) }
else { $script:Log | ForEach-Object { Write-Host $_ } }
+55
View File
@@ -0,0 +1,55 @@
param([Parameter(Mandatory=$true)][string]$OutFile)
$lines = New-Object System.Collections.Generic.List[string]
function L($s) { $lines.Add($s) }
function MK([int[]]$cp) { -join ($cp | ForEach-Object { [char]$_ }) }
$raw = $Host.UI.RawUI
$cases = @(
,@('8 ASCII ', 'A1b2C3d4')
,@('2 CJK ', (MK @(0x4E2D, 0x6587)))
,@('4 CJK ', (MK @(0x4E2D, 0x6587, 0x5907, 0x4EFD)))
,@('3 boxdraw ', (MK @(0x250C, 0x2500, 0x2510)))
,@('1 emoji ', [char]::ConvertFromUtf32(0x1F600))
,@('2 fullwidth', (MK @(0xFF21, 0xFF22)))
,@('2 halfkata ', (MK @(0xFF71, 0xFF72)))
,@('CJK+colon ', (MK @(0x4E2D, 0x6587, 0xFF1A, 0x0020)) + 'X')
,@('punctuation', (MK @(0x3002, 0x3001, 0xFF0C)))
,@('hangul ', (MK @(0xD55C, 0xAE00)))
,@('kana ', (MK @(0x3042, 0x3044)))
,@('cyrillic ', (MK @(0x0416, 0x0418)))
,@('accented ', (MK @(0x00E9, 0x00E8)))
,@('tabs+ctrl ', "a`tb")
)
L ("PSVersion: " + $PSVersionTable.PSVersion + " PSEdition: " + $PSVersionTable.PSEdition)
L ("IsOutputRedirected: " + [Console]::IsOutputRedirected)
L ""
L "### `$Host.UI.RawUI.LengthInBufferCells() -- BUILT-IN cell-width API ###"
L (" method present? " + [bool]($raw | Get-Member -Name LengthInBufferCells -MemberType Method))
$mi = $raw | Get-Member -Name LengthInBufferCells -MemberType Method
if ($mi) { foreach ($d in $mi) { L (" overload: " + $d.Definition) } }
L ""
L " {0,-12} {1,8} {2,14} {3,10}" -f 'case', '.Length', 'LengthInBuffer', 'expected~'
foreach ($c in $cases) {
$nm = $c[0]; $s = [string]$c[1]
$w = 'n/a'
try { $w = $raw.LengthInBufferCells($s) } catch { $w = 'THREW: ' + $_.Exception.Message }
L (" {0,-12} {1,8} {2,14} {3,10}" -f $nm, $s.Length, $w, '')
}
L ""
L "### per-character version: LengthInBufferCells(char, offset) ###"
foreach ($c in $cases) {
$nm = $c[0]; $s = [string]$c[1]
$sum = 0; $eb = ''
foreach ($ch in $s.ToCharArray()) {
try { $u = $raw.LengthInBufferCells($ch); $eb += $u } catch { $eb += '?'; $sum = -999 }
}
L (" {0,-12} sum(char overload)={1} perchar={2}" -f $nm, $sum, $eb)
}
L ""
L "### Other RawUI helpers that exist in BOTH hosts ###"
L (" NewBufferCellArray overloads: " + (((($raw | Get-Member -Name NewBufferCellArray -MemberType Method).Definition) -join ' | ')))
L (" SetBufferContents overloads : " + (((($raw | Get-Member -Name SetBufferContents -MemberType Method).Definition) -join ' | ')))
L (" GetBufferContents overloads : " + (((($raw | Get-Member -Name GetBufferContents -MemberType Method).Definition) -join ' | ')))
L (" ScrollBufferContents overloads: " + (((($raw | Get-Member -Name ScrollBufferContents -MemberType Method).Definition) -join ' | ')))
L (" ReadKey overloads : " + (((($raw | Get-Member -Name ReadKey -MemberType Method).Definition) -join ' | ')))
L (" FlushInputBuffer overloads : " + (((($raw | Get-Member -Name FlushInputBuffer -MemberType Method).Definition) -join ' | ')))
[System.IO.File]::WriteAllLines($OutFile, $lines, (New-Object System.Text.UTF8Encoding($false)))
+59
View File
@@ -0,0 +1,59 @@
$out = Join-Path $env:TEMP 'console-probe.txt'
$lines = New-Object System.Collections.Generic.List[string]
function L($s) { $lines.Add($s) }
$src = @"
using System;
using System.Runtime.InteropServices;
public static class K32 {
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool FreeConsole();
[DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int n);
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetConsoleMode(IntPtr h, out uint m);
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool SetConsoleMode(IntPtr h, uint m);
[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
}
"@
Add-Type -TypeDefinition $src -ErrorAction Stop
$hwndBefore = [K32]::GetConsoleWindow()
L ("PSVersion : " + $PSVersionTable.PSVersion)
L ("CLR : " + [System.Environment]::Version)
L ("GetConsoleWindow(before AllocConsole) : " + $hwndBefore)
$ok = [K32]::AllocConsole()
L ("AllocConsole() : " + $ok)
$hOut = [K32]::GetStdHandle(-11); $hIn = [K32]::GetStdHandle(-10)
$mOut = 0; $mIn = 0
$okOut = [K32]::GetConsoleMode($hOut, [ref]$mOut)
$okIn = [K32]::GetConsoleMode($hIn, [ref]$mIn)
L ("STDOUT GetConsoleMode: " + $okOut + " mode=0x" + ('{0:X}' -f $mOut))
L ("STDIN GetConsoleMode: " + $okIn + " mode=0x" + ('{0:X}' -f $mIn))
L (" STDOUT ENABLE_VIRTUAL_TERMINAL_PROCESSING (0x0004) : " + (($mOut -band 4) -eq 4))
L (" STDOUT ENABLE_PROCESSED_OUTPUT (0x0001) : " + (($mOut -band 1) -eq 1))
L (" STDOUT ENABLE_WRAP_AT_EOL_OUTPUT (0x0002) : " + (($mOut -band 2) -eq 2))
L (" STDIN ENABLE_VIRTUAL_TERMINAL_INPUT (0x0200) : " + (($mIn -band 0x200) -eq 0x200))
$raw = $Host.UI.RawUI
$vtProp = $raw | Get-Member -Name SupportsVirtualTerminal -ErrorAction SilentlyContinue
$rawMembers = ($raw | Get-Member -MemberType Property | Select-Object -ExpandProperty Name) -join ','
L ("RawUI property list : " + $rawMembers)
L ("RawUI has SupportsVirtualTerminal member? " + [bool]$vtProp)
try { L ("RawUI.SupportsVirtualTerminal value : [" + $raw.SupportsVirtualTerminal + "]") } catch { L ("RawUI.SupportsVirtualTerminal THREW: " + $_.Exception.Message) }
try { L ("`$Host.UI.SupportsVirtualTerminal : [" + $Host.UI.SupportsVirtualTerminal + "]") } catch { L ("`$Host.UI.SupportsVirtualTerminal THREW: " + $_.Exception.Message) }
L ("[Console]::OutputEncoding : " + [Console]::OutputEncoding.WebName)
L ("[Console]::IsOutputRedirected : " + [Console]::IsOutputRedirected)
L ("[Console] ReinitCheck : skipped")
$w = $raw.WindowSize; $b = $raw.BufferSize
L ("RawUI.WindowSize : " + $w.Width + "x" + $w.Height)
L ("RawUI.BufferSize : " + $b.Width + "x" + $b.Height)
# activate VT and re-read
$okSet = [K32]::SetConsoleMode($hOut, $mOut -bor 4)
$mOut2 = 0
[void][K32]::GetConsoleMode($hOut, [ref]$mOut2)
L ("SetConsoleMode(out, mode|0x4) : " + $okSet + " -> new mode=0x" + ('{0:X}' -f $mOut2))
$tmp = join-path $env:TEMP 'vt-write-test.txt'
$lines | Set-Content -Path $tmp -Encoding UTF8
Write-Host ""
Write-Host "=== PROBE OUTPUT ON ATTACHED CONSOLE ===" -ForegroundColor Cyan
Write-Host "Try ANSI: [31mRED[0m [32mGREEN[0m"
$lines | ForEach-Object { Write-Host $_ }
Write-Host "=== END ===" -ForegroundColor Cyan
Start-Sleep -Seconds 2
[void][K32]::FreeConsole()
+66
View File
@@ -0,0 +1,66 @@
param([Parameter(Mandatory=$true)][string]$HostLabel, [Parameter(Mandatory=$true)][string]$OutFile)
$o = New-Object System.Collections.Specialized.OrderedDictionary
$src = @"
using System;
using System.Runtime.InteropServices;
public static class K32 {
[DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr GetStdHandle(int n);
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetConsoleMode(IntPtr h, out uint m);
[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll")] public static extern uint GetConsoleOutputCP();
[DllImport("kernel32.dll")] public static extern uint GetACP();
}
"@
Add-Type -TypeDefinition $src -ErrorAction Stop
$hOut = [K32]::GetStdHandle(-11); $hIn = [K32]::GetStdHandle(-10)
$mOut = 0; $mIn = 0
$okOut = [K32]::GetConsoleMode($hOut, [ref]$mOut)
$okIn = [K32]::GetConsoleMode($hIn, [ref]$mIn)
$o['HostLabel'] = $HostLabel
$o['PSVersion'] = $PSVersionTable.PSVersion.ToString()
$o['CLR'] = [System.Environment]::Version.ToString()
$o['IsOutputRedirected'] = [Console]::IsOutputRedirected
$o['IsInputRedirected'] = [Console]::IsInputRedirected
$o['IsErrorRedirected'] = [Console]::IsErrorRedirected
$o['GetConsoleWindow'] = ('0x{0:X}' -f ([K32]::GetConsoleWindow().ToInt64()))
$o['GetConsoleOutputCP'] = [K32]::GetConsoleOutputCP()
$o['GetACP'] = [K32]::GetACP()
$o['OutputEncoding'] = [Console]::OutputEncoding.WebName
$o['GetConsoleMode_STDOUT_ok'] = $okOut
$o['STDOUT_mode_hex'] = ('0x{0:X}' -f $mOut)
$o['STDOUT_VT_PROCESSING_0x4'] = (($mOut -band 4) -eq 4)
$o['STDOUT_PROCESSED_OUTPUT_0x1'] = (($mOut -band 1) -eq 1)
$o['STDOUT_WRAP_EOL_0x2'] = (($mOut -band 2) -eq 2)
$o['STDIN_GetConsoleMode_ok'] = $okIn
$o['STDIN_mode_hex'] = ('0x{0:X}' -f $mIn)
$o['STDIN_VT_INPUT_0x200'] = (($mIn -band 0x200) -eq 0x200)
$raw = $Host.UI.RawUI
$o['RawUI_TypeName'] = $raw.GetType().FullName
$o['RawUI_members'] = (($raw | Get-Member -MemberType Property | Select-Object -ExpandProperty Name) -join ',')
$uiProp = $Host.UI | Get-Member -Name SupportsVirtualTerminal -MemberType Property -ErrorAction SilentlyContinue
$o['HostUI_has_SupportsVirtualTerminal_member'] = [bool]$uiProp
$o['Value_Host_UI_SupportsVirtualTerminal'] = [string]$Host.UI.SupportsVirtualTerminal
$o['RawUI_has_SupportsVirtualTerminal'] = [bool]($raw | Get-Member -Name SupportsVirtualTerminal -ErrorAction SilentlyContinue)
try { $o['Value_RawUI_SupportsVirtualTerminal'] = [string]$raw.SupportsVirtualTerminal } catch { $o['Value_RawUI_SupportsVirtualTerminal'] = 'THREW: ' + $_.Exception.Message }
try { $w = $raw.WindowSize; $o['RawUI_WindowSize'] = "$($w.Width)x$($w.Height)" } catch { $o['RawUI_WindowSize'] = 'THREW: ' + $_.Exception.Message }
try { $b = $raw.BufferSize; $o['RawUI_BufferSize'] = "$($b.Width)x$($b.Height)" } catch { $o['RawUI_BufferSize'] = 'THREW: ' + $_.Exception.Message }
try { $o['RawUI_MaxPhysicalWindowSize'] = (($raw.MaxPhysicalWindowSize).Width.ToString() + 'x' + ($raw.MaxPhysicalWindowSize).Height.ToString()) } catch { $o['RawUI_MaxPhysicalWindowSize'] = 'THREW: ' + $_.Exception.Message }
try { $raw.CursorPosition = (New-Object System.Management.Automation.Host.Coordinates 3,3); $o['SetCursorPosition_ok'] = $true; $o['CursorPosition_readback'] = "$($raw.CursorPosition.X),$($raw.CursorPosition.Y)" } catch { $o['SetCursorPosition_ok'] = 'THREW: ' + $_.Exception.Message }
try { $o['KeyAvailable_read_ok'] = 'ok: ' + $raw.KeyAvailable } catch { $o['KeyAvailable_read_ok'] = 'THREW: ' + $_.Exception.Message }
# Chinese width probe, ASCII-source via code points
$zh = [string]::Concat([char]0x4E2D, [char]0x6587) # Zhong Wen
$ascii = 'AB'
$o['zh_string'] = $zh
$o['zh_Length'] = $zh.Length
$o['ascii_Length'] = $ascii.Length
$o['GetConsoleWindowTitle'] = [Console]::Title
$json = $o | ConvertTo-Json -Compress
[System.IO.File]::WriteAllText($OutFile, $json, (New-Object System.Text.UTF8Encoding($false)))
$raw.CursorPosition = (New-Object System.Management.Automation.Host.Coordinates 0,0)
Write-Host ''
Write-Host ("--- console probe [{0}] PS {1} VT={2} mode={3} ---" -f $HostLabel, $PSVersionTable.PSVersion, $Host.UI.SupportsVirtualTerminal, ('0x{0:X}' -f $mOut)) -ForegroundColor Yellow
Write-Host ("ANSI test: " + [char]27 + "[31mRED-ANSI" + [char]27 + "[0m " + [char]27 + "[42mBG-GREEN" + [char]27 + "[0m " + [char]27 + "[1;36mBOLD-CYAN" + [char]27 + "[0m")
Write-Host ("Unicode box: " + [char]0x250C + [char]0x2500 + [char]0x2510)
Write-Host ("Chinese: " + $zh + " Length=" + $zh.Length)
Write-Host '--- end ---'
Start-Sleep -Seconds 4
+57
View File
@@ -0,0 +1,57 @@
param([Parameter(Mandatory=$true)][string]$HostLabel, [Parameter(Mandatory=$true)][string]$OutFile, [int]$WatchSeconds = 3)
$o = New-Object System.Collections.Specialized.OrderedDictionary
$src = @"
using System;
using System.Runtime.InteropServices;
public static class K32 {
[DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int n);
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetConsoleMode(IntPtr h, out uint m);
[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll")] public static extern uint GetConsoleOutputCP();
}
"@
Add-Type -TypeDefinition $src -ErrorAction Stop
$mOut = 0; $ok = [K32]::GetConsoleMode([K32]::GetStdHandle(-11), [ref]$mOut)
$o['HostLabel'] = $HostLabel
$o['PSVersion'] = $PSVersionTable.PSVersion.ToString()
$o['IsOutputRedirected'] = [Console]::IsOutputRedirected
$o['HWND'] = ('0x{0:X}' -f ([K32]::GetConsoleWindow().ToInt64()))
$o['ConsoleTitle'] = [Console]::Title
$o['GetConsoleOutputCP'] = [K32]::GetConsoleOutputCP()
$o['OutputEncoding'] = [Console]::OutputEncoding.WebName
$o['STDOUT_mode_hex'] = ('0x{0:X}' -f $mOut)
$o['STDOUT_VT_PROCESSING_0x4'] = (($mOut -band 4) -eq 4)
$o['Host_UI_SupportsVirtualTerminal'] = [string]$Host.UI.SupportsVirtualTerminal
$o['RawUI_SupportsVirtualTerminal_member'] = [bool]($Host.UI.RawUI | Get-Member -Name SupportsVirtualTerminal -ErrorAction SilentlyContinue)
$o['WT_SESSION'] = [string]$env:WT_SESSION
$o['WT_PROFILE_ID'] = [string]$env:WT_PROFILE_ID
$o['TERM_PROGRAM'] = [string]$env:TERM_PROGRAM
$o['TERM'] = [string]$env:TERM
$o['ConEmuANSI'] = [string]$env:ConEmuANSI
$o['PSEdition'] = $PSVersionTable.PSEdition
try { $o['RawUI_KeyAvailable'] = 'ok:' + $Host.UI.RawUI.KeyAvailable } catch { $o['RawUI_KeyAvailable'] = 'THREW: ' + $_.Exception.Message }
try { $o['Console_KeyAvailable'] = 'ok:' + [Console]::KeyAvailable } catch { $o['Console_KeyAvailable'] = 'THREW: ' + $_.Exception.Message }
try {
[Console]::SetCursorPosition(5, 5)
$o['Console_SetCursorPosition_5_5'] = 'ok -> CursorLeft=' + [Console]::CursorLeft + ' CursorTop=' + [Console]::CursorTop
} catch { $o['Console_SetCursorPosition_5_5'] = 'THREW: ' + $_.Exception.Message }
try {
$c = New-Object System.Management.Automation.Host.Coordinates 7, 7
$Host.UI.RawUI.CursorPosition = $c
$o['RawUI_SetCursorPosition'] = 'ok -> ' + $Host.UI.RawUI.CursorPosition.X + ',' + $Host.UI.RawUI.CursorPosition.Y
} catch { $o['RawUI_SetCursorPosition'] = 'THREW: ' + $_.Exception.Message }
try { $o['RawUI_WindowSize'] = "$($Host.UI.RawUI.WindowSize.Width)x$($Host.UI.RawUI.WindowSize.Height)" } catch { $o['RawUI_WindowSize'] = 'THREW: ' + $_.Exception.Message }
try { $o['RawUI_BufferSize'] = "$($Host.UI.RawUI.BufferSize.Width)x$($Host.UI.RawUI.BufferSize.Height)" } catch { $o['RawUI_BufferSize'] = 'THREW: ' + $_.Exception.Message }
try { $o['RawUI_MaxWindowSize'] = "$($Host.UI.RawUI.MaxWindowSize.Width)x$($Host.UI.RawUI.MaxWindowSize.Height)" } catch { $o['RawUI_MaxWindowSize'] = 'THREW: ' + $_.Exception.Message }
try { $o['RawUI_MaxPhysicalWindowSize'] = "$($Host.UI.RawUI.MaxPhysicalWindowSize.Width)x$($Host.UI.RawUI.MaxPhysicalWindowSize.Height)" } catch { $o['RawUI_MaxPhysicalWindowSize'] = 'THREW: ' + $_.Exception.Message }
try { $o['CursorSize'] = [string]$Host.UI.RawUI.CursorSize } catch { $o['CursorSize'] = 'THREW' }
try { $o['ForegroundColor'] = [string]$Host.UI.RawUI.ForegroundColor } catch { $o['ForegroundColor'] = 'THREW' }
try { $o['WindowTitle'] = [string]$Host.UI.RawUI.WindowTitle } catch { $o['WindowTitle'] = 'THREW' }
$o['ProgressPreference'] = [string]$ProgressPreference
$o['HostName'] = $Host.Name
$json = $o | ConvertTo-Json -Compress
[System.IO.File]::WriteAllText($OutFile, $json, (New-Object System.Text.UTF8Encoding($false)))
Write-Host ("[{0}] PS {1} VT={2} redirected={3} WT_SESSION={4}" -f $HostLabel, $PSVersionTable.PSVersion, $Host.UI.SupportsVirtualTerminal, [Console]::IsOutputRedirected, $env:WT_SESSION) -ForegroundColor Yellow
Write-Host (" ANSI: " + [char]27 + "[31mRED" + [char]27 + "[0m " + [char]27 + "[104mBG-BRIGHT-BLUE" + [char]27 + "[0m " + [char]27 + "[38;2;255;128;0mTRUECOLOR" + [char]27 + "[0m")
Write-Host (" Positioned: " + [char]27 + "[2;10HAT-ROW2-COL10")
if ($WatchSeconds -gt 0) { Start-Sleep -Seconds $WatchSeconds }
+77
View File
@@ -0,0 +1,77 @@
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' }))
}
+109
View File
@@ -0,0 +1,109 @@
param([Parameter(Mandatory=$true)][string]$OutFile, [Parameter(Mandatory=$true)][string]$Work, [int]$HoldSeconds = 5)
$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))) }
function Braino([string]$m) { return $null }
L ("PSVersion: " + $PSVersionTable.PSVersion + " CLR " + [System.Environment]::Version + " Redirected=" + [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 A([string]$fullName, [string]$asmName) { return ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $asmName }).GetType($fullName) }
function GetT([string]$fullName) { return ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Terminal.Gui' }).GetType($fullName) }
function GetS([string]$fullName) { return ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Spectre.Console' }).GetType($fullName) }
L ""
L "############ QUESTION: after Add-Type -Path, do PS type literals work? ############"
Add-Type -Path $nstack -ErrorAction Stop
Add-Type -Path $tg -ErrorAction Stop
L (" [bool]([Type]::GetType('Terminal.Gui.Application, Terminal.Gui')) = " + [bool]([Type]::GetType('Terminal.Gui.Application, Terminal.Gui')))
L (" [bool]([Type]::GetType('Terminal.Gui.Application, ' + full)) = " + [bool]([Type]::GetType('Terminal.Gui.Application, ' + (GetT 'Terminal.Gui.Application').AssemblyQualifiedName)))
L (" script GetT('Terminal.Gui.Application') = " + [bool](GetT 'Terminal.Gui.Application') + " <-- reliable in BOTH hosts")
$litOk = $false
try { $litOk = [bool]([Terminal.Gui.Application] -is [Type]) } catch { $litOk = "THREW: " + $_.Exception.Message }
L (" PS type literal [Terminal.Gui.Application] = " + $litOk)
L ""
L "############ A) Terminal.Gui 1.15.0 (net472) DRIVER-WIRED RUN ############"
$tgResult = 'not-attempted'
try {
$appT = GetT 'Terminal.Gui.Application'
$netT = GetT 'Terminal.Gui.NetDriver'
L (" Application type : " + [bool]$appT + " NetDriver type: " + [bool]$netT)
$initM = $appT.GetMethod('Init', [Type[]]@($netT, ($netT.GetInterfaces() | Where-Object { $_.FullName -eq 'Terminal.Gui.IMainLoopDriver' })[0]))
L (" Init(NetDriver, IMainLoopDriver) overload: " + [bool]$initM)
if ([Console]::IsOutputRedirected) {
L " stdout redirected -> Terminal.Gui CANNOT paint; skipping the live run (documented limitation)"
try { $initM.Invoke($null, @([Activator]::CreateInstance($netT), $null)) | Out-Null; L " unexpectedly Init'ed" } catch { L (" expected failure: " + $_.Exception.InnerException.Message.Split([char]10)[0]) }
$tgResult = 'blocked-by-redirect'
} else {
$drv = [Activator]::CreateInstance($netT)
$initM.Invoke($null, @($drv, $null)) | Out-Null
L " Application.Init(NetDriver) -> SUCCEEDED *** Terminal.Gui 1.15.0 is LIVE inside PowerShell " + $PSVersionTable.PSVersion + " ***"
$winT = GetT 'Terminal.Gui.Window'; $lblT = GetT 'Terminal.Gui.Label'; $btnT = GetT 'Terminal.Gui.Button'
$w = [Activator]::CreateInstance($winT, [object[]]@('BakNRet / Terminal.Gui - PS ' + $PSVersionTable.PSVersion))
$l1 = [Activator]::CreateInstance($lblT, [object[]]@('Terminal.Gui 1.15.0 net472 - running on .NET Framework ' + [System.Environment]::Version))
$l2 = [Activator]::CreateInstance($lblT, [object[]]@('CJK cell-width check:'))
$l3 = [Activator]::CreateInstance($lblT, [object[]]@(' 2 CJK chars -> 4 columns'))
$b1 = [Activator]::CreateInstance($btnT, [object[]]@('OK'))
$w.Add($l1); $w.Add($l2); $w.Add($l3); $w.Add($b1)
$w.X = 1; $w.Y = 1; $w.Width = 70; $w.Height = 9
$l1.Y = 1; $l2.Y = 3; $l3.Y = 4; $b1.Y = 6; $b1.X = 2
$homeT = GetT 'Terminal.Gui.Toplevel'
($appT.GetProperty('TopLevel')).SetValue($null, $w)
$w.SetFocus()
$w.Redraw($w.Bounds)
$appT.GetMethod('Refresh', [Type[]]@()).Invoke($null, @()) | Out-Null
L " Window(70x9) + 3 Labels + Button painted. Holding " + $HoldSeconds + "s..."
Start-Sleep -Seconds $HoldSeconds
$appT.GetMethod('Shutdown', [Type[]]@()).Invoke($null, @()) | Out-Null
L " Application.Shutdown() -> clean"
$tgResult = 'WORKS'
}
} catch {
L (" Terminal.Gui FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
$tgResult = 'FAILED'
}
L ""
L "############ B) Spectre.Console 0.49.1 (netstandard2.0) REAL RENDERING ############"
$scResult = 'not-attempted'
try {
Add-Type -Path $sc -ErrorAction Stop
$ansiT = GetS 'Spectre.Console.AnsiConsole'
$tableT = GetS 'Spectre.Console.Table'
$extT = GetS 'Spectre.Console.TableExtensions'
L (" AnsiConsole/Table/TableExtensions types: " + [bool]$ansiT + "/" + [bool]$tableT + "/" + [bool]$extT)
# Build a table WITHOUT AddRow extension methods (instance API only) to isolate extension-call support
$t = [Activator]::CreateInstance($tableT)
$colT = GetS 'Spectre.Console.TableColumn'
$t.AddColumn([Activator]::CreateInstance($colT, [object[]]@('项目 Item')))
$t.AddColumn([Activator]::CreateInstance($colT, [object[]]@('大小 Size')))
L " 2 columns added via INSTANCE AddColumn(TableColumn) -> OK"
# extension method called the PowerShell way
$addRow = $extT.GetMethod('AddRow', [Type[]]@($tableT, [string[]]))
L (" found extension TableExtensions.AddRow(Table, string[]): " + [bool]$addRow)
$addRow.Invoke($null, [object[]]@($t, [string[]]@('中文备份 alpha', '1.2 GB'))) | Out-Null
$addRow.Invoke($null, [object[]]@($t, [string[]]@('beta', 'OK'))) | Out-Null
L " 2 rows added via reflection-invoked EXTENSION method -> OK"
$ansiT::Write($t)
L " >>> AnsiConsole.Write(Table) with mixed-width CJK content rendered above <<<"
try { $ansiT::Markup('[red]red[/] [green]green[/] [bold]bold[/] [yellow]yellow[/]') ; L " AnsiConsole.Markup color test -> OK" } catch { L (" Markup FAILED: " + $_.Exception.Message) }
Write-Host ""
Write-Host " -- Spectre.Console table rendered from PowerShell $($PSVersionTable.PSVersion) --" -ForegroundColor Cyan
$ansiT::Write($t)
Start-Sleep -Seconds 3
# Progress demo (bounded)
try {
$progT = GetS 'Spectre.Console.Progress'
L (" Progress type: " + [bool]$progT)
$act = { }
L " (Progress/Status need a script-block -> Action`1 delegate; skipped live run to stay bounded)"
} catch { L (" progress probe failed: " + $_.Exception.Message) }
$scResult = 'WORKS'
} catch {
L (" Spectre FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
$scResult = 'FAILED'
}
L ""
L "RESULT: TerminalGui=$tgResult SpectreConsole=$scResult"
+116
View File
@@ -0,0 +1,116 @@
param([Parameter(Mandatory=$true)][string]$OutFile, [Parameter(Mandatory=$true)][string]$Work, [int]$HoldSeconds = 5)
$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 + " CLR " + [System.Environment]::Version + " Redirected=" + [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'
$mem461 = Join-Path $Work 'system.memory-4.5.5\lib\net461\System.Memory.dll'
$memNs = Join-Path $Work 'system.memory-4.5.5\lib\netstandard2.0\System.Memory.dll'
function Asm([string]$n) { return [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $n } | Select-Object -First 1 }
function T([string]$n, [string]$a) { $x = Asm $a; if ($x) { return $x.GetType($n) } else { return $null } }
L ""
L "############ STEP 0: side-load the ONE dependency Spectre.Console needs ############"
$mem = if ($PSVersionTable.PSEdition -eq 'Desktop') { $mem461 } else { $memNs }
L (" chosen System.Memory for this host: " + $mem)
try {
Add-Type -Path $mem -ErrorAction Stop
$m = Asm 'System.Memory'
L (" Add-Type -Path System.Memory -> OK: " + $m.GetName().FullName)
L (" [System.Span[int]] resolves now: " + [bool]([Type]::GetType('System.Span`1')) )
} catch { L (" System.Memory load FAILED: " + $_.Exception.Message) }
L ""
L "############ A) Terminal.Gui 1.15.0 net472 - CORRECTED driver wiring ############"
$tgResult = 'not-attempted'
try {
Add-Type -Path $nstack -ErrorAction Stop
Add-Type -Path $tg -ErrorAction Stop
$appT = T 'Terminal.Gui.Application' 'Terminal.Gui'
$netT = T 'Terminal.Gui.NetDriver' 'Terminal.Gui'
L (" Application: " + [bool]$appT + " NetDriver: " + [bool]$netT)
L (" NetDriver interfaces: " + (($netT.GetInterfaces() | ForEach-Object { $_.FullName }) -join ', '))
$mlT = T 'Terminal.Gui.IMainLoopDriver' 'Terminal.Gui'
L (" IMainLoopDriver found as type: " + [bool]$mlT)
$initM = $appT.GetMethod('Init', [Type[]]@($netT, $mlT))
L (" Init(NetDriver, IMainLoopDriver) overload: " + [bool]$initM)
if ([Console]::IsOutputRedirected) {
L " stdout redirected -> Terminal.Gui cannot paint. Expected failure follows:"
try { $initM.Invoke($null, @([Activator]::CreateInstance($netT), $null)) | Out-Null; L " unexpectedly Init'ed" }
catch { L (" expected: " + ($_.Exception.InnerException.Message -split "`n")[0]) }
$tgResult = 'works-only-with-a-real-console'
} else {
$drv = [Activator]::CreateInstance($netT)
L (" NetDriver instance: " + $drv.GetType().FullName + " is IMainLoopDriver: " + [bool]($mlT.IsInstanceOfType($drv)))
$initM.Invoke($null, @($drv, $drv)) | Out-Null
L " Application.Init(driver, driver) -> SUCCEEDED *** Terminal.Gui is LIVE on PowerShell " + $PSVersionTable.PSVersion + " ***"
IEX "0"
$winT = T 'Terminal.Gui.Window' 'Terminal.Gui'; $lblT = T 'Terminal.Gui.Label' 'Terminal.Gui'; $btnT = T 'Terminal.Gui.Button' 'Terminal.Gui'
$w = [Activator]::CreateInstance($winT, [object[]]@('BakNRet - Terminal.Gui on PS ' + $PSVersionTable.PSVersion))
$l1 = [Activator]::CreateInstance($lblT, [object[]]@('Terminal.Gui 1.15.0 (net472) on ' + $(if ($PSVersionTable.PSEdition -eq 'Desktop') { '.NET Framework ' } else { '.NET ' }) + [System.Environment]::Version))
$l2 = [Activator]::CreateInstance($lblT, [object[]]@('CJK test: 2 CJK chars occupy 4 screen columns'))
$l3 = [Activator]::CreateInstance($lblT, [object[]]@('Box drawing: expected to align'))
$b1 = [Activator]::CreateInstance($btnT, [object[]]@('OK'))
$w.Add($l1); $w.Add($l2); $w.Add($l3); $w.Add($b1)
$w.X = 1; $w.Y = 1; $w.Width = 72; $w.Height = 9
$l1.Y = 1; $l2.Y = 3; $l3.Y = 4; $b1.Y = 6; $b1.X = 2
($appT.GetProperty('TopLevel')).SetValue($null, $w)
$w.SetFocus(); $w.Redraw($w.Bounds)
$appT.GetMethod('Refresh', [Type[]]@()).Invoke($null, @()) | Out-Null
L " Painted Window(72x9)+3 Labels+Button. Holding " + $HoldSeconds + "s for visual confirmation..."
Start-Sleep -Seconds $HoldSeconds
$appT.GetMethod('Shutdown', [Type[]]@()).Invoke($null, @()) | Out-Null
L " Application.Shutdown() -> clean"
$tgResult = 'WORKS'
}
} catch {
L (" Terminal.Gui FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
$tgResult = 'FAILED'
}
L ""
L "############ B) Spectre.Console 0.49.1 netstandard2.0 - with System.Memory present ############"
$scResult = 'not-attempted'
try {
Add-Type -Path $sc -ErrorAction Stop
$ansiT = T 'Spectre.Console.AnsiConsole' 'Spectre.Console'
$tableT = T 'Spectre.Console.Table' 'Spectre.Console'
$colT = T 'Spectre.Console.TableColumn' 'Spectre.Console'
$extT = T 'Spectre.Console.TableExtensions' 'Spectre.Console'
$t = [Activator]::CreateInstance($tableT)
L " new Table() -> OK *** THIS FAILED BEFORE System.Memory WAS SIDE-LOADED ***"
$t.AddColumn([Activator]::CreateInstance($colT, [object[]]@('项目 / Item')))
$t.AddColumn([Activator]::CreateInstance($colT, [object[]]@('大小 / Size')))
$addRow = $extT.GetMethod('AddRow', [Type[]]@($tableT, [string[]]))
$addRow.Invoke($null, [object[]]@($t, [string[]]@('中文备份归档 alpha', '1.2 GB'))) | Out-Null
$addRow.Invoke($null, [object[]]@($t, [string[]]@('beta', 'OK'))) | Out-Null
Write-Host ""
Write-Host (" === Spectre.Console table, rendered by PowerShell $($PSVersionTable.PSVersion) ===") -ForegroundColor Cyan
$ansiT::Write($t)
Write-Host ""
$ansiT::Markup('[red]red[/] [green]green[/] [yellow]yellow[/] [bold]bold[/] [blue on white]reverse[/]')
L " AnsiConsole.Write(Table + CJK) + Markup colors -> INVOKED"
# Now confirm extension methods callable with PowerShell's native :: syntax
try {
$t2 = [Activator]::CreateInstance($tableT)
$ok1 = $false
try { [Spectre.Console.TableExtensions]::AddColumn($t2, 'ColA') | Out-Null; $ok1 = $true } catch { $ok1 = 'THREW: ' + $_.Exception.Message }
L (" PowerShell-native [Spectre.Console.TableExtensions]::AddColumn(`$table,'ColA') -> " + $ok1)
$t3 = [Activator]::CreateInstance($tableT)
[Spectre.Console.TableExtensions]::AddColumns($t3, [string[]]@('A','B')) | Out-Null
[Spectre.Console.TableExtensions]::AddRow($t3, [string[]]@('x','y')) | Out-Null
L " [Spectre.Console.TableExtensions]::AddColumns/AddRow via PS-native static call -> OK"
$t4 = [Activator]::CreateInstance($tableT)
try { $t4.AddColumns('A','B'); L " INSTANCE SYNTAX `$t4.AddColumns('A','B') -> OK (PS resolves extension methods)" } catch { L (" INSTANCE SYNTAX `$t4.AddColumns -> THREW: " + $_.Exception.Message) }
} catch { L (" native-syntax probe failed: " + $_.Exception.Message) }
$scResult = 'WORKS'
} catch {
L (" Spectre FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
$scResult = 'FAILED'
}
L ""
L "RESULT: TerminalGui=$tgResult SpectreConsole=$scResult"
if (-not [Console]::IsOutputRedirected) { Write-Host ""; Write-Host ("[final PS " + $PSVersionTable.PSVersion + "] TerminalGui=$tgResult Spectre=$scResult (closing in $HoldSeconds s)") -ForegroundColor Yellow; Start-Sleep -Seconds $HoldSeconds }
+93
View File
@@ -0,0 +1,93 @@
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) }
L ("PSVersion: " + $PSVersionTable.PSVersion + " CLR " + [System.Environment]::Version)
L ("Work: " + $Work)
$tgNet472 = Join-Path $Work 'tg115\lib\net472\Terminal.Gui.dll'
$tgNs21 = Join-Path $Work 'tg115\lib\netstandard2.1\Terminal.Gui.dll'
$tgNs20 = Join-Path $Work 'tg115\lib\netstandard2.0\Terminal.Gui.dll'
$nstack = Join-Path $Work 'nstack\lib\netstandard2.0\NStack.dll'
$scNs20 = Join-Path $Work 'sc049\lib\netstandard2.0\Spectre.Console.dll'
L ""
L "### STEP 1: try [Reflection.Assembly]::LoadFrom on Terminal.Gui net472 (5.1's native TFM) WITHOUT dependencies ###"
try { $a = [System.Reflection.Assembly]::LoadFrom($tgNet472); L (" LoadFrom tg/net472 -> OK: " + $a.FullName) } catch { L (" LoadFrom tg/net472 FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message) }
L ""
L "### STEP 2: try GetTypes() -- this forces dependency resolution ###"
if ($a) {
try { $ts = $a.GetTypes(); L (" GetTypes() -> OK, " + $ts.Count + " types. Sample: " + (($ts | Select-Object -First 5 | ForEach-Object { $_.FullName }) -join ', ')) }
catch {
L (" GetTypes() FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException
while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + $ie.Message); $ie = $ie.InnerException }
$le = $_.Exception.LoaderExceptions
if ($le) { foreach ($x in $le) { L (" loaderEx: " + $x.Message) } }
}
try { $appType = $a.GetType('Terminal.Gui.Application'); L (" GetType('Terminal.Gui.Application') -> " + $(if ($appType) { 'FOUND: ' + $appType.FullName } else { 'null' })) } catch { L (" GetType(Application) THREW: " + $_.Exception.Message) }
}
L ""
L "### STEP 3: load NStack.Core first (the declared dependency), then Terminal.Gui ###"
try { $ns = [System.Reflection.Assembly]::LoadFrom($nstack); L (" LoadFrom NStack.Core/netstandard2.0 -> " + $ns.FullName) } catch { L (" NStack load FAILED: " + $_.Exception.Message) }
try {
$a2 = [System.Reflection.Assembly]::LoadFrom($tgNet472)
$ts2 = $a2.GetTypes()
L (" after NStack preload: GetTypes() -> OK, " + $ts2.Count + " types")
$appType = $a2.GetType('Terminal.Gui.Application')
L (" Terminal.Gui.Application -> " + $(if ($appType) { 'FOUND' } else { 'null' }))
} catch {
L (" after NStack preload: FAILED " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$le = $_.Exception.LoaderExceptions; if ($le) { foreach ($x in $le) { L (" loaderEx: " + $x.Message) } }
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + $ie.Message); $ie = $ie.InnerException }
}
L ""
L "### STEP 4: ACTUAL INSTANTIATION + a real TUI win. Does Application.Init() work in 5.1? ###"
$didInit = $false
try {
$a3 = [System.Reflection.Assembly]::LoadFrom($tgNet472)
$t = $a3.GetType('Terminal.Gui.Application')
L (" Application type: " + $(if ($t) { $t.FullName } else { 'null' }))
if ($t) {
$init = $t.GetMethod('Init', [Type[]]@())
L (" Application.Init() method found: " + [bool]$init)
if ($init) {
if ([Console]::IsOutputRedirected) {
L " SKIPPING Init() because stdout is redirected (it would hang / corrupt output)"
} else {
$init.Invoke($null, @()) | Out-Null
$didInit = $true
L " Application.Init() -> SUCCEEDED on this host"
}
}
}
} catch {
L (" Application.Init probe FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + $ie.Message); $ie = $ie.InnerException }
}
if ($didInit) {
try {
$t = ([System.Reflection.Assembly]::LoadFrom($tgNet472)).GetType('Terminal.Gui.Application')
$shutdown = $t.GetMethod('Shutdown', [Type[]]@())
if ($shutdown) { $shutdown.Invoke($null, @()) | Out-Null; L " Application.Shutdown() -> called" }
} catch { L (" Shutdown failed: " + $_.Exception.Message) }
}
L ""
L "### STEP 5: Spectre.Console netstandard2.0 in this host ###"
try {
$sc = [System.Reflection.Assembly]::LoadFrom($scNs20)
L (" LoadFrom Spectre.Console/netstandard2.0 -> " + $sc.FullName)
try { $t2 = $sc.GetTypes(); L (" GetTypes() -> OK, " + $t2.Count + " types") } catch { L (" GetTypes() FAILED: " + $_.Exception.Message); $ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + $ie.Message); $ie = $ie.InnerException } }
try {
$ansiType = $sc.GetType('Spectre.Console.AnsiConsole')
L (" Spectre.Console.AnsiConsole -> " + $(if ($ansiType) { 'FOUND' } else { 'null' }))
if ($ansiType) {
$w = $ansiType.GetMethod('Write', [Type[]]@([string]))
L (" AnsiConsole.Write(string) overload: " + [bool]$w)
}
} catch { L (" AnsiConsole probe FAILED: " + $_.Exception.Message) }
} catch { L (" LoadFrom Spectre.Console FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message) }
L ""
L "### STEP 6: Add-Type -Path variant (what a user would literally type) ###"
try { Add-Type -Path $tgNet472 -ErrorAction Stop; L " Add-Type -Path Terminal.Gui(net472) -> OK" } catch { L (" Add-Type -Path Terminal.Gui(net472) FAILED: " + $_.Exception.Message) }
try { Add-Type -Path $scNs20 -ErrorAction Stop; L " Add-Type -Path Spectre.Console(netstandard2.0) -> OK" } catch { L (" Add-Type -Path Spectre.Console(ns2.0) FAILED: " + $_.Exception.Message) }
try { Add-Type -Path $tgNs21 -ErrorAction Stop; L " Add-Type -Path Terminal.Gui(netstandard2.1) -> OK" } catch { L (" Add-Type -Path Terminal.Gui(ns2.1) FAILED: " + $_.Exception.Message) }
L (" (tg netstandard2.0 path exists? " + (Test-Path $tgNs20) + ")")
[System.IO.File]::WriteAllLines($OutFile, $lines, (New-Object System.Text.UTF8Encoding($false)))
+96
View File
@@ -0,0 +1,96 @@
param([Parameter(Mandatory=$true)][string]$OutFile, [Parameter(Mandatory=$true)][string]$Work, [int]$HoldSeconds = 4)
$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 + " CLR " + [System.Environment]::Version)
L ("IsOutputRedirected: " + [Console]::IsOutputRedirected)
L ""
$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'
L "############ A) Terminal.Gui 1.15.0 (lib/net472) ############"
$tgOk = $false
try {
Add-Type -Path $nstack -ErrorAction Stop
L " Add-Type -Path NStack.Core 1.1.1 (netstandard2.0) : OK"
Add-Type -Path $tg -ErrorAction Stop
L " Add-Type -Path Terminal.Gui 1.15.0 (net472) : OK"
foreach ($a in ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -in @('NStack', 'Terminal.Gui', 'System.Management') })) {
L (" loaded assembly: " + $a.GetName().Name + " v" + $a.GetName().Version + " <- " + $(try { $a.Location } catch { 'dynamic' }))
}
$appType = [Type]::GetType('Terminal.Gui.Application, Terminal.Gui')
L (" [Type]::GetType('Terminal.Gui.Application, Terminal.Gui') -> " + $(if ($appType) { 'FOUND ' + $appType.AssemblyQualifiedName.Split(',')[0] } else { 'NULL' }))
if ($appType) {
if ([Console]::IsOutputRedirected) { L " SKIP Init()/paint: stdout is redirected (no real console)" }
else {
$initM = $appType.GetMethod('Init', [Type[]]@())
L (" Application.Init() method: " + [bool]$initM)
$initM.Invoke($null, @()) | Out-Null
$tgOk = $true
L " Application.Init() -> SUCCEEDED *** Terminal.Gui is LIVE inside this PowerShell ***"
$winT = [Type]::GetType('Terminal.Gui.Window, Terminal.Gui')
$lblT = [Type]::GetType('Terminal.Gui.Label, Terminal.Gui')
$btnT = [Type]::GetType('Terminal.Gui.Button, Terminal.Gui')
$lbl2T = [Type]::GetType('Terminal.Gui.Label, Terminal.Gui')
L (" types: Window=$([bool]$winT) Label=$([bool]$lblT) Button=$([bool]$btnT)")
$w = [Activator]::CreateInstance($winT, [object[]]@("BakNRet - Terminal.Gui on PS $($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)"))
$l1 = [Activator]::CreateInstance($lblT, [object[]]@('Window drawn by Terminal.Gui 1.15.0 (net472)'))
$l2 = [Activator]::CreateInstance($lbl2T, [object[]]@('CJK check: Chinese chars, box-drawing'))
$b1 = [Activator]::CreateInstance($btnT, [object[]]@('OK'))
$w.Add($l1); $w.Add($l2); $w.Add($b1)
$w.X = 1; $w.Y = 1; $w.Width = 62; $w.Height = 9
$l1.Y = 1; $l2.Y = 2; $b1.Y = 4
$appType::TopLevel = $w
$w.SetFocus()
$w.Redraw($w.Bounds)
$appType::Refresh()
L " Window + 2 Labels + Button added, TopLevel set, Redraw+Refresh called (look at the window)"
Start-Sleep -Seconds $HoldSeconds
$shut = $appType.GetMethod('Shutdown', [Type[]]@())
$shut.Invoke($null, @()) | Out-Null
L " Application.Shutdown() -> called cleanly"
}
}
} catch {
L (" FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + $ie.Message); $ie = $ie.InnerException }
$le = $_.Exception.LoaderExceptions; if ($le) { foreach ($x in $le) { L (" loaderEx: " + $x.Message) } }
$tgOk = $false
}
L ""
L "############ B) Spectre.Console 0.49.1 (lib/netstandard2.0) ############"
$scOk = $false
try {
Add-Type -Path $sc -ErrorAction Stop
L " Add-Type -Path Spectre.Console 0.49.1 (netstandard2.0) -> OK"
$ansiT = [Type]::GetType('Spectre.Console.AnsiConsole, Spectre.Console')
L (" Spectre.Console.AnsiConsole -> " + $(if ($ansiT) { 'FOUND' } else { 'NULL' }))
if ($ansiT) {
$m = $ansiT.GetMethod('Markup', [Type[]]@([string]))
L (" AnsiConsole.Markup(string) : " + [bool]$m)
$m.Invoke($null, [object[]]@('[red]red markup[/] [green]green[/] [bold]bold[/]')) | Out-Null
L " Markup(red/green/bold) INVOKED"
$tableT = [Type]::GetType('Spectre.Console.Table, Spectre.Console')
$t = [Activator]::CreateInstance($tableT)
$t.AddColumn('Item'); $t.AddColumn('Size')
$t.AddRow('alpha', '1.2 GB'); $t.AddRow('beta', 'OK')
$ansiT::Write($t)
L " AnsiConsole.Write(Table with 2 cols, 2 rows) INVOKED"
$ansiT::Write($t)
L " (table drawn a second time)"
foreach ($n in @('Spectre.Console.ConfirmationPrompt', 'Spectre.Console.SelectionPrompt`1', 'Spectre.Console.MultiSelectionPrompt`1', 'Spectre.Console.Progress', 'Spectre.Console.Status', 'Spectre.Console.Live', 'Spectre.Console.TextPrompt`1', 'Spectre.Console.Layout', 'Spectre.Console.Panel', 'Spectre.Console.BarChart', 'Spectre.Console.BreakdownChart', 'Spectre.Console.Tree', 'Spectre.Console.Rule', 'Spectre.Console.Exception`1')) {
$tt = [Type]::GetType("$n, Spectre.Console")
L (" primitive " + $n.PadRight(42) + " -> " + $(if ($tt) { 'AVAILABLE' } else { 'not found' }))
}
$scOk = $true
}
} catch {
L (" FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + $ie.Message); $ie = $ie.InnerException }
}
L ""
L "RESULT: TerminalGuiInitOK=$tgOk SpectreConsoleOK=$scOk"
if (-not [Console]::IsOutputRedirected) {
Write-Host ""
Write-Host ("[loadtest PS {0}] TerminalGui={1} Spectre.Console={2} -- window closes in {3}s" -f $PSVersionTable.PSVersion, $tgOk, $scOk, $HoldSeconds) -ForegroundColor Yellow
Start-Sleep -Seconds $HoldSeconds
}
+58
View File
@@ -0,0 +1,58 @@
$outFile = $args[0]
if (-not $outFile) { $outFile = Join-Path $env:TEMP 'noconsole-probe.txt' }
$lines = New-Object System.Collections.Generic.List[string]
function L($s) { $lines.Add($s); Write-Host $s }
$src = @"
using System;
using System.Runtime.InteropServices;
public static class K32b {
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool FreeConsole();
[DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int n);
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetConsoleMode(IntPtr h, out uint m);
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool SetConsoleMode(IntPtr h, uint m);
[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll")] public static extern uint GetConsoleOutputCP();
}
"@
Add-Type -TypeDefinition $src -ErrorAction Stop
L "##### PART A: no console attached (stdout redirected) #####"
L ("PSVersion : " + $PSVersionTable.PSVersion)
L ("IsOutputRedirected : " + [Console]::IsOutputRedirected)
L ("GetConsoleWindow : 0x{0:X}" -f ([K32b]::GetConsoleWindow().ToInt64()))
L ("Host.UI.SupportsVirtualTerminal (property exists? " + [bool]($Host.UI | Get-Member -Name SupportsVirtualTerminal -ErrorAction SilentlyContinue) + ") = " + [string]$Host.UI.SupportsVirtualTerminal)
L ("RawUI has SupportsVirtualTerminal? " + [bool]($Host.UI.RawUI | Get-Member -Name SupportsVirtualTerminal -ErrorAction SilentlyContinue))
$m = 0; $ok = [K32b]::GetConsoleMode([K32b]::GetStdHandle(-11), [ref]$m); $err = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
L ("GetConsoleMode(STDOUT) : $ok mode=0x{0:X} lastErr=$err" -f $m)
L ("--- try to wrap POSIX bytes (what Set-Item Function:Write-Output would do) ---")
try { $b = [System.Text.Encoding]::UTF8.GetBytes("x")[0]; $os = [Console]::OpenStandardOutput(); $os.WriteByte(0x1B); $os.Flush(); L "OpenStandardOutput().WriteByte(0x1B): OK (wrote raw ESC into redirected stdout)" } catch { L ("OpenStandardOutput().WriteByte THREW: " + $_.Exception.Message) }
try { [Console]::SetCursorPosition(3,3); L ("[Console]::SetCursorPosition(3,3) : OK -> " + [Console]::CursorLeft + "," + [Console]::CursorTop) } catch { L ("[Console]::SetCursorPosition(3,3) THREW: " + $_.Exception.Message) }
try { $w = $Host.UI.RawUI.WindowSize; L ("RawUI.WindowSize : " + $w.Width + 'x' + $w.Height) } catch { L ("RawUI.WindowSize THREW: " + $_.Exception.Message) }
try { L ("RawUI.ForegroundColor : " + [string]$Host.UI.RawUI.ForegroundColor) } catch { L ("RawUI.ForegroundColor THREW: " + $_.Exception.Message) }
try { [Console]::ForegroundColor = 'Red'; L ("[Console]::ForegroundColor setter : OK (no throw)") } catch { L ("[Console]::ForegroundColor setter THREW: " + $_.Exception.Message) }
try { [Console]::Title = 'probe'; L "[Console]::Title setter : OK (no throw)" } catch { L ("[Console]::Title setter THREW: " + $_.Exception.Message) }
L ("[Console]::OutputEncoding : " + [Console]::OutputEncoding.WebName + " (CodePage " + [Console]::OutputEncoding.CodePage + ")")
L ("`$OutputEncoding : " + $OutputEncoding.WebName)
L ("chcp (GetConsoleOutputCP) : " + [K32b]::GetConsoleOutputCP())
try { L ("`$TypingCompletion / TabExpansion2 n/a") } catch {}
L ""
L "##### PART B: AllocConsole() -- now attached to a brand new console #####"
$alloc = [K32b]::AllocConsole()
L ("AllocConsole() returned : " + $alloc)
[Console]::SetOut([System.IO.StreamWriter]::new([Console]::OpenStandardOutput(), [Console]::OutputEncoding)) 2>$null
L ("GetConsoleWindow : 0x{0:X}" -f ([K32b]::GetConsoleWindow().ToInt64()))
$m2 = 0; $ok2 = [K32b]::GetConsoleMode([K32b]::GetStdHandle(-11), [ref]$m2)
L ("GetConsoleMode(STDOUT) : $ok2 mode=0x{0:X}" -f $m2)
L (" ENABLE_VIRTUAL_TERMINAL_PROCESSING (0x4) : " + (($m2 -band 4) -eq 4))
L (" ENABLE_PROCESSED_OUTPUT (0x1) : " + (($m2 -band 1) -eq 1))
$ok3 = [K32b]::SetConsoleMode([K32b]::GetStdHandle(-11), $m2 -band (-bnot 4))
$m3 = 0; [void][K32b]::GetConsoleMode([K32b]::GetStdHandle(-11), [ref]$m3)
L ("After SetConsoleMode(mode AND NOT 0x4) -> " + $ok3 + " mode=0x{0:X}" -f $m3)
L (" VT now off? " + ((($m3 -band 4) -eq 0)))
[void][K32b]::SetConsoleMode([K32b]::GetStdHandle(-11), $m3 -bor 4)
$m4 = 0; [void][K32b]::GetConsoleMode([K32b]::GetStdHandle(-11), [ref]$m4)
L ("After re-enabling 0x4 -> mode=0x{0:X}" -f $m4)
L ("GetConsoleOutputCP now : " + [K32b]::GetConsoleOutputCP())
try { L ("RawUI.WindowSize now : " + $Host.UI.RawUI.WindowSize.Width + 'x' + $Host.UI.RawUI.WindowSize.Height) } catch { L ("RawUI.WindowSize THREW: " + $_.Exception.Message) }
[System.IO.File]::WriteAllLines($outFile, $lines, (New-Object System.Text.UTF8Encoding($false)))
[void][K32b]::FreeConsole()
+106
View File
@@ -0,0 +1,106 @@
param([Parameter(Mandatory=$true)][string]$OutFile, [Parameter(Mandatory=$true)][string]$Work, [int]$HoldSeconds = 5)
$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 + " CLR " + [System.Environment]::Version + " Redirected=" + [Console]::IsOutputRedirected)
function Asm([string]$n) { return [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $n } | Select-Object -First 1 }
function T([string]$n, [string]$a) { $x = Asm $a; if ($x) { return $x.GetType($n) } else { return $null } }
$isDesktop = ($PSVersionTable.PSEdition -eq 'Desktop')
$fx = if ($isDesktop) { 'net461' } else { 'netstandard2.0' }
L ""
L "############ A) Spectre.Console 0.49.1 with the COMPLETE System.Memory closure side-loaded ############"
$scOk = $false
try {
$order = @(
(Join-Path $Work "system.runtime.compilerservices.unsafe-4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll"),
(Join-Path $Work "system.buffers-4.5.1\lib\net461\System.Buffers.dll"),
(Join-Path $Work "system.numerics.vectors-4.5.0\lib\net46\System.Numerics.Vectors.dll"),
(Join-Path $Work "system.memory-4.5.5\lib\$fx\System.Memory.dll"),
(Join-Path $Work "sc049\lib\netstandard2.0\Spectre.Console.dll")
)
L (" host=$($PSVersionTable.PSEdition) using System.Memory lib/$fx")
foreach ($p in $order) {
if (-not (Test-Path $p)) { throw "missing file: $p" }
Add-Type -Path $p -ErrorAction Stop
L (" loaded $([System.IO.Path]::GetFileName($p)) v$(([System.Reflection.AssemblyName]::GetAssemblyName($p)).Version) [$([System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName($p)))]")
}
L " ALL dependencies loaded."
$ansiT = T 'Spectre.Console.AnsiConsole' 'Spectre.Console'
$tableT = T 'Spectre.Console.Table' 'Spectre.Console'
$colT = T 'Spectre.Console.TableColumn' 'Spectre.Console'
$extT = T 'Spectre.Console.TableExtensions' 'Spectre.Console'
$t = [Activator]::CreateInstance($tableT)
L " new Spectre.Console.Table() -> OK"
$t.AddColumn([Activator]::CreateInstance($colT, [object[]]@('项目 / Item')))
$t.AddColumn([Activator]::CreateInstance($colT, [object[]]@('大小 / Size')))
$t.AddColumn([Activator]::CreateInstance($colT, [object[]]@('状态 / Status')))
$m = $extT.GetMethod('AddRow', [Type[]]@($tableT, [string[]]))
$m.Invoke($null, [object[]]@($t, [string[]]@('中文备份归档 alpha', '1.2 GB', 'OK'))) | Out-Null
$m.Invoke($null, [object[]]@($t, [string[]]@('beta', '340 MB', 'WARN'))) | Out-Null
$m.Invoke($null, [object[]]@($t, [string[]]@('gamma-ascii', '2.0 GB', 'OK'))) | Out-Null
Write-Host ""
Write-Host (" ===== Spectre.Console 0.49.1 (netstandard2.0) on PowerShell " + $PSVersionTable.PSVersion + " =====") -ForegroundColor Cyan
$ansiT::Write($t)
Write-Host ""
$ansiT::Markup('[red]red[/] [green]green[/] [yellow]yellow[/] [bold]bold[/] [blue on white]reverse[/]')
$ansiT::Markup('[grey]grey[/] ' + [char]0x2713 + ' check ' + [char]0x26A0 + ' warn')
L " AnsiConsole.Write(Table with CJK) + Markup colors + unicode symbols -> INVOKED, no exception"
L (" Table rendered width (SafeBorder default): rows=" + ($t.Rows.Count) + " cols=" + ($t.Columns.Count))
$scOk = $true
L "RESULT-A: SpectreConsole=WORKS"
} catch {
L (" FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
L "RESULT-A: SpectreConsole=FAILED"
}
L ""
L "############ B) Terminal.Gui 1.15.0 - can the driver be hand-wired from PowerShell at all? ############"
try {
Add-Type -Path (Join-Path $Work 'nstack111\lib\netstandard2.0\NStack.dll') -ErrorAction Stop
Add-Type -Path (Join-Path $Work 'tg115\lib\net472\Terminal.Gui.dll') -ErrorAction Stop
$tgAsm = Asm 'Terminal.Gui'
$appT = $tgAsm.GetType('Terminal.Gui.Application')
$drvT = $tgAsm.GetType('Terminal.Gui.NetDriver')
$mlT = $tgAsm.GetType('Terminal.Gui.IMainLoopDriver')
$loopT = $tgAsm.GetType('Terminal.Gui.NetMainLoop')
L (" Application=" + [bool]$appT + " NetDriver=" + [bool]$drvT + " NetMainLoop=" + [bool]$loopT)
L (" Is NetMainLoop public? " + $loopT.IsPublic + " (a PowerShell user cannot 'new' a non-public type without reflection)")
foreach ($c in $loopT.GetConstructors([System.Reflection.BindingFlags]'Public,NonPublic,Instance')) {
L (" ctor " + $(if ($c.IsPublic) { 'PUBLIC' } else { 'internal' }) + ": NetMainLoop(" + (($c.GetParameters() | ForEach-Object { $_.ParameterType.Name + ' ' + $_.Name }) -join ', ') + ")")
}
$initM = $appT.GetMethod('Init', [Type[]]@($drvT, $mlT))
$drv = [Activator]::CreateInstance($drvT)
# build NetMainLoop via reflection on the internal ctor
$ctor = $loopT.GetConstructors([System.Reflection.BindingFlags]'Public,NonPublic,Instance') | Select-Object -First 1
$args = @()
foreach ($p in $ctor.GetParameters()) {
if ($p.ParameterType -eq $drvT) { $args += , $drv } elseif ($p.ParameterType -eq [bool]) { $args += , $true } else { $args += , $null }
}
$loop = $ctor.Invoke($args)
L (" NetMainLoop constructed via reflection: " + [bool]$loop)
$initM.Invoke($null, @($drv, $loop)) | Out-Null
L " Application.Init(NetDriver, NetMainLoop) -> SUCCEEDED *** Terminal.Gui IS live on PowerShell " + $PSVersionTable.PSVersion + " ***"
$winT = $tgAsm.GetType('Terminal.Gui.Window'); $lblT = $tgAsm.GetType('Terminal.Gui.Label'); $btnT = $tgAsm.GetType('Terminal.Gui.Button')
$w = [Activator]::CreateInstance($winT, [object[]]@('BakNRet - Terminal.Gui on PS ' + $PSVersionTable.PSVersion))
$l1 = [Activator]::CreateInstance($lblT, [object[]]@('Terminal.Gui 1.15.0 (net472)'))
$l2 = [Activator]::CreateInstance($lblT, [object[]]@('CJK: 2 Chinese chars = 4 columns'))
$b1 = [Activator]::CreateInstance($btnT, [object[]]@('OK'))
$w.Add($l1); $w.Add($l2); $w.Add($b1)
$w.X = 1; $w.Y = 1; $w.Width = 78; $w.Height = 9
$l1.Y = 1; $l2.Y = 3; $b1.Y = 5; $b1.X = 2
($appT.GetProperty('Top')).SetValue($null, $w)
($appT.GetProperty('Current')).SetValue($null, $w)
$w.SetFocus(); $w.Redraw($w.Bounds)
$appT.GetMethod('Refresh', [Type[]]@()).Invoke($null, @()) | Out-Null
L " painted Window(78x9)+2 Labels+Button, holding " + $HoldSeconds + "s"
Start-Sleep -Seconds $HoldSeconds
$appT.GetMethod('Shutdown', [Type[]]@()).Invoke($null, @()) | Out-Null
L " Application.Shutdown() -> clean"
L "RESULT-B: TerminalGui=WORKS"
} catch {
L (" FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
L "RESULT-B: TerminalGui=FAILED"
}
if (-not [Console]::IsOutputRedirected) { Write-Host ""; Write-Host ("[done PS " + $PSVersionTable.PSVersion + "] closing in $HoldSeconds s") -ForegroundColor Yellow; Start-Sleep -Seconds $HoldSeconds }
+95
View File
@@ -0,0 +1,95 @@
param([Parameter(Mandatory=$true)][string]$OutFile, [Parameter(Mandatory=$true)][string]$Work, [int]$HoldSeconds = 6)
$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))) }
function Asm([string]$n) { return [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $n } | Select-Object -First 1 }
L ("PSVersion: " + $PSVersionTable.PSVersion + " CLR " + [System.Environment]::Version)
$isDesktop = ($PSVersionTable.PSEdition -eq 'Desktop')
$fx = if ($isDesktop) { 'net461' } else { 'netstandard2.0' }
L ""
L "############ A) Spectre.Console on PowerShell $($PSVersionTable.PSVersion) - CJK ALIGNMENT CHECK ############"
$scOk = $false
try {
foreach ($p in @(
(Join-Path $Work "system.runtime.compilerservices.unsafe-4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll"),
(Join-Path $Work "system.buffers-4.5.1\lib\net461\System.Buffers.dll"),
(Join-Path $Work "system.numerics.vectors-4.5.0\lib\net46\System.Numerics.Vectors.dll"),
(Join-Path $Work "system.memory-4.5.5\lib\$fx\System.Memory.dll"),
(Join-Path $Work "sc049\lib\netstandard2.0\Spectre.Console.dll")
)) { Add-Type -Path $p -ErrorAction Stop }
$ansiT = (Asm 'Spectre.Console').GetType('Spectre.Console.AnsiConsole')
$tableT = (Asm 'Spectre.Console').GetType('Spectre.Console.Table')
$colT = (Asm 'Spectre.Console').GetType('Spectre.Console.TableColumn')
$extT = (Asm 'Spectre.Console').GetType('Spectre.Console.TableExtensions')
$t = [Activator]::CreateInstance($tableT)
$t.AddColumn([Activator]::CreateInstance($colT, [object[]]@('Description')))
$t.AddColumn([Activator]::CreateInstance($colT, [object[]]@('Size')))
$t.AddColumn([Activator]::CreateInstance($colT, [object[]]@('State')))
$m = $extT.GetMethod('AddRow', [Type[]]@($tableT, [string[]]))
$m.Invoke($null, [object[]]@($t, [string[]]@('alpha-8c', '1.2 GB', 'OK'))) | Out-Null
$m.Invoke($null, [object[]]@($t, [string[]]@('中文备份', '1.2 GB', 'OK'))) | Out-Null
$m.Invoke($null, [object[]]@($t, [string[]]@('z', '1.2 GB', 'OK'))) | Out-Null
$m.Invoke($null, [object[]]@($t, [string[]]@('混合 mixed 文本', '340 MB', 'WARN'))) | Out-Null
Write-Host ""
Write-Host (" ===== Spectre.Console 0.49.1 / PS $($PSVersionTable.PSVersion) : is the CJK row's LAST column aligned? =====") -ForegroundColor Cyan
$ansiT::Write($t)
Write-Host ""
$ansiT::Markup('[red]red[/] [green]green[/] [yellow]yellow[/] [bold]bold[/]')
Write-Host ""
$scOk = $true
L " Table with a 2-CJK-char row and an 8-ASCII-char row rendered. Visually compare whether the"
L " 'Size' column starts at the same screen column for 'alpha-8c' (8 chars) and '中文备份' (4 chars / 8 columns)."
L "RESULT-A: SpectreConsole=WORKS on PS $($PSVersionTable.PSVersion)"
} catch {
L (" FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
L "RESULT-A: SpectreConsole=FAILED"
}
L ""
L "############ B) Terminal.Gui 1.15.0 - corrected NetMainLoop construction ############"
try {
Add-Type -Path (Join-Path $Work 'nstack111\lib\netstandard2.0\NStack.dll') -ErrorAction Stop
Add-Type -Path (Join-Path $Work 'tg115\lib\net472\Terminal.Gui.dll') -ErrorAction Stop
$tgAsm = Asm 'Terminal.Gui'
$appT = $tgAsm.GetType('Terminal.Gui.Application')
$drvT = $tgAsm.GetType('Terminal.Gui.NetDriver')
$mlT = $tgAsm.GetType('Terminal.Gui.IMainLoopDriver')
$loopT = $tgAsm.GetType('Terminal.Gui.NetMainLoop')
$drv = [Activator]::CreateInstance($drvT)
L (" NetDriver instance created: " + $drv.GetType().FullName)
$ctor = $loopT.GetConstructors([System.Reflection.BindingFlags]'Public,NonPublic,Instance') | Select-Object -First 1
$args = @()
foreach ($p in $ctor.GetParameters()) {
if ($p.ParameterType.IsAssignableFrom($drv.GetType())) { $args += , $drv }
elseif ($p.ParameterType -eq [bool]) { $args += , $true }
else { $args += , $null }
}
L (" NetMainLoop ctor args: " + (($args | ForEach-Object { if ($_ -eq $null) { 'null' } else { $_.GetType().Name } }) -join ', '))
$loop = $ctor.Invoke($args)
L (" NetMainLoop constructed: " + ($loop.GetType().FullName))
$initM = $appT.GetMethod('Init', [Type[]]@($drvT, $mlT))
$initM.Invoke($null, @($drv, $loop)) | Out-Null
L " Application.Init(NetDriver, NetMainLoop) -> SUCCEEDED *** Terminal.Gui 1.15.0 IS LIVE on PowerShell $($PSVersionTable.PSVersion) ***"
$winT = $tgAsm.GetType('Terminal.Gui.Window'); $lblT = $tgAsm.GetType('Terminal.Gui.Label'); $btnT = $tgAsm.GetType('Terminal.Gui.Button')
$w = [Activator]::CreateInstance($winT, [object[]]@("BakNRet / Terminal.Gui - PowerShell $($PSVersionTable.PSVersion)"))
$l1 = [Activator]::CreateInstance($lblT, [object[]]@("Terminal.Gui 1.15.0 (net472) on $(if ($isDesktop) { '.NET Framework ' } else { '.NET ' })" + [System.Environment]::Version))
$l2 = [Activator]::CreateInstance($lblT, [object[]]@('CJK cell test: 2 CJK chars -> 4 columns'))
$l3 = [Activator]::CreateInstance($lblT, [object[]]@('Box drawing'))
$b1 = [Activator]::CreateInstance($btnT, [object[]]@('OK'))
$w.Add($l1); $w.Add($l2); $w.Add($l3); $w.Add($b1)
$w.X = 1; $w.Y = 1; $w.Width = 78; $w.Height = 10
$l1.Y = 1; $l2.Y = 3; $l3.Y = 4; $b1.Y = 6; $b1.X = 2
($appT.GetProperty('Top')).SetValue($null, $w)
($appT.GetProperty('Current')).SetValue($null, $w)
$w.SetFocus(); $w.Redraw($w.Bounds)
$appT.GetMethod('Refresh', [Type[]]@()).Invoke($null, @()) | Out-Null
L " painted Window(78x10) + 3 Labels + Button; holding $HoldSeconds s"
Start-Sleep -Seconds $HoldSeconds
$appT.GetMethod('Shutdown', [Type[]]@()).Invoke($null, @()) | Out-Null
L " Application.Shutdown() -> clean"
L "RESULT-B: TerminalGui=WORKS on PS $($PSVersionTable.PSVersion)"
} catch {
L (" FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
L "RESULT-B: TerminalGui=FAILED"
}
if (-not [Console]::IsOutputRedirected) { Write-Host ""; Write-Host ("[settled PS " + $PSVersionTable.PSVersion + "] closing in $HoldSeconds s") -ForegroundColor Yellow; Start-Sleep -Seconds $HoldSeconds }
+60
View File
@@ -0,0 +1,60 @@
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)
$nstack = Join-Path $Work 'nstack111\lib\netstandard2.0\NStack.dll'
$tg = Join-Path $Work 'tg115\lib\net472\Terminal.Gui.dll'
Add-Type -Path $nstack -ErrorAction Stop
Add-Type -Path $tg -ErrorAction Stop
$asm = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Terminal.Gui' } | Select-Object -First 1
L "=== WHO implements IMainLoopDriver? ==="
$mlT = $asm.GetType('Terminal.Gui.IMainLoopDriver')
L (" IMainLoopDriver: " + [bool]$mlT + " members: " + (($mlT.GetMembers() | ForEach-Object { $_.Name }) -join ', '))
foreach ($t in $asm.GetTypes()) {
if ($t.IsClass -and -not $t.IsAbstract -and $mlT.IsAssignableFrom($t)) { L (" IMPLEMENTOR: " + $t.FullName + " public ctor: " + [bool]($t.GetConstructor([Type[]]@()))) }
}
L ""
L "=== also list Net* driver types ==="
foreach ($t in ($asm.GetTypes() | Where-Object { $_.Name -like 'Net*' -or $_.Name -like '*MainLoop*' })) { L (" " + $t.FullName + " interfaces: " + (($t.GetInterfaces() | ForEach-Object { $_.Name }) -join ',')) }
L ""
L "=== ALL static Init/Run overloads on Application (exhaustive) ==="
$appT = $asm.GetType('Terminal.Gui.Application')
foreach ($m in ($appT.GetMethods([System.Reflection.BindingFlags]'Public,Static') | Where-Object { $_.Name -in @('Init','Run','Shutdown','Refresh','Begin','End','RunState') })) {
L (" " + $m.ReturnType.Name + " " + $m.Name + "(" + (($m.GetParameters() | ForEach-Object { $_.ParameterType.Name + " " + $_.Name }) -join ', ') + ")")
}
L ""
L "=== Application static PROPERTIES ==="
foreach ($p in ($appT.GetProperties([System.Reflection.BindingFlags]'Public,Static'))) { L (" " + $p.PropertyType.Name + " " + $p.Name + " CanWrite=" + $p.CanWrite) }
L ""
L "=== does Terminal.Gui touch the console only at Init() time? test with redirected stdout ==="
L (" IsOutputRedirected = " + [Console]::IsOutputRedirected)
try {
$drvT = $asm.GetType('Terminal.Gui.NetDriver')
$loopT = $asm.GetTypes() | Where-Object { $_.FullName -and $mlT.IsAssignableFrom($_) -and -not $_.IsAbstract -and $_.IsClass } | Select-Object -First 1
L (" using driver=" + $drvT.FullName + " mainloop=" + $loopT.FullName)
$initM = $appT.GetMethod('Init', [Type[]]@($drvT, $mlT))
$drv = [Activator]::CreateInstance($drvT)
$loop = [Activator]::CreateInstance($loopT)
$initM.Invoke($null, @($drv, $loop)) | Out-Null
L " Application.Init(NetDriver, NetMainLoop) -> SUCCEEDED *** Terminal.Gui 1.15.0 is LIVE on PowerShell " + $PSVersionTable.PSVersion + " ***"
$winT = $asm.GetType('Terminal.Gui.Window'); $lblT = $asm.GetType('Terminal.Gui.Label'); $btnT = $asm.GetType('Terminal.Gui.Button')
$w = [Activator]::CreateInstance($winT, [object[]]@('BakNRet - Terminal.Gui on PowerShell ' + $PSVersionTable.PSVersion))
$l1 = [Activator]::CreateInstance($lblT, [object[]]@('Terminal.Gui 1.15.0 (net472) on ' + $(if ($PSVersionTable.PSEdition -eq 'Desktop') { '.NET Framework ' } else { '.NET ' }) + [System.Environment]::Version))
$l2 = [Activator]::CreateInstance($lblT, [object[]]@('CJK: 2 Chinese chars = 4 columns'))
$b1 = [Activator]::CreateInstance($btnT, [object[]]@('OK'))
$w.Add($l1); $w.Add($l2); $w.Add($b1)
$w.X = 1; $w.Y = 1; $w.Width = 76; $w.Height = 8
$l1.Y = 1; $l2.Y = 3; $b1.Y = 5; $b1.X = 2
($appT.GetProperty('TopLevel')).SetValue($null, $w)
$w.SetFocus(); $w.Redraw($w.Bounds)
$appT.GetMethod('Refresh', [Type[]]@()).Invoke($null, @()) | Out-Null
L " painted Window(76x8) + 2 Labels + Button"
Start-Sleep -Seconds 5
$appT.GetMethod('Shutdown', [Type[]]@()).Invoke($null, @()) | Out-Null
L " Application.Shutdown() -> clean"
L "RESULT: TerminalGui=WORKS"
} catch {
L (" FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
L "RESULT: TerminalGui=FAILED"
}
+57
View File
@@ -0,0 +1,57 @@
param([Parameter(Mandatory=$true)][string]$OutFile, [Parameter(Mandatory=$true)][string]$Work, [int]$HoldSeconds = 6)
$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))) }
function Asm([string]$n) { return [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $n } | Select-Object -First 1 }
L ("PSVersion: " + $PSVersionTable.PSVersion)
try {
Add-Type -Path (Join-Path $Work 'nstack111\lib\netstandard2.0\NStack.dll') -ErrorAction Stop
Add-Type -Path (Join-Path $Work 'tg115\lib\net472\Terminal.Gui.dll') -ErrorAction Stop
$tgAsm = Asm 'Terminal.Gui'
$usT = (Asm 'NStack').GetType('NStack.ustring')
L (" NStack.ustring type: " + [bool]$usT)
$winT = $tgAsm.GetType('Terminal.Gui.Window')
L " Window constructors:"
foreach ($c in $winT.GetConstructors()) { L (" (" + (($c.GetParameters() | ForEach-Object { $_.ParameterType.Name + ' ' + $_.Name }) -join ', ') + ")") }
L " Note: title parameters are NStack.ustring, not System.String -> PowerShell cannot bind a string literal."
# convert
$implicit = $usT.GetMethod('op_Implicit', [Type[]]@([string]))
L (" ustring implicit conversion from string: " + [bool]$implicit)
$us = $implicit.Invoke($null, @('BakNRet - Terminal.Gui'))
L (" converted title object: " + $us.GetType().FullName)
$appT = $tgAsm.GetType('Terminal.Gui.Application')
$drvT = $tgAsm.GetType('Terminal.Gui.NetDriver')
$mlT = $tgAsm.GetType('Terminal.Gui.IMainLoopDriver')
$loopT = $tgAsm.GetType('Terminal.Gui.NetMainLoop')
$drv = [Activator]::CreateInstance($drvT)
$ctor = $loopT.GetConstructors([System.Reflection.BindingFlags]'Public,NonPublic,Instance') | Select-Object -First 1
$loop = $ctor.Invoke(@($drv))
$appT.GetMethod('Init', [Type[]]@($drvT, $mlT)).Invoke($null, @($drv, $loop)) | Out-Null
L " Application.Init -> OK"
$lblT = $tgAsm.GetType('Terminal.Gui.Label'); $btnT = $tgAsm.GetType('Terminal.Gui.Button')
$w = [Activator]::CreateInstance($winT, [object[]]@($us))
L (" Window created with ustring title: " + ($w.GetType().FullName))
# Labels also take ustring; use implicit conversion
$l1 = [Activator]::CreateInstance($lblT, [object[]]@($implicit.Invoke($null, @('Terminal.Gui 1.15.0 / Label via ustring conversion'))))
$l2 = [Activator]::CreateInstance($lblT, [object[]]@($implicit.Invoke($null, @('CJK cell test: 2 CJK chars -> 4 columns'))))
$b1 = [Activator]::CreateInstance($btnT, [object[]]@($implicit.Invoke($null, @('OK'))))
$w.Add($l1); $w.Add($l2); $w.Add($b1)
$w.X = 1; $w.Y = 1; $w.Width = 78; $w.Height = 9
$l1.Y = 1; $l2.Y = 3
$b1.Y = 5; $b1.X = 2
$appT.GetProperty('Top').SetValue($null, $w)
$appT.GetProperty('Current').SetValue($null, $w)
$w.SetFocus(); $w.Redraw($w.Bounds)
$appT.GetMethod('Refresh', [Type[]]@()).Invoke($null, @()) | Out-Null
L " *** FULLY WORKING Terminal.Gui TUI painted: Window(78x9) + 2 Labels + Button ***"
L "RESULT: TerminalGui=FULL-WIN via reflection + ustring conversion"
Write-Host ""
Write-Host (" ===== Terminal.Gui 1.15.0 live on PowerShell $($PSVersionTable.PSVersion) =====") -ForegroundColor Cyan
Start-Sleep -Seconds $HoldSeconds
$appT.GetMethod('Shutdown', [Type[]]@()).Invoke($null, @()) | Out-Null
L " Shutdown clean"
} catch {
L (" FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
L "RESULT: TerminalGui=FAILED"
}
if (-not [Console]::IsOutputRedirected) { Start-Sleep -Seconds 2 }
+55
View File
@@ -0,0 +1,55 @@
param([Parameter(Mandatory=$true)][string]$OutFile, [Parameter(Mandatory=$true)][string]$Work, [int]$HoldSeconds = 6)
$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))) }
function Asm([string]$n) { return [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $n } | Select-Object -First 1 }
L ("PSVersion: " + $PSVersionTable.PSVersion)
try {
Add-Type -Path (Join-Path $Work 'nstack111\lib\netstandard2.0\NStack.dll') -ErrorAction Stop
Add-Type -Path (Join-Path $Work 'tg115\lib\net472\Terminal.Gui.dll') -ErrorAction Stop
$tgAsm = Asm 'Terminal.Gui'
$drvT = $tgAsm.GetType('Terminal.Gui.NetDriver')
$mlT = $tgAsm.GetType('Terminal.Gui.IMainLoopDriver')
$appT = $tgAsm.GetType('Terminal.Gui.Application')
$loopT = $tgAsm.GetType('Terminal.Gui.NetMainLoop')
$drv = [Activator]::CreateInstance($drvT)
$loop = ($loopT.GetConstructors([System.Reflection.BindingFlags]'Public,NonPublic,Instance') | Select-Object -First 1).Invoke(@($drv))
$appT.GetMethod('Init', [Type[]]@($drvT, $mlT)).Invoke($null, @($drv, $loop)) | Out-Null
L " Application.Init(NetDriver, NetMainLoop) -> SUCCEEDED"
$winT = $tgAsm.GetType('Terminal.Gui.Window')
$lblT = $tgAsm.GetType('Terminal.Gui.Label')
$btnT = $tgAsm.GetType('Terminal.Gui.Button')
# ALL parameterless ctors -> the interop-safe path
$w = [Activator]::CreateInstance($winT); L (" Window() parameterless -> " + ($w -ne $null))
$l1 = [Activator]::CreateInstance($lblT); L (" Label() parameterless -> " + ($l1 -ne $null))
$b1 = [Activator]::CreateInstance($btnT); L (" Button() parameterless -> " + ($b1 -ne $null))
# set text via .Text property
$textProp = $lblT.GetProperty('Text')
L (" Label.Text property type: " + $textProp.PropertyType.FullName)
$usT = (Asm 'NStack').GetType('NStack.ustring')
$conv = $usT.GetMethod('op_Implicit', [Type[]]@([string]))
$textProp.SetValue($l1, $conv.Invoke($null, @("Terminal.Gui 1.15.0 on PowerShell $($PSVersionTable.PSVersion)")), $null)
L (" Label.Text set via ustring implicit conversion -> OK: " + $l1.Text.ToString())
$titleProp = $winT.GetProperty('Title')
if ($titleProp) { $titleProp.SetValue($w, $conv.Invoke($null, @('BakNRet TUI')), $null); L (" Window.Title set -> OK: " + $w.Title.ToString()) }
$l2 = [Activator]::CreateInstance($lblT)
$textProp.SetValue($l2, $conv.Invoke($null, @('CJK cell test: 2 CJK chars occupy 4 columns')), $null)
$w.Add($l1); $w.Add($l2); $w.Add($b1)
$w.X = 1; $w.Y = 1; $w.Width = 80; $w.Height = 9
$l1.Y = 1; $l2.Y = 3; $b1.Y = 5; $b1.X = 2
$appT.GetProperty('Top').SetValue($null, $w)
$appT.GetProperty('Current').SetValue($null, $w)
$w.SetFocus(); $w.Redraw($w.Bounds)
$appT.GetMethod('Refresh', [Type[]]@()).Invoke($null, @()) | Out-Null
L " *** WORKING Terminal.Gui TUI: Window(80x9) + 2 Labels (via ustring) + Button ***"
L "RESULT: TerminalGui=WORKS on PowerShell $($PSVersionTable.PSVersion)"
Write-Host ""
Write-Host (" ==== Terminal.Gui 1.15.0 rendered by PowerShell $($PSVersionTable.PSVersion) ====") -ForegroundColor Cyan
Start-Sleep -Seconds $HoldSeconds
$appT.GetMethod('Shutdown', [Type[]]@()).Invoke($null, @()) | Out-Null
L " Application.Shutdown() -> clean"
} catch {
L (" FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + ($ie.Message -split "`n")[0]); $ie = $ie.InnerException }
L "RESULT: TerminalGui=FAILED"
}
if (-not [Console]::IsOutputRedirected) { Start-Sleep -Seconds 3 }
+37
View File
@@ -0,0 +1,37 @@
param([Parameter(Mandatory=$true)][string]$OutFile)
$lines = New-Object System.Collections.Generic.List[string]
function L($s) { $lines.Add($s) }
L ("PSVersion: " + $PSVersionTable.PSVersion + " PSEdition: " + $PSVersionTable.PSEdition)
L ""
L "### (1) can the host be asked whether it wants VT? ###"
$p = $Host.UI | Get-Member -Name SupportsVirtualTerminal -MemberType Property -ErrorAction SilentlyContinue
L (" [bool](`$Host.UI | Get-Member -Name SupportsVirtualTerminal) = " + [bool]$p + " (a REAL property, not a stub)")
$rw = $Host.UI.RawUI | Get-Member -Name SupportsVirtualTerminal -MemberType Property -ErrorAction SilentlyContinue
L (" [bool](`$Host.UI.RawUI | Get-Member -Name SupportsVirtualTerminal) = " + [bool]$rw + " (does NOT exist on RawUI)")
L (" value of `$Host.UI.SupportsVirtualTerminal = " + [string]$Host.UI.SupportsVirtualTerminal)
L (" property TypeName: " + $(if ($p) { $p.Definition } else { 'n/a' }))
L ""
L "### (2) is it WRITABLE? (the documented way to turn VT off: `$Host.UI.SupportsVirtualTerminal = `$false) ###"
$before = [string]$Host.UI.SupportsVirtualTerminal
try { $Host.UI.SupportsVirtualTerminal = $false; $setOk = $true } catch { $setOk = $false; L (" setter THREW: " + $_.Exception.Message) }
if ($setOk) { L (" set `$false -> value is now [" + [string]$Host.UI.SupportsVirtualTerminal + "] (before [$before])") }
try { $Host.UI.SupportsVirtualTerminal = $true } catch {}
L (" restored -> value is now [" + [string]$Host.UI.SupportsVirtualTerminal + "]")
L ""
L "### (3) WIDTH / HEIGHT REPORTING WHEN THERE IS NO CONSOLE (scheduled-task-like) ###"
L (" [Console]::IsOutputRedirected = " + [Console]::IsOutputRedirected)
try { L (" [Console]::WindowWidth = " + [Console]::WindowWidth) } catch { L (" [Console]::WindowWidth THREW: " + $_.Exception.Message) }
try { L (" [Console]::BufferWidth = " + [Console]::BufferWidth) } catch { L (" [Console]::BufferWidth THREW: " + $_.Exception.Message) }
try { L (" `$Host.UI.RawUI.WindowSize = " + $Host.UI.RawUI.WindowSize.Width + 'x' + $Host.UI.RawUI.WindowSize.Height + " <-- WRONG/hidden console, does not throw") } catch { L (" RawUI.WindowSize THREW: " + $_.Exception.Message) }
try { [Console]::SetCursorPosition(2, 2); L " [Console]::SetCursorPosition = OK" } catch { L (" [Console]::SetCursorPosition THREW: " + $_.Exception.Message) }
try { $c = New-Object System.Management.Automation.Host.Coordinates 2, 2; $Host.UI.RawUI.CursorPosition = $c; L (" `$Host.UI.RawUI.CursorPosition = OK -> " + $Host.UI.RawUI.CursorPosition.X + ',' + $Host.UI.RawUI.CursorPosition.Y) } catch { L (" RawUI.CursorPosition setter THREW: " + $_.Exception.Message) }
L ""
L "### (4) EXIT CODE PATH (the two entry scripts end with exit <code>) ###"
L (" `$LASTEXITCODE before = " + $LASTEXITCODE)
L " (run: powershell -File x.ps1 ; echo %ERRORLEVEL%)"
L "### (5) [Console]::ReadKey / RawUI.ReadKey availability ###"
try { L (" `$Host.UI.RawUI.KeyAvailable = " + $Host.UI.RawUI.KeyAvailable) } catch { L (" RawUI.KeyAvailable THREW: " + $_.Exception.Message) }
try { L (" [Console]::KeyAvailable = " + [Console]::KeyAvailable) } catch { L (" [Console]::KeyAvailable THREW: " + $_.Exception.Message) }
L (" RawUI members: " + (($Host.UI.RawUI | Get-Member -MemberType Property,Method | Select-Object -ExpandProperty Name) -join ','))
[System.IO.File]::WriteAllLines($OutFile, $lines, (New-Object System.Text.UTF8Encoding($false)))
Write-Host ("[vt-probe {0}] wrote {1}" -f $PSVersionTable.PSVersion, $OutFile) -ForegroundColor Yellow
+60
View File
@@ -0,0 +1,60 @@
param([Parameter(Mandatory=$true)][string]$HostLabel, [Parameter(Mandatory=$true)][string]$OutFile, [int]$WatchSeconds = 2)
$lines = New-Object System.Collections.Generic.List[string]
function L($s) { $lines.Add($s) }
$zh = [string]::Concat([char]0x4E2D, [char]0x6587)
$zh2 = [string]::Concat([char]0x4E2D, [char]0x6587, [char]0x5907, [char]0x4EFD)
$ascii8 = '12345678'
$emoji = [string]::Concat([char]::ConvertFromUtf32(0x1F600))
$box = [string]::Concat([char]0x250C, [char]0x2500, [char]0x2510)
$boxOdd = [string]::Concat([char]0x250C, [char]0x2502, [char]0x2510)
L "===== Chinese / fullwidth width measurement [$HostLabel] ====="
L ("PSVersion : " + $PSVersionTable.PSVersion)
L ("Console.WindowWidth : " + [Console]::WindowWidth)
L ("Console.BufferWidth : " + [Console]::BufferWidth)
try { L ("RawUI.WindowSize : " + $Host.UI.RawUI.WindowSize.Width + 'x' + $Host.UI.RawUI.WindowSize.Height) } catch { L ("RawUI.WindowSize THREW: " + $_.Exception.Message) }
L ("Console.OutputEncoding : " + [Console]::OutputEncoding.WebName)
L ("IsOutputRedirected : " + [Console]::IsOutputRedirected)
L ""
L ".Length (CHARACTER COUNT) vs REAL COLUMNS:"
foreach ($item in @(@('ascii 8 chars ', $ascii8), @('Chinese 2 chars', $zh), @('Chinese 4 chars', $zh2), @('box-drawing 3 ', $box), @('emoji 1 ', $emoji))) {
$name = $item[0]; $s = $item[1]
$chars = $s.ToCharArray()
$codes = ($chars | ForEach-Object { 'U+{0:X4}' -f [int]$_ }) -join ' '
$surr = if ($s.Length -gt 0) { ($s.ToCharArray() | Where-Object { [int]$_ -ge 0xD800 -and [int]$_ -le 0xDFFF }).Count } else { 0 }
L (" $name : .Length=$($s.Length) UTF16-surrogates=$surr Measure-Object -Character=$(($s | Measure-Object -Character).Characters) codeunits=$codes")
}
$ms = [string]::Concat($zh, ':', [System.IO.File]::ReadAllText((Join-Path $env:TEMP 'nowhere.txt')) ) 2>$null
L ""
L "WHICH BUILT-IN CAN MEASURE DISPLAY WIDTH? (probe of plausible names)"
$candidates = @('Get-DisplayWidth','Get-StringWidth','Measure-String','Get-VisualLength','Get-ConsoleStringWidth','Format-WideString','Get-WCWidth','Get-CharWidth','Get-TextWidth','Measure-Text','Get-EastAsianWidth')
foreach ($c in $candidates) {
$cmd = Get-Command $c -ErrorAction SilentlyContinue
L (" Get-Command $c -> " + $(if ($cmd) { $cmd.CommandType.ToString() + ' ' + $cmd.Source } else { 'NOT FOUND' }))
}
L (" [System.Globalization.StringInfo]::ParseCombiningCharacters('$zh').Count = " + [System.Globalization.StringInfo]::ParseCombiningCharacters($zh).Count + " (text-element count, NOT width)")
L (" TextInfo: CurrentCulture = " + [System.Globalization.CultureInfo]::CurrentCulture.Name)
L (" IsInputRedirected/IsOutputRedirected : " + [Console]::IsInputRedirected + '/' + [Console]::IsOutputRedirected)
L ""
L "CURSOR POSITION TEST (needs a real console; skipped if redirected)"
$skip = $false
try { if ([Console]::IsOutputRedirected) { L " SKIPPED: stdout is redirected, [Console]::SetCursorPosition would throw 'The handle is invalid'"; $skip = $true } } catch { $skip = $true }
if (-not $skip) {
try {
[Console]::CursorVisible = $false
foreach ($pair in @(@('8 ASCII chars ', $ascii8), @('2 CJK chars ', $zh), @('4 CJK chars ', $zh2), @('3 box-drawing ', $box), @('1 emoji ', $emoji))) {
$name = $pair[0]; $s = $pair[1]
[Console]::BufferWidth = [Console]::BufferWidth
[Console]::SetCursorPosition(0, 10)
[Console]::Write($s)
$after = [Console]::CursorLeft
L (" write '$name' from column 0 -> CursorLeft = $after (implied display columns = $after)")
}
[Console]::SetCursorPosition(0, 10); [Console]::Write('.' * 40)
[Console]::SetCursorPosition(0, 10); [Console]::Write($zh); [Console]::Write('X')
L " visual check: expect 'ZhongWenX' where X starts at column 4 (2 CJK = 4 columns)"
[Console]::CursorVisible = $true
} catch { L (" CURSOR TEST THREW: " + $_.Exception.Message) }
}
[System.IO.File]::WriteAllLines($OutFile, $lines, (New-Object System.Text.UTF8Encoding($false)))
Write-Host ("[width-probe {0}] done, wrote {1}" -f $HostLabel, $OutFile) -ForegroundColor Yellow
if ($WatchSeconds -gt 0) { Start-Sleep -Seconds $WatchSeconds }
+70
View File
@@ -0,0 +1,70 @@
param([Parameter(Mandatory=$true)][string]$HostLabel, [Parameter(Mandatory=$true)][string]$OutFile, [int]$WatchSeconds = 2)
$lines = New-Object System.Collections.Generic.List[string]
function L($s) { $lines.Add($s) }
function MK([int[]]$cp) { -join ($cp | ForEach-Object { [char]$_ }) }
$zh2 = MK @(0x4E2D, 0x6587) # 2 CJK
$zh4 = MK @(0x4E2D, 0x6587, 0x5907, 0x4EFD) # 4 CJK
$ascii8 = '12345678'
$emoji = [char]::ConvertFromUtf32(0x1F600)
$box3 = MK @(0x250C, 0x2500, 0x2510) # box drawing light
$fullw = MK @(0xFF21, 0xFF22) # fullwidth latin A B
$halfk = MK @(0xFF71, 0xFF72) # halfwidth katakana
L "===== DISPLAY-WIDTH PROBE [$HostLabel] ====="
L ("PSVersion : " + $PSVersionTable.PSVersion)
L ("PSEdition : " + $PSVersionTable.PSEdition)
L ("IsOutputRedirected : " + [Console]::IsOutputRedirected)
L ("Console.OutputEncoding : " + [Console]::OutputEncoding.WebName + " (CP " + [Console]::OutputEncoding.CodePage + ")")
L ("`$OutputEncoding : " + $OutputEncoding.WebName)
try { L ("Console.WindowWidth : " + [Console]::WindowWidth) } catch { L ("Console.WindowWidth THREW") }
try { L ("RawUI.WindowSize : " + $Host.UI.RawUI.WindowSize.Width + 'x' + $Host.UI.RawUI.WindowSize.Height) } catch { L ("RawUI.WindowSize THREW") }
L ""
L "--- .Length / Measure-Object -Character / CODEPOINTS ---"
$cases = @(
,@('8 ASCII ', $ascii8)
,@('2 CJK ', $zh2)
,@('4 CJK ', $zh4)
,@('3 boxdraw ', $box3)
,@('1 emoji ', $emoji)
,@('2 fullwidth', $fullw)
,@('2 halfkana ', $halfk)
)
foreach ($c in $cases) {
$nm = $c[0]; $s = $c[1]
$s = [string]$s
$cps = ($s.ToCharArray() | ForEach-Object { 'U+{0:X4}' -f [int]$_ }) -join ' '
L (" $nm .Length=" + $s.Length + " Measure-Object-Character=" + (($s | Measure-Object -Character).Characters) + " cps=[$cps]")
}
L ""
L "--- BUILT-IN DISPLAY-WIDTH FUNCTION? ---"
$found = @()
foreach ($c in (Get-Command -CommandType Cmdlet,Function -ErrorAction SilentlyContinue)) {
if ($c.Name -match 'Width|Wide') { $found += ($c.Name + '(' + $c.CommandType + '/' + $c.Source + ')') }
}
L (" commands matching *Width*|*Wide* : " + $(if ($found.Count) { $found -join ', ' } else { 'NONE' }))
L (" [System.Globalization.StringInfo]::ParseCombiningCharacters(2CJK).Count = " + [System.Globalization.StringInfo]::ParseCombiningCharacters($zh2).Count + " (text elements, not columns)")
L ""
L "--- MEASURED COLUMNS (cursor teleport to column 0 of row 12, write, read CursorLeft) ---"
$skip = $false
try { if ([Console]::IsOutputRedirected) { L " SKIPPED (redirected)"; $skip = $true } } catch { $skip = $true }
if (-not $skip) {
$row = 12
if ([Console]::BufferWidth -lt 40) { $row = 5 }
$vis = [Console]::CursorVisible
try { [Console]::CursorVisible = $false } catch {}
foreach ($c in $cases) {
$nm = $c[0]; $s = [string]$c[1]
try {
[Console]::SetCursorPosition(0, $row)
[Console]::Write((' ' * 60))
[Console]::SetCursorPosition(0, $row)
[Console]::Write($s)
$col = [Console]::CursorLeft
L (" write [$nm] at col 0 -> CursorLeft=" + $col + " => title column width = " + $col + " (.Length=" + $s.Length + ")")
} catch { L (" write [$nm] THREW: " + $_.Exception.Message) }
}
try { [Console]::SetCursorPosition(0, $row); [Console]::Write((' ' * 60)); [Console]::SetCursorPosition(0, $row); [Console]::Write($zh2 + 'X'); [Console]::SetCursorPosition(0, $row + 1) } catch {}
try { [Console]::CursorVisible = $vis } catch {}
}
[System.IO.File]::WriteAllLines($OutFile, $lines, (New-Object System.Text.UTF8Encoding($false)))
Write-Host ("[width-probe2 {0}] wrote {1}" -f $HostLabel, $OutFile) -ForegroundColor Yellow
if ($WatchSeconds -gt 0) { Start-Sleep -Seconds $WatchSeconds }
+68
View File
@@ -0,0 +1,68 @@
# TUI 与入口重组 —— 验收锚点
本文件是这次改造的 spec:目标、已定决策、每轮的验收判据。**改动这个范围之外的代码要重新走一轮
确认**;改这个文件里的判据要说明为什么。
## 目标
把四个入口(主入口 + 备份 / 恢复 / 配置三个动作)与一个**零依赖 TUI** 落地,同时**不破坏**已经
建立的四条底线:
1. 两个 PowerShell 版本(5.1 与 7)都能跑;
2. 全仓解析零错、`test.ps1` 全绿、真实清单只读冒烟通过;
3. 无头/无人值守路径**行为与今天一致**(`exit` 退出码语义不变);
4. 零依赖承诺不被破坏(运行备份/恢复不需要任何模块)。
## 已定决策(细节见对应 ADR)
| # | 决策 | 出处 |
| --- | --- | --- |
| D1 | 零依赖自研 TUI;不用 ConsoleGuiTools / Spectre / Terminal.Gui | ADR-0010 |
| D2 | 界面**行内**(不平屏),只有状态行原地重画 | 本轮决策 |
| D3 | 第一道闸门是 `[Console]::IsOutputRedirected`;TUI 异常**不改退出码** | ADR-0010 |
| D4 | 日志:文件全量不变,TUI 下控制台输出降级为只写文件 | 本轮决策 |
| D5 | 进度用可注入回调(无头为 `$null`) | ADR-0011 |
| D6 | 入口改名 + 只留一轮的薄垫片;`Handle-Config` → `Edit-Config` | ADR-0012 |
| D7 | 三个配置界面:清单=行编辑器、配置=表单、名录=树+只读字段 | ADR-0013 |
| D8 | 写回=外科式改写,校验通过才原子替换;保存前留时间戳备份 | ADR-0013 |
| D9 | `-InputScript @('Down','Enter')` 作为输入缝;序列用尽仍在交互界面 → 直接报错退出 | 本轮决策 |
## 分三轮(每轮独立提交、独立断言、门禁全绿)
### 第一轮:TuiKit + 菜单骨架 + 清单编辑器
- `BakNRet/Public/` 里新增 TUI 积木:`Get-BakNRetCellWidth`(East-Asian-Wide 表 + 代理对)、
read-key 包装、`Write-BakNRetAt`(带"无控制台"退化)、菜单渲染、多选、确认。
- 入口重组:`Manage-Backup.ps1`(TUI + `-Action`/`-Quiet` 无头)、`Backup-Data.ps1`、
`Restore-Data.ps1`、`Edit-Config.ps1`;`Backup.ps1`/`Restore.ps1` 变垫片。
- `Edit-Config` 只做**清单行编辑器**。
**判据**:
- `Get-BakNRetCellWidth`:ASCII=列数、中文 2 列、字体边框 1 列、全角拉丁 2 列、半角片假名 1 列、
emoji(代理对)2 列、控制字符 0 列 —— 全部有断言。
- 清单编辑器**往返断言**:解析 → 改一个字段 → 写回 → 重新解析,除目标字段外逐字段相等,
注释行数不变。
- 无头闸门:把 `test.ps1` 的输出重定向时跑 `Manage-Backup -Action Backup -DryRun`,
断言不出现任何 TUI 输出、且退出码语义与今天一致。
- `-InputScript` 驱动:`@('Down','Enter')` 能走完"打开菜单 → 选备份 → 干跑 → 退出",序列用尽
却仍在交互界面时报错退出(不是挂起)。
- 垫片:`Backup.ps1` 转发后退出码与 `Backup-Data.ps1` 一致。
### 第二轮:配置编辑器(`BackupConfig.psd1` 扁平表单)
**判据**:往返断言(同上);非法取值被拒绝且**不落盘**;`logs/config-backups/` 里出现时间戳副本。
### 第三轮:名录编辑器(树 + 只读字段)
**判据**:往返断言;`Encrypt`/`Description` 可改,`Path`/`Exclude`/`Include` 只读;改完写回后
`Import-BaknretDataFile` 仍能读出**与改前逐字段相等**的内容(动态表达式与拼接串原样保留)。
## 风险
| 风险 | 表现 | 对策 |
| --- | --- | --- |
| 无控制台时 API 静默返回假值 | `RawUI.WindowSize` 给 `160x40`、`KeyAvailable` 恒 True | D3 的闸门;TUI 段全包 try/finally |
| 中文宽度算错 | 菜单右边框歪、光标定位偏 | 第一轮的宽度断言(含半角片假名/emoji) |
| TUI 吃掉了退出码 | 计划任务显示成功但备份失败 | TUI 异常绝不 `exit`;断言退出码语义 |
| 外科式改写改坏了别的行 | 配置注释丢失、表达式被毁 | 往返断言 + 校验通过才替换 + 时间戳备份 |
| 一次性做三个编辑器把质量摊薄 | 三个都半成品 | 分三轮,每轮独立验收 |