Fix styled switch-back repaint

This commit is contained in:
2026-05-14 17:20:23 +01:00
parent d5ee50fa65
commit 36e738b5c6
12 changed files with 423 additions and 62 deletions

View File

@@ -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()