psmux 3.3.2

Terminal multiplexer for Windows - tmux alternative for PowerShell and Windows Terminal
# test_issue52_cursor.ps1 — Diagnostic test for GitHub Issue #52
# "Cursor not in the right position in Claude session"
#
# Tests:
#  1. CSI s/u (SCOSC/SCORC) — cursor save/restore via CSI sequences
#  2. hide_cursor flag propagation — cursor hidden during render cycles
#  3. End-to-end cursor position validation with TUI-like rendering
#
# Requires: psmux built and in PATH (or cargo build --release done)

$ErrorActionPreference = "Stop"
$script:pass = 0
$script:fail = 0
$script:results = @()

function Log($msg) { Write-Host $msg }
function Pass($name) { $script:pass++; $script:results += "PASS: $name"; Write-Host "  PASS: $name" -ForegroundColor Green }
function Fail($name, $detail) { $script:fail++; $script:results += "FAIL: $name — $detail"; Write-Host "  FAIL: $name — $detail" -ForegroundColor Red }

# Kill any existing psmux server
psmux kill-server 2>$null
Start-Sleep 1

# Start a detached session
Log "Starting psmux session..."
psmux new-session -d 2>$null
Start-Sleep 2

# ─── Test 1: CSI s / CSI u (save/restore cursor) ────────────────────────────
Log ""
Log "=== Test 1: CSI s / CSI u (save/restore cursor) ==="
Log "  Sending: move to (5,10), save, move to (1,1), restore, query position"

# Move cursor to row 5, col 10, then save with CSI s
# Then move to row 1, col 1, then restore with CSI u
# The cursor should be back at row 5, col 10
$script = @'
printf '\x1b[5;10H'
printf '\x1b[s'
printf '\x1b[1;1H'
printf '\x1b[u'
printf 'CURSOR_RESTORED'
'@
# Use send-keys to write a test to the pane
psmux send-keys "printf '\x1b[5;10H'" Enter 2>$null
Start-Sleep -Milliseconds 500
psmux send-keys "printf '\x1b[s'" Enter 2>$null
Start-Sleep -Milliseconds 500
psmux send-keys "printf '\x1b[1;1H'" Enter 2>$null
Start-Sleep -Milliseconds 500
psmux send-keys "printf '\x1b[u'" Enter 2>$null
Start-Sleep -Milliseconds 500

# Capture the pane and check cursor position
$capture = psmux capture-pane -p 2>&1 | Out-String
Log "  Capture after CSI s/u: $($capture.Length) chars"

# We can't directly query cursor position from outside, so let's use a different approach
# Write a Python script that tests the vt100 parser directly
Log ""
Log "=== Test 1b: Direct vt100 parser CSI s/u test ==="

# Create a simple test program
$testCode = @'
use std::sync::{Arc, Mutex};

fn main() {
    let parser = Arc::new(Mutex::new(vt100::Parser::new(24, 80, 0)));
    let mut p = parser.lock().unwrap();
    
    // Move cursor to row 5, col 10 (1-based: 6, 11)
    p.process(b"\x1b[6;11H");
    let (r, c) = p.screen().cursor_position();
    println!("After CUP(6,11): row={}, col={}", r, c);
    assert_eq!((r, c), (5, 10), "CUP positioning failed");
    
    // Save cursor with CSI s
    p.process(b"\x1b[s");
    
    // Move to a different position
    p.process(b"\x1b[1;1H");
    let (r, c) = p.screen().cursor_position();
    println!("After CUP(1,1): row={}, col={}", r, c);
    assert_eq!((r, c), (0, 0), "CUP to origin failed");
    
    // Restore cursor with CSI u
    p.process(b"\x1b[u");
    let (r, c) = p.screen().cursor_position();
    println!("After CSI u restore: row={}, col={}", r, c);
    
    if (r, c) == (5, 10) {
        println!("TEST1_PASS: CSI s/u cursor save/restore works correctly");
    } else {
        println!("TEST1_FAIL: CSI u restored to ({}, {}), expected (5, 10)", r, c);
    }
    
    // Test 2: CSI f (HVP - same as CUP)
    p.process(b"\x1b[10;20f");
    let (r, c) = p.screen().cursor_position();
    println!("After HVP(10,20): row={}, col={}", r, c);
    if (r, c) == (9, 19) {
        println!("TEST2_PASS: CSI f (HVP) works correctly");
    } else {
        println!("TEST2_FAIL: HVP set cursor to ({}, {}), expected (9, 19)", r, c);
    }
    
    // Test 3: hide_cursor flag
    p.process(b"\x1b[?25l");  // Hide cursor
    let hidden = p.screen().hide_cursor();
    println!("After CSI ?25l: hide_cursor={}", hidden);
    if hidden {
        println!("TEST3a_PASS: hide_cursor correctly set");
    } else {
        println!("TEST3a_FAIL: hide_cursor should be true");
    }
    
    p.process(b"\x1b[?25h");  // Show cursor
    let hidden = p.screen().hide_cursor();
    println!("After CSI ?25h: hide_cursor={}", hidden);
    if !hidden {
        println!("TEST3b_PASS: show_cursor correctly set");
    } else {
        println!("TEST3b_FAIL: hide_cursor should be false");
    }
    
    // Test 4: Simulate Claude-like TUI rendering pattern
    // Claude: hide cursor -> render -> position cursor at input -> show cursor
    p.process(b"\x1b[?25l");          // Hide cursor
    p.process(b"\x1b[1;1H");          // Move to top for rendering
    p.process(b"\x1b[2J");            // Clear screen
    // Simulate rendering header
    p.process(b"\x1b[1;1H");
    p.process(b"Claude Code CLI");
    // Simulate rendering content area
    p.process(b"\x1b[5;1H");
    p.process(b"Some conversation text...");
    // Position cursor at input box (row 20, col 3)
    p.process(b"\x1b[20;3H");
    p.process(b"\x1b[?25h");          // Show cursor
    
    let (r, c) = p.screen().cursor_position();
    let hidden = p.screen().hide_cursor();
    println!("After TUI render: cursor=({}, {}), hidden={}", r, c, hidden);
    if (r, c) == (19, 2) && !hidden {
        println!("TEST4_PASS: TUI render cursor position correct");
    } else {
        println!("TEST4_FAIL: cursor=({}, {}), hidden={}, expected (19, 2, false)", r, c, hidden);
    }
    
    // Test 5: CSI s/u in TUI pattern (Ink-style on Windows)
    // Ink on Windows uses CSI s/u for cursor save/restore
    p.process(b"\x1b[?25l");          // Hide cursor
    p.process(b"\x1b[s");             // Save cursor position (at input box)
    p.process(b"\x1b[1;1H");          // Move to top for status update
    p.process(b"[Updated status]");
    p.process(b"\x1b[u");             // Restore cursor (should go back to input box)
    p.process(b"\x1b[?25h");          // Show cursor
    
    let (r, c) = p.screen().cursor_position();
    let hidden = p.screen().hide_cursor();
    println!("After Ink-style s/u: cursor=({}, {}), hidden={}", r, c, hidden);
    if (r, c) == (19, 2) && !hidden {
        println!("TEST5_PASS: Ink-style CSI s/u works correctly");
    } else {
        println!("TEST5_FAIL: cursor=({}, {}), hidden={}, expected (19, 2, false)", r, c, hidden);
    }
    
    println!("\n=== All tests complete ===");
}
'@

# Write the test as a Rust example
$testDir = Join-Path $PSScriptRoot ".." "examples"
$testFile = Join-Path $testDir "test_cursor_issue52.rs"
Set-Content -Path $testFile -Value $testCode -Encoding UTF8

Log "  Running vt100 parser cursor tests..."
$output = cargo run --example test_cursor_issue52 --release 2>&1 | Out-String
Log $output

# Parse results
if ($output -match "TEST1_PASS") { Pass "CSI s/u save/restore" } else { Fail "CSI s/u save/restore" "CSI s/u not handled by vt100 parser" }
if ($output -match "TEST2_PASS") { Pass "CSI f (HVP)" } else { Fail "CSI f (HVP)" "HVP not handled by vt100 parser" }
if ($output -match "TEST3a_PASS") { Pass "hide_cursor set" } else { Fail "hide_cursor set" "hide_cursor flag not working" }
if ($output -match "TEST3b_PASS") { Pass "show_cursor set" } else { Fail "show_cursor set" "show_cursor flag not working" }
if ($output -match "TEST4_PASS") { Pass "TUI render cursor" } else { Fail "TUI render cursor" "TUI render cursor position wrong" }
if ($output -match "TEST5_PASS") { Pass "Ink-style CSI s/u" } else { Fail "Ink-style CSI s/u" "Ink-style save/restore dropped — ROOT CAUSE of issue #52" }

# ─── Summary ─────────────────────────────────────────────────────────────────
Log ""
Log "═══════════════════════════════════════"
Log "  Results: $($script:pass) passed, $($script:fail) failed"
Log "═══════════════════════════════════════"
foreach ($r in $script:results) { Log "  $r" }
Log ""

# Cleanup
psmux kill-server 2>$null
Remove-Item $testFile -ErrorAction SilentlyContinue

if ($script:fail -gt 0) {
    Log "SOME TESTS FAILED — Issue #52 is reproducible"
    exit 1
} else {
    Log "ALL TESTS PASSED"
    exit 0
}