package app import ( "bytes" "testing" ) func TestCursorShifterCUP(t *testing.T) { cs := newCursorShifter(1, 36, 80) got := cs.Shift([]byte("\x1b[H")) want := []byte("\x1b[2;1H") if !bytes.Equal(got, want) { t.Fatalf("CUP home: got %q want %q", got, want) } } func TestCursorShifterCUPRowCol(t *testing.T) { cs := newCursorShifter(1, 36, 80) got := cs.Shift([]byte("\x1b[10;5H")) if string(got) != "\x1b[11;5H" { t.Fatalf("CUP 10;5: got %q", got) } } func TestCursorShifterVPA(t *testing.T) { cs := newCursorShifter(1, 36, 80) got := cs.Shift([]byte("\x1b[7d")) if string(got) != "\x1b[8d" { t.Fatalf("VPA 7: got %q", got) } } func TestCursorShifterDECSTBM(t *testing.T) { cs := newCursorShifter(1, 36, 80) got := cs.Shift([]byte("\x1b[2;20r")) if string(got) != "\x1b[3;21r" { t.Fatalf("DECSTBM: got %q", got) } } // Empty DECSTBM (\x1b[r) is a reset request; without a viewport-aware // fix it would default to (1,1) and shift to a one-row scrolling // region — that's what was scrolling claude's content up after a // focus switch from codex. func TestCursorShifterDECSTBMEmptyResetsToViewport(t *testing.T) { cs := newCursorShifter(3, 36, 80) // mainTop=4, childRows=36 got := cs.Shift([]byte("\x1b[r")) if string(got) != "\x1b[4;39r" { t.Fatalf("empty DECSTBM reset: got %q want \\x1b[4;39r", got) } } func TestCursorShifterPrivateCSIPassthrough(t *testing.T) { cs := newCursorShifter(1, 36, 80) // Alt-screen toggle — private CSI. got := cs.Shift([]byte("\x1b[?1049h")) if string(got) != "\x1b[?1049h" { t.Fatalf("alt-screen: got %q", got) } } func TestCursorShifterSGRPassthrough(t *testing.T) { cs := newCursorShifter(1, 36, 80) got := cs.Shift([]byte("\x1b[1;31mhello\x1b[0m")) if string(got) != "\x1b[1;31mhello\x1b[0m" { t.Fatalf("SGR: got %q", got) } } func TestCursorShifterStraddleChunks(t *testing.T) { cs := newCursorShifter(1, 36, 80) a := cs.Shift([]byte("\x1b[")) b := cs.Shift([]byte("5;3H")) got := string(a) + string(b) if got != "\x1b[6;3H" { t.Fatalf("straddle: got %q", got) } } func TestCursorShifterOSCNotRewritten(t *testing.T) { cs := newCursorShifter(1, 36, 80) // OSC body containing what looks like a CSI cursor move — should // NOT be rewritten. in := []byte("\x1b]0;\x1b[5;3Htitle\x07") got := cs.Shift(in) if string(got) != string(in) { t.Fatalf("OSC: got %q want %q", got, in) } } func TestCursorShifterClampsCUPColumn(t *testing.T) { cs := newCursorShifter(1, 36, 80) got := cs.Shift([]byte("\x1b[5;120H")) if string(got) != "\x1b[6;80H" { t.Fatalf("CUP col 120 should clamp to childCols=80: got %q", got) } } func TestCursorShifterClampsCHAColumn(t *testing.T) { cs := newCursorShifter(1, 36, 80) got := cs.Shift([]byte("\x1b[120G")) if string(got) != "\x1b[80G" { t.Fatalf("CHA col 120 should clamp to childCols=80: got %q", got) } } func TestCursorShifterCUPNoClampWhenChildColsZero(t *testing.T) { cs := newCursorShifter(1, 36, 0) got := cs.Shift([]byte("\x1b[5;120H")) if string(got) != "\x1b[6;120H" { t.Fatalf("childCols=0 should disable col clamping: got %q", got) } }