From d73d94722594ffa8872a7ed3e7070d3975cda6e9 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 25 Oct 2024 22:00:54 -0400 Subject: [PATCH 1/8] refactor(cellbuf): rename At to Cell and Set to SetCell --- cellbuf/buffer.go | 16 ++++++++-------- cellbuf/grid.go | 18 +++++++++--------- cellbuf/grid_write.go | 16 ++++++++-------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/cellbuf/buffer.go b/cellbuf/buffer.go index a581b33a..f3a7fbbd 100644 --- a/cellbuf/buffer.go +++ b/cellbuf/buffer.go @@ -19,24 +19,24 @@ func (b *Buffer) Height() int { return len(b.cells) / b.width } -// At returns the cell at the given x, y position. -func (b *Buffer) At(x, y int) (Cell, error) { +// Cell returns the cell at the given x, y position. +func (b *Buffer) Cell(x, y int) (Cell, bool) { if b.width == 0 { - return Cell{}, ErrOutOfBounds + return Cell{}, false } height := len(b.cells) / b.width if x < 0 || x >= b.width || y < 0 || y >= height { - return Cell{}, ErrOutOfBounds + return Cell{}, false } idx := y*b.width + x if idx < 0 || idx >= len(b.cells) { - return Cell{}, ErrOutOfBounds + return Cell{}, false } - return b.cells[idx], nil + return b.cells[idx], true } -// Set sets the cell at the given x, y position. -func (b *Buffer) Set(x, y int, c Cell) (v bool) { +// SetCell sets the cell at the given x, y position. +func (b *Buffer) SetCell(x, y int, c Cell) (v bool) { if b.width == 0 { return } diff --git a/cellbuf/grid.go b/cellbuf/grid.go index a75834f7..77cc74fb 100644 --- a/cellbuf/grid.go +++ b/cellbuf/grid.go @@ -21,12 +21,12 @@ type Grid interface { // Height returns the height of the grid. Height() int - // Set writes a cell to the grid at the given position. It returns true if - // the cell was written successfully. - Set(x, y int, c Cell) bool + // SetCell writes a cell to the grid at the given position. It returns true + // if the cell was written successfully. + SetCell(x, y int, c Cell) bool - // At returns the cell at the given position. - At(x, y int) (Cell, error) + // Cell returns the cell at the given position. + Cell(x, y int) (Cell, bool) // Resize resizes the grid to the given width and height. Resize(width, height int) @@ -67,7 +67,7 @@ func RenderLine(g Grid, n int) (w int, line string) { var pendingLine string var pendingWidth int // this ignores space cells until we hit a non-space cell for x := 0; x < g.Width(); x++ { - if cell, err := g.At(x, n); err == nil && cell.Width > 0 { + if cell, ok := g.Cell(x, n); ok && cell.Width > 0 { if cell.Style.Empty() && !pen.Empty() { buf.WriteString(ansi.ResetStyle) //nolint:errcheck pen.Reset() @@ -114,7 +114,7 @@ func RenderLine(g Grid, n int) (w int, line string) { func Fill(g Grid, c Cell) { for y := 0; y < g.Height(); y++ { for x := 0; x < g.Width(); x++ { - g.Set(x, y, c) //nolint:errcheck + g.SetCell(x, y, c) //nolint:errcheck } } } @@ -126,8 +126,8 @@ func Equal(a, b Grid) bool { } for y := 0; y < a.Height(); y++ { for x := 0; x < a.Width(); x++ { - ca, _ := a.At(x, y) - cb, _ := b.At(x, y) + ca, _ := a.Cell(x, y) + cb, _ := b.Cell(x, y) if !ca.Equal(cb) { return false } diff --git a/cellbuf/grid_write.go b/cellbuf/grid_write.go index eef10453..a1ae4301 100644 --- a/cellbuf/grid_write.go +++ b/cellbuf/grid_write.go @@ -61,7 +61,7 @@ func setContent[ // Mark wide cells with emptyCell zero width // We set the wide cell down below for j := 1; j < width; j++ { - buf.Set(x+j, y, emptyCell) //nolint:errcheck + buf.SetCell(x+j, y, emptyCell) //nolint:errcheck } fallthrough case 1: @@ -73,20 +73,20 @@ func setContent[ // When a wide cell is partially overwritten, we need // to fill the rest of the cell with space cells to // avoid rendering issues. - if prev, err := buf.At(x, y); err == nil { + if prev, ok := buf.Cell(x, y); ok { if !cell.Equal(prev) && prev.Width > 1 { c := prev c.Content = " " c.Width = 1 for j := 0; j < prev.Width; j++ { - buf.Set(x+j, y, c) //nolint:errcheck + buf.SetCell(x+j, y, c) //nolint:errcheck } } else if prev.Width == 0 { // Find the wide cell and set it to space cell. var wide Cell var wx, wy int for j := 1; j < 4; j++ { - if c, err := buf.At(x-j, y); err == nil && c.Width > 1 { + if c, ok := buf.Cell(x-j, y); ok && c.Width > 1 { wide = c wx, wy = x-j, y break @@ -97,13 +97,13 @@ func setContent[ c.Content = " " c.Width = 1 for j := 0; j < wide.Width; j++ { - buf.Set(wx+j, wy, c) //nolint:errcheck + buf.SetCell(wx+j, wy, c) //nolint:errcheck } } } } - buf.Set(x, y, cell) //nolint:errcheck + buf.SetCell(x, y, cell) //nolint:errcheck // Advance the cursor and line width x += cell.Width @@ -230,7 +230,7 @@ func setContent[ case ansi.Equal(seq, T("\n")): // Reset the rest of the line for x < w { - buf.Set(x, y, spaceCell) //nolint:errcheck + buf.SetCell(x, y, spaceCell) //nolint:errcheck x++ } @@ -248,7 +248,7 @@ func setContent[ } for x < w { - buf.Set(x, y, spaceCell) //nolint:errcheck + buf.SetCell(x, y, spaceCell) //nolint:errcheck x++ } From 76c2a83b5d4927577fb204ab20579bac8a9fadd9 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 25 Oct 2024 22:01:20 -0400 Subject: [PATCH 2/8] fix(cellbuf): remove spaceStyleEqual hack from Cell.Equal --- cellbuf/cell.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/cellbuf/cell.go b/cellbuf/cell.go index 7e4feea9..72475aec 100644 --- a/cellbuf/cell.go +++ b/cellbuf/cell.go @@ -29,18 +29,9 @@ type Cell struct { // Equal returns whether the cell is equal to the other cell. func (c Cell) Equal(o Cell) bool { - spaceStyleEqual := func(lhs, rhs Style) bool { - return colorEqual(lhs.Bg, rhs.Bg) && - colorEqual(lhs.Ul, rhs.Ul) && - lhs.Attrs == rhs.Attrs && - lhs.UlStyle == rhs.UlStyle - } - - return c.Content == o.Content && - // OPTIM: If the cell is a space, we don't care about its FG color. - ((c.Content == " " && spaceStyleEqual(c.Style, o.Style)) || - (c.Style.Equal(o.Style))) && - c.Width == o.Width && + return c.Width == o.Width && + c.Content == o.Content && + c.Style.Equal(o.Style) && c.Link.Equal(o.Link) } From b971df2eaa62d9783a8a56f1d64e89874fc0a99e Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 25 Oct 2024 22:02:02 -0400 Subject: [PATCH 3/8] fix(cellbuf): we should write pending content on style and link changes --- cellbuf/grid.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/cellbuf/grid.go b/cellbuf/grid.go index 77cc74fb..6f0e6284 100644 --- a/cellbuf/grid.go +++ b/cellbuf/grid.go @@ -66,13 +66,27 @@ func RenderLine(g Grid, n int) (w int, line string) { var buf bytes.Buffer var pendingLine string var pendingWidth int // this ignores space cells until we hit a non-space cell + + writePending := func() { + // If there's no pending line, we don't need to do anything. + if len(pendingLine) == 0 { + return + } + buf.WriteString(pendingLine) + w += pendingWidth + pendingWidth = 0 + pendingLine = "" + } + for x := 0; x < g.Width(); x++ { if cell, ok := g.Cell(x, n); ok && cell.Width > 0 { if cell.Style.Empty() && !pen.Empty() { + writePending() buf.WriteString(ansi.ResetStyle) //nolint:errcheck pen.Reset() } if !cell.Style.Equal(pen) { + writePending() seq := cell.Style.DiffSequence(pen) buf.WriteString(seq) // nolint:errcheck pen = cell.Style @@ -80,24 +94,25 @@ func RenderLine(g Grid, n int) (w int, line string) { // Write the URL escape sequence if cell.Link != link && link.URL != "" { + writePending() buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck link.Reset() } if cell.Link != link { + writePending() buf.WriteString(ansi.SetHyperlink(cell.Link.URL, cell.Link.URLID)) //nolint:errcheck link = cell.Link } // We only write the cell content if it's not empty. If it is, we // append it to the pending line and width to be evaluated later. - if cell.Style.Empty() && len(strings.TrimSpace(cell.Content)) == 0 { + if cell.Equal(spaceCell) { pendingLine += cell.Content pendingWidth += cell.Width } else { - buf.WriteString(pendingLine + cell.Content) - w += pendingWidth + cell.Width - pendingWidth = 0 - pendingLine = "" + writePending() + buf.WriteString(cell.Content) + w += cell.Width } } } From d9ee83f25bb21a8ad6515abbc756ba7f3a47d5b4 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 25 Oct 2024 22:07:47 -0400 Subject: [PATCH 4/8] fix(cellbuf): fill wide cells with spaces when partially overwritten When a wide cell is partially overwritten, we need to fill the rest of the cell with space cells to avoid rendering issues. --- cellbuf/buffer.go | 28 ++++++++++++++++++++++++++++ cellbuf/grid_write.go | 33 --------------------------------- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/cellbuf/buffer.go b/cellbuf/buffer.go index f3a7fbbd..3f76d98c 100644 --- a/cellbuf/buffer.go +++ b/cellbuf/buffer.go @@ -49,6 +49,34 @@ func (b *Buffer) SetCell(x, y int, c Cell) (v bool) { return } + // When a wide cell is partially overwritten, we need + // to fill the rest of the cell with space cells to + // avoid rendering issues. + prev := b.cells[idx] + if prev.Width > 1 { + // Writing to the first wide cell + for j := 0; j < prev.Width; j++ { + newCell := prev + newCell.Content = " " + newCell.Width = 1 + b.cells[idx+j] = newCell + } + } else if prev.Width == 0 { + // Writing to wide cell placeholders + for j := 1; j < 4; j++ { + wide := b.cells[idx-j] + if wide.Width > 1 { + for k := 0; k < wide.Width; k++ { + newCell := wide + newCell.Content = " " + newCell.Width = 1 + b.cells[idx-j+k] = newCell + } + break + } + } + } + b.cells[idx] = c return true } diff --git a/cellbuf/grid_write.go b/cellbuf/grid_write.go index a1ae4301..35e82541 100644 --- a/cellbuf/grid_write.go +++ b/cellbuf/grid_write.go @@ -70,39 +70,6 @@ func setContent[ cell.Style = pen cell.Link = link - // When a wide cell is partially overwritten, we need - // to fill the rest of the cell with space cells to - // avoid rendering issues. - if prev, ok := buf.Cell(x, y); ok { - if !cell.Equal(prev) && prev.Width > 1 { - c := prev - c.Content = " " - c.Width = 1 - for j := 0; j < prev.Width; j++ { - buf.SetCell(x+j, y, c) //nolint:errcheck - } - } else if prev.Width == 0 { - // Find the wide cell and set it to space cell. - var wide Cell - var wx, wy int - for j := 1; j < 4; j++ { - if c, ok := buf.Cell(x-j, y); ok && c.Width > 1 { - wide = c - wx, wy = x-j, y - break - } - } - if !wide.Empty() { - c := wide - c.Content = " " - c.Width = 1 - for j := 0; j < wide.Width; j++ { - buf.SetCell(wx+j, wy, c) //nolint:errcheck - } - } - } - } - buf.SetCell(x, y, cell) //nolint:errcheck // Advance the cursor and line width From 075d691010e94becf5707bf74f51c9d21a0bb21b Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 25 Oct 2024 23:11:54 -0400 Subject: [PATCH 5/8] fix(cellbuf): setting ghost cells should be done in SetCell --- cellbuf/buffer.go | 9 +++++++++ cellbuf/grid_write.go | 6 ------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cellbuf/buffer.go b/cellbuf/buffer.go index 3f76d98c..804064a0 100644 --- a/cellbuf/buffer.go +++ b/cellbuf/buffer.go @@ -78,6 +78,15 @@ func (b *Buffer) SetCell(x, y int, c Cell) (v bool) { } b.cells[idx] = c + + // Mark wide cells with emptyCell zero width + // We set the wide cell down below + if c.Width > 1 { + for j := 1; j < c.Width; j++ { + b.cells[idx+j] = emptyCell + } + } + return true } diff --git a/cellbuf/grid_write.go b/cellbuf/grid_write.go index 35e82541..a7bd181d 100644 --- a/cellbuf/grid_write.go +++ b/cellbuf/grid_write.go @@ -57,12 +57,6 @@ func setContent[ case GraphemeWidth: // [ansi.DecodeSequence] already handles grapheme clusters } - - // Mark wide cells with emptyCell zero width - // We set the wide cell down below - for j := 1; j < width; j++ { - buf.SetCell(x+j, y, emptyCell) //nolint:errcheck - } fallthrough case 1: cell.Content = string(seq) From ae502d73ec63f9234281d5becae84511c43805a7 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 25 Oct 2024 23:12:30 -0400 Subject: [PATCH 6/8] refactor(cellbuf): simplify SetContent --- cellbuf/grid.go | 9 +-------- cellbuf/grid_write.go | 36 +++++++++++++++--------------------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/cellbuf/grid.go b/cellbuf/grid.go index 6f0e6284..07a39e37 100644 --- a/cellbuf/grid.go +++ b/cellbuf/grid.go @@ -3,7 +3,6 @@ package cellbuf import ( "bytes" "strings" - "unicode/utf8" "github.com/charmbracelet/x/ansi" ) @@ -32,15 +31,9 @@ type Grid interface { Resize(width, height int) } -// SetContentAt writes the given data to the grid starting from the given -// position and with the given width and height. -func (m WidthMethod) SetContentAt(b Grid, c string, x, y, w, h int) []int { - return setContent(b, c, x, y, w, h, m, strings.ReplaceAll, utf8.DecodeRuneInString) -} - // SetContent writes the given data to the grid starting from the first cell. func (m WidthMethod) SetContent(g Grid, content string) []int { - return m.SetContentAt(g, content, 0, 0, g.Width(), Height(content)) + return setContent(g, content, m) } // Render returns a string representation of the grid with ANSI escape sequences. diff --git a/cellbuf/grid_write.go b/cellbuf/grid_write.go index a7bd181d..977873a1 100644 --- a/cellbuf/grid_write.go +++ b/cellbuf/grid_write.go @@ -2,6 +2,7 @@ package cellbuf import ( "bytes" + "strings" "unicode/utf8" "github.com/charmbracelet/x/ansi" @@ -10,32 +11,24 @@ import ( // setContent writes the given data to the buffer starting from the first cell. // It accepts both string and []byte data types. -func setContent[ - T string | []byte, - TReplaceAllFunc func(s T, old T, new T) T, //nolint:predeclared - TDecodeRuneFunc func(p T) (rune, int), -]( +func setContent( buf Grid, - data T, - x, y int, - w, h int, + data string, method WidthMethod, - replaceAll TReplaceAllFunc, - decodeRune TDecodeRuneFunc, ) []int { var cell Cell var pen Style var link Link - origX := x + var x, y int p := ansi.GetParser() defer ansi.PutParser(p) - data = replaceAll(data, T("\r\n"), T("\n")) + data = strings.ReplaceAll(data, "\r\n", "\n") // linew is a slice of line widths. We use this to keep track of the // written widths of each line. We use this information later to optimize // rendering of the buffer. - linew := make([]int, h) + linew := make([]int, buf.Height()) var pendingWidth int @@ -48,10 +41,10 @@ func setContent[ switch method { case WcWidth: - if r, rw := decodeRune(data); r != utf8.RuneError { + if r, rw := utf8.DecodeRuneInString(data); r != utf8.RuneError { n = rw width = wcwidth.RuneWidth(r) - seq = T(string(r)) + seq = string(r) newState = 0 } case GraphemeWidth: @@ -59,7 +52,7 @@ func setContent[ } fallthrough case 1: - cell.Content = string(seq) + cell.Content = seq cell.Width = width cell.Style = pen cell.Link = link @@ -70,7 +63,7 @@ func setContent[ x += cell.Width if cell.Equal(spaceCell) { pendingWidth += cell.Width - } else { + } else if y < len(linew) { linew[y] += cell.Width + pendingWidth pendingWidth = 0 } @@ -85,6 +78,7 @@ func setContent[ case 'm': // SGR - Select Graphic Rendition if p.ParamsLen == 0 { pen.Reset() + break } for i := 0; i < len(params); i++ { r := ansi.Param(params[i]) @@ -188,9 +182,9 @@ func setContent[ link.URLID = id link.URL = string(params[2]) } - case ansi.Equal(seq, T("\n")): + case ansi.Equal(seq, "\n"): // Reset the rest of the line - for x < w { + for x < buf.Width() { buf.SetCell(x, y, spaceCell) //nolint:errcheck x++ } @@ -199,7 +193,7 @@ func setContent[ // XXX: We gotta reset the x position here because we're moving // to the next line. We shouldn't have any "\r\n" sequences, // those are replaced above. - x = origX + x = 0 } } @@ -208,7 +202,7 @@ func setContent[ data = data[n:] } - for x < w { + for x < buf.Width() { buf.SetCell(x, y, spaceCell) //nolint:errcheck x++ } From 1c2bac8697edb5e7561ef1054a62c20c76d6157f Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 25 Oct 2024 23:32:11 -0400 Subject: [PATCH 7/8] refactor(cellbuf): rename grid to screen and clean up --- cellbuf/grid_write.go | 211 ------------------------------- cellbuf/method.go | 8 +- cellbuf/{grid.go => screen.go} | 32 ++--- cellbuf/screen_write.go | 220 +++++++++++++++++++++++++++++++++ 4 files changed, 240 insertions(+), 231 deletions(-) delete mode 100644 cellbuf/grid_write.go rename cellbuf/{grid.go => screen.go} (83%) create mode 100644 cellbuf/screen_write.go diff --git a/cellbuf/grid_write.go b/cellbuf/grid_write.go deleted file mode 100644 index 977873a1..00000000 --- a/cellbuf/grid_write.go +++ /dev/null @@ -1,211 +0,0 @@ -package cellbuf - -import ( - "bytes" - "strings" - "unicode/utf8" - - "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/wcwidth" -) - -// setContent writes the given data to the buffer starting from the first cell. -// It accepts both string and []byte data types. -func setContent( - buf Grid, - data string, - method WidthMethod, -) []int { - var cell Cell - var pen Style - var link Link - var x, y int - - p := ansi.GetParser() - defer ansi.PutParser(p) - data = strings.ReplaceAll(data, "\r\n", "\n") - - // linew is a slice of line widths. We use this to keep track of the - // written widths of each line. We use this information later to optimize - // rendering of the buffer. - linew := make([]int, buf.Height()) - - var pendingWidth int - - var state byte - for len(data) > 0 { - seq, width, n, newState := ansi.DecodeSequence(data, state, p) - - switch width { - case 2, 3, 4: // wide cells can go up to 4 cells wide - - switch method { - case WcWidth: - if r, rw := utf8.DecodeRuneInString(data); r != utf8.RuneError { - n = rw - width = wcwidth.RuneWidth(r) - seq = string(r) - newState = 0 - } - case GraphemeWidth: - // [ansi.DecodeSequence] already handles grapheme clusters - } - fallthrough - case 1: - cell.Content = seq - cell.Width = width - cell.Style = pen - cell.Link = link - - buf.SetCell(x, y, cell) //nolint:errcheck - - // Advance the cursor and line width - x += cell.Width - if cell.Equal(spaceCell) { - pendingWidth += cell.Width - } else if y < len(linew) { - linew[y] += cell.Width + pendingWidth - pendingWidth = 0 - } - - cell.Reset() - default: - // Valid sequences always have a non-zero Cmd. - switch { - case ansi.HasCsiPrefix(seq) && p.Cmd != 0: - params := p.Params[:p.ParamsLen] - switch p.Cmd { - case 'm': // SGR - Select Graphic Rendition - if p.ParamsLen == 0 { - pen.Reset() - break - } - for i := 0; i < len(params); i++ { - r := ansi.Param(params[i]) - param, hasMore := r.Param(), r.HasMore() // Are there more subparameters i.e. separated by ":"? - switch param { - case 0: // Reset - pen.Reset() - case 1: // Bold - pen.Bold(true) - case 2: // Dim/Faint - pen.Faint(true) - case 3: // Italic - pen.Italic(true) - case 4: // Underline - if hasMore { // Only accept subparameters i.e. separated by ":" - nextParam := ansi.Param(params[i+1]).Param() - switch nextParam { - case 0: // No Underline - pen.UnderlineStyle(NoUnderline) - case 1: // Single Underline - pen.UnderlineStyle(SingleUnderline) - case 2: // Double Underline - pen.UnderlineStyle(DoubleUnderline) - case 3: // Curly Underline - pen.UnderlineStyle(CurlyUnderline) - case 4: // Dotted Underline - pen.UnderlineStyle(DottedUnderline) - case 5: // Dashed Underline - pen.UnderlineStyle(DashedUnderline) - } - } else { - // Single Underline - pen.Underline(true) - } - case 5: // Slow Blink - pen.SlowBlink(true) - case 6: // Rapid Blink - pen.RapidBlink(true) - case 7: // Reverse - pen.Reverse(true) - case 8: // Conceal - pen.Conceal(true) - case 9: // Crossed-out/Strikethrough - pen.Strikethrough(true) - case 22: // Normal Intensity (not bold or faint) - pen.Bold(false).Faint(false) - case 23: // Not italic, not Fraktur - pen.Italic(false) - case 24: // Not underlined - pen.Underline(false) - case 25: // Blink off - pen.SlowBlink(false).RapidBlink(false) - case 27: // Positive (not reverse) - pen.Reverse(false) - case 28: // Reveal - pen.Conceal(false) - case 29: // Not crossed out - pen.Strikethrough(false) - case 30, 31, 32, 33, 34, 35, 36, 37: // Set foreground - pen.Foreground(ansi.Black + ansi.BasicColor(param-30)) //nolint:gosec - case 38: // Set foreground 256 or truecolor - if c := readColor(&i, params); c != nil { - pen.Foreground(c) - } - case 39: // Default foreground - pen.Foreground(nil) - case 40, 41, 42, 43, 44, 45, 46, 47: // Set background - pen.Background(ansi.Black + ansi.BasicColor(param-40)) //nolint:gosec - case 48: // Set background 256 or truecolor - if c := readColor(&i, params); c != nil { - pen.Background(c) - } - case 49: // Default Background - pen.Background(nil) - case 58: // Set underline color - if c := readColor(&i, params); c != nil { - pen.UnderlineColor(c) - } - case 59: // Default underline color - pen.UnderlineColor(nil) - case 90, 91, 92, 93, 94, 95, 96, 97: // Set bright foreground - pen.Foreground(ansi.BrightBlack + ansi.BasicColor(param-90)) //nolint:gosec - case 100, 101, 102, 103, 104, 105, 106, 107: // Set bright background - pen.Background(ansi.BrightBlack + ansi.BasicColor(param-100)) //nolint:gosec - } - } - } - case ansi.HasOscPrefix(seq) && p.Cmd != 0: - switch p.Cmd { - case 8: // Hyperlinks - params := bytes.Split(p.Data[:p.DataLen], []byte{';'}) - if len(params) != 3 { - break - } - var id string - for _, param := range bytes.Split(params[1], []byte{':'}) { - if bytes.HasPrefix(param, []byte("id=")) { - id = string(param) - } - } - link.URLID = id - link.URL = string(params[2]) - } - case ansi.Equal(seq, "\n"): - // Reset the rest of the line - for x < buf.Width() { - buf.SetCell(x, y, spaceCell) //nolint:errcheck - x++ - } - - y++ - // XXX: We gotta reset the x position here because we're moving - // to the next line. We shouldn't have any "\r\n" sequences, - // those are replaced above. - x = 0 - } - } - - // Advance the state and data - state = newState - data = data[n:] - } - - for x < buf.Width() { - buf.SetCell(x, y, spaceCell) //nolint:errcheck - x++ - } - - return linew -} diff --git a/cellbuf/method.go b/cellbuf/method.go index 8bc7de2d..815e8a81 100644 --- a/cellbuf/method.go +++ b/cellbuf/method.go @@ -1,11 +1,11 @@ package cellbuf -// WidthMethod is a type that represents the how the renderer should calculate -// the display width of cells. -type WidthMethod uint8 +// Method is a type that represents the how the renderer should calculate the +// display width of cells. +type Method uint8 // Display width modes. const ( - WcWidth WidthMethod = iota + WcWidth Method = iota GraphemeWidth ) diff --git a/cellbuf/grid.go b/cellbuf/screen.go similarity index 83% rename from cellbuf/grid.go rename to cellbuf/screen.go index 07a39e37..05359a46 100644 --- a/cellbuf/grid.go +++ b/cellbuf/screen.go @@ -11,9 +11,9 @@ import ( // attributes and hyperlink. type Segment = Cell -// Grid represents an interface for a grid of cells that can be written to and -// read from. -type Grid interface { +// Screen represents an interface for a grid of cells that can be written to +// and read from. +type Screen interface { // Width returns the width of the grid. Width() int @@ -32,17 +32,17 @@ type Grid interface { } // SetContent writes the given data to the grid starting from the first cell. -func (m WidthMethod) SetContent(g Grid, content string) []int { - return setContent(g, content, m) +func SetContent(d Screen, m Method, content string) []int { + return setContent(d, content, m) } // Render returns a string representation of the grid with ANSI escape sequences. // Use [ansi.Strip] to remove them. -func Render(g Grid) string { +func Render(d Screen) string { var buf bytes.Buffer - height := g.Height() + height := d.Height() for y := 0; y < height; y++ { - _, line := RenderLine(g, y) + _, line := RenderLine(d, y) buf.WriteString(line) if y < height-1 { buf.WriteString("\r\n") @@ -53,7 +53,7 @@ func Render(g Grid) string { // RenderLine returns a string representation of the yth line of the grid along // with the width of the line. -func RenderLine(g Grid, n int) (w int, line string) { +func RenderLine(d Screen, n int) (w int, line string) { var pen Style var link Link var buf bytes.Buffer @@ -71,8 +71,8 @@ func RenderLine(g Grid, n int) (w int, line string) { pendingLine = "" } - for x := 0; x < g.Width(); x++ { - if cell, ok := g.Cell(x, n); ok && cell.Width > 0 { + for x := 0; x < d.Width(); x++ { + if cell, ok := d.Cell(x, n); ok && cell.Width > 0 { if cell.Style.Empty() && !pen.Empty() { writePending() buf.WriteString(ansi.ResetStyle) //nolint:errcheck @@ -119,16 +119,16 @@ func RenderLine(g Grid, n int) (w int, line string) { } // Fill fills the grid with the given cell. -func Fill(g Grid, c Cell) { - for y := 0; y < g.Height(); y++ { - for x := 0; x < g.Width(); x++ { - g.SetCell(x, y, c) //nolint:errcheck +func Fill(d Screen, c Cell) { + for y := 0; y < d.Height(); y++ { + for x := 0; x < d.Width(); x++ { + d.SetCell(x, y, c) //nolint:errcheck } } } // Equal returns whether two grids are equal. -func Equal(a, b Grid) bool { +func Equal(a, b Screen) bool { if a.Width() != b.Width() || a.Height() != b.Height() { return false } diff --git a/cellbuf/screen_write.go b/cellbuf/screen_write.go new file mode 100644 index 00000000..a2765b55 --- /dev/null +++ b/cellbuf/screen_write.go @@ -0,0 +1,220 @@ +package cellbuf + +import ( + "bytes" + "strings" + "unicode/utf8" + + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/wcwidth" +) + +// setContent writes the given data to the buffer starting from the first cell. +// It accepts both string and []byte data types. +func setContent( + dis Screen, + data string, + method Method, +) []int { + var cell Cell + var pen Style + var link Link + var x, y int + + p := ansi.GetParser() + defer ansi.PutParser(p) + data = strings.ReplaceAll(data, "\r\n", "\n") + + // linew is a slice of line widths. We use this to keep track of the + // written widths of each line. We use this information later to optimize + // rendering of the buffer. + linew := make([]int, dis.Height()) + + var pendingWidth int + + var state byte + for len(data) > 0 { + seq, width, n, newState := ansi.DecodeSequence(data, state, p) + + switch width { + case 2, 3, 4: // wide cells can go up to 4 cells wide + + switch method { + case WcWidth: + if r, rw := utf8.DecodeRuneInString(data); r != utf8.RuneError { + n = rw + width = wcwidth.RuneWidth(r) + seq = string(r) + newState = 0 + } + case GraphemeWidth: + // [ansi.DecodeSequence] already handles grapheme clusters + } + fallthrough + case 1: + cell.Content = seq + cell.Width = width + cell.Style = pen + cell.Link = link + + dis.SetCell(x, y, cell) //nolint:errcheck + + // Advance the cursor and line width + x += cell.Width + if cell.Equal(spaceCell) { + pendingWidth += cell.Width + } else if y < len(linew) { + linew[y] += cell.Width + pendingWidth + pendingWidth = 0 + } + + cell.Reset() + default: + // Valid sequences always have a non-zero Cmd. + switch { + case ansi.HasCsiPrefix(seq) && p.Cmd != 0: + switch p.Cmd { + case 'm': // SGR - Select Graphic Rendition + handleSgr(p, &pen) + } + case ansi.HasOscPrefix(seq) && p.Cmd != 0: + switch p.Cmd { + case 8: // Hyperlinks + handleHyperlinks(p, &link) + } + case ansi.Equal(seq, "\n"): + // Reset the rest of the line + for x < dis.Width() { + dis.SetCell(x, y, spaceCell) //nolint:errcheck + x++ + } + + y++ + // XXX: We gotta reset the x position here because we're moving + // to the next line. We shouldn't have any "\r\n" sequences, + // those are replaced above. + x = 0 + } + } + + // Advance the state and data + state = newState + data = data[n:] + } + + for x < dis.Width() { + dis.SetCell(x, y, spaceCell) //nolint:errcheck + x++ + } + + return linew +} + +// handleSgr handles Select Graphic Rendition (SGR) escape sequences. +func handleSgr(p *ansi.Parser, pen *Style) { + if p.ParamsLen == 0 { + pen.Reset() + return + } + + params := p.Params[:p.ParamsLen] + for i := 0; i < len(params); i++ { + r := ansi.Param(params[i]) + param, hasMore := r.Param(), r.HasMore() // Are there more subparameters i.e. separated by ":"? + switch param { + case 0: // Reset + pen.Reset() + case 1: // Bold + pen.Bold(true) + case 2: // Dim/Faint + pen.Faint(true) + case 3: // Italic + pen.Italic(true) + case 4: // Underline + if hasMore { // Only accept subparameters i.e. separated by ":" + nextParam := ansi.Param(params[i+1]).Param() + switch nextParam { + case 0: // No Underline + pen.UnderlineStyle(NoUnderline) + case 1: // Single Underline + pen.UnderlineStyle(SingleUnderline) + case 2: // Double Underline + pen.UnderlineStyle(DoubleUnderline) + case 3: // Curly Underline + pen.UnderlineStyle(CurlyUnderline) + case 4: // Dotted Underline + pen.UnderlineStyle(DottedUnderline) + case 5: // Dashed Underline + pen.UnderlineStyle(DashedUnderline) + } + } else { + // Single Underline + pen.Underline(true) + } + case 5: // Slow Blink + pen.SlowBlink(true) + case 6: // Rapid Blink + pen.RapidBlink(true) + case 7: // Reverse + pen.Reverse(true) + case 8: // Conceal + pen.Conceal(true) + case 9: // Crossed-out/Strikethrough + pen.Strikethrough(true) + case 22: // Normal Intensity (not bold or faint) + pen.Bold(false).Faint(false) + case 23: // Not italic, not Fraktur + pen.Italic(false) + case 24: // Not underlined + pen.Underline(false) + case 25: // Blink off + pen.SlowBlink(false).RapidBlink(false) + case 27: // Positive (not reverse) + pen.Reverse(false) + case 28: // Reveal + pen.Conceal(false) + case 29: // Not crossed out + pen.Strikethrough(false) + case 30, 31, 32, 33, 34, 35, 36, 37: // Set foreground + pen.Foreground(ansi.Black + ansi.BasicColor(param-30)) //nolint:gosec + case 38: // Set foreground 256 or truecolor + if c := readColor(&i, params); c != nil { + pen.Foreground(c) + } + case 39: // Default foreground + pen.Foreground(nil) + case 40, 41, 42, 43, 44, 45, 46, 47: // Set background + pen.Background(ansi.Black + ansi.BasicColor(param-40)) //nolint:gosec + case 48: // Set background 256 or truecolor + if c := readColor(&i, params); c != nil { + pen.Background(c) + } + case 49: // Default Background + pen.Background(nil) + case 58: // Set underline color + if c := readColor(&i, params); c != nil { + pen.UnderlineColor(c) + } + case 59: // Default underline color + pen.UnderlineColor(nil) + case 90, 91, 92, 93, 94, 95, 96, 97: // Set bright foreground + pen.Foreground(ansi.BrightBlack + ansi.BasicColor(param-90)) //nolint:gosec + case 100, 101, 102, 103, 104, 105, 106, 107: // Set bright background + pen.Background(ansi.BrightBlack + ansi.BasicColor(param-100)) //nolint:gosec + } + } +} + +// handleHyperlinks handles hyperlink escape sequences. +func handleHyperlinks(p *ansi.Parser, link *Link) { + params := bytes.Split(p.Data[:p.DataLen], []byte{';'}) + if len(params) != 3 { + return + } + for _, param := range bytes.Split(params[1], []byte{':'}) { + if bytes.HasPrefix(param, []byte("id=")) { + link.URLID = string(param) + } + } + link.URL = string(params[2]) +} From 9c548f1514f5fb42f2a2926cdb8f42b9ca076e14 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 28 Oct 2024 10:47:55 -0400 Subject: [PATCH 8/8] chore(deps): cellbuf: bump ansi from 0.3.2 to 0.4.0 --- cellbuf/go.mod | 2 +- cellbuf/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cellbuf/go.mod b/cellbuf/go.mod index 422f3502..f327154b 100644 --- a/cellbuf/go.mod +++ b/cellbuf/go.mod @@ -3,7 +3,7 @@ module github.com/charmbracelet/x/cellbuf go 1.18 require ( - github.com/charmbracelet/x/ansi v0.3.2 + github.com/charmbracelet/x/ansi v0.4.0 github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 ) diff --git a/cellbuf/go.sum b/cellbuf/go.sum index b21911da..4a3cddc5 100644 --- a/cellbuf/go.sum +++ b/cellbuf/go.sum @@ -1,5 +1,5 @@ -github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= -github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.4.0 h1:NqwHA4B23VwsDn4H3VcNX1W1tOmgnvY1NDx5tOXdnOU= +github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 h1:D5OO0lVavz7A+Swdhp62F9gbkibxmz9B2hZ/jVdMPf0= github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91/go.mod h1:Ey8PFmYwH+/td9bpiEx07Fdx9ZVkxfIjWXxBluxF4Nw= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=