Files
patterm/internal/app/viewport_renderer_test.go
Harry Bayliss 39a042bda8 Polish chrome and rework tab-switch repaint
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.
2026-05-14 16:02:40 +01:00

66 lines
2.0 KiB
Go

package app
import (
"strings"
"testing"
)
func TestViewportRendererShiftsCursor(t *testing.T) {
vr := newViewportRenderer(newTerminalLayout(120, 40))
got := string(vr.Render([]byte("\x1b[H")))
if got != "\x1b[4;1H" {
t.Fatalf("CUP home: got %q", got)
}
}
func TestViewportRendererSwallowsAltScreenToggles(t *testing.T) {
vr := newViewportRenderer(newTerminalLayout(120, 40))
got := string(vr.Render([]byte("a\x1b[?1049hb\x1b[?1049lc")))
if got != "abc" {
t.Fatalf("alt-screen toggles: got %q", got)
}
}
func TestViewportRendererClearScreenIsViewportOnly(t *testing.T) {
// hostRows=7 leaves three viewport rows after the 3-row tab bar and
// 1-row status reservation.
vr := newViewportRenderer(newTerminalLayout(20, 7))
got := string(vr.Render([]byte("\x1b[2J")))
if strings.Contains(got, "\x1b[2J") {
t.Fatalf("host clear-screen leaked through: %q", got)
}
if strings.Count(got, " ") != 3 {
t.Fatalf("clear rows: got %q", got)
}
if !strings.Contains(got, "\x1b[4;1H") || !strings.Contains(got, "\x1b[6;1H") {
t.Fatalf("clear did not target viewport rows: %q", got)
}
}
func TestViewportRendererClearLineUsesEraseChars(t *testing.T) {
vr := newViewportRenderer(newTerminalLayout(20, 5))
got := string(vr.Render([]byte("\x1b[K")))
if strings.Contains(got, "\x1b[K") {
t.Fatalf("host clear-line leaked through: %q", got)
}
if got != "\x1b[20X" {
t.Fatalf("clear-line: got %q want ECH", got)
}
}
func TestViewportRendererClearLineStopsAtViewportRight(t *testing.T) {
vr := newViewportRenderer(newTerminalLayout(20, 5))
got := string(vr.Render([]byte("\x1b[10G\x1b[K")))
if !strings.HasSuffix(got, "\x1b[11X") {
t.Fatalf("clear-line from col 10 should erase 11 cells: %q", got)
}
}
func TestViewportRendererTracksPrintableCursor(t *testing.T) {
vr := newViewportRenderer(newTerminalLayout(20, 5))
got := string(vr.Render([]byte("hello\x1b[K")))
if !strings.HasSuffix(got, "\x1b[15X") {
t.Fatalf("clear-line after five chars should erase 15 cells: %q", got)
}
}