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.
This commit is contained in:
@@ -4,13 +4,18 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const tabBarRows = 1
|
||||
// Three-row tab bar: labels row, subtitle row, underline row. The PTY
|
||||
// viewport's top row is therefore mainTop == tabBarRows + 1.
|
||||
const tabBarRows = 3
|
||||
|
||||
// drawTabBar renders SPEC §4's top tab bar at row 1. Tabs are top-level
|
||||
// children (ParentID == ""); the focused tab is highlighted. The PTY
|
||||
// region begins at row 2.
|
||||
// drawTabBar renders the top tab strip across the full host width. The
|
||||
// strip has three rows: labels (with horizontal padding), a dim
|
||||
// subtitle showing each child's argv, and an underline that's thick +
|
||||
// accent for the focused tab and faint for the rest. Subtitles are
|
||||
// truncated with `…` to the tab's width.
|
||||
func (st *uiState) drawTabBar() {
|
||||
st.mu.Lock()
|
||||
palOpen := st.palette != nil
|
||||
@@ -21,6 +26,9 @@ func (st *uiState) drawTabBar() {
|
||||
}
|
||||
layout := st.layoutSnapshot()
|
||||
width := int(layout.childCols())
|
||||
if width < 8 {
|
||||
return
|
||||
}
|
||||
|
||||
var sessions []*Child
|
||||
for _, c := range st.sess.Children() {
|
||||
@@ -29,42 +37,124 @@ func (st *uiState) drawTabBar() {
|
||||
}
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("\x1b[1;1H")
|
||||
cur := 0
|
||||
type tabRect struct {
|
||||
startCol int
|
||||
width int
|
||||
label string
|
||||
subtitle string
|
||||
active bool
|
||||
}
|
||||
|
||||
const (
|
||||
leadingPad = 2 // host columns before the first tab
|
||||
tabPad = 2 // spaces on each side of the label inside the tab
|
||||
tabGap = 1 // gap columns between adjacent tabs
|
||||
tailReserve = 8 // reserve room for the trailing "+ new" hint
|
||||
)
|
||||
|
||||
tabs := make([]tabRect, 0, len(sessions))
|
||||
cur := leadingPad + 1
|
||||
for _, c := range sessions {
|
||||
label := c.Name
|
||||
seg := " " + label + " "
|
||||
if cur+len(seg) > width-2 {
|
||||
labelW := utf8.RuneCountInString(label)
|
||||
tabW := labelW + tabPad*2
|
||||
|
||||
// If the tab won't fit, try truncating the label down to whatever
|
||||
// space is left (label still has to leave room for "…").
|
||||
if cur+tabW+tabGap+tailReserve > width+1 {
|
||||
avail := width + 1 - cur - tabGap - tailReserve - tabPad*2
|
||||
if avail < 3 {
|
||||
break
|
||||
}
|
||||
label = clipRunes(label, avail-1) + "…"
|
||||
labelW = utf8.RuneCountInString(label)
|
||||
tabW = labelW + tabPad*2
|
||||
tabs = append(tabs, tabRect{
|
||||
startCol: cur, width: tabW,
|
||||
label: label, subtitle: strings.Join(c.Argv, " "),
|
||||
active: c.ID == focus,
|
||||
})
|
||||
cur += tabW + tabGap
|
||||
break
|
||||
}
|
||||
if c.ID == focus {
|
||||
b.WriteString("\x1b[7m")
|
||||
|
||||
tabs = append(tabs, tabRect{
|
||||
startCol: cur, width: tabW,
|
||||
label: label, subtitle: strings.Join(c.Argv, " "),
|
||||
active: c.ID == focus,
|
||||
})
|
||||
cur += tabW + tabGap
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
// Clear all three rows up front so a stale label from the previous
|
||||
// frame can't bleed through.
|
||||
b.WriteString("\x1b[1;1H\x1b[2K")
|
||||
b.WriteString("\x1b[2;1H\x1b[2K")
|
||||
b.WriteString("\x1b[3;1H\x1b[2K")
|
||||
|
||||
for _, t := range tabs {
|
||||
// Row 1: label
|
||||
fmt.Fprintf(&b, "\x1b[1;%dH", t.startCol)
|
||||
if t.active {
|
||||
b.WriteString(styleActive)
|
||||
} else {
|
||||
b.WriteString("\x1b[2m")
|
||||
b.WriteString(styleHint)
|
||||
}
|
||||
b.WriteString(seg)
|
||||
b.WriteString("\x1b[0m")
|
||||
cur += len(seg)
|
||||
b.WriteString(strings.Repeat(" ", tabPad))
|
||||
b.WriteString(t.label)
|
||||
b.WriteString(strings.Repeat(" ", tabPad))
|
||||
b.WriteString(styleReset)
|
||||
|
||||
// Row 2: subtitle, truncated to tab width and dimmed.
|
||||
sub := t.subtitle
|
||||
if utf8.RuneCountInString(sub) > t.width {
|
||||
if t.width > 1 {
|
||||
sub = clipRunes(sub, t.width-1) + "…"
|
||||
} else {
|
||||
sub = ""
|
||||
}
|
||||
}
|
||||
padR := t.width - utf8.RuneCountInString(sub)
|
||||
if padR < 0 {
|
||||
padR = 0
|
||||
}
|
||||
fmt.Fprintf(&b, "\x1b[2;%dH%s%s%s%s",
|
||||
t.startCol, styleDim, sub, strings.Repeat(" ", padR), styleReset)
|
||||
|
||||
// Row 3: underline. Thick accent for the active tab, faint
|
||||
// border for the rest.
|
||||
fmt.Fprintf(&b, "\x1b[3;%dH", t.startCol)
|
||||
if t.active {
|
||||
b.WriteString(styleAccent)
|
||||
b.WriteString(strings.Repeat("━", t.width))
|
||||
} else {
|
||||
b.WriteString(styleBorder)
|
||||
b.WriteString(strings.Repeat("─", t.width))
|
||||
}
|
||||
b.WriteString(styleReset)
|
||||
}
|
||||
// "+" hint at end.
|
||||
hint := "+"
|
||||
if cur > 0 {
|
||||
hint = " +"
|
||||
|
||||
// "+ new" hint at the end of the labels row, in dim.
|
||||
if cur+3 <= width {
|
||||
fmt.Fprintf(&b, "\x1b[1;%dH%s+ new%s", cur+1, styleDim, styleReset)
|
||||
}
|
||||
if cur+len(hint) <= width {
|
||||
b.WriteString("\x1b[2m")
|
||||
b.WriteString(hint)
|
||||
b.WriteString("\x1b[0m")
|
||||
cur += len(hint)
|
||||
|
||||
// Extend the faint underline across the rest of the host width so
|
||||
// the tab strip reads as one continuous divider.
|
||||
if cur <= width {
|
||||
remain := width - cur + 1
|
||||
if remain > 0 {
|
||||
fmt.Fprintf(&b, "\x1b[3;%dH%s%s%s",
|
||||
cur, styleBorder, strings.Repeat("─", remain), styleReset)
|
||||
}
|
||||
}
|
||||
// Fill the rest of the tab-bar row so stale chars don't linger.
|
||||
if width-cur > 0 {
|
||||
b.WriteString(strings.Repeat(" ", width-cur))
|
||||
if leadingPad > 0 {
|
||||
fmt.Fprintf(&b, "\x1b[3;1H%s%s%s",
|
||||
styleBorder, strings.Repeat("─", leadingPad), styleReset)
|
||||
}
|
||||
|
||||
st.outMu.Lock()
|
||||
defer st.outMu.Unlock()
|
||||
// Save cursor, paint, restore.
|
||||
fmt.Fprintf(os.Stdout, "\x1b7%s\x1b8", b.String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user