diff --git a/.gitignore b/.gitignore index 94f8872..d062b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ desktop.ini # 单文件构建产物(tools\Build-BakNRetModule.ps1 可随时重建) dist/ +# 调研探针的解包产物(NuGet 解包件与 .nupkg):结论进 ADR,二进制不进仓库 +.scratch/_probe/load/ +.scratch/_probe/nuget/ diff --git a/.scratch/_probe/TuiKit.ps1 b/.scratch/_probe/TuiKit.ps1 new file mode 100644 index 0000000..069ed19 --- /dev/null +++ b/.scratch/_probe/TuiKit.ps1 @@ -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 $_ } } diff --git a/.scratch/_probe/cells-probe.ps1 b/.scratch/_probe/cells-probe.ps1 new file mode 100644 index 0000000..b25264a --- /dev/null +++ b/.scratch/_probe/cells-probe.ps1 @@ -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))) diff --git a/.scratch/_probe/console-probe.ps1 b/.scratch/_probe/console-probe.ps1 new file mode 100644 index 0000000..3cfa57d --- /dev/null +++ b/.scratch/_probe/console-probe.ps1 @@ -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() diff --git a/.scratch/_probe/console-probe2.ps1 b/.scratch/_probe/console-probe2.ps1 new file mode 100644 index 0000000..0a6f2ae --- /dev/null +++ b/.scratch/_probe/console-probe2.ps1 @@ -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 diff --git a/.scratch/_probe/console-probe3.ps1 b/.scratch/_probe/console-probe3.ps1 new file mode 100644 index 0000000..b387700 --- /dev/null +++ b/.scratch/_probe/console-probe3.ps1 @@ -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 } diff --git a/.scratch/_probe/diag-probe.ps1 b/.scratch/_probe/diag-probe.ps1 new file mode 100644 index 0000000..2899dd9 --- /dev/null +++ b/.scratch/_probe/diag-probe.ps1 @@ -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' })) +} diff --git a/.scratch/_probe/final-probe.ps1 b/.scratch/_probe/final-probe.ps1 new file mode 100644 index 0000000..ed05d83 --- /dev/null +++ b/.scratch/_probe/final-probe.ps1 @@ -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" diff --git a/.scratch/_probe/final-probe2.ps1 b/.scratch/_probe/final-probe2.ps1 new file mode 100644 index 0000000..39fc148 --- /dev/null +++ b/.scratch/_probe/final-probe2.ps1 @@ -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 } diff --git a/.scratch/_probe/load-probe.ps1 b/.scratch/_probe/load-probe.ps1 new file mode 100644 index 0000000..57497f3 --- /dev/null +++ b/.scratch/_probe/load-probe.ps1 @@ -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))) diff --git a/.scratch/_probe/load-probe2.ps1 b/.scratch/_probe/load-probe2.ps1 new file mode 100644 index 0000000..0a2cac6 --- /dev/null +++ b/.scratch/_probe/load-probe2.ps1 @@ -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 +} diff --git a/.scratch/_probe/noconsole-probe.ps1 b/.scratch/_probe/noconsole-probe.ps1 new file mode 100644 index 0000000..5847ec6 --- /dev/null +++ b/.scratch/_probe/noconsole-probe.ps1 @@ -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() diff --git a/.scratch/_probe/settle-probe.ps1 b/.scratch/_probe/settle-probe.ps1 new file mode 100644 index 0000000..27df488 --- /dev/null +++ b/.scratch/_probe/settle-probe.ps1 @@ -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 } diff --git a/.scratch/_probe/settle2-probe.ps1 b/.scratch/_probe/settle2-probe.ps1 new file mode 100644 index 0000000..da59325 --- /dev/null +++ b/.scratch/_probe/settle2-probe.ps1 @@ -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 } diff --git a/.scratch/_probe/tg-probe.ps1 b/.scratch/_probe/tg-probe.ps1 new file mode 100644 index 0000000..3bd16ea --- /dev/null +++ b/.scratch/_probe/tg-probe.ps1 @@ -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" +} diff --git a/.scratch/_probe/tgwin-probe.ps1 b/.scratch/_probe/tgwin-probe.ps1 new file mode 100644 index 0000000..0ccde5b --- /dev/null +++ b/.scratch/_probe/tgwin-probe.ps1 @@ -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 } diff --git a/.scratch/_probe/tgwin2-probe.ps1 b/.scratch/_probe/tgwin2-probe.ps1 new file mode 100644 index 0000000..82e8c48 --- /dev/null +++ b/.scratch/_probe/tgwin2-probe.ps1 @@ -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 } diff --git a/.scratch/_probe/vt-probe.ps1 b/.scratch/_probe/vt-probe.ps1 new file mode 100644 index 0000000..b8805fb --- /dev/null +++ b/.scratch/_probe/vt-probe.ps1 @@ -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 ) ###" +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 diff --git a/.scratch/_probe/width-probe.ps1 b/.scratch/_probe/width-probe.ps1 new file mode 100644 index 0000000..c439d6a --- /dev/null +++ b/.scratch/_probe/width-probe.ps1 @@ -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 } diff --git a/.scratch/_probe/width-probe2.ps1 b/.scratch/_probe/width-probe2.ps1 new file mode 100644 index 0000000..dbc27e6 --- /dev/null +++ b/.scratch/_probe/width-probe2.ps1 @@ -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 } diff --git a/.scratch/tui/spec.md b/.scratch/tui/spec.md new file mode 100644 index 0000000..0b1e9dc --- /dev/null +++ b/.scratch/tui/spec.md @@ -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`;断言退出码语义 | +| 外科式改写改坏了别的行 | 配置注释丢失、表达式被毁 | 往返断言 + 校验通过才替换 + 时间戳备份 | +| 一次性做三个编辑器把质量摊薄 | 三个都半成品 | 分三轮,每轮独立验收 | diff --git a/docs/adr/0010-zero-dependency-tui.md b/docs/adr/0010-zero-dependency-tui.md new file mode 100644 index 0000000..a881a6d --- /dev/null +++ b/docs/adr/0010-zero-dependency-tui.md @@ -0,0 +1,34 @@ +# TUI 用零依赖自研,不引入任何 TUI 库 + +三个候选库**全部实测排除**(不是偏好,是硬障碍): + +- **`Microsoft.PowerShell.ConsoleGuiTools`(`Out-ConsoleGridView`)**:PSGallery 元数据声明 + `PowerShellVersion = 7.2`,**5.1 装不上**。 +- **`Spectre.Console` 0.49.1(netstandard2.0)**:能在 5.1 上加载并渲染,但必须往仓库塞 + **4 个第三方 DLL**(`System.Memory` / `System.Runtime.CompilerServices.Unsafe` / + `System.Buffers` / `System.Numerics.Vectors`)——直接违反 README 的"运行备份/恢复不需要任何 + 模块"承诺。而且它**不支持鼠标**。(最新稳定版 0.57.2 还要多两个依赖,所以连版本都得钉在 0.49.1。) +- **`Terminal.Gui` 1.15.0(net472)**:反射接线后 `Window` + `Label` + `Button` 在 5.1 与 7 上 + **真的渲染出来了**,但 `Application.Init()` 在这个版本没有无参重载,`NetMainLoop` 是 `internal`, + 且 **`Application.Shutdown()` 在两端都抛 `NullReferenceException`** —— 不能用于生产。 + +所以自研,只用四样东西:`$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')`、 +`[Console]::SetCursorPosition`、`Write-Host -ForegroundColor`、自算的 `Get-CellWidth`。 +零依赖承诺因此保住;代价是**不支持鼠标、不做复杂控件**。 + +## 两条必须记住、都实测过的坑 + +**一、无控制台时 `$Host.UI.SupportsVirtualTerminal` 会撒谎。** 它返回 `True`,而同一时刻 +`GetConsoleMode` 已经因句柄无效而失败;`[Console]::SetCursorPosition` / `WindowWidth` / +`KeyAvailable` 全部抛异常;`$Host.UI.RawUI.WindowSize` 不抛错但**静默返回假的 `160x40`**。所以: + +- 唯一可靠的第一道闸门是 **`[Console]::IsOutputRedirected`**,不是那个属性; +- **TUI 的异常绝不改退出码** —— 退出码只属于 `$failed` 语义(计划任务靠它判断成败)。 + +**二、中文列宽必须自己算。** `.Length` 是 UTF-16 code unit(中文 1 个 char、2 列);而 5.1 的 +`$Host.UI.RawUI.LengthInBufferCells('中文')` 返回 **2**(把中文当单列)、对边框字符返回 **6**(把 +制表符当宽字符)——**5.1 上绝对不能拿它排版**,7 上它是对的,所以这个坑只在一边出现、更危险。 +自建的 East-Asian-Wide 表必须覆盖全角拉丁、半角片假名(后者是 1 列)、emoji(代理对)。 + +另注:VT 在本机**默认已启用**(新 conhost 与 Windows Terminal 的 `STDOUT mode` 都是 `0x7`), +所以彩色与光标定位不需要 `SetConsoleMode`;但代码里保留"无 VT 时退化"的分支以防老环境。 diff --git a/docs/adr/0011-progress-hook-exception.md b/docs/adr/0011-progress-hook-exception.md new file mode 100644 index 0000000..36cb086 --- /dev/null +++ b/docs/adr/0011-progress-hook-exception.md @@ -0,0 +1,15 @@ +# 进度用"注入的钩子",这是对"模块不持有运行状态"的一次有意例外 + +TUI 需要知道"正在处理哪个条目、处在哪个阶段",而目前唯一的通道是日志文本。解析日志行是脆的 +——这个仓库已经因为"从文本里反推结构"吃过一次亏(方向标记贴在名字上时解析不出来,27/28 个条目 +被静默跳过)。 + +所以核心在几个确定的点上调用一个**可注入的进度回调**(建暂存 / 压缩 / 校验 / 安全描述符); +无头路径下它是 `$null`,行为与今天**完全一致**。 + +**为什么这不是"又把全局可变状态请回来"**:它是一个注入点,不是一份状态——除了运行期被调用之外 +不被读、不被写、不跨条目累积,也不参与任何判断。ADR-0007(运行锁用文件句柄而非模块级变量)的 +同一条原则仍然成立:**状态属于调用方,模块只提供能力**。 + +代价:这些回调点会成为核心里的"缝"。以后新增或改动回调点时,要同时想清楚它对**两个前端** +(TUI 与无头)分别意味着什么——尤其是"跳过"这种既不是成功也不是失败的状态,两个前端都得有说法。 diff --git a/docs/adr/0012-entry-rename-and-shims.md b/docs/adr/0012-entry-rename-and-shims.md new file mode 100644 index 0000000..c16c79a --- /dev/null +++ b/docs/adr/0012-entry-rename-and-shims.md @@ -0,0 +1,30 @@ +# 入口改名:新名字 + 只留一轮的薄垫片 + +入口从一个(按方向各一个脚本)变成四个: + +| 名字 | 角色 | +| --- | --- | +| `Manage-Backup.ps1` | **主入口**:TUI;同一个文件的无头模式(`-Action Backup -Quiet`)供计划任务调用 | +| `Backup-Data.ps1` | 备份动作(无头) | +| `Restore-Data.ps1` | 恢复动作(无头) | +| `Edit-Config.ps1` | 配置管理(TUI 三个界面) | + +旧的 `Backup.ps1` / `Restore.ps1` 留**薄垫片**(转发 + 打印一句改名提示),**只留一轮**,下一轮删。 + +## 为什么这次留垫片,而 ADR-0001 里 `Common.psm1` 是直接删 + +因为两者的性质不同:`Common.psm1` 是**内部实现**(只有仓库内引用,全仓 grep 就能改完),而入口 +脚本是**外部接口**——README 里 25 处、使用者的肌肉记忆、以及 `tools/Register-BackupTask.ps1` 里 +硬编码的 `-File <仓库>\Backup.ps1`。内部实现断了当场报错,外部接口断了是**静默没用**。 + +## 一处实测纠正 + +写这条决策时我原以为"有个正在跑的计划任务依赖旧路径"。实测:**本机没有注册任何 BakNRet 计划 +任务**(枚举 200 个任务,按任务名与 Action 两个维度过滤,0 条命中)。所以那个风险**现在不存在**, +垫片的理由只剩"使用者的肌肉记忆 + 文档引用"。改名时必须**同步改 `Register-BackupTask.ps1`**, +否则将来注册出来的任务会指向一个垫片。 + +## 命名上的一处偏离 + +用户最初提的是 `Handle-Config`。`Handle` **不在** PowerShell 官方 approved verbs 里,而本仓库的 +静态分析门禁里 `PSUseApprovedVerbs` 是恒开的(当前 0 告警),所以改用 **`Edit-Config`**。 diff --git a/docs/adr/0013-tui-writes-config-surgically.md b/docs/adr/0013-tui-writes-config-surgically.md new file mode 100644 index 0000000..d84da14 --- /dev/null +++ b/docs/adr/0013-tui-writes-config-surgically.md @@ -0,0 +1,32 @@ +# TUI 写配置:外科式改写,校验通过才原子替换 + +三个配置文件都含**无法往返**的内容: + +- `SoftwareCatalog.psd1` 里有 `$(if ($env:SCOOP_GLOBAL) { ... } else { ... })\persist` 这类动态 + 表达式、`$(scoop prefix translucenttb)\settings.json`,以及**跨行字符串拼接**的 `Exclude`。 + `Import-PowerShellDataFile` **实测读不了这个文件**(报 "cannot generate a PowerShell object + for a ScriptBlock evaluating dynamic expressions")。 +- `BackupList.txt` 与 `BackupConfig.psd1` 里的注释**本身就是文档**(记录每个开关的取舍)。 + +所以任何"解析成对象 → 改 → 重新序列化"的方案都会把它们毁掉。做法: + +1. **只改写被编辑的那一行/那一块**,其余逐字节保持; +2. 写临时文件 → 用模块自己的读取器解析 + 语义校验(清单逐行 `ConvertFrom-BackupListLine`、 + 配置的类型与取值、名录的展开与 `Test-Path`); +3. **只有全过才调用已有的 `Write-BaknretAtomicText` 替换**;不过就**拒绝保存**并把原文错误打出来。 + +这条让 TUI 编辑器从"方便"升级为"**比手改更安全**"——手改没有校验,而它有。 + +## 可编辑面按"能不能安全往返"来定 + +| 文件 | 模型 | 可编辑 | +| --- | --- | --- | +| `BackupList.txt` | 行编辑器(复用已有 token 解析器) | 方向 / 目标 / 覆盖 / 排除 / 追加 / 加密 / 键值 / 说明 | +| `BackupConfig.psd1` | 扁平表单 | 目录、阈值、开关及其合法取值 | +| `SoftwareCatalog.psd1` | 树 + 只读字段 | **只有 `Encrypt` 与 `Description`**;`Path`/`Exclude`/`Include` 显示为只读并提示用文本编辑器 | + +## 两条配套约定 + +- 每次保存前把原文件按时间戳复制到 `logs/config-backups/`(已 gitignore,不污染仓库)。 +- 每个编辑器带一条**往返断言**:解析 → 改一个字段 → 写回 → 重新解析,**除目标字段外逐字段相等, + 且注释行数不变**。没有这条断言,TUI 编辑器就是"看起来能保存"的典型。