Module renamed github.com/harrybrwn/patterm → github.com/hjbdev/patterm across imports. Chrome: - Palette redrawn with rounded box-drawing borders, accent left-bar for the selected item, dim hints, and a separator-aware footer. - Tab bar grew from 1 row to 3: labels with breathing room, a dim argv subtitle truncated to each tab's width, and an accent thick underline for the focused tab with a faint divider extending across the rest of the host width. Layout, viewport-renderer, and screen- renderer tests updated for the new mainTop. - Sidebar reuses the same palette: accent section headers, `▎` selection marker, `●`/`○` status glyphs, dim previews. - Shared SGR constants moved into internal/app/style.go. Palette input: - Adjacent duplicate arrow events (legacy `\x1b[B` + kitty `\x1b[57353u` for one keypress, or two of the same form) are now collapsed via peekArrowEvent + chunk-level dedupe in processStdin. - On open, push `\x1b[>0u` onto the host's kitty keyboard stack so palette input is in plain legacy mode regardless of what the child pushed (codex/ratatui pushes its own flags which had been leaking to the host). Popped on close. Tab-switch repaint (repaintFocused): - Use the emulator's SerializeVT bytes (with SGR / cursor / DECSTBM / tabstops) instead of plain text, fed through the per-focused viewport renderer so the shifter translates row positions. - Prelude resets host SGR / DECOM / DECSTBM (pinned to viewport) / cursor visibility before the replay, so leftover modes from the previously-focused child don't distort the new snapshot. - Re-emit the saved cursor as a child-space CUP after the serialized bytes so the host cursor lands at the emulator's actual position (overriding DECSTBM's home side-effect and the tabstop-setup CHA sequences) AND the renderer's vr.row/vr.col get re-synced via trackCSI. - cursorShifter now carries childRows and rewrites empty `\x1b[r` to `\x1b[<mainTop>;<mainBottom>r` (host coords) — the default (1,1) shifted to (4,4) was producing a one-row scrolling region that scroll-exploded the replay. - After the snapshot lands, nudge the focused child with a one-row PTY winsize toggle so the kernel emits SIGWINCH and ratatui-style TUIs throw away their diff state and emit a fresh frame. Codex still renders incorrectly after a focus switch; see TODO.md "Switch-back render divergence" for the deep investigation handoff.
90 lines
2.3 KiB
Go
90 lines
2.3 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
)
|
|
|
|
func TestCursorShifterCUP(t *testing.T) {
|
|
cs := newCursorShifter(1, 36)
|
|
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)
|
|
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)
|
|
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)
|
|
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) // 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)
|
|
// 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)
|
|
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)
|
|
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)
|
|
// 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)
|
|
}
|
|
}
|