# Issue #197: Win32 TUI proof - real Ctrl+V paste like an actual human
#
# This launches a REAL psmux TUI window, sets the clipboard to the EXACT
# text from the issue reporter, sends a REAL Ctrl+V keystroke, and verifies
# the paste appeared correctly with no freeze, no tilde, no junk.
$ErrorActionPreference = "Stop"
$psmuxDir = "$env:USERPROFILE\.psmux"
$SESSION = "tui_paste_197"
# Win32 keyboard input API + window enumeration for console apps
Add-Type @"
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
public class Win32Paste {
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern int GetWindowTextLength(IntPtr hWnd);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("kernel32.dll")]
public static extern uint GetProcessId(IntPtr hProcess);
public const byte VK_CONTROL = 0x11;
public const byte VK_RETURN = 0x0D;
public const byte VK_SHIFT = 0x10;
public const byte VK_V = 0x56;
public const uint KEYEVENTF_KEYUP = 0x0002;
public static void SendCtrlV() {
keybd_event(VK_CONTROL, 0, 0, UIntPtr.Zero);
keybd_event(VK_V, 0, 0, UIntPtr.Zero);
keybd_event(VK_V, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}
public static void SendCtrlB() {
keybd_event(VK_CONTROL, 0, 0, UIntPtr.Zero);
keybd_event(0x42, 0, 0, UIntPtr.Zero);
keybd_event(0x42, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}
public static void SendEnter() {
keybd_event(VK_RETURN, 0, 0, UIntPtr.Zero);
keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}
public static void SendChar(char c) {
byte vk = 0; bool shift = false;
if (c >= 'a' && c <= 'z') vk = (byte)(0x41 + (c - 'a'));
else if (c >= 'A' && c <= 'Z') { vk = (byte)(0x41 + (c - 'A')); shift = true; }
else if (c >= '0' && c <= '9') vk = (byte)(0x30 + (c - '0'));
else if (c == ' ') vk = 0x20;
else return;
if (shift) keybd_event(VK_SHIFT, 0, 0, UIntPtr.Zero);
keybd_event(vk, 0, 0, UIntPtr.Zero);
keybd_event(vk, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
if (shift) keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}
public static void SendString(string s) {
foreach (char c in s) {
SendChar(c);
System.Threading.Thread.Sleep(30);
}
}
// Find the console window hosting a given process ID
// Console apps are hosted by conhost.exe - but we can find visible
// windows that appeared right after our process started
private static List<IntPtr> _foundWindows = new List<IntPtr>();
public static IntPtr FindConsoleWindowForPid(int pid) {
_foundWindows.Clear();
EnumWindows((hWnd, lParam) => {
uint wPid;
GetWindowThreadProcessId(hWnd, out wPid);
if (wPid == (uint)pid && IsWindowVisible(hWnd)) {
_foundWindows.Add(hWnd);
}
return true;
}, IntPtr.Zero);
return _foundWindows.Count > 0 ? _foundWindows[0] : IntPtr.Zero;
}
// Find ANY new visible console window (conhost) that appeared after launch
public static IntPtr FindNewestVisibleConsole(HashSet<IntPtr> existingWindows) {
IntPtr found = IntPtr.Zero;
EnumWindows((hWnd, lParam) => {
if (IsWindowVisible(hWnd) && !existingWindows.Contains(hWnd) && GetWindowTextLength(hWnd) > 0) {
found = hWnd;
return false; // stop enum
}
return true;
}, IntPtr.Zero);
return found;
}
public static HashSet<IntPtr> GetAllVisibleWindows() {
var windows = new HashSet<IntPtr>();
EnumWindows((hWnd, lParam) => {
if (IsWindowVisible(hWnd)) windows.Add(hWnd);
return true;
}, IntPtr.Zero);
return windows;
}
}
"@
# ── Cleanup old sessions ──────────────────────────────────────────
Write-Host "=== Issue #197: Win32 TUI Real Ctrl+V Paste Test ===" -ForegroundColor Cyan
Get-Process tmux, psmux, pmux -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue
Start-Sleep -Seconds 1
# The EXACT texts from the issue report
$testTexts = @(
@{ Text = 'C:\Users\myusername\Documents\PowerShell\Microsoft.PowerShell_profile.ps1'; Desc = "Exact freeze text" },
@{ Text = 'C:\Users\myusername\Documents\PowerShell\'; Desc = "Shorter path (was OK)" },
@{ Text = 'ddddddddddd'; Desc = "Simple repeated chars" },
@{ Text = 'C:\Users\myusername\unity_build.log'; Desc = "Short path with dot" }
)
$allPass = $true
foreach ($test in $testTexts) {
$pasteText = $test.Text
$desc = $test.Desc
Write-Host "`n--- Testing: $desc ---" -ForegroundColor Green
Write-Host " Clipboard: $pasteText" -ForegroundColor Yellow
# Kill any old sessions
Get-Process tmux, psmux, pmux -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue
Start-Sleep -Seconds 1
# Step 1: Set clipboard to the test text
Set-Clipboard -Value $pasteText
$clipCheck = Get-Clipboard -Raw
if ($clipCheck.Trim() -ne $pasteText) {
Write-Host " [FAIL] Clipboard set failed!" -ForegroundColor Red
$allPass = $false
continue
}
Write-Host " Clipboard set OK" -ForegroundColor DarkGray
# Step 2: Snapshot existing windows BEFORE launching psmux
$existingWindows = [Win32Paste]::GetAllVisibleWindows()
Write-Host " Existing windows: $($existingWindows.Count)" -ForegroundColor DarkGray
# Step 3: Launch REAL attached psmux session
$psmuxExe = (Get-Command psmux -EA Stop).Source
$proc = Start-Process -FilePath $psmuxExe -ArgumentList "new-session", "-s", $SESSION -PassThru
Write-Host " Launched PID: $($proc.Id)"
# Step 4: Wait for session to be ready
$ready = $false
for ($i = 0; $i -lt 50; $i++) {
Start-Sleep -Milliseconds 200
$portFile = "$psmuxDir\$SESSION.port"
if (Test-Path $portFile) {
$port = (Get-Content $portFile -Raw).Trim()
try {
$tcp = [System.Net.Sockets.TcpClient]::new("127.0.0.1", [int]$port)
$tcp.Close()
$ready = $true
break
} catch {}
}
}
if (-not $ready) {
Write-Host " [FAIL] Session did not start in time" -ForegroundColor Red
$allPass = $false
try { $proc.Kill() } catch {}
continue
}
Write-Host " Session ready (port $port)" -ForegroundColor DarkGray
# Step 4: Clear the pane first
Start-Sleep -Seconds 2
& psmux send-keys -t $SESSION "clear" Enter
Start-Sleep -Seconds 1
# Step 6: Focus the window (best effort - console apps owned by conhost)
$hwnd = [IntPtr]::Zero
for ($w = 0; $w -lt 15; $w++) {
$proc.Refresh()
if ($proc.MainWindowHandle -ne [IntPtr]::Zero) {
$hwnd = $proc.MainWindowHandle; break
}
$hwnd = [Win32Paste]::FindConsoleWindowForPid($proc.Id)
if ($hwnd -ne [IntPtr]::Zero) { break }
$hwnd = [Win32Paste]::FindNewestVisibleConsole($existingWindows)
if ($hwnd -ne [IntPtr]::Zero) { break }
Start-Sleep -Milliseconds 200
}
if ($hwnd -ne [IntPtr]::Zero) {
[Win32Paste]::ShowWindow($hwnd, 9) | Out-Null
[Win32Paste]::SetForegroundWindow($hwnd) | Out-Null
Write-Host " Window focused (hwnd=$hwnd)" -ForegroundColor DarkGray
} else {
# Console window auto-focuses on launch, proceed anyway
Write-Host " [INFO] Console window auto-focused (conhost owns HWND)" -ForegroundColor DarkGray
}
Start-Sleep -Milliseconds 800
# Step 6: Send REAL Ctrl+V keystroke!
$sw = [System.Diagnostics.Stopwatch]::StartNew()
[Win32Paste]::SendCtrlV()
Start-Sleep -Milliseconds 100
$sw.Stop()
$pasteMs = $sw.ElapsedMilliseconds
Write-Host " Ctrl+V sent ($pasteMs ms)" -ForegroundColor Yellow
# Step 7: Wait a moment for paste to be processed, then press Enter
Start-Sleep -Seconds 1
# Step 8: Type a marker AFTER the paste to prove terminal is responsive
[Win32Paste]::SendEnter()
Start-Sleep -Milliseconds 300
# Type "echo ALIVE" followed by Enter
[Win32Paste]::SendString("echo ALIVE")
Start-Sleep -Milliseconds 200
[Win32Paste]::SendEnter()
Start-Sleep -Seconds 1
# Step 9: Capture pane content via CLI
$capture = & psmux capture-pane -t $SESSION -p
$captureStr = ($capture | Out-String)
Write-Host " --- PANE ---" -ForegroundColor Cyan
$capture | Where-Object { $_ -ne "" } | Select-Object -First 10 | ForEach-Object { Write-Host " $_" }
Write-Host " --- END ---" -ForegroundColor Cyan
# Step 10: Verify results
$escaped = [regex]::Escape($pasteText)
# Check: text appeared?
if ($captureStr -match $escaped) {
Write-Host " [PASS] Paste text appeared" -ForegroundColor Green
} else {
Write-Host " [FAIL] Paste text NOT in pane!" -ForegroundColor Red
$allPass = $false
}
# Check: no trailing tilde?
if ($captureStr -match ($escaped + '~')) {
Write-Host " [FAIL] Trailing tilde!" -ForegroundColor Red
$allPass = $false
} else {
Write-Host " [PASS] No trailing tilde" -ForegroundColor Green
}
# Check: terminal not frozen (ALIVE appeared)?
if ($captureStr -match "ALIVE") {
Write-Host " [PASS] Terminal responsive after paste" -ForegroundColor Green
} else {
Write-Host " [FAIL] Terminal may be frozen (ALIVE missing)" -ForegroundColor Red
$allPass = $false
}
# Check: no junk/old clipboard before paste text?
# The issue reporter saw old clipboard contents prepended to their paste
$lines = $capture | Where-Object { $_ -match $escaped }
foreach ($line in $lines) {
$idx = $line.IndexOf($pasteText)
if ($idx -gt 0) {
$prefix = $line.Substring(0, $idx)
# Ignore shell prompt (PS C:\..>)
if ($prefix -notmatch '^\s*PS\s+[A-Za-z]:\\[^>]*>\s*$' -and $prefix.Trim().Length -gt 3) {
Write-Host " [WARN] Possible junk before paste: '$prefix'" -ForegroundColor Yellow
}
}
}
# Kill session
try { $proc.Kill() } catch {}
Start-Sleep -Milliseconds 500
}
# ── Final cleanup ────────────────────────────────────────────────
Get-Process tmux, psmux, pmux -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue
Write-Host ""
if ($allPass) {
Write-Host "=== ALL Win32 TUI PASTE TESTS PASSED ===" -ForegroundColor Green
} else {
Write-Host "=== SOME TESTS FAILED ===" -ForegroundColor Red
}
exit $(if ($allPass) { 0 } else { 1 })