151 lines
4.3 KiB
Go
151 lines
4.3 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
// In longer claude sessions the cursor's internal row state could drift
|
|
// past the viewport height. CUP / HVP / VPA without row clamping would
|
|
// then land the host cursor on the status row or above the tab bar,
|
|
// where the next printable wipes the chrome.
|
|
func TestCursorShifterClampsCUPRowToMainBottom(t *testing.T) {
|
|
// rowOffset=2 (mainTop=3), childRows=36 → mainBottom=38.
|
|
cs := newCursorShifter(2, 36, 80)
|
|
got := cs.Shift([]byte("\x1b[40;5H"))
|
|
if string(got) != "\x1b[38;5H" {
|
|
t.Fatalf("CUP row 40 (post-shift 42) should clamp to 38: got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCursorShifterClampsHVPRowToMainBottom(t *testing.T) {
|
|
cs := newCursorShifter(2, 36, 80)
|
|
got := cs.Shift([]byte("\x1b[99;1f"))
|
|
if string(got) != "\x1b[38;1f" {
|
|
t.Fatalf("HVP row 99 should clamp to mainBottom: got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCursorShifterClampsVPARow(t *testing.T) {
|
|
cs := newCursorShifter(2, 36, 80)
|
|
got := cs.Shift([]byte("\x1b[60d"))
|
|
if string(got) != "\x1b[38d" {
|
|
t.Fatalf("VPA row 60 should clamp to mainBottom: got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCursorShifterCUPRowNoClampWhenChildRowsZero(t *testing.T) {
|
|
cs := newCursorShifter(2, 0, 80)
|
|
got := cs.Shift([]byte("\x1b[40;5H"))
|
|
if string(got) != "\x1b[42;5H" {
|
|
t.Fatalf("childRows=0 should disable row clamping: got %q", got)
|
|
}
|
|
}
|