Fix styled switch-back repaint
This commit is contained in:
@@ -215,7 +215,9 @@ type uiState struct {
|
||||
// renderer confines focused-child live output to the main viewport.
|
||||
// A fresh renderer is allocated per focused child so partial-escape
|
||||
// state cannot bleed between panes.
|
||||
renderer *viewportRenderer
|
||||
renderer *viewportRenderer
|
||||
repaintNextPTY string
|
||||
repaintNextPTYBudget int
|
||||
|
||||
// attention is the latest request_human_attention surfaced via MCP;
|
||||
// rendered in the status line until cleared.
|
||||
@@ -350,15 +352,33 @@ func (st *uiState) OnChildExited(c *Child) {
|
||||
// disabled only around the replay so long styled runs cannot wrap into
|
||||
// the right rail.
|
||||
func (st *uiState) OnPTYOut(childID string, chunk []byte) {
|
||||
layout := st.layoutSnapshot()
|
||||
st.mu.Lock()
|
||||
focus := st.focusedID
|
||||
palOpen := st.palette != nil
|
||||
renderer := st.renderer
|
||||
forceRepaint := focus == childID && st.repaintNextPTY == childID && st.repaintNextPTYBudget > 0
|
||||
if forceRepaint {
|
||||
renderer = newViewportRenderer(layout)
|
||||
st.renderer = renderer
|
||||
st.repaintNextPTYBudget--
|
||||
if st.repaintNextPTYBudget == 0 {
|
||||
st.repaintNextPTY = ""
|
||||
}
|
||||
}
|
||||
st.mu.Unlock()
|
||||
if palOpen || focus != childID || renderer == nil {
|
||||
return
|
||||
}
|
||||
out := renderer.Render(chunk)
|
||||
var out []byte
|
||||
if forceRepaint {
|
||||
out = st.renderFocusedSnapshot(childID, renderer, layout)
|
||||
if len(out) == 0 {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
out = renderer.Render(chunk)
|
||||
}
|
||||
st.outMu.Lock()
|
||||
_, _ = os.Stdout.Write([]byte("\x1b[?7l"))
|
||||
_, _ = os.Stdout.Write(out)
|
||||
@@ -798,11 +818,11 @@ func (st *uiState) flashTransient(msg string) {
|
||||
// Callers must NOT hold st.mu — repaintFocused takes it
|
||||
// briefly itself.
|
||||
//
|
||||
// We feed the emulator's VT serialization through the viewport
|
||||
// renderer so SGR styling, alt-screen state, and the cursor position
|
||||
// survive a focus switch. The plain-text path (renderScreenSnapshot)
|
||||
// is kept as a fallback for environments where SerializeVT is
|
||||
// unavailable (e.g. the nocgo stub).
|
||||
// We replay the emulator's padded grid snapshot rather than its VT
|
||||
// serialization. SerializeVT can preserve style, but for diff-based TUIs
|
||||
// we've seen it replay stale prompt layout that no longer matches the
|
||||
// emulator grid; the padded snapshot is the source of truth for visible
|
||||
// cells.
|
||||
func (st *uiState) repaintFocused() {
|
||||
st.mu.Lock()
|
||||
id := st.focusedID
|
||||
@@ -824,16 +844,28 @@ func (st *uiState) repaintFocused() {
|
||||
defer c.NudgeRedraw(cols, rows)
|
||||
}
|
||||
|
||||
out := st.renderFocusedSnapshot(id, renderer, layout)
|
||||
if len(out) == 0 {
|
||||
return
|
||||
}
|
||||
st.mu.Lock()
|
||||
if st.focusedID == id {
|
||||
st.repaintNextPTY = id
|
||||
st.repaintNextPTYBudget = 8
|
||||
}
|
||||
st.mu.Unlock()
|
||||
st.outMu.Lock()
|
||||
defer st.outMu.Unlock()
|
||||
_, _ = os.Stdout.Write(out)
|
||||
}
|
||||
|
||||
func (st *uiState) renderFocusedSnapshot(id string, renderer *viewportRenderer, layout terminalLayout) []byte {
|
||||
text, cursor, err := st.sess.SnapshotChild(id)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if renderer != nil {
|
||||
if serialized, err := st.sess.SerializeChild(id); err == nil && len(serialized) > 0 {
|
||||
// Reset host terminal state before replaying so leftover
|
||||
// modes from the previously-focused child (DECSTBM,
|
||||
// DECOM, SGR) don't distort the snapshot. The DECSTBM is
|
||||
// pinned to the viewport region in host coordinates; the
|
||||
// cursor parks at the viewport's top-left. The replayed
|
||||
// SerializeVT may re-set these modes if the child
|
||||
// configured them, which is fine — we're just guaranteeing
|
||||
// a known starting baseline.
|
||||
if styled, err := st.sess.StyledSnapshotChild(id); err == nil && len(styled) > 0 {
|
||||
mainBottom := int(layout.statusRow) - statusRows
|
||||
prelude := fmt.Sprintf(
|
||||
"\x1b[0m\x1b[?6l\x1b[%d;%dr\x1b[?25h\x1b[%d;%dH",
|
||||
@@ -841,40 +873,19 @@ func (st *uiState) repaintFocused() {
|
||||
int(layout.mainTop), int(layout.mainLeft),
|
||||
)
|
||||
out := []byte(prelude)
|
||||
out = append(out, renderer.Render(serialized)...)
|
||||
// Ghostty's VT serialization emits the cursor CUP, then
|
||||
// DECSTBM (which moves the cursor to region home as a
|
||||
// documented side effect), then tab-stop setup using CHA
|
||||
// (\x1b[NG) — which leaves the renderer's internal vr.col
|
||||
// tracking pointing at the last tab-stop column, not
|
||||
// where the cursor actually ended up. Re-emit the saved
|
||||
// cursor as a child-space CUP through the renderer so
|
||||
// (a) the host cursor lands at the right place and (b)
|
||||
// the renderer's internal row/col tracking is brought
|
||||
// back in sync with the host. Without this, subsequent
|
||||
// relative moves (CSI C/D) and erase-line widths (CSI K
|
||||
// uses vr.col) operate from a stale column and the input
|
||||
// box gets drawn at the wrong width / row.
|
||||
if _, cursor, err := st.sess.SnapshotChild(id); err == nil {
|
||||
cup := fmt.Sprintf("\x1b[%d;%dH",
|
||||
int(cursor.Row)+1, int(cursor.Col)+1)
|
||||
out = append(out, renderer.Render([]byte(cup))...)
|
||||
}
|
||||
st.outMu.Lock()
|
||||
defer st.outMu.Unlock()
|
||||
_, _ = os.Stdout.Write(out)
|
||||
return
|
||||
out = append(out, renderer.ClearViewport()...)
|
||||
out = append(out, renderer.Render(styled)...)
|
||||
cup := fmt.Sprintf("\x1b[%d;%dH", int(cursor.Row)+1, int(cursor.Col)+1)
|
||||
out = append(out, renderer.Render([]byte(cup))...)
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
text, cursor, err := st.sess.SnapshotChild(id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out := renderScreenSnapshot(text, cursor, layout)
|
||||
st.outMu.Lock()
|
||||
defer st.outMu.Unlock()
|
||||
_, _ = os.Stdout.Write(out)
|
||||
if renderer != nil {
|
||||
cup := fmt.Sprintf("\x1b[%d;%dH", int(cursor.Row)+1, int(cursor.Col)+1)
|
||||
out = append(out, renderer.Render([]byte(cup))...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (st *uiState) requestExit() {
|
||||
|
||||
@@ -379,9 +379,10 @@ func (c *Child) signal(sig syscall.Signal) error {
|
||||
// ratatui/ink TUIs re-render coherently against the snapshot we just
|
||||
// replayed. We toggle the PTY size by one row so the kernel reliably
|
||||
// emits SIGWINCH (TIOCSWINSZ skips the signal if the size didn't
|
||||
// change). The emulator is left alone — it already matches our intended
|
||||
// size and the brief mismatch only affects what the child writes during
|
||||
// the second redraw.
|
||||
// change), then send SIGWINCH explicitly for TUIs that miss or coalesce
|
||||
// the size-toggled signal. The emulator is left alone — it already
|
||||
// matches our intended size and the brief mismatch only affects what the
|
||||
// child writes during the second redraw.
|
||||
func (c *Child) NudgeRedraw(cols, rows uint16) {
|
||||
pty := c.PTY()
|
||||
if pty == nil || rows < 2 {
|
||||
@@ -389,6 +390,7 @@ func (c *Child) NudgeRedraw(cols, rows uint16) {
|
||||
}
|
||||
_ = pty.Resize(cols, rows-1)
|
||||
_ = pty.Resize(cols, rows)
|
||||
_ = c.signal(syscall.SIGWINCH)
|
||||
}
|
||||
|
||||
func (c *Child) markExited(err error) {
|
||||
|
||||
@@ -428,6 +428,18 @@ func (s *Session) SerializeChild(id string) ([]byte, error) {
|
||||
return em.SerializeVT()
|
||||
}
|
||||
|
||||
func (s *Session) StyledSnapshotChild(id string) ([]byte, error) {
|
||||
c := s.FindChild(id)
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("no such child %q", id)
|
||||
}
|
||||
em := c.Emulator()
|
||||
if em == nil {
|
||||
return nil, fmt.Errorf("child %q has no emulator", id)
|
||||
}
|
||||
return em.StyledScreenVT()
|
||||
}
|
||||
|
||||
func (s *Session) SnapshotChild(id string) (string, vt.CursorState, error) {
|
||||
c := s.FindChild(id)
|
||||
if c == nil {
|
||||
|
||||
@@ -61,6 +61,12 @@ func (vr *viewportRenderer) Render(in []byte) []byte {
|
||||
return []byte(vr.pending.String())
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) ClearViewport() []byte {
|
||||
vr.mu.Lock()
|
||||
defer vr.mu.Unlock()
|
||||
return []byte(vr.clearViewport())
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) feed(b byte) {
|
||||
switch vr.state {
|
||||
case vpNormal:
|
||||
@@ -191,7 +197,7 @@ func (vr *viewportRenderer) clearViewport() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("\x1b7")
|
||||
for r := uint16(0); r < vr.layout.childRows(); r++ {
|
||||
fmt.Fprintf(&b, "\x1b[%d;%dH%s", int(vr.layout.mainTop+r), int(vr.layout.mainLeft), strings.Repeat(" ", int(vr.layout.childCols())))
|
||||
fmt.Fprintf(&b, "\x1b[%d;%dH\x1b[%dX", int(vr.layout.mainTop+r), int(vr.layout.mainLeft), int(vr.layout.childCols()))
|
||||
}
|
||||
b.WriteString("\x1b8")
|
||||
return b.String()
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestViewportRendererClearScreenIsViewportOnly(t *testing.T) {
|
||||
if strings.Contains(got, "\x1b[2J") {
|
||||
t.Fatalf("host clear-screen leaked through: %q", got)
|
||||
}
|
||||
if strings.Count(got, " ") != 3 {
|
||||
if strings.Count(got, "\x1b[20X") != 3 {
|
||||
t.Fatalf("clear rows: got %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "\x1b[4;1H") || !strings.Contains(got, "\x1b[6;1H") {
|
||||
|
||||
@@ -3,6 +3,7 @@ package harness
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -74,6 +75,43 @@ func runStep(s *Session, step Step, results map[string]json.RawMessage) error {
|
||||
return fmt.Errorf("screen does not contain %q:\n%s", step.Contains, screen)
|
||||
}
|
||||
return nil
|
||||
case "assert_not_contains":
|
||||
screen, err := s.Screen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(screen, step.Contains) {
|
||||
return fmt.Errorf("screen contains %q:\n%s", step.Contains, screen)
|
||||
}
|
||||
return nil
|
||||
case "mark_raw":
|
||||
if step.SaveAs == "" {
|
||||
return fmt.Errorf("mark_raw requires save_as")
|
||||
}
|
||||
raw, err := json.Marshal(s.RawOffset())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
results[step.SaveAs] = raw
|
||||
return nil
|
||||
case "assert_raw_since_regex":
|
||||
raw, ok := results[step.From]
|
||||
if !ok {
|
||||
return fmt.Errorf("no saved result %q", step.From)
|
||||
}
|
||||
var offset int
|
||||
if err := json.Unmarshal(raw, &offset); err != nil {
|
||||
return fmt.Errorf("saved result %q is not a raw offset: %w", step.From, err)
|
||||
}
|
||||
re, err := regexp.Compile(step.Regex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := s.RawSince(offset)
|
||||
if !re.Match(b) {
|
||||
return fmt.Errorf("raw output since %q does not match %q:\n%s", step.From, step.Regex, string(b))
|
||||
}
|
||||
return nil
|
||||
case "assert_regex":
|
||||
return s.WaitForRegex(step.Regex, timeoutMS(step.TimeoutMS))
|
||||
case "wait_text":
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "switch_replay_clears_viewport",
|
||||
"cols": 80,
|
||||
"rows": 24,
|
||||
"scripts": [
|
||||
{
|
||||
"name": "blanktop",
|
||||
"body": "#!/bin/sh\nprintf '\\033[2;1HFIRST-ROW-TWO\\n'\nsleep 5\n"
|
||||
}
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"type": "mcp_call",
|
||||
"method": "spawn_process",
|
||||
"params": { "kind": "command", "argv": ["blanktop"], "name": "first" },
|
||||
"save_as": "first"
|
||||
},
|
||||
{ "type": "wait_text", "contains": "FIRST-ROW-TWO", "timeout_ms": 5000 },
|
||||
{
|
||||
"type": "mcp_call",
|
||||
"method": "spawn_process",
|
||||
"params": { "kind": "command", "argv": ["sh", "-lc", "echo SECOND READY; sleep 5"], "name": "second" },
|
||||
"save_as": "second"
|
||||
},
|
||||
{ "type": "wait_text", "contains": "SECOND READY", "timeout_ms": 5000 },
|
||||
{
|
||||
"type": "mcp_call",
|
||||
"method": "select_process",
|
||||
"params": { "process_id": "{{first.process_id}}" }
|
||||
},
|
||||
{ "type": "wait_stable", "timeout_ms": 5000 },
|
||||
{ "type": "assert_contains", "contains": "FIRST-ROW-TWO" },
|
||||
{ "type": "assert_not_contains", "contains": "SECOND READY" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "switch_replay_preserves_color",
|
||||
"cols": 80,
|
||||
"rows": 24,
|
||||
"scripts": [
|
||||
{
|
||||
"name": "color-frame",
|
||||
"body": "#!/bin/sh\nprintf '\\033[31mREDMARK\\033[0m\\n'\nsleep 5\n"
|
||||
}
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"type": "mcp_call",
|
||||
"method": "spawn_process",
|
||||
"params": { "kind": "command", "argv": ["color-frame"], "name": "color" },
|
||||
"save_as": "color"
|
||||
},
|
||||
{ "type": "wait_text", "contains": "REDMARK", "timeout_ms": 5000 },
|
||||
{
|
||||
"type": "mcp_call",
|
||||
"method": "spawn_process",
|
||||
"params": { "kind": "command", "argv": ["sh", "-lc", "echo SECOND READY; sleep 5"], "name": "second" },
|
||||
"save_as": "second"
|
||||
},
|
||||
{ "type": "wait_text", "contains": "SECOND READY", "timeout_ms": 5000 },
|
||||
{ "type": "mark_raw", "save_as": "before_switch_back" },
|
||||
{
|
||||
"type": "mcp_call",
|
||||
"method": "select_process",
|
||||
"params": { "process_id": "{{color.process_id}}" }
|
||||
},
|
||||
{ "type": "wait_stable", "timeout_ms": 5000 },
|
||||
{ "type": "assert_contains", "contains": "REDMARK" },
|
||||
{ "type": "assert_raw_since_regex", "from": "before_switch_back", "regex": "\u001b\\[[0-9;]*38;2;[^m]*mREDMARK" }
|
||||
]
|
||||
}
|
||||
@@ -262,3 +262,23 @@ func (s *Session) rawBytes() []byte {
|
||||
copy(out, s.bytes)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Session) RawOffset() int {
|
||||
s.bytesMu.Lock()
|
||||
defer s.bytesMu.Unlock()
|
||||
return len(s.bytes)
|
||||
}
|
||||
|
||||
func (s *Session) RawSince(offset int) []byte {
|
||||
s.bytesMu.Lock()
|
||||
defer s.bytesMu.Unlock()
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if offset > len(s.bytes) {
|
||||
offset = len(s.bytes)
|
||||
}
|
||||
out := make([]byte, len(s.bytes)-offset)
|
||||
copy(out, s.bytes[offset:])
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ type Emulator interface {
|
||||
// frame" for newly-attached clients.
|
||||
SerializeVT() ([]byte, error)
|
||||
|
||||
// StyledScreenVT returns the active screen's visible cell grid as VT
|
||||
// bytes with SGR styling and child-space cursor movement, but without
|
||||
// terminal modes, scroll regions, tabstops, or formatter cursor side
|
||||
// effects.
|
||||
StyledScreenVT() ([]byte, error)
|
||||
|
||||
// Cursor returns cursor position and visibility on the active screen.
|
||||
Cursor() (CursorState, error)
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
@@ -309,6 +310,199 @@ func (e *GhosttyEmulator) SerializeVT() ([]byte, error) {
|
||||
return C.GoBytes(unsafe.Pointer(buf), C.int(n)), nil
|
||||
}
|
||||
|
||||
type styledCellSGR struct {
|
||||
fgSet, bgSet bool
|
||||
fgR, fgG, fgB uint8
|
||||
bgR, bgG, bgB uint8
|
||||
|
||||
bold, italic, faint, blink, inverse, invisible, strikethrough, overline bool
|
||||
underline int
|
||||
}
|
||||
|
||||
func (s styledCellSGR) equal(o styledCellSGR) bool {
|
||||
return s == o
|
||||
}
|
||||
|
||||
func sgrSeq(s styledCellSGR) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("\x1b[0")
|
||||
if s.bold {
|
||||
b.WriteString(";1")
|
||||
}
|
||||
if s.faint {
|
||||
b.WriteString(";2")
|
||||
}
|
||||
if s.italic {
|
||||
b.WriteString(";3")
|
||||
}
|
||||
if s.underline != 0 {
|
||||
b.WriteString(";4")
|
||||
}
|
||||
if s.blink {
|
||||
b.WriteString(";5")
|
||||
}
|
||||
if s.inverse {
|
||||
b.WriteString(";7")
|
||||
}
|
||||
if s.invisible {
|
||||
b.WriteString(";8")
|
||||
}
|
||||
if s.strikethrough {
|
||||
b.WriteString(";9")
|
||||
}
|
||||
if s.overline {
|
||||
b.WriteString(";53")
|
||||
}
|
||||
if s.fgSet {
|
||||
fmt.Fprintf(&b, ";38;2;%d;%d;%d", s.fgR, s.fgG, s.fgB)
|
||||
}
|
||||
if s.bgSet {
|
||||
fmt.Fprintf(&b, ";48;2;%d;%d;%d", s.bgR, s.bgG, s.bgB)
|
||||
}
|
||||
b.WriteByte('m')
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (e *GhosttyEmulator) StyledScreenVT() ([]byte, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.closed {
|
||||
return nil, errors.New("vt: emulator closed")
|
||||
}
|
||||
|
||||
var state C.GhosttyRenderState
|
||||
if rc := C.ghostty_render_state_new(nil, &state); rc != C.GHOSTTY_SUCCESS {
|
||||
return nil, fmt.Errorf("vt: render_state_new failed: %s", ghosttyResultStr(rc))
|
||||
}
|
||||
defer C.ghostty_render_state_free(state)
|
||||
if rc := C.ghostty_render_state_update(state, e.term); rc != C.GHOSTTY_SUCCESS {
|
||||
return nil, fmt.Errorf("vt: render_state_update failed: %s", ghosttyResultStr(rc))
|
||||
}
|
||||
|
||||
var rows C.uint16_t
|
||||
if rc := C.ghostty_render_state_get(state, C.GHOSTTY_RENDER_STATE_DATA_ROWS, unsafe.Pointer(&rows)); rc != C.GHOSTTY_SUCCESS {
|
||||
return nil, fmt.Errorf("vt: render_state rows failed: %s", ghosttyResultStr(rc))
|
||||
}
|
||||
|
||||
var iter C.GhosttyRenderStateRowIterator
|
||||
if rc := C.ghostty_render_state_row_iterator_new(nil, &iter); rc != C.GHOSTTY_SUCCESS {
|
||||
return nil, fmt.Errorf("vt: row_iterator_new failed: %s", ghosttyResultStr(rc))
|
||||
}
|
||||
defer C.ghostty_render_state_row_iterator_free(iter)
|
||||
if rc := C.ghostty_render_state_get(state, C.GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, unsafe.Pointer(&iter)); rc != C.GHOSTTY_SUCCESS {
|
||||
return nil, fmt.Errorf("vt: render_state row iterator failed: %s", ghosttyResultStr(rc))
|
||||
}
|
||||
|
||||
var cells C.GhosttyRenderStateRowCells
|
||||
if rc := C.ghostty_render_state_row_cells_new(nil, &cells); rc != C.GHOSTTY_SUCCESS {
|
||||
return nil, fmt.Errorf("vt: row_cells_new failed: %s", ghosttyResultStr(rc))
|
||||
}
|
||||
defer C.ghostty_render_state_row_cells_free(cells)
|
||||
|
||||
var out strings.Builder
|
||||
for row := 0; row < int(rows) && C.ghostty_render_state_row_iterator_next(iter); row++ {
|
||||
if rc := C.ghostty_render_state_row_get(iter, C.GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, unsafe.Pointer(&cells)); rc != C.GHOSTTY_SUCCESS {
|
||||
return nil, fmt.Errorf("vt: render_state row cells failed: %s", ghosttyResultStr(rc))
|
||||
}
|
||||
|
||||
rowCells := make([]struct {
|
||||
text string
|
||||
sgr styledCellSGR
|
||||
draw bool
|
||||
}, 0, int(e.cols))
|
||||
lastDraw := -1
|
||||
for col := 0; col < int(e.cols) && C.ghostty_render_state_row_cells_next(cells); col++ {
|
||||
var cell C.GhosttyCell
|
||||
_ = C.ghostty_render_state_row_cells_get(cells, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW, unsafe.Pointer(&cell))
|
||||
var wide C.GhosttyCellWide
|
||||
_ = C.ghostty_cell_get(cell, C.GHOSTTY_CELL_DATA_WIDE, unsafe.Pointer(&wide))
|
||||
if wide == C.GHOSTTY_CELL_WIDE_SPACER_TAIL || wide == C.GHOSTTY_CELL_WIDE_SPACER_HEAD {
|
||||
rowCells = append(rowCells, struct {
|
||||
text string
|
||||
sgr styledCellSGR
|
||||
draw bool
|
||||
}{})
|
||||
continue
|
||||
}
|
||||
|
||||
var style C.GhosttyStyle
|
||||
style.size = C.size_t(unsafe.Sizeof(style))
|
||||
_ = C.ghostty_render_state_row_cells_get(cells, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE, unsafe.Pointer(&style))
|
||||
|
||||
sgr := styledCellSGR{
|
||||
bold: bool(style.bold),
|
||||
italic: bool(style.italic),
|
||||
faint: bool(style.faint),
|
||||
blink: bool(style.blink),
|
||||
inverse: bool(style.inverse),
|
||||
invisible: bool(style.invisible),
|
||||
strikethrough: bool(style.strikethrough),
|
||||
overline: bool(style.overline),
|
||||
underline: int(style.underline),
|
||||
}
|
||||
var fg C.GhosttyColorRgb
|
||||
if rc := C.ghostty_render_state_row_cells_get(cells, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR, unsafe.Pointer(&fg)); rc == C.GHOSTTY_SUCCESS {
|
||||
sgr.fgSet, sgr.fgR, sgr.fgG, sgr.fgB = true, uint8(fg.r), uint8(fg.g), uint8(fg.b)
|
||||
}
|
||||
var bg C.GhosttyColorRgb
|
||||
if rc := C.ghostty_render_state_row_cells_get(cells, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR, unsafe.Pointer(&bg)); rc == C.GHOSTTY_SUCCESS {
|
||||
sgr.bgSet, sgr.bgR, sgr.bgG, sgr.bgB = true, uint8(bg.r), uint8(bg.g), uint8(bg.b)
|
||||
}
|
||||
|
||||
var graphemeLen C.uint32_t
|
||||
_ = C.ghostty_render_state_row_cells_get(cells, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, unsafe.Pointer(&graphemeLen))
|
||||
text := ""
|
||||
if graphemeLen > 0 {
|
||||
buf := make([]C.uint32_t, int(graphemeLen))
|
||||
_ = C.ghostty_render_state_row_cells_get(cells, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, unsafe.Pointer(&buf[0]))
|
||||
rs := make([]rune, len(buf))
|
||||
for i, r := range buf {
|
||||
rs[i] = rune(r)
|
||||
}
|
||||
text = string(rs)
|
||||
}
|
||||
|
||||
draw := text != "" || sgr.bgSet
|
||||
if draw {
|
||||
lastDraw = col
|
||||
if text == "" {
|
||||
text = " "
|
||||
}
|
||||
}
|
||||
rowCells = append(rowCells, struct {
|
||||
text string
|
||||
sgr styledCellSGR
|
||||
draw bool
|
||||
}{text: text, sgr: sgr, draw: draw})
|
||||
}
|
||||
if lastDraw < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(&out, "\x1b[%d;1H", row+1)
|
||||
cur := styledCellSGR{}
|
||||
out.WriteString("\x1b[0m")
|
||||
for col := 0; col <= lastDraw && col < len(rowCells); col++ {
|
||||
cell := rowCells[col]
|
||||
if !cell.draw {
|
||||
if !cur.equal(styledCellSGR{}) {
|
||||
cur = styledCellSGR{}
|
||||
out.WriteString("\x1b[0m")
|
||||
}
|
||||
out.WriteByte(' ')
|
||||
continue
|
||||
}
|
||||
if !cell.sgr.equal(cur) {
|
||||
cur = cell.sgr
|
||||
out.WriteString(sgrSeq(cur))
|
||||
}
|
||||
out.WriteString(cell.text)
|
||||
}
|
||||
out.WriteString("\x1b[0m")
|
||||
}
|
||||
return []byte(out.String()), nil
|
||||
}
|
||||
|
||||
func (e *GhosttyEmulator) Cursor() (CursorState, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
@@ -15,16 +15,17 @@ func NewGhosttyEmulator(cols, rows uint16) (*GhosttyEmulator, error) {
|
||||
return nil, errors.New("vt: built with -tags nocgo; libghostty-vt is unavailable")
|
||||
}
|
||||
|
||||
func (e *GhosttyEmulator) Write(p []byte) (int, error) { return 0, errStub }
|
||||
func (e *GhosttyEmulator) Resize(cols, rows uint16) error { return errStub }
|
||||
func (e *GhosttyEmulator) Size() (uint16, uint16) { return 0, 0 }
|
||||
func (e *GhosttyEmulator) PlainText() (string, error) { return "", errStub }
|
||||
func (e *GhosttyEmulator) ScreenText() (string, error) { return "", errStub }
|
||||
func (e *GhosttyEmulator) SerializeVT() ([]byte, error) { return nil, errStub }
|
||||
func (e *GhosttyEmulator) Cursor() (CursorState, error) { return CursorState{}, errStub }
|
||||
func (e *GhosttyEmulator) ActiveScreen() (Screen, error) { return 0, errStub }
|
||||
func (e *GhosttyEmulator) OnWritePTY(fn func([]byte)) {}
|
||||
func (e *GhosttyEmulator) Close() error { return nil }
|
||||
func (e *GhosttyEmulator) Write(p []byte) (int, error) { return 0, errStub }
|
||||
func (e *GhosttyEmulator) Resize(cols, rows uint16) error { return errStub }
|
||||
func (e *GhosttyEmulator) Size() (uint16, uint16) { return 0, 0 }
|
||||
func (e *GhosttyEmulator) PlainText() (string, error) { return "", errStub }
|
||||
func (e *GhosttyEmulator) ScreenText() (string, error) { return "", errStub }
|
||||
func (e *GhosttyEmulator) SerializeVT() ([]byte, error) { return nil, errStub }
|
||||
func (e *GhosttyEmulator) StyledScreenVT() ([]byte, error) { return nil, errStub }
|
||||
func (e *GhosttyEmulator) Cursor() (CursorState, error) { return CursorState{}, errStub }
|
||||
func (e *GhosttyEmulator) ActiveScreen() (Screen, error) { return 0, errStub }
|
||||
func (e *GhosttyEmulator) OnWritePTY(fn func([]byte)) {}
|
||||
func (e *GhosttyEmulator) Close() error { return nil }
|
||||
|
||||
var errStub = errors.New("vt: built with -tags nocgo")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user