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.
87 lines
1.9 KiB
Go
87 lines
1.9 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hjbdev/patterm/internal/vt"
|
|
)
|
|
|
|
func renderScreenSnapshot(text string, cursor vt.CursorState, layout terminalLayout) []byte {
|
|
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
|
|
cols := int(layout.childCols())
|
|
rows := int(layout.childRows())
|
|
|
|
var b strings.Builder
|
|
b.WriteString("\x1b[?25l")
|
|
for r := 0; r < rows; r++ {
|
|
line := ""
|
|
if r < len(lines) {
|
|
line = truncateCells(lines[r], cols)
|
|
}
|
|
fmt.Fprintf(&b, "\x1b[%d;%dH%s", int(layout.mainTop)+r, int(layout.mainLeft), padRight(line, cols))
|
|
}
|
|
|
|
if cursor.Visible {
|
|
row := int(layout.mainTop) + int(cursor.Row)
|
|
col := int(layout.mainLeft) + int(cursor.Col)
|
|
if row < int(layout.mainTop) {
|
|
row = int(layout.mainTop)
|
|
}
|
|
maxRow := int(layout.mainTop) + rows - 1
|
|
if row > maxRow {
|
|
row = maxRow
|
|
}
|
|
if col < int(layout.mainLeft) {
|
|
col = int(layout.mainLeft)
|
|
}
|
|
maxCol := int(layout.mainLeft) + cols - 1
|
|
if col > maxCol {
|
|
col = maxCol
|
|
}
|
|
fmt.Fprintf(&b, "\x1b[?25h\x1b[%d;%dH", row, col)
|
|
}
|
|
return []byte(b.String())
|
|
}
|
|
|
|
func renderCursor(cursor vt.CursorState, layout terminalLayout) []byte {
|
|
cols := int(layout.childCols())
|
|
rows := int(layout.childRows())
|
|
row := int(layout.mainTop) + int(cursor.Row)
|
|
col := int(layout.mainLeft) + int(cursor.Col)
|
|
if row < int(layout.mainTop) {
|
|
row = int(layout.mainTop)
|
|
}
|
|
maxRow := int(layout.mainTop) + rows - 1
|
|
if row > maxRow {
|
|
row = maxRow
|
|
}
|
|
if col < int(layout.mainLeft) {
|
|
col = int(layout.mainLeft)
|
|
}
|
|
maxCol := int(layout.mainLeft) + cols - 1
|
|
if col > maxCol {
|
|
col = maxCol
|
|
}
|
|
return []byte(fmt.Sprintf("\x1b[?25h\x1b[%d;%dH", row, col))
|
|
}
|
|
|
|
func truncateCells(s string, width int) string {
|
|
if width <= 0 {
|
|
return ""
|
|
}
|
|
if visibleLen(s) <= width {
|
|
return s
|
|
}
|
|
var b strings.Builder
|
|
n := 0
|
|
for _, r := range s {
|
|
if n >= width {
|
|
break
|
|
}
|
|
b.WriteRune(r)
|
|
n++
|
|
}
|
|
return b.String()
|
|
}
|