summaryrefslogtreecommitdiff
path: root/vendor/github.com/charmbracelet/x/cellbuf
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-07-22 17:35:49 -0600
committermo khan <mo@mokhan.ca>2025-07-22 17:35:49 -0600
commit20ef0d92694465ac86b550df139e8366a0a2b4fa (patch)
tree3f14589e1ce6eb9306a3af31c3a1f9e1af5ed637 /vendor/github.com/charmbracelet/x/cellbuf
parent44e0d272c040cdc53a98b9f1dc58ae7da67752e6 (diff)
feat: connect to spicedb
Diffstat (limited to 'vendor/github.com/charmbracelet/x/cellbuf')
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/LICENSE21
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/buffer.go473
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/cell.go503
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/errors.go6
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/geom.go21
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/hardscroll.go272
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/hashmap.go301
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/link.go14
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/screen.go1457
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/sequence.go131
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/style.go31
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/tabstop.go137
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/utils.go38
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/wrap.go178
-rw-r--r--vendor/github.com/charmbracelet/x/cellbuf/writer.go339
15 files changed, 3922 insertions, 0 deletions
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/LICENSE b/vendor/github.com/charmbracelet/x/cellbuf/LICENSE
new file mode 100644
index 0000000..65a5654
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Charmbracelet, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/buffer.go b/vendor/github.com/charmbracelet/x/cellbuf/buffer.go
new file mode 100644
index 0000000..790d1f7
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/buffer.go
@@ -0,0 +1,473 @@
+package cellbuf
+
+import (
+ "strings"
+
+ "github.com/mattn/go-runewidth"
+ "github.com/rivo/uniseg"
+)
+
+// NewCell returns a new cell. This is a convenience function that initializes a
+// new cell with the given content. The cell's width is determined by the
+// content using [runewidth.RuneWidth].
+// This will only account for the first combined rune in the content. If the
+// content is empty, it will return an empty cell with a width of 0.
+func NewCell(r rune, comb ...rune) (c *Cell) {
+ c = new(Cell)
+ c.Rune = r
+ c.Width = runewidth.RuneWidth(r)
+ for _, r := range comb {
+ if runewidth.RuneWidth(r) > 0 {
+ break
+ }
+ c.Comb = append(c.Comb, r)
+ }
+ c.Comb = comb
+ c.Width = runewidth.StringWidth(string(append([]rune{r}, comb...)))
+ return
+}
+
+// NewCellString returns a new cell with the given string content. This is a
+// convenience function that initializes a new cell with the given content. The
+// cell's width is determined by the content using [runewidth.StringWidth].
+// This will only use the first combined rune in the string. If the string is
+// empty, it will return an empty cell with a width of 0.
+func NewCellString(s string) (c *Cell) {
+ c = new(Cell)
+ for i, r := range s {
+ if i == 0 {
+ c.Rune = r
+ // We only care about the first rune's width
+ c.Width = runewidth.RuneWidth(r)
+ } else {
+ if runewidth.RuneWidth(r) > 0 {
+ break
+ }
+ c.Comb = append(c.Comb, r)
+ }
+ }
+ return
+}
+
+// NewGraphemeCell returns a new cell. This is a convenience function that
+// initializes a new cell with the given content. The cell's width is determined
+// by the content using [uniseg.FirstGraphemeClusterInString].
+// This is used when the content is a grapheme cluster i.e. a sequence of runes
+// that form a single visual unit.
+// This will only return the first grapheme cluster in the string. If the
+// string is empty, it will return an empty cell with a width of 0.
+func NewGraphemeCell(s string) (c *Cell) {
+ g, _, w, _ := uniseg.FirstGraphemeClusterInString(s, -1)
+ return newGraphemeCell(g, w)
+}
+
+func newGraphemeCell(s string, w int) (c *Cell) {
+ c = new(Cell)
+ c.Width = w
+ for i, r := range s {
+ if i == 0 {
+ c.Rune = r
+ } else {
+ c.Comb = append(c.Comb, r)
+ }
+ }
+ return
+}
+
+// Line represents a line in the terminal.
+// A nil cell represents an blank cell, a cell with a space character and a
+// width of 1.
+// If a cell has no content and a width of 0, it is a placeholder for a wide
+// cell.
+type Line []*Cell
+
+// Width returns the width of the line.
+func (l Line) Width() int {
+ return len(l)
+}
+
+// Len returns the length of the line.
+func (l Line) Len() int {
+ return len(l)
+}
+
+// String returns the string representation of the line. Any trailing spaces
+// are removed.
+func (l Line) String() (s string) {
+ for _, c := range l {
+ if c == nil {
+ s += " "
+ } else if c.Empty() {
+ continue
+ } else {
+ s += c.String()
+ }
+ }
+ s = strings.TrimRight(s, " ")
+ return
+}
+
+// At returns the cell at the given x position.
+// If the cell does not exist, it returns nil.
+func (l Line) At(x int) *Cell {
+ if x < 0 || x >= len(l) {
+ return nil
+ }
+
+ c := l[x]
+ if c == nil {
+ newCell := BlankCell
+ return &newCell
+ }
+
+ return c
+}
+
+// Set sets the cell at the given x position. If a wide cell is given, it will
+// set the cell and the following cells to [EmptyCell]. It returns true if the
+// cell was set.
+func (l Line) Set(x int, c *Cell) bool {
+ return l.set(x, c, true)
+}
+
+func (l Line) set(x int, c *Cell, clone bool) bool {
+ width := l.Width()
+ if x < 0 || x >= width {
+ return false
+ }
+
+ // When a wide cell is partially overwritten, we need
+ // to fill the rest of the cell with space cells to
+ // avoid rendering issues.
+ prev := l.At(x)
+ if prev != nil && prev.Width > 1 {
+ // Writing to the first wide cell
+ for j := 0; j < prev.Width && x+j < l.Width(); j++ {
+ l[x+j] = prev.Clone().Blank()
+ }
+ } else if prev != nil && prev.Width == 0 {
+ // Writing to wide cell placeholders
+ for j := 1; j < maxCellWidth && x-j >= 0; j++ {
+ wide := l.At(x - j)
+ if wide != nil && wide.Width > 1 && j < wide.Width {
+ for k := 0; k < wide.Width; k++ {
+ l[x-j+k] = wide.Clone().Blank()
+ }
+ break
+ }
+ }
+ }
+
+ if clone && c != nil {
+ // Clone the cell if not nil.
+ c = c.Clone()
+ }
+
+ if c != nil && x+c.Width > width {
+ // If the cell is too wide, we write blanks with the same style.
+ for i := 0; i < c.Width && x+i < width; i++ {
+ l[x+i] = c.Clone().Blank()
+ }
+ } else {
+ l[x] = c
+
+ // Mark wide cells with an empty cell zero width
+ // We set the wide cell down below
+ if c != nil && c.Width > 1 {
+ for j := 1; j < c.Width && x+j < l.Width(); j++ {
+ var wide Cell
+ l[x+j] = &wide
+ }
+ }
+ }
+
+ return true
+}
+
+// Buffer is a 2D grid of cells representing a screen or terminal.
+type Buffer struct {
+ // Lines holds the lines of the buffer.
+ Lines []Line
+}
+
+// NewBuffer creates a new buffer with the given width and height.
+// This is a convenience function that initializes a new buffer and resizes it.
+func NewBuffer(width int, height int) *Buffer {
+ b := new(Buffer)
+ b.Resize(width, height)
+ return b
+}
+
+// String returns the string representation of the buffer.
+func (b *Buffer) String() (s string) {
+ for i, l := range b.Lines {
+ s += l.String()
+ if i < len(b.Lines)-1 {
+ s += "\r\n"
+ }
+ }
+ return
+}
+
+// Line returns a pointer to the line at the given y position.
+// If the line does not exist, it returns nil.
+func (b *Buffer) Line(y int) Line {
+ if y < 0 || y >= len(b.Lines) {
+ return nil
+ }
+ return b.Lines[y]
+}
+
+// Cell implements Screen.
+func (b *Buffer) Cell(x int, y int) *Cell {
+ if y < 0 || y >= len(b.Lines) {
+ return nil
+ }
+ return b.Lines[y].At(x)
+}
+
+// maxCellWidth is the maximum width a terminal cell can get.
+const maxCellWidth = 4
+
+// SetCell sets the cell at the given x, y position.
+func (b *Buffer) SetCell(x, y int, c *Cell) bool {
+ return b.setCell(x, y, c, true)
+}
+
+// setCell sets the cell at the given x, y position. This will always clone and
+// allocates a new cell if c is not nil.
+func (b *Buffer) setCell(x, y int, c *Cell, clone bool) bool {
+ if y < 0 || y >= len(b.Lines) {
+ return false
+ }
+ return b.Lines[y].set(x, c, clone)
+}
+
+// Height implements Screen.
+func (b *Buffer) Height() int {
+ return len(b.Lines)
+}
+
+// Width implements Screen.
+func (b *Buffer) Width() int {
+ if len(b.Lines) == 0 {
+ return 0
+ }
+ return b.Lines[0].Width()
+}
+
+// Bounds returns the bounds of the buffer.
+func (b *Buffer) Bounds() Rectangle {
+ return Rect(0, 0, b.Width(), b.Height())
+}
+
+// Resize resizes the buffer to the given width and height.
+func (b *Buffer) Resize(width int, height int) {
+ if width == 0 || height == 0 {
+ b.Lines = nil
+ return
+ }
+
+ if width > b.Width() {
+ line := make(Line, width-b.Width())
+ for i := range b.Lines {
+ b.Lines[i] = append(b.Lines[i], line...)
+ }
+ } else if width < b.Width() {
+ for i := range b.Lines {
+ b.Lines[i] = b.Lines[i][:width]
+ }
+ }
+
+ if height > len(b.Lines) {
+ for i := len(b.Lines); i < height; i++ {
+ b.Lines = append(b.Lines, make(Line, width))
+ }
+ } else if height < len(b.Lines) {
+ b.Lines = b.Lines[:height]
+ }
+}
+
+// FillRect fills the buffer with the given cell and rectangle.
+func (b *Buffer) FillRect(c *Cell, rect Rectangle) {
+ cellWidth := 1
+ if c != nil && c.Width > 1 {
+ cellWidth = c.Width
+ }
+ for y := rect.Min.Y; y < rect.Max.Y; y++ {
+ for x := rect.Min.X; x < rect.Max.X; x += cellWidth {
+ b.setCell(x, y, c, false) //nolint:errcheck
+ }
+ }
+}
+
+// Fill fills the buffer with the given cell and rectangle.
+func (b *Buffer) Fill(c *Cell) {
+ b.FillRect(c, b.Bounds())
+}
+
+// Clear clears the buffer with space cells and rectangle.
+func (b *Buffer) Clear() {
+ b.ClearRect(b.Bounds())
+}
+
+// ClearRect clears the buffer with space cells within the specified
+// rectangles. Only cells within the rectangle's bounds are affected.
+func (b *Buffer) ClearRect(rect Rectangle) {
+ b.FillRect(nil, rect)
+}
+
+// InsertLine inserts n lines at the given line position, with the given
+// optional cell, within the specified rectangles. If no rectangles are
+// specified, it inserts lines in the entire buffer. Only cells within the
+// rectangle's horizontal bounds are affected. Lines are pushed out of the
+// rectangle bounds and lost. This follows terminal [ansi.IL] behavior.
+// It returns the pushed out lines.
+func (b *Buffer) InsertLine(y, n int, c *Cell) {
+ b.InsertLineRect(y, n, c, b.Bounds())
+}
+
+// InsertLineRect inserts new lines at the given line position, with the
+// given optional cell, within the rectangle bounds. Only cells within the
+// rectangle's horizontal bounds are affected. Lines are pushed out of the
+// rectangle bounds and lost. This follows terminal [ansi.IL] behavior.
+func (b *Buffer) InsertLineRect(y, n int, c *Cell, rect Rectangle) {
+ if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() {
+ return
+ }
+
+ // Limit number of lines to insert to available space
+ if y+n > rect.Max.Y {
+ n = rect.Max.Y - y
+ }
+
+ // Move existing lines down within the bounds
+ for i := rect.Max.Y - 1; i >= y+n; i-- {
+ for x := rect.Min.X; x < rect.Max.X; x++ {
+ // We don't need to clone c here because we're just moving lines down.
+ b.setCell(x, i, b.Lines[i-n][x], false)
+ }
+ }
+
+ // Clear the newly inserted lines within bounds
+ for i := y; i < y+n; i++ {
+ for x := rect.Min.X; x < rect.Max.X; x++ {
+ b.setCell(x, i, c, true)
+ }
+ }
+}
+
+// DeleteLineRect deletes lines at the given line position, with the given
+// optional cell, within the rectangle bounds. Only cells within the
+// rectangle's bounds are affected. Lines are shifted up within the bounds and
+// new blank lines are created at the bottom. This follows terminal [ansi.DL]
+// behavior.
+func (b *Buffer) DeleteLineRect(y, n int, c *Cell, rect Rectangle) {
+ if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() {
+ return
+ }
+
+ // Limit deletion count to available space in scroll region
+ if n > rect.Max.Y-y {
+ n = rect.Max.Y - y
+ }
+
+ // Shift cells up within the bounds
+ for dst := y; dst < rect.Max.Y-n; dst++ {
+ src := dst + n
+ for x := rect.Min.X; x < rect.Max.X; x++ {
+ // We don't need to clone c here because we're just moving cells up.
+ // b.lines[dst][x] = b.lines[src][x]
+ b.setCell(x, dst, b.Lines[src][x], false)
+ }
+ }
+
+ // Fill the bottom n lines with blank cells
+ for i := rect.Max.Y - n; i < rect.Max.Y; i++ {
+ for x := rect.Min.X; x < rect.Max.X; x++ {
+ b.setCell(x, i, c, true)
+ }
+ }
+}
+
+// DeleteLine deletes n lines at the given line position, with the given
+// optional cell, within the specified rectangles. If no rectangles are
+// specified, it deletes lines in the entire buffer.
+func (b *Buffer) DeleteLine(y, n int, c *Cell) {
+ b.DeleteLineRect(y, n, c, b.Bounds())
+}
+
+// InsertCell inserts new cells at the given position, with the given optional
+// cell, within the specified rectangles. If no rectangles are specified, it
+// inserts cells in the entire buffer. This follows terminal [ansi.ICH]
+// behavior.
+func (b *Buffer) InsertCell(x, y, n int, c *Cell) {
+ b.InsertCellRect(x, y, n, c, b.Bounds())
+}
+
+// InsertCellRect inserts new cells at the given position, with the given
+// optional cell, within the rectangle bounds. Only cells within the
+// rectangle's bounds are affected, following terminal [ansi.ICH] behavior.
+func (b *Buffer) InsertCellRect(x, y, n int, c *Cell, rect Rectangle) {
+ if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() ||
+ x < rect.Min.X || x >= rect.Max.X || x >= b.Width() {
+ return
+ }
+
+ // Limit number of cells to insert to available space
+ if x+n > rect.Max.X {
+ n = rect.Max.X - x
+ }
+
+ // Move existing cells within rectangle bounds to the right
+ for i := rect.Max.X - 1; i >= x+n && i-n >= rect.Min.X; i-- {
+ // We don't need to clone c here because we're just moving cells to the
+ // right.
+ // b.lines[y][i] = b.lines[y][i-n]
+ b.setCell(i, y, b.Lines[y][i-n], false)
+ }
+
+ // Clear the newly inserted cells within rectangle bounds
+ for i := x; i < x+n && i < rect.Max.X; i++ {
+ b.setCell(i, y, c, true)
+ }
+}
+
+// DeleteCell deletes cells at the given position, with the given optional
+// cell, within the specified rectangles. If no rectangles are specified, it
+// deletes cells in the entire buffer. This follows terminal [ansi.DCH]
+// behavior.
+func (b *Buffer) DeleteCell(x, y, n int, c *Cell) {
+ b.DeleteCellRect(x, y, n, c, b.Bounds())
+}
+
+// DeleteCellRect deletes cells at the given position, with the given
+// optional cell, within the rectangle bounds. Only cells within the
+// rectangle's bounds are affected, following terminal [ansi.DCH] behavior.
+func (b *Buffer) DeleteCellRect(x, y, n int, c *Cell, rect Rectangle) {
+ if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() ||
+ x < rect.Min.X || x >= rect.Max.X || x >= b.Width() {
+ return
+ }
+
+ // Calculate how many positions we can actually delete
+ remainingCells := rect.Max.X - x
+ if n > remainingCells {
+ n = remainingCells
+ }
+
+ // Shift the remaining cells to the left
+ for i := x; i < rect.Max.X-n; i++ {
+ if i+n < rect.Max.X {
+ // We don't need to clone c here because we're just moving cells to
+ // the left.
+ // b.lines[y][i] = b.lines[y][i+n]
+ b.setCell(i, y, b.Lines[y][i+n], false)
+ }
+ }
+
+ // Fill the vacated positions with the given cell
+ for i := rect.Max.X - n; i < rect.Max.X; i++ {
+ b.setCell(i, y, c, true)
+ }
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/cell.go b/vendor/github.com/charmbracelet/x/cellbuf/cell.go
new file mode 100644
index 0000000..991c919
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/cell.go
@@ -0,0 +1,503 @@
+package cellbuf
+
+import (
+ "github.com/charmbracelet/x/ansi"
+)
+
+var (
+ // BlankCell is a cell with a single space, width of 1, and no style or link.
+ BlankCell = Cell{Rune: ' ', Width: 1}
+
+ // EmptyCell is just an empty cell used for comparisons and as a placeholder
+ // for wide cells.
+ EmptyCell = Cell{}
+)
+
+// Cell represents a single cell in the terminal screen.
+type Cell struct {
+ // The style of the cell. Nil style means no style. Zero value prints a
+ // reset sequence.
+ Style Style
+
+ // Link is the hyperlink of the cell.
+ Link Link
+
+ // Comb is the combining runes of the cell. This is nil if the cell is a
+ // single rune or if it's a zero width cell that is part of a wider cell.
+ Comb []rune
+
+ // Width is the mono-space width of the grapheme cluster.
+ Width int
+
+ // Rune is the main rune of the cell. This is zero if the cell is part of a
+ // wider cell.
+ Rune rune
+}
+
+// Append appends runes to the cell without changing the width. This is useful
+// when we want to use the cell to store escape sequences or other runes that
+// don't affect the width of the cell.
+func (c *Cell) Append(r ...rune) {
+ for i, r := range r {
+ if i == 0 && c.Rune == 0 {
+ c.Rune = r
+ continue
+ }
+ c.Comb = append(c.Comb, r)
+ }
+}
+
+// String returns the string content of the cell excluding any styles, links,
+// and escape sequences.
+func (c Cell) String() string {
+ if c.Rune == 0 {
+ return ""
+ }
+ if len(c.Comb) == 0 {
+ return string(c.Rune)
+ }
+ return string(append([]rune{c.Rune}, c.Comb...))
+}
+
+// Equal returns whether the cell is equal to the other cell.
+func (c *Cell) Equal(o *Cell) bool {
+ return o != nil &&
+ c.Width == o.Width &&
+ c.Rune == o.Rune &&
+ runesEqual(c.Comb, o.Comb) &&
+ c.Style.Equal(&o.Style) &&
+ c.Link.Equal(&o.Link)
+}
+
+// Empty returns whether the cell is an empty cell. An empty cell is a cell
+// with a width of 0, a rune of 0, and no combining runes.
+func (c Cell) Empty() bool {
+ return c.Width == 0 &&
+ c.Rune == 0 &&
+ len(c.Comb) == 0
+}
+
+// Reset resets the cell to the default state zero value.
+func (c *Cell) Reset() {
+ c.Rune = 0
+ c.Comb = nil
+ c.Width = 0
+ c.Style.Reset()
+ c.Link.Reset()
+}
+
+// Clear returns whether the cell consists of only attributes that don't
+// affect appearance of a space character.
+func (c *Cell) Clear() bool {
+ return c.Rune == ' ' && len(c.Comb) == 0 && c.Width == 1 && c.Style.Clear() && c.Link.Empty()
+}
+
+// Clone returns a copy of the cell.
+func (c *Cell) Clone() (n *Cell) {
+ n = new(Cell)
+ *n = *c
+ return
+}
+
+// Blank makes the cell a blank cell by setting the rune to a space, comb to
+// nil, and the width to 1.
+func (c *Cell) Blank() *Cell {
+ c.Rune = ' '
+ c.Comb = nil
+ c.Width = 1
+ return c
+}
+
+// Link represents a hyperlink in the terminal screen.
+type Link struct {
+ URL string
+ Params string
+}
+
+// String returns a string representation of the hyperlink.
+func (h Link) String() string {
+ return h.URL
+}
+
+// Reset resets the hyperlink to the default state zero value.
+func (h *Link) Reset() {
+ h.URL = ""
+ h.Params = ""
+}
+
+// Equal returns whether the hyperlink is equal to the other hyperlink.
+func (h *Link) Equal(o *Link) bool {
+ return o != nil && h.URL == o.URL && h.Params == o.Params
+}
+
+// Empty returns whether the hyperlink is empty.
+func (h Link) Empty() bool {
+ return h.URL == "" && h.Params == ""
+}
+
+// AttrMask is a bitmask for text attributes that can change the look of text.
+// These attributes can be combined to create different styles.
+type AttrMask uint8
+
+// These are the available text attributes that can be combined to create
+// different styles.
+const (
+ BoldAttr AttrMask = 1 << iota
+ FaintAttr
+ ItalicAttr
+ SlowBlinkAttr
+ RapidBlinkAttr
+ ReverseAttr
+ ConcealAttr
+ StrikethroughAttr
+
+ ResetAttr AttrMask = 0
+)
+
+// UnderlineStyle is the style of underline to use for text.
+type UnderlineStyle = ansi.UnderlineStyle
+
+// These are the available underline styles.
+const (
+ NoUnderline = ansi.NoUnderlineStyle
+ SingleUnderline = ansi.SingleUnderlineStyle
+ DoubleUnderline = ansi.DoubleUnderlineStyle
+ CurlyUnderline = ansi.CurlyUnderlineStyle
+ DottedUnderline = ansi.DottedUnderlineStyle
+ DashedUnderline = ansi.DashedUnderlineStyle
+)
+
+// Style represents the Style of a cell.
+type Style struct {
+ Fg ansi.Color
+ Bg ansi.Color
+ Ul ansi.Color
+ Attrs AttrMask
+ UlStyle UnderlineStyle
+}
+
+// Sequence returns the ANSI sequence that sets the style.
+func (s Style) Sequence() string {
+ if s.Empty() {
+ return ansi.ResetStyle
+ }
+
+ var b ansi.Style
+
+ if s.Attrs != 0 {
+ if s.Attrs&BoldAttr != 0 {
+ b = b.Bold()
+ }
+ if s.Attrs&FaintAttr != 0 {
+ b = b.Faint()
+ }
+ if s.Attrs&ItalicAttr != 0 {
+ b = b.Italic()
+ }
+ if s.Attrs&SlowBlinkAttr != 0 {
+ b = b.SlowBlink()
+ }
+ if s.Attrs&RapidBlinkAttr != 0 {
+ b = b.RapidBlink()
+ }
+ if s.Attrs&ReverseAttr != 0 {
+ b = b.Reverse()
+ }
+ if s.Attrs&ConcealAttr != 0 {
+ b = b.Conceal()
+ }
+ if s.Attrs&StrikethroughAttr != 0 {
+ b = b.Strikethrough()
+ }
+ }
+ if s.UlStyle != NoUnderline {
+ switch s.UlStyle {
+ case SingleUnderline:
+ b = b.Underline()
+ case DoubleUnderline:
+ b = b.DoubleUnderline()
+ case CurlyUnderline:
+ b = b.CurlyUnderline()
+ case DottedUnderline:
+ b = b.DottedUnderline()
+ case DashedUnderline:
+ b = b.DashedUnderline()
+ }
+ }
+ if s.Fg != nil {
+ b = b.ForegroundColor(s.Fg)
+ }
+ if s.Bg != nil {
+ b = b.BackgroundColor(s.Bg)
+ }
+ if s.Ul != nil {
+ b = b.UnderlineColor(s.Ul)
+ }
+
+ return b.String()
+}
+
+// DiffSequence returns the ANSI sequence that sets the style as a diff from
+// another style.
+func (s Style) DiffSequence(o Style) string {
+ if o.Empty() {
+ return s.Sequence()
+ }
+
+ var b ansi.Style
+
+ if !colorEqual(s.Fg, o.Fg) {
+ b = b.ForegroundColor(s.Fg)
+ }
+
+ if !colorEqual(s.Bg, o.Bg) {
+ b = b.BackgroundColor(s.Bg)
+ }
+
+ if !colorEqual(s.Ul, o.Ul) {
+ b = b.UnderlineColor(s.Ul)
+ }
+
+ var (
+ noBlink bool
+ isNormal bool
+ )
+
+ if s.Attrs != o.Attrs {
+ if s.Attrs&BoldAttr != o.Attrs&BoldAttr {
+ if s.Attrs&BoldAttr != 0 {
+ b = b.Bold()
+ } else if !isNormal {
+ isNormal = true
+ b = b.NormalIntensity()
+ }
+ }
+ if s.Attrs&FaintAttr != o.Attrs&FaintAttr {
+ if s.Attrs&FaintAttr != 0 {
+ b = b.Faint()
+ } else if !isNormal {
+ b = b.NormalIntensity()
+ }
+ }
+ if s.Attrs&ItalicAttr != o.Attrs&ItalicAttr {
+ if s.Attrs&ItalicAttr != 0 {
+ b = b.Italic()
+ } else {
+ b = b.NoItalic()
+ }
+ }
+ if s.Attrs&SlowBlinkAttr != o.Attrs&SlowBlinkAttr {
+ if s.Attrs&SlowBlinkAttr != 0 {
+ b = b.SlowBlink()
+ } else if !noBlink {
+ noBlink = true
+ b = b.NoBlink()
+ }
+ }
+ if s.Attrs&RapidBlinkAttr != o.Attrs&RapidBlinkAttr {
+ if s.Attrs&RapidBlinkAttr != 0 {
+ b = b.RapidBlink()
+ } else if !noBlink {
+ b = b.NoBlink()
+ }
+ }
+ if s.Attrs&ReverseAttr != o.Attrs&ReverseAttr {
+ if s.Attrs&ReverseAttr != 0 {
+ b = b.Reverse()
+ } else {
+ b = b.NoReverse()
+ }
+ }
+ if s.Attrs&ConcealAttr != o.Attrs&ConcealAttr {
+ if s.Attrs&ConcealAttr != 0 {
+ b = b.Conceal()
+ } else {
+ b = b.NoConceal()
+ }
+ }
+ if s.Attrs&StrikethroughAttr != o.Attrs&StrikethroughAttr {
+ if s.Attrs&StrikethroughAttr != 0 {
+ b = b.Strikethrough()
+ } else {
+ b = b.NoStrikethrough()
+ }
+ }
+ }
+
+ if s.UlStyle != o.UlStyle {
+ b = b.UnderlineStyle(s.UlStyle)
+ }
+
+ return b.String()
+}
+
+// Equal returns true if the style is equal to the other style.
+func (s *Style) Equal(o *Style) bool {
+ return s.Attrs == o.Attrs &&
+ s.UlStyle == o.UlStyle &&
+ colorEqual(s.Fg, o.Fg) &&
+ colorEqual(s.Bg, o.Bg) &&
+ colorEqual(s.Ul, o.Ul)
+}
+
+func colorEqual(c, o ansi.Color) bool {
+ if c == nil && o == nil {
+ return true
+ }
+ if c == nil || o == nil {
+ return false
+ }
+ cr, cg, cb, ca := c.RGBA()
+ or, og, ob, oa := o.RGBA()
+ return cr == or && cg == og && cb == ob && ca == oa
+}
+
+// Bold sets the bold attribute.
+func (s *Style) Bold(v bool) *Style {
+ if v {
+ s.Attrs |= BoldAttr
+ } else {
+ s.Attrs &^= BoldAttr
+ }
+ return s
+}
+
+// Faint sets the faint attribute.
+func (s *Style) Faint(v bool) *Style {
+ if v {
+ s.Attrs |= FaintAttr
+ } else {
+ s.Attrs &^= FaintAttr
+ }
+ return s
+}
+
+// Italic sets the italic attribute.
+func (s *Style) Italic(v bool) *Style {
+ if v {
+ s.Attrs |= ItalicAttr
+ } else {
+ s.Attrs &^= ItalicAttr
+ }
+ return s
+}
+
+// SlowBlink sets the slow blink attribute.
+func (s *Style) SlowBlink(v bool) *Style {
+ if v {
+ s.Attrs |= SlowBlinkAttr
+ } else {
+ s.Attrs &^= SlowBlinkAttr
+ }
+ return s
+}
+
+// RapidBlink sets the rapid blink attribute.
+func (s *Style) RapidBlink(v bool) *Style {
+ if v {
+ s.Attrs |= RapidBlinkAttr
+ } else {
+ s.Attrs &^= RapidBlinkAttr
+ }
+ return s
+}
+
+// Reverse sets the reverse attribute.
+func (s *Style) Reverse(v bool) *Style {
+ if v {
+ s.Attrs |= ReverseAttr
+ } else {
+ s.Attrs &^= ReverseAttr
+ }
+ return s
+}
+
+// Conceal sets the conceal attribute.
+func (s *Style) Conceal(v bool) *Style {
+ if v {
+ s.Attrs |= ConcealAttr
+ } else {
+ s.Attrs &^= ConcealAttr
+ }
+ return s
+}
+
+// Strikethrough sets the strikethrough attribute.
+func (s *Style) Strikethrough(v bool) *Style {
+ if v {
+ s.Attrs |= StrikethroughAttr
+ } else {
+ s.Attrs &^= StrikethroughAttr
+ }
+ return s
+}
+
+// UnderlineStyle sets the underline style.
+func (s *Style) UnderlineStyle(style UnderlineStyle) *Style {
+ s.UlStyle = style
+ return s
+}
+
+// Underline sets the underline attribute.
+// This is a syntactic sugar for [UnderlineStyle].
+func (s *Style) Underline(v bool) *Style {
+ if v {
+ return s.UnderlineStyle(SingleUnderline)
+ }
+ return s.UnderlineStyle(NoUnderline)
+}
+
+// Foreground sets the foreground color.
+func (s *Style) Foreground(c ansi.Color) *Style {
+ s.Fg = c
+ return s
+}
+
+// Background sets the background color.
+func (s *Style) Background(c ansi.Color) *Style {
+ s.Bg = c
+ return s
+}
+
+// UnderlineColor sets the underline color.
+func (s *Style) UnderlineColor(c ansi.Color) *Style {
+ s.Ul = c
+ return s
+}
+
+// Reset resets the style to default.
+func (s *Style) Reset() *Style {
+ s.Fg = nil
+ s.Bg = nil
+ s.Ul = nil
+ s.Attrs = ResetAttr
+ s.UlStyle = NoUnderline
+ return s
+}
+
+// Empty returns true if the style is empty.
+func (s *Style) Empty() bool {
+ return s.Fg == nil && s.Bg == nil && s.Ul == nil && s.Attrs == ResetAttr && s.UlStyle == NoUnderline
+}
+
+// Clear returns whether the style consists of only attributes that don't
+// affect appearance of a space character.
+func (s *Style) Clear() bool {
+ return s.UlStyle == NoUnderline &&
+ s.Attrs&^(BoldAttr|FaintAttr|ItalicAttr|SlowBlinkAttr|RapidBlinkAttr) == 0 &&
+ s.Fg == nil &&
+ s.Bg == nil &&
+ s.Ul == nil
+}
+
+func runesEqual(a, b []rune) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i, r := range a {
+ if r != b[i] {
+ return false
+ }
+ }
+ return true
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/errors.go b/vendor/github.com/charmbracelet/x/cellbuf/errors.go
new file mode 100644
index 0000000..64258fe
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/errors.go
@@ -0,0 +1,6 @@
+package cellbuf
+
+import "errors"
+
+// ErrOutOfBounds is returned when the given x, y position is out of bounds.
+var ErrOutOfBounds = errors.New("out of bounds")
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/geom.go b/vendor/github.com/charmbracelet/x/cellbuf/geom.go
new file mode 100644
index 0000000..c12e6fb
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/geom.go
@@ -0,0 +1,21 @@
+package cellbuf
+
+import (
+ "image"
+)
+
+// Position represents an x, y position.
+type Position = image.Point
+
+// Pos is a shorthand for Position{X: x, Y: y}.
+func Pos(x, y int) Position {
+ return image.Pt(x, y)
+}
+
+// Rectange represents a rectangle.
+type Rectangle = image.Rectangle
+
+// Rect is a shorthand for Rectangle.
+func Rect(x, y, w, h int) Rectangle {
+ return image.Rect(x, y, x+w, y+h)
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/hardscroll.go b/vendor/github.com/charmbracelet/x/cellbuf/hardscroll.go
new file mode 100644
index 0000000..402ac06
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/hardscroll.go
@@ -0,0 +1,272 @@
+package cellbuf
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/x/ansi"
+)
+
+// scrollOptimize optimizes the screen to transform the old buffer into the new
+// buffer.
+func (s *Screen) scrollOptimize() {
+ height := s.newbuf.Height()
+ if s.oldnum == nil || len(s.oldnum) < height {
+ s.oldnum = make([]int, height)
+ }
+
+ // Calculate the indices
+ s.updateHashmap()
+ if len(s.hashtab) < height {
+ return
+ }
+
+ // Pass 1 - from top to bottom scrolling up
+ for i := 0; i < height; {
+ for i < height && (s.oldnum[i] == newIndex || s.oldnum[i] <= i) {
+ i++
+ }
+ if i >= height {
+ break
+ }
+
+ shift := s.oldnum[i] - i // shift > 0
+ start := i
+
+ i++
+ for i < height && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
+ i++
+ }
+ end := i - 1 + shift
+
+ if !s.scrolln(shift, start, end, height-1) {
+ continue
+ }
+ }
+
+ // Pass 2 - from bottom to top scrolling down
+ for i := height - 1; i >= 0; {
+ for i >= 0 && (s.oldnum[i] == newIndex || s.oldnum[i] >= i) {
+ i--
+ }
+ if i < 0 {
+ break
+ }
+
+ shift := s.oldnum[i] - i // shift < 0
+ end := i
+
+ i--
+ for i >= 0 && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
+ i--
+ }
+
+ start := i + 1 - (-shift)
+ if !s.scrolln(shift, start, end, height-1) {
+ continue
+ }
+ }
+}
+
+// scrolln scrolls the screen up by n lines.
+func (s *Screen) scrolln(n, top, bot, maxY int) (v bool) { //nolint:unparam
+ const (
+ nonDestScrollRegion = false
+ memoryBelow = false
+ )
+
+ blank := s.clearBlank()
+ if n > 0 {
+ // Scroll up (forward)
+ v = s.scrollUp(n, top, bot, 0, maxY, blank)
+ if !v {
+ s.buf.WriteString(ansi.SetTopBottomMargins(top+1, bot+1))
+
+ // XXX: How should we handle this in inline mode when not using alternate screen?
+ s.cur.X, s.cur.Y = -1, -1
+ v = s.scrollUp(n, top, bot, top, bot, blank)
+ s.buf.WriteString(ansi.SetTopBottomMargins(1, maxY+1))
+ s.cur.X, s.cur.Y = -1, -1
+ }
+
+ if !v {
+ v = s.scrollIdl(n, top, bot-n+1, blank)
+ }
+
+ // Clear newly shifted-in lines.
+ if v &&
+ (nonDestScrollRegion || (memoryBelow && bot == maxY)) {
+ if bot == maxY {
+ s.move(0, bot-n+1)
+ s.clearToBottom(nil)
+ } else {
+ for i := 0; i < n; i++ {
+ s.move(0, bot-i)
+ s.clearToEnd(nil, false)
+ }
+ }
+ }
+ } else if n < 0 {
+ // Scroll down (backward)
+ v = s.scrollDown(-n, top, bot, 0, maxY, blank)
+ if !v {
+ s.buf.WriteString(ansi.SetTopBottomMargins(top+1, bot+1))
+
+ // XXX: How should we handle this in inline mode when not using alternate screen?
+ s.cur.X, s.cur.Y = -1, -1
+ v = s.scrollDown(-n, top, bot, top, bot, blank)
+ s.buf.WriteString(ansi.SetTopBottomMargins(1, maxY+1))
+ s.cur.X, s.cur.Y = -1, -1
+
+ if !v {
+ v = s.scrollIdl(-n, bot+n+1, top, blank)
+ }
+
+ // Clear newly shifted-in lines.
+ if v &&
+ (nonDestScrollRegion || (memoryBelow && top == 0)) {
+ for i := 0; i < -n; i++ {
+ s.move(0, top+i)
+ s.clearToEnd(nil, false)
+ }
+ }
+ }
+ }
+
+ if !v {
+ return
+ }
+
+ s.scrollBuffer(s.curbuf, n, top, bot, blank)
+
+ // shift hash values too, they can be reused
+ s.scrollOldhash(n, top, bot)
+
+ return true
+}
+
+// scrollBuffer scrolls the buffer by n lines.
+func (s *Screen) scrollBuffer(b *Buffer, n, top, bot int, blank *Cell) {
+ if top < 0 || bot < top || bot >= b.Height() {
+ // Nothing to scroll
+ return
+ }
+
+ if n < 0 {
+ // shift n lines downwards
+ limit := top - n
+ for line := bot; line >= limit && line >= 0 && line >= top; line-- {
+ copy(b.Lines[line], b.Lines[line+n])
+ }
+ for line := top; line < limit && line <= b.Height()-1 && line <= bot; line++ {
+ b.FillRect(blank, Rect(0, line, b.Width(), 1))
+ }
+ }
+
+ if n > 0 {
+ // shift n lines upwards
+ limit := bot - n
+ for line := top; line <= limit && line <= b.Height()-1 && line <= bot; line++ {
+ copy(b.Lines[line], b.Lines[line+n])
+ }
+ for line := bot; line > limit && line >= 0 && line >= top; line-- {
+ b.FillRect(blank, Rect(0, line, b.Width(), 1))
+ }
+ }
+
+ s.touchLine(b.Width(), b.Height(), top, bot-top+1, true)
+}
+
+// touchLine marks the line as touched.
+func (s *Screen) touchLine(width, height, y, n int, changed bool) {
+ if n < 0 || y < 0 || y >= height {
+ return // Nothing to touch
+ }
+
+ for i := y; i < y+n && i < height; i++ {
+ if changed {
+ s.touch[i] = lineData{firstCell: 0, lastCell: width - 1}
+ } else {
+ delete(s.touch, i)
+ }
+ }
+}
+
+// scrollUp scrolls the screen up by n lines.
+func (s *Screen) scrollUp(n, top, bot, minY, maxY int, blank *Cell) bool {
+ if n == 1 && top == minY && bot == maxY {
+ s.move(0, bot)
+ s.updatePen(blank)
+ s.buf.WriteByte('\n')
+ } else if n == 1 && bot == maxY {
+ s.move(0, top)
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.DeleteLine(1))
+ } else if top == minY && bot == maxY {
+ if s.xtermLike {
+ s.move(0, bot)
+ } else {
+ s.move(0, top)
+ }
+ s.updatePen(blank)
+ if s.xtermLike {
+ s.buf.WriteString(ansi.ScrollUp(n))
+ } else {
+ s.buf.WriteString(strings.Repeat("\n", n))
+ }
+ } else if bot == maxY {
+ s.move(0, top)
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.DeleteLine(n))
+ } else {
+ return false
+ }
+ return true
+}
+
+// scrollDown scrolls the screen down by n lines.
+func (s *Screen) scrollDown(n, top, bot, minY, maxY int, blank *Cell) bool {
+ if n == 1 && top == minY && bot == maxY {
+ s.move(0, top)
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.ReverseIndex)
+ } else if n == 1 && bot == maxY {
+ s.move(0, top)
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.InsertLine(1))
+ } else if top == minY && bot == maxY {
+ s.move(0, top)
+ s.updatePen(blank)
+ if s.xtermLike {
+ s.buf.WriteString(ansi.ScrollDown(n))
+ } else {
+ s.buf.WriteString(strings.Repeat(ansi.ReverseIndex, n))
+ }
+ } else if bot == maxY {
+ s.move(0, top)
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.InsertLine(n))
+ } else {
+ return false
+ }
+ return true
+}
+
+// scrollIdl scrolls the screen n lines by using [ansi.DL] at del and using
+// [ansi.IL] at ins.
+func (s *Screen) scrollIdl(n, del, ins int, blank *Cell) bool {
+ if n < 0 {
+ return false
+ }
+
+ // Delete lines
+ s.move(0, del)
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.DeleteLine(n))
+
+ // Insert lines
+ s.move(0, ins)
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.InsertLine(n))
+
+ return true
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/hashmap.go b/vendor/github.com/charmbracelet/x/cellbuf/hashmap.go
new file mode 100644
index 0000000..0d25b54
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/hashmap.go
@@ -0,0 +1,301 @@
+package cellbuf
+
+import (
+ "github.com/charmbracelet/x/ansi"
+)
+
+// hash returns the hash value of a [Line].
+func hash(l Line) (h uint64) {
+ for _, c := range l {
+ var r rune
+ if c == nil {
+ r = ansi.SP
+ } else {
+ r = c.Rune
+ }
+ h += (h << 5) + uint64(r)
+ }
+ return
+}
+
+// hashmap represents a single [Line] hash.
+type hashmap struct {
+ value uint64
+ oldcount, newcount int
+ oldindex, newindex int
+}
+
+// The value used to indicate lines created by insertions and scrolls.
+const newIndex = -1
+
+// updateHashmap updates the hashmap with the new hash value.
+func (s *Screen) updateHashmap() {
+ height := s.newbuf.Height()
+ if len(s.oldhash) >= height && len(s.newhash) >= height {
+ // rehash changed lines
+ for i := 0; i < height; i++ {
+ _, ok := s.touch[i]
+ if ok {
+ s.oldhash[i] = hash(s.curbuf.Line(i))
+ s.newhash[i] = hash(s.newbuf.Line(i))
+ }
+ }
+ } else {
+ // rehash all
+ if len(s.oldhash) != height {
+ s.oldhash = make([]uint64, height)
+ }
+ if len(s.newhash) != height {
+ s.newhash = make([]uint64, height)
+ }
+ for i := 0; i < height; i++ {
+ s.oldhash[i] = hash(s.curbuf.Line(i))
+ s.newhash[i] = hash(s.newbuf.Line(i))
+ }
+ }
+
+ s.hashtab = make([]hashmap, height*2)
+ for i := 0; i < height; i++ {
+ hashval := s.oldhash[i]
+
+ // Find matching hash or empty slot
+ idx := 0
+ for idx < len(s.hashtab) && s.hashtab[idx].value != 0 {
+ if s.hashtab[idx].value == hashval {
+ break
+ }
+ idx++
+ }
+
+ s.hashtab[idx].value = hashval // in case this is a new hash
+ s.hashtab[idx].oldcount++
+ s.hashtab[idx].oldindex = i
+ }
+ for i := 0; i < height; i++ {
+ hashval := s.newhash[i]
+
+ // Find matching hash or empty slot
+ idx := 0
+ for idx < len(s.hashtab) && s.hashtab[idx].value != 0 {
+ if s.hashtab[idx].value == hashval {
+ break
+ }
+ idx++
+ }
+
+ s.hashtab[idx].value = hashval // in case this is a new hash
+ s.hashtab[idx].newcount++
+ s.hashtab[idx].newindex = i
+
+ s.oldnum[i] = newIndex // init old indices slice
+ }
+
+ // Mark line pair corresponding to unique hash pairs.
+ for i := 0; i < len(s.hashtab) && s.hashtab[i].value != 0; i++ {
+ hsp := &s.hashtab[i]
+ if hsp.oldcount == 1 && hsp.newcount == 1 && hsp.oldindex != hsp.newindex {
+ s.oldnum[hsp.newindex] = hsp.oldindex
+ }
+ }
+
+ s.growHunks()
+
+ // Eliminate bad or impossible shifts. This includes removing those hunks
+ // which could not grow because of conflicts, as well those which are to be
+ // moved too far, they are likely to destroy more than carry.
+ for i := 0; i < height; {
+ var start, shift, size int
+ for i < height && s.oldnum[i] == newIndex {
+ i++
+ }
+ if i >= height {
+ break
+ }
+ start = i
+ shift = s.oldnum[i] - i
+ i++
+ for i < height && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
+ i++
+ }
+ size = i - start
+ if size < 3 || size+min(size/8, 2) < abs(shift) {
+ for start < i {
+ s.oldnum[start] = newIndex
+ start++
+ }
+ }
+ }
+
+ // After clearing invalid hunks, try grow the rest.
+ s.growHunks()
+}
+
+// scrollOldhash
+func (s *Screen) scrollOldhash(n, top, bot int) {
+ if len(s.oldhash) == 0 {
+ return
+ }
+
+ size := bot - top + 1 - abs(n)
+ if n > 0 {
+ // Move existing hashes up
+ copy(s.oldhash[top:], s.oldhash[top+n:top+n+size])
+ // Recalculate hashes for newly shifted-in lines
+ for i := bot; i > bot-n; i-- {
+ s.oldhash[i] = hash(s.curbuf.Line(i))
+ }
+ } else {
+ // Move existing hashes down
+ copy(s.oldhash[top-n:], s.oldhash[top:top+size])
+ // Recalculate hashes for newly shifted-in lines
+ for i := top; i < top-n; i++ {
+ s.oldhash[i] = hash(s.curbuf.Line(i))
+ }
+ }
+}
+
+func (s *Screen) growHunks() {
+ var (
+ backLimit int // limits for cells to fill
+ backRefLimit int // limit for references
+ i int
+ nextHunk int
+ )
+
+ height := s.newbuf.Height()
+ for i < height && s.oldnum[i] == newIndex {
+ i++
+ }
+ for ; i < height; i = nextHunk {
+ var (
+ forwardLimit int
+ forwardRefLimit int
+ end int
+ start = i
+ shift = s.oldnum[i] - i
+ )
+
+ // get forward limit
+ i = start + 1
+ for i < height &&
+ s.oldnum[i] != newIndex &&
+ s.oldnum[i]-i == shift {
+ i++
+ }
+
+ end = i
+ for i < height && s.oldnum[i] == newIndex {
+ i++
+ }
+
+ nextHunk = i
+ forwardLimit = i
+ if i >= height || s.oldnum[i] >= i {
+ forwardRefLimit = i
+ } else {
+ forwardRefLimit = s.oldnum[i]
+ }
+
+ i = start - 1
+
+ // grow back
+ if shift < 0 {
+ backLimit = backRefLimit + (-shift)
+ }
+ for i >= backLimit {
+ if s.newhash[i] == s.oldhash[i+shift] ||
+ s.costEffective(i+shift, i, shift < 0) {
+ s.oldnum[i] = i + shift
+ } else {
+ break
+ }
+ i--
+ }
+
+ i = end
+ // grow forward
+ if shift > 0 {
+ forwardLimit = forwardRefLimit - shift
+ }
+ for i < forwardLimit {
+ if s.newhash[i] == s.oldhash[i+shift] ||
+ s.costEffective(i+shift, i, shift > 0) {
+ s.oldnum[i] = i + shift
+ } else {
+ break
+ }
+ i++
+ }
+
+ backLimit = i
+ backRefLimit = backLimit
+ if shift > 0 {
+ backRefLimit += shift
+ }
+ }
+}
+
+// costEffective returns true if the cost of moving line 'from' to line 'to' seems to be
+// cost effective. 'blank' indicates whether the line 'to' would become blank.
+func (s *Screen) costEffective(from, to int, blank bool) bool {
+ if from == to {
+ return false
+ }
+
+ newFrom := s.oldnum[from]
+ if newFrom == newIndex {
+ newFrom = from
+ }
+
+ // On the left side of >= is the cost before moving. On the right side --
+ // cost after moving.
+
+ // Calculate costs before moving.
+ var costBeforeMove int
+ if blank {
+ // Cost of updating blank line at destination.
+ costBeforeMove = s.updateCostBlank(s.newbuf.Line(to))
+ } else {
+ // Cost of updating exiting line at destination.
+ costBeforeMove = s.updateCost(s.curbuf.Line(to), s.newbuf.Line(to))
+ }
+
+ // Add cost of updating source line
+ costBeforeMove += s.updateCost(s.curbuf.Line(newFrom), s.newbuf.Line(from))
+
+ // Calculate costs after moving.
+ var costAfterMove int
+ if newFrom == from {
+ // Source becomes blank after move
+ costAfterMove = s.updateCostBlank(s.newbuf.Line(from))
+ } else {
+ // Source gets updated from another line
+ costAfterMove = s.updateCost(s.curbuf.Line(newFrom), s.newbuf.Line(from))
+ }
+
+ // Add cost of moving source line to destination
+ costAfterMove += s.updateCost(s.curbuf.Line(from), s.newbuf.Line(to))
+
+ // Return true if moving is cost effective (costs less or equal)
+ return costBeforeMove >= costAfterMove
+}
+
+func (s *Screen) updateCost(from, to Line) (cost int) {
+ var fidx, tidx int
+ for i := s.newbuf.Width() - 1; i > 0; i, fidx, tidx = i-1, fidx+1, tidx+1 {
+ if !cellEqual(from.At(fidx), to.At(tidx)) {
+ cost++
+ }
+ }
+ return
+}
+
+func (s *Screen) updateCostBlank(to Line) (cost int) {
+ var tidx int
+ for i := s.newbuf.Width() - 1; i > 0; i, tidx = i-1, tidx+1 {
+ if !cellEqual(nil, to.At(tidx)) {
+ cost++
+ }
+ }
+ return
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/link.go b/vendor/github.com/charmbracelet/x/cellbuf/link.go
new file mode 100644
index 0000000..112f8e8
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/link.go
@@ -0,0 +1,14 @@
+package cellbuf
+
+import (
+ "github.com/charmbracelet/colorprofile"
+)
+
+// Convert converts a hyperlink to respect the given color profile.
+func ConvertLink(h Link, p colorprofile.Profile) Link {
+ if p == colorprofile.NoTTY {
+ return Link{}
+ }
+
+ return h
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/screen.go b/vendor/github.com/charmbracelet/x/cellbuf/screen.go
new file mode 100644
index 0000000..963b9ca
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/screen.go
@@ -0,0 +1,1457 @@
+package cellbuf
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "os"
+ "strings"
+ "sync"
+
+ "github.com/charmbracelet/colorprofile"
+ "github.com/charmbracelet/x/ansi"
+ "github.com/charmbracelet/x/term"
+)
+
+// ErrInvalidDimensions is returned when the dimensions of a window are invalid
+// for the operation.
+var ErrInvalidDimensions = errors.New("invalid dimensions")
+
+// notLocal returns whether the coordinates are not considered local movement
+// using the defined thresholds.
+// This takes the number of columns, and the coordinates of the current and
+// target positions.
+func notLocal(cols, fx, fy, tx, ty int) bool {
+ // The typical distance for a [ansi.CUP] sequence. Anything less than this
+ // is considered local movement.
+ const longDist = 8 - 1
+ return (tx > longDist) &&
+ (tx < cols-1-longDist) &&
+ (abs(ty-fy)+abs(tx-fx) > longDist)
+}
+
+// relativeCursorMove returns the relative cursor movement sequence using one or two
+// of the following sequences [ansi.CUU], [ansi.CUD], [ansi.CUF], [ansi.CUB],
+// [ansi.VPA], [ansi.HPA].
+// When overwrite is true, this will try to optimize the sequence by using the
+// screen cells values to move the cursor instead of using escape sequences.
+func relativeCursorMove(s *Screen, fx, fy, tx, ty int, overwrite, useTabs, useBackspace bool) string {
+ var seq strings.Builder
+
+ width, height := s.newbuf.Width(), s.newbuf.Height()
+ if ty != fy {
+ var yseq string
+ if s.xtermLike && !s.opts.RelativeCursor {
+ yseq = ansi.VerticalPositionAbsolute(ty + 1)
+ }
+
+ // OPTIM: Use [ansi.LF] and [ansi.ReverseIndex] as optimizations.
+
+ if ty > fy {
+ n := ty - fy
+ if cud := ansi.CursorDown(n); yseq == "" || len(cud) < len(yseq) {
+ yseq = cud
+ }
+ shouldScroll := !s.opts.AltScreen && fy+n >= s.scrollHeight
+ if lf := strings.Repeat("\n", n); shouldScroll || (fy+n < height && len(lf) < len(yseq)) {
+ // TODO: Ensure we're not unintentionally scrolling the screen down.
+ yseq = lf
+ s.scrollHeight = max(s.scrollHeight, fy+n)
+ }
+ } else if ty < fy {
+ n := fy - ty
+ if cuu := ansi.CursorUp(n); yseq == "" || len(cuu) < len(yseq) {
+ yseq = cuu
+ }
+ if n == 1 && fy-1 > 0 {
+ // TODO: Ensure we're not unintentionally scrolling the screen up.
+ yseq = ansi.ReverseIndex
+ }
+ }
+
+ seq.WriteString(yseq)
+ }
+
+ if tx != fx {
+ var xseq string
+ if s.xtermLike && !s.opts.RelativeCursor {
+ xseq = ansi.HorizontalPositionAbsolute(tx + 1)
+ }
+
+ if tx > fx {
+ n := tx - fx
+ if useTabs {
+ var tabs int
+ var col int
+ for col = fx; s.tabs.Next(col) <= tx; col = s.tabs.Next(col) {
+ tabs++
+ if col == s.tabs.Next(col) || col >= width-1 {
+ break
+ }
+ }
+
+ if tabs > 0 {
+ cht := ansi.CursorHorizontalForwardTab(tabs)
+ tab := strings.Repeat("\t", tabs)
+ if false && s.xtermLike && len(cht) < len(tab) {
+ // TODO: The linux console and some terminals such as
+ // Alacritty don't support [ansi.CHT]. Enable this when
+ // we have a way to detect this, or after 5 years when
+ // we're sure everyone has updated their terminals :P
+ seq.WriteString(cht)
+ } else {
+ seq.WriteString(tab)
+ }
+
+ n = tx - col
+ fx = col
+ }
+ }
+
+ if cuf := ansi.CursorForward(n); xseq == "" || len(cuf) < len(xseq) {
+ xseq = cuf
+ }
+
+ // If we have no attribute and style changes, overwrite is cheaper.
+ var ovw string
+ if overwrite && ty >= 0 {
+ for i := 0; i < n; i++ {
+ cell := s.newbuf.Cell(fx+i, ty)
+ if cell != nil && cell.Width > 0 {
+ i += cell.Width - 1
+ if !cell.Style.Equal(&s.cur.Style) || !cell.Link.Equal(&s.cur.Link) {
+ overwrite = false
+ break
+ }
+ }
+ }
+ }
+
+ if overwrite && ty >= 0 {
+ for i := 0; i < n; i++ {
+ cell := s.newbuf.Cell(fx+i, ty)
+ if cell != nil && cell.Width > 0 {
+ ovw += cell.String()
+ i += cell.Width - 1
+ } else {
+ ovw += " "
+ }
+ }
+ }
+
+ if overwrite && len(ovw) < len(xseq) {
+ xseq = ovw
+ }
+ } else if tx < fx {
+ n := fx - tx
+ if useTabs && s.xtermLike {
+ // VT100 does not support backward tabs [ansi.CBT].
+
+ col := fx
+
+ var cbt int // cursor backward tabs count
+ for s.tabs.Prev(col) >= tx {
+ col = s.tabs.Prev(col)
+ cbt++
+ if col == s.tabs.Prev(col) || col <= 0 {
+ break
+ }
+ }
+
+ if cbt > 0 {
+ seq.WriteString(ansi.CursorBackwardTab(cbt))
+ n = col - tx
+ }
+ }
+
+ if cub := ansi.CursorBackward(n); xseq == "" || len(cub) < len(xseq) {
+ xseq = cub
+ }
+
+ if useBackspace && n < len(xseq) {
+ xseq = strings.Repeat("\b", n)
+ }
+ }
+
+ seq.WriteString(xseq)
+ }
+
+ return seq.String()
+}
+
+// moveCursor moves and returns the cursor movement sequence to move the cursor
+// to the specified position.
+// When overwrite is true, this will try to optimize the sequence by using the
+// screen cells values to move the cursor instead of using escape sequences.
+func moveCursor(s *Screen, x, y int, overwrite bool) (seq string) {
+ fx, fy := s.cur.X, s.cur.Y
+
+ if !s.opts.RelativeCursor {
+ // Method #0: Use [ansi.CUP] if the distance is long.
+ seq = ansi.CursorPosition(x+1, y+1)
+ if fx == -1 || fy == -1 || notLocal(s.newbuf.Width(), fx, fy, x, y) {
+ return
+ }
+ }
+
+ // Optimize based on options.
+ trials := 0
+ if s.opts.HardTabs {
+ trials |= 2 // 0b10 in binary
+ }
+ if s.opts.Backspace {
+ trials |= 1 // 0b01 in binary
+ }
+
+ // Try all possible combinations of hard tabs and backspace optimizations.
+ for i := 0; i <= trials; i++ {
+ // Skip combinations that are not enabled.
+ if i & ^trials != 0 {
+ continue
+ }
+
+ useHardTabs := i&2 != 0
+ useBackspace := i&1 != 0
+
+ // Method #1: Use local movement sequences.
+ nseq := relativeCursorMove(s, fx, fy, x, y, overwrite, useHardTabs, useBackspace)
+ if (i == 0 && len(seq) == 0) || len(nseq) < len(seq) {
+ seq = nseq
+ }
+
+ // Method #2: Use [ansi.CR] and local movement sequences.
+ nseq = "\r" + relativeCursorMove(s, 0, fy, x, y, overwrite, useHardTabs, useBackspace)
+ if len(nseq) < len(seq) {
+ seq = nseq
+ }
+
+ if !s.opts.RelativeCursor {
+ // Method #3: Use [ansi.CursorHomePosition] and local movement sequences.
+ nseq = ansi.CursorHomePosition + relativeCursorMove(s, 0, 0, x, y, overwrite, useHardTabs, useBackspace)
+ if len(nseq) < len(seq) {
+ seq = nseq
+ }
+ }
+ }
+
+ return
+}
+
+// moveCursor moves the cursor to the specified position.
+func (s *Screen) moveCursor(x, y int, overwrite bool) {
+ if !s.opts.AltScreen && s.cur.X == -1 && s.cur.Y == -1 {
+ // First cursor movement in inline mode, move the cursor to the first
+ // column before moving to the target position.
+ s.buf.WriteByte('\r') //nolint:errcheck
+ s.cur.X, s.cur.Y = 0, 0
+ }
+ s.buf.WriteString(moveCursor(s, x, y, overwrite)) //nolint:errcheck
+ s.cur.X, s.cur.Y = x, y
+}
+
+func (s *Screen) move(x, y int) {
+ // XXX: Make sure we use the max height and width of the buffer in case
+ // we're in the middle of a resize operation.
+ width := max(s.newbuf.Width(), s.curbuf.Width())
+ height := max(s.newbuf.Height(), s.curbuf.Height())
+
+ if width > 0 && x >= width {
+ // Handle autowrap
+ y += (x / width)
+ x %= width
+ }
+
+ // XXX: Disable styles if there's any
+ // Some move operations such as [ansi.LF] can apply styles to the new
+ // cursor position, thus, we need to reset the styles before moving the
+ // cursor.
+ blank := s.clearBlank()
+ resetPen := y != s.cur.Y && !blank.Equal(&BlankCell)
+ if resetPen {
+ s.updatePen(nil)
+ }
+
+ // Reset wrap around (phantom cursor) state
+ if s.atPhantom {
+ s.cur.X = 0
+ s.buf.WriteByte('\r') //nolint:errcheck
+ s.atPhantom = false // reset phantom cell state
+ }
+
+ // TODO: Investigate if we need to handle this case and/or if we need the
+ // following code.
+ //
+ // if width > 0 && s.cur.X >= width {
+ // l := (s.cur.X + 1) / width
+ //
+ // s.cur.Y += l
+ // if height > 0 && s.cur.Y >= height {
+ // l -= s.cur.Y - height - 1
+ // }
+ //
+ // if l > 0 {
+ // s.cur.X = 0
+ // s.buf.WriteString("\r" + strings.Repeat("\n", l)) //nolint:errcheck
+ // }
+ // }
+
+ if height > 0 {
+ if s.cur.Y > height-1 {
+ s.cur.Y = height - 1
+ }
+ if y > height-1 {
+ y = height - 1
+ }
+ }
+
+ if x == s.cur.X && y == s.cur.Y {
+ // We give up later because we need to run checks for the phantom cell
+ // and others before we can determine if we can give up.
+ return
+ }
+
+ // We set the new cursor in [Screen.moveCursor].
+ s.moveCursor(x, y, true) // Overwrite cells if possible
+}
+
+// Cursor represents a terminal Cursor.
+type Cursor struct {
+ Style
+ Link
+ Position
+}
+
+// ScreenOptions are options for the screen.
+type ScreenOptions struct {
+ // Term is the terminal type to use when writing to the screen. When empty,
+ // `$TERM` is used from [os.Getenv].
+ Term string
+ // Profile is the color profile to use when writing to the screen.
+ Profile colorprofile.Profile
+ // RelativeCursor is whether to use relative cursor movements. This is
+ // useful when alt-screen is not used or when using inline mode.
+ RelativeCursor bool
+ // AltScreen is whether to use the alternate screen buffer.
+ AltScreen bool
+ // ShowCursor is whether to show the cursor.
+ ShowCursor bool
+ // HardTabs is whether to use hard tabs to optimize cursor movements.
+ HardTabs bool
+ // Backspace is whether to use backspace characters to move the cursor.
+ Backspace bool
+}
+
+// lineData represents the metadata for a line.
+type lineData struct {
+ // first and last changed cell indices
+ firstCell, lastCell int
+ // old index used for scrolling
+ oldIndex int //nolint:unused
+}
+
+// Screen represents the terminal screen.
+type Screen struct {
+ w io.Writer
+ buf *bytes.Buffer // buffer for writing to the screen
+ curbuf *Buffer // the current buffer
+ newbuf *Buffer // the new buffer
+ tabs *TabStops
+ touch map[int]lineData
+ queueAbove []string // the queue of strings to write above the screen
+ oldhash, newhash []uint64 // the old and new hash values for each line
+ hashtab []hashmap // the hashmap table
+ oldnum []int // old indices from previous hash
+ cur, saved Cursor // the current and saved cursors
+ opts ScreenOptions
+ mu sync.Mutex
+ method ansi.Method
+ scrollHeight int // keeps track of how many lines we've scrolled down (inline mode)
+ altScreenMode bool // whether alternate screen mode is enabled
+ cursorHidden bool // whether text cursor mode is enabled
+ clear bool // whether to force clear the screen
+ xtermLike bool // whether to use xterm-like optimizations, otherwise, it uses vt100 only
+ queuedText bool // whether we have queued non-zero width text queued up
+ atPhantom bool // whether the cursor is out of bounds and at a phantom cell
+}
+
+// SetMethod sets the method used to calculate the width of cells.
+func (s *Screen) SetMethod(method ansi.Method) {
+ s.method = method
+}
+
+// UseBackspaces sets whether to use backspace characters to move the cursor.
+func (s *Screen) UseBackspaces(v bool) {
+ s.opts.Backspace = v
+}
+
+// UseHardTabs sets whether to use hard tabs to optimize cursor movements.
+func (s *Screen) UseHardTabs(v bool) {
+ s.opts.HardTabs = v
+}
+
+// SetColorProfile sets the color profile to use when writing to the screen.
+func (s *Screen) SetColorProfile(p colorprofile.Profile) {
+ s.opts.Profile = p
+}
+
+// SetRelativeCursor sets whether to use relative cursor movements.
+func (s *Screen) SetRelativeCursor(v bool) {
+ s.opts.RelativeCursor = v
+}
+
+// EnterAltScreen enters the alternate screen buffer.
+func (s *Screen) EnterAltScreen() {
+ s.opts.AltScreen = true
+ s.clear = true
+ s.saved = s.cur
+}
+
+// ExitAltScreen exits the alternate screen buffer.
+func (s *Screen) ExitAltScreen() {
+ s.opts.AltScreen = false
+ s.clear = true
+ s.cur = s.saved
+}
+
+// ShowCursor shows the cursor.
+func (s *Screen) ShowCursor() {
+ s.opts.ShowCursor = true
+}
+
+// HideCursor hides the cursor.
+func (s *Screen) HideCursor() {
+ s.opts.ShowCursor = false
+}
+
+// Bounds implements Window.
+func (s *Screen) Bounds() Rectangle {
+ // Always return the new buffer bounds.
+ return s.newbuf.Bounds()
+}
+
+// Cell implements Window.
+func (s *Screen) Cell(x int, y int) *Cell {
+ return s.newbuf.Cell(x, y)
+}
+
+// Redraw forces a full redraw of the screen.
+func (s *Screen) Redraw() {
+ s.mu.Lock()
+ s.clear = true
+ s.mu.Unlock()
+}
+
+// Clear clears the screen with blank cells. This is a convenience method for
+// [Screen.Fill] with a nil cell.
+func (s *Screen) Clear() bool {
+ return s.ClearRect(s.newbuf.Bounds())
+}
+
+// ClearRect clears the given rectangle with blank cells. This is a convenience
+// method for [Screen.FillRect] with a nil cell.
+func (s *Screen) ClearRect(r Rectangle) bool {
+ return s.FillRect(nil, r)
+}
+
+// SetCell implements Window.
+func (s *Screen) SetCell(x int, y int, cell *Cell) (v bool) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ cellWidth := 1
+ if cell != nil {
+ cellWidth = cell.Width
+ }
+ if prev := s.curbuf.Cell(x, y); !cellEqual(prev, cell) {
+ chg, ok := s.touch[y]
+ if !ok {
+ chg = lineData{firstCell: x, lastCell: x + cellWidth}
+ } else {
+ chg.firstCell = min(chg.firstCell, x)
+ chg.lastCell = max(chg.lastCell, x+cellWidth)
+ }
+ s.touch[y] = chg
+ }
+
+ return s.newbuf.SetCell(x, y, cell)
+}
+
+// Fill implements Window.
+func (s *Screen) Fill(cell *Cell) bool {
+ return s.FillRect(cell, s.newbuf.Bounds())
+}
+
+// FillRect implements Window.
+func (s *Screen) FillRect(cell *Cell, r Rectangle) bool {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.newbuf.FillRect(cell, r)
+ for i := r.Min.Y; i < r.Max.Y; i++ {
+ s.touch[i] = lineData{firstCell: r.Min.X, lastCell: r.Max.X}
+ }
+ return true
+}
+
+// isXtermLike returns whether the terminal is xterm-like. This means that the
+// terminal supports ECMA-48 and ANSI X3.64 escape sequences.
+// TODO: Should this be a lookup table into each $TERM terminfo database? Like
+// we could keep a map of ANSI escape sequence to terminfo capability name and
+// check if the database supports the escape sequence. Instead of keeping a
+// list of terminal names here.
+func isXtermLike(termtype string) (v bool) {
+ parts := strings.Split(termtype, "-")
+ if len(parts) == 0 {
+ return
+ }
+
+ switch parts[0] {
+ case
+ "alacritty",
+ "contour",
+ "foot",
+ "ghostty",
+ "kitty",
+ "linux",
+ "rio",
+ "screen",
+ "st",
+ "tmux",
+ "wezterm",
+ "xterm":
+ v = true
+ }
+
+ return
+}
+
+// NewScreen creates a new Screen.
+func NewScreen(w io.Writer, width, height int, opts *ScreenOptions) (s *Screen) {
+ s = new(Screen)
+ s.w = w
+ if opts != nil {
+ s.opts = *opts
+ }
+
+ if s.opts.Term == "" {
+ s.opts.Term = os.Getenv("TERM")
+ }
+
+ if width <= 0 || height <= 0 {
+ if f, ok := w.(term.File); ok {
+ width, height, _ = term.GetSize(f.Fd())
+ }
+ }
+ if width < 0 {
+ width = 0
+ }
+ if height < 0 {
+ height = 0
+ }
+
+ s.buf = new(bytes.Buffer)
+ s.xtermLike = isXtermLike(s.opts.Term)
+ s.curbuf = NewBuffer(width, height)
+ s.newbuf = NewBuffer(width, height)
+ s.cur = Cursor{Position: Pos(-1, -1)} // start at -1 to force a move
+ s.saved = s.cur
+ s.reset()
+
+ return
+}
+
+// Width returns the width of the screen.
+func (s *Screen) Width() int {
+ return s.newbuf.Width()
+}
+
+// Height returns the height of the screen.
+func (s *Screen) Height() int {
+ return s.newbuf.Height()
+}
+
+// cellEqual returns whether the two cells are equal. A nil cell is considered
+// a [BlankCell].
+func cellEqual(a, b *Cell) bool {
+ if a == b {
+ return true
+ }
+ if a == nil {
+ a = &BlankCell
+ }
+ if b == nil {
+ b = &BlankCell
+ }
+ return a.Equal(b)
+}
+
+// putCell draws a cell at the current cursor position.
+func (s *Screen) putCell(cell *Cell) {
+ width, height := s.newbuf.Width(), s.newbuf.Height()
+ if s.opts.AltScreen && s.cur.X == width-1 && s.cur.Y == height-1 {
+ s.putCellLR(cell)
+ } else {
+ s.putAttrCell(cell)
+ }
+}
+
+// wrapCursor wraps the cursor to the next line.
+//
+//nolint:unused
+func (s *Screen) wrapCursor() {
+ const autoRightMargin = true
+ if autoRightMargin {
+ // Assume we have auto wrap mode enabled.
+ s.cur.X = 0
+ s.cur.Y++
+ } else {
+ s.cur.X--
+ }
+}
+
+func (s *Screen) putAttrCell(cell *Cell) {
+ if cell != nil && cell.Empty() {
+ // XXX: Zero width cells are special and should not be written to the
+ // screen no matter what other attributes they have.
+ // Zero width cells are used for wide characters that are split into
+ // multiple cells.
+ return
+ }
+
+ if cell == nil {
+ cell = s.clearBlank()
+ }
+
+ // We're at pending wrap state (phantom cell), incoming cell should
+ // wrap.
+ if s.atPhantom {
+ s.wrapCursor()
+ s.atPhantom = false
+ }
+
+ s.updatePen(cell)
+ s.buf.WriteRune(cell.Rune) //nolint:errcheck
+ for _, c := range cell.Comb {
+ s.buf.WriteRune(c) //nolint:errcheck
+ }
+
+ s.cur.X += cell.Width
+
+ if cell.Width > 0 {
+ s.queuedText = true
+ }
+
+ if s.cur.X >= s.newbuf.Width() {
+ s.atPhantom = true
+ }
+}
+
+// putCellLR draws a cell at the lower right corner of the screen.
+func (s *Screen) putCellLR(cell *Cell) {
+ // Optimize for the lower right corner cell.
+ curX := s.cur.X
+ if cell == nil || !cell.Empty() {
+ s.buf.WriteString(ansi.ResetAutoWrapMode) //nolint:errcheck
+ s.putAttrCell(cell)
+ // Writing to lower-right corner cell should not wrap.
+ s.atPhantom = false
+ s.cur.X = curX
+ s.buf.WriteString(ansi.SetAutoWrapMode) //nolint:errcheck
+ }
+}
+
+// updatePen updates the cursor pen styles.
+func (s *Screen) updatePen(cell *Cell) {
+ if cell == nil {
+ cell = &BlankCell
+ }
+
+ if s.opts.Profile != 0 {
+ // Downsample colors to the given color profile.
+ cell.Style = ConvertStyle(cell.Style, s.opts.Profile)
+ cell.Link = ConvertLink(cell.Link, s.opts.Profile)
+ }
+
+ if !cell.Style.Equal(&s.cur.Style) {
+ seq := cell.Style.DiffSequence(s.cur.Style)
+ if cell.Style.Empty() && len(seq) > len(ansi.ResetStyle) {
+ seq = ansi.ResetStyle
+ }
+ s.buf.WriteString(seq) //nolint:errcheck
+ s.cur.Style = cell.Style
+ }
+ if !cell.Link.Equal(&s.cur.Link) {
+ s.buf.WriteString(ansi.SetHyperlink(cell.Link.URL, cell.Link.Params)) //nolint:errcheck
+ s.cur.Link = cell.Link
+ }
+}
+
+// emitRange emits a range of cells to the buffer. It it equivalent to calling
+// [Screen.putCell] for each cell in the range. This is optimized to use
+// [ansi.ECH] and [ansi.REP].
+// Returns whether the cursor is at the end of interval or somewhere in the
+// middle.
+func (s *Screen) emitRange(line Line, n int) (eoi bool) {
+ for n > 0 {
+ var count int
+ for n > 1 && !cellEqual(line.At(0), line.At(1)) {
+ s.putCell(line.At(0))
+ line = line[1:]
+ n--
+ }
+
+ cell0 := line[0]
+ if n == 1 {
+ s.putCell(cell0)
+ return false
+ }
+
+ count = 2
+ for count < n && cellEqual(line.At(count), cell0) {
+ count++
+ }
+
+ ech := ansi.EraseCharacter(count)
+ cup := ansi.CursorPosition(s.cur.X+count, s.cur.Y)
+ rep := ansi.RepeatPreviousCharacter(count)
+ if s.xtermLike && count > len(ech)+len(cup) && cell0 != nil && cell0.Clear() {
+ s.updatePen(cell0)
+ s.buf.WriteString(ech) //nolint:errcheck
+
+ // If this is the last cell, we don't need to move the cursor.
+ if count < n {
+ s.move(s.cur.X+count, s.cur.Y)
+ } else {
+ return true // cursor in the middle
+ }
+ } else if s.xtermLike && count > len(rep) &&
+ (cell0 == nil || (len(cell0.Comb) == 0 && cell0.Rune < 256)) {
+ // We only support ASCII characters. Most terminals will handle
+ // non-ASCII characters correctly, but some might not, ahem xterm.
+ //
+ // NOTE: [ansi.REP] only repeats the last rune and won't work
+ // if the last cell contains multiple runes.
+
+ wrapPossible := s.cur.X+count >= s.newbuf.Width()
+ repCount := count
+ if wrapPossible {
+ repCount--
+ }
+
+ s.updatePen(cell0)
+ s.putCell(cell0)
+ repCount-- // cell0 is a single width cell ASCII character
+
+ s.buf.WriteString(ansi.RepeatPreviousCharacter(repCount)) //nolint:errcheck
+ s.cur.X += repCount
+ if wrapPossible {
+ s.putCell(cell0)
+ }
+ } else {
+ for i := 0; i < count; i++ {
+ s.putCell(line.At(i))
+ }
+ }
+
+ line = line[clamp(count, 0, len(line)):]
+ n -= count
+ }
+
+ return
+}
+
+// putRange puts a range of cells from the old line to the new line.
+// Returns whether the cursor is at the end of interval or somewhere in the
+// middle.
+func (s *Screen) putRange(oldLine, newLine Line, y, start, end int) (eoi bool) {
+ inline := min(len(ansi.CursorPosition(start+1, y+1)),
+ min(len(ansi.HorizontalPositionAbsolute(start+1)),
+ len(ansi.CursorForward(start+1))))
+ if (end - start + 1) > inline {
+ var j, same int
+ for j, same = start, 0; j <= end; j++ {
+ oldCell, newCell := oldLine.At(j), newLine.At(j)
+ if same == 0 && oldCell != nil && oldCell.Empty() {
+ continue
+ }
+ if cellEqual(oldCell, newCell) {
+ same++
+ } else {
+ if same > end-start {
+ s.emitRange(newLine[start:], j-same-start)
+ s.move(j, y)
+ start = j
+ }
+ same = 0
+ }
+ }
+
+ i := s.emitRange(newLine[start:], j-same-start)
+
+ // Always return 1 for the next [Screen.move] after a [Screen.putRange] if
+ // we found identical characters at end of interval.
+ if same == 0 {
+ return i
+ }
+ return true
+ }
+
+ return s.emitRange(newLine[start:], end-start+1)
+}
+
+// clearToEnd clears the screen from the current cursor position to the end of
+// line.
+func (s *Screen) clearToEnd(blank *Cell, force bool) { //nolint:unparam
+ if s.cur.Y >= 0 {
+ curline := s.curbuf.Line(s.cur.Y)
+ for j := s.cur.X; j < s.curbuf.Width(); j++ {
+ if j >= 0 {
+ c := curline.At(j)
+ if !cellEqual(c, blank) {
+ curline.Set(j, blank)
+ force = true
+ }
+ }
+ }
+ }
+
+ if force {
+ s.updatePen(blank)
+ count := s.newbuf.Width() - s.cur.X
+ if s.el0Cost() <= count {
+ s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck
+ } else {
+ for i := 0; i < count; i++ {
+ s.putCell(blank)
+ }
+ }
+ }
+}
+
+// clearBlank returns a blank cell based on the current cursor background color.
+func (s *Screen) clearBlank() *Cell {
+ c := BlankCell
+ if !s.cur.Style.Empty() || !s.cur.Link.Empty() {
+ c.Style = s.cur.Style
+ c.Link = s.cur.Link
+ }
+ return &c
+}
+
+// insertCells inserts the count cells pointed by the given line at the current
+// cursor position.
+func (s *Screen) insertCells(line Line, count int) {
+ if s.xtermLike {
+ // Use [ansi.ICH] as an optimization.
+ s.buf.WriteString(ansi.InsertCharacter(count)) //nolint:errcheck
+ } else {
+ // Otherwise, use [ansi.IRM] mode.
+ s.buf.WriteString(ansi.SetInsertReplaceMode) //nolint:errcheck
+ }
+
+ for i := 0; count > 0; i++ {
+ s.putAttrCell(line[i])
+ count--
+ }
+
+ if !s.xtermLike {
+ s.buf.WriteString(ansi.ResetInsertReplaceMode) //nolint:errcheck
+ }
+}
+
+// el0Cost returns the cost of using [ansi.EL] 0 i.e. [ansi.EraseLineRight]. If
+// this terminal supports background color erase, it can be cheaper to use
+// [ansi.EL] 0 i.e. [ansi.EraseLineRight] to clear
+// trailing spaces.
+func (s *Screen) el0Cost() int {
+ if s.xtermLike {
+ return 0
+ }
+ return len(ansi.EraseLineRight)
+}
+
+// transformLine transforms the given line in the current window to the
+// corresponding line in the new window. It uses [ansi.ICH] and [ansi.DCH] to
+// insert or delete characters.
+func (s *Screen) transformLine(y int) {
+ var firstCell, oLastCell, nLastCell int // first, old last, new last index
+ oldLine := s.curbuf.Line(y)
+ newLine := s.newbuf.Line(y)
+
+ // Find the first changed cell in the line
+ var lineChanged bool
+ for i := 0; i < s.newbuf.Width(); i++ {
+ if !cellEqual(newLine.At(i), oldLine.At(i)) {
+ lineChanged = true
+ break
+ }
+ }
+
+ const ceolStandoutGlitch = false
+ if ceolStandoutGlitch && lineChanged {
+ s.move(0, y)
+ s.clearToEnd(nil, false)
+ s.putRange(oldLine, newLine, y, 0, s.newbuf.Width()-1)
+ } else {
+ blank := newLine.At(0)
+
+ // It might be cheaper to clear leading spaces with [ansi.EL] 1 i.e.
+ // [ansi.EraseLineLeft].
+ if blank == nil || blank.Clear() {
+ var oFirstCell, nFirstCell int
+ for oFirstCell = 0; oFirstCell < s.curbuf.Width(); oFirstCell++ {
+ if !cellEqual(oldLine.At(oFirstCell), blank) {
+ break
+ }
+ }
+ for nFirstCell = 0; nFirstCell < s.newbuf.Width(); nFirstCell++ {
+ if !cellEqual(newLine.At(nFirstCell), blank) {
+ break
+ }
+ }
+
+ if nFirstCell == oFirstCell {
+ firstCell = nFirstCell
+
+ // Find the first differing cell
+ for firstCell < s.newbuf.Width() &&
+ cellEqual(oldLine.At(firstCell), newLine.At(firstCell)) {
+ firstCell++
+ }
+ } else if oFirstCell > nFirstCell {
+ firstCell = nFirstCell
+ } else if oFirstCell < nFirstCell {
+ firstCell = oFirstCell
+ el1Cost := len(ansi.EraseLineLeft)
+ if el1Cost < nFirstCell-oFirstCell {
+ if nFirstCell >= s.newbuf.Width() {
+ s.move(0, y)
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck
+ } else {
+ s.move(nFirstCell-1, y)
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.EraseLineLeft) //nolint:errcheck
+ }
+
+ for firstCell < nFirstCell {
+ oldLine.Set(firstCell, blank)
+ firstCell++
+ }
+ }
+ }
+ } else {
+ // Find the first differing cell
+ for firstCell < s.newbuf.Width() && cellEqual(newLine.At(firstCell), oldLine.At(firstCell)) {
+ firstCell++
+ }
+ }
+
+ // If we didn't find one, we're done
+ if firstCell >= s.newbuf.Width() {
+ return
+ }
+
+ blank = newLine.At(s.newbuf.Width() - 1)
+ if blank != nil && !blank.Clear() {
+ // Find the last differing cell
+ nLastCell = s.newbuf.Width() - 1
+ for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), oldLine.At(nLastCell)) {
+ nLastCell--
+ }
+
+ if nLastCell >= firstCell {
+ s.move(firstCell, y)
+ s.putRange(oldLine, newLine, y, firstCell, nLastCell)
+ if firstCell < len(oldLine) && firstCell < len(newLine) {
+ copy(oldLine[firstCell:], newLine[firstCell:])
+ } else {
+ copy(oldLine, newLine)
+ }
+ }
+
+ return
+ }
+
+ // Find last non-blank cell in the old line.
+ oLastCell = s.curbuf.Width() - 1
+ for oLastCell > firstCell && cellEqual(oldLine.At(oLastCell), blank) {
+ oLastCell--
+ }
+
+ // Find last non-blank cell in the new line.
+ nLastCell = s.newbuf.Width() - 1
+ for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), blank) {
+ nLastCell--
+ }
+
+ if nLastCell == firstCell && s.el0Cost() < oLastCell-nLastCell {
+ s.move(firstCell, y)
+ if !cellEqual(newLine.At(firstCell), blank) {
+ s.putCell(newLine.At(firstCell))
+ }
+ s.clearToEnd(blank, false)
+ } else if nLastCell != oLastCell &&
+ !cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) {
+ s.move(firstCell, y)
+ if oLastCell-nLastCell > s.el0Cost() {
+ if s.putRange(oldLine, newLine, y, firstCell, nLastCell) {
+ s.move(nLastCell+1, y)
+ }
+ s.clearToEnd(blank, false)
+ } else {
+ n := max(nLastCell, oLastCell)
+ s.putRange(oldLine, newLine, y, firstCell, n)
+ }
+ } else {
+ nLastNonBlank := nLastCell
+ oLastNonBlank := oLastCell
+
+ // Find the last cells that really differ.
+ // Can be -1 if no cells differ.
+ for cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) {
+ if !cellEqual(newLine.At(nLastCell-1), oldLine.At(oLastCell-1)) {
+ break
+ }
+ nLastCell--
+ oLastCell--
+ if nLastCell == -1 || oLastCell == -1 {
+ break
+ }
+ }
+
+ n := min(oLastCell, nLastCell)
+ if n >= firstCell {
+ s.move(firstCell, y)
+ s.putRange(oldLine, newLine, y, firstCell, n)
+ }
+
+ if oLastCell < nLastCell {
+ m := max(nLastNonBlank, oLastNonBlank)
+ if n != 0 {
+ for n > 0 {
+ wide := newLine.At(n + 1)
+ if wide == nil || !wide.Empty() {
+ break
+ }
+ n--
+ oLastCell--
+ }
+ } else if n >= firstCell && newLine.At(n) != nil && newLine.At(n).Width > 1 {
+ next := newLine.At(n + 1)
+ for next != nil && next.Empty() {
+ n++
+ oLastCell++
+ }
+ }
+
+ s.move(n+1, y)
+ ichCost := 3 + nLastCell - oLastCell
+ if s.xtermLike && (nLastCell < nLastNonBlank || ichCost > (m-n)) {
+ s.putRange(oldLine, newLine, y, n+1, m)
+ } else {
+ s.insertCells(newLine[n+1:], nLastCell-oLastCell)
+ }
+ } else if oLastCell > nLastCell {
+ s.move(n+1, y)
+ dchCost := 3 + oLastCell - nLastCell
+ if dchCost > len(ansi.EraseLineRight)+nLastNonBlank-(n+1) {
+ if s.putRange(oldLine, newLine, y, n+1, nLastNonBlank) {
+ s.move(nLastNonBlank+1, y)
+ }
+ s.clearToEnd(blank, false)
+ } else {
+ s.updatePen(blank)
+ s.deleteCells(oLastCell - nLastCell)
+ }
+ }
+ }
+ }
+
+ // Update the old line with the new line
+ if firstCell < len(oldLine) && firstCell < len(newLine) {
+ copy(oldLine[firstCell:], newLine[firstCell:])
+ } else {
+ copy(oldLine, newLine)
+ }
+}
+
+// deleteCells deletes the count cells at the current cursor position and moves
+// the rest of the line to the left. This is equivalent to [ansi.DCH].
+func (s *Screen) deleteCells(count int) {
+ // [ansi.DCH] will shift in cells from the right margin so we need to
+ // ensure that they are the right style.
+ s.buf.WriteString(ansi.DeleteCharacter(count)) //nolint:errcheck
+}
+
+// clearToBottom clears the screen from the current cursor position to the end
+// of the screen.
+func (s *Screen) clearToBottom(blank *Cell) {
+ row, col := s.cur.Y, s.cur.X
+ if row < 0 {
+ row = 0
+ }
+
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.EraseScreenBelow) //nolint:errcheck
+ // Clear the rest of the current line
+ s.curbuf.ClearRect(Rect(col, row, s.curbuf.Width()-col, 1))
+ // Clear everything below the current line
+ s.curbuf.ClearRect(Rect(0, row+1, s.curbuf.Width(), s.curbuf.Height()-row-1))
+}
+
+// clearBottom tests if clearing the end of the screen would satisfy part of
+// the screen update. Scan backwards through lines in the screen checking if
+// each is blank and one or more are changed.
+// It returns the top line.
+func (s *Screen) clearBottom(total int) (top int) {
+ if total <= 0 {
+ return
+ }
+
+ top = total
+ last := s.newbuf.Width()
+ blank := s.clearBlank()
+ canClearWithBlank := blank == nil || blank.Clear()
+
+ if canClearWithBlank {
+ var row int
+ for row = total - 1; row >= 0; row-- {
+ oldLine := s.curbuf.Line(row)
+ newLine := s.newbuf.Line(row)
+
+ var col int
+ ok := true
+ for col = 0; ok && col < last; col++ {
+ ok = cellEqual(newLine.At(col), blank)
+ }
+ if !ok {
+ break
+ }
+
+ for col = 0; ok && col < last; col++ {
+ ok = len(oldLine) == last && cellEqual(oldLine.At(col), blank)
+ }
+ if !ok {
+ top = row
+ }
+ }
+
+ if top < total {
+ s.move(0, top-1) // top is 1-based
+ s.clearToBottom(blank)
+ if s.oldhash != nil && s.newhash != nil &&
+ row < len(s.oldhash) && row < len(s.newhash) {
+ for row := top; row < s.newbuf.Height(); row++ {
+ s.oldhash[row] = s.newhash[row]
+ }
+ }
+ }
+ }
+
+ return
+}
+
+// clearScreen clears the screen and put cursor at home.
+func (s *Screen) clearScreen(blank *Cell) {
+ s.updatePen(blank)
+ s.buf.WriteString(ansi.CursorHomePosition) //nolint:errcheck
+ s.buf.WriteString(ansi.EraseEntireScreen) //nolint:errcheck
+ s.cur.X, s.cur.Y = 0, 0
+ s.curbuf.Fill(blank)
+}
+
+// clearBelow clears everything below and including the row.
+func (s *Screen) clearBelow(blank *Cell, row int) {
+ s.move(0, row)
+ s.clearToBottom(blank)
+}
+
+// clearUpdate forces a screen redraw.
+func (s *Screen) clearUpdate() {
+ blank := s.clearBlank()
+ var nonEmpty int
+ if s.opts.AltScreen {
+ // XXX: We're using the maximum height of the two buffers to ensure
+ // we write newly added lines to the screen in [Screen.transformLine].
+ nonEmpty = max(s.curbuf.Height(), s.newbuf.Height())
+ s.clearScreen(blank)
+ } else {
+ nonEmpty = s.newbuf.Height()
+ s.clearBelow(blank, 0)
+ }
+ nonEmpty = s.clearBottom(nonEmpty)
+ for i := 0; i < nonEmpty; i++ {
+ s.transformLine(i)
+ }
+}
+
+// Flush flushes the buffer to the screen.
+func (s *Screen) Flush() (err error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.flush()
+}
+
+func (s *Screen) flush() (err error) {
+ // Write the buffer
+ if s.buf.Len() > 0 {
+ _, err = s.w.Write(s.buf.Bytes()) //nolint:errcheck
+ if err == nil {
+ s.buf.Reset()
+ }
+ }
+
+ return
+}
+
+// Render renders changes of the screen to the internal buffer. Call
+// [Screen.Flush] to flush pending changes to the screen.
+func (s *Screen) Render() {
+ s.mu.Lock()
+ s.render()
+ s.mu.Unlock()
+}
+
+func (s *Screen) render() {
+ // Do we need to render anything?
+ if s.opts.AltScreen == s.altScreenMode &&
+ !s.opts.ShowCursor == s.cursorHidden &&
+ !s.clear &&
+ len(s.touch) == 0 &&
+ len(s.queueAbove) == 0 {
+ return
+ }
+
+ // TODO: Investigate whether this is necessary. Theoretically, terminals
+ // can add/remove tab stops and we should be able to handle that. We could
+ // use [ansi.DECTABSR] to read the tab stops, but that's not implemented in
+ // most terminals :/
+ // // Are we using hard tabs? If so, ensure tabs are using the
+ // // default interval using [ansi.DECST8C].
+ // if s.opts.HardTabs && !s.initTabs {
+ // s.buf.WriteString(ansi.SetTabEvery8Columns)
+ // s.initTabs = true
+ // }
+
+ // Do we need alt-screen mode?
+ if s.opts.AltScreen != s.altScreenMode {
+ if s.opts.AltScreen {
+ s.buf.WriteString(ansi.SetAltScreenSaveCursorMode)
+ } else {
+ s.buf.WriteString(ansi.ResetAltScreenSaveCursorMode)
+ }
+ s.altScreenMode = s.opts.AltScreen
+ }
+
+ // Do we need text cursor mode?
+ if !s.opts.ShowCursor != s.cursorHidden {
+ s.cursorHidden = !s.opts.ShowCursor
+ if s.cursorHidden {
+ s.buf.WriteString(ansi.HideCursor)
+ }
+ }
+
+ // Do we have queued strings to write above the screen?
+ if len(s.queueAbove) > 0 {
+ // TODO: Use scrolling region if available.
+ // TODO: Use [Screen.Write] [io.Writer] interface.
+
+ // We need to scroll the screen up by the number of lines in the queue.
+ // We can't use [ansi.SU] because we want the cursor to move down until
+ // it reaches the bottom of the screen.
+ s.move(0, s.newbuf.Height()-1)
+ s.buf.WriteString(strings.Repeat("\n", len(s.queueAbove)))
+ s.cur.Y += len(s.queueAbove)
+ // XXX: Now go to the top of the screen, insert new lines, and write
+ // the queued strings. It is important to use [Screen.moveCursor]
+ // instead of [Screen.move] because we don't want to perform any checks
+ // on the cursor position.
+ s.moveCursor(0, 0, false)
+ s.buf.WriteString(ansi.InsertLine(len(s.queueAbove)))
+ for _, line := range s.queueAbove {
+ s.buf.WriteString(line + "\r\n")
+ }
+
+ // Clear the queue
+ s.queueAbove = s.queueAbove[:0]
+ }
+
+ var nonEmpty int
+
+ // XXX: In inline mode, after a screen resize, we need to clear the extra
+ // lines at the bottom of the screen. This is because in inline mode, we
+ // don't use the full screen height and the current buffer size might be
+ // larger than the new buffer size.
+ partialClear := !s.opts.AltScreen && s.cur.X != -1 && s.cur.Y != -1 &&
+ s.curbuf.Width() == s.newbuf.Width() &&
+ s.curbuf.Height() > 0 &&
+ s.curbuf.Height() > s.newbuf.Height()
+
+ if !s.clear && partialClear {
+ s.clearBelow(nil, s.newbuf.Height()-1)
+ }
+
+ if s.clear {
+ s.clearUpdate()
+ s.clear = false
+ } else if len(s.touch) > 0 {
+ if s.opts.AltScreen {
+ // Optimize scrolling for the alternate screen buffer.
+ // TODO: Should we optimize for inline mode as well? If so, we need
+ // to know the actual cursor position to use [ansi.DECSTBM].
+ s.scrollOptimize()
+ }
+
+ var changedLines int
+ var i int
+
+ if s.opts.AltScreen {
+ nonEmpty = min(s.curbuf.Height(), s.newbuf.Height())
+ } else {
+ nonEmpty = s.newbuf.Height()
+ }
+
+ nonEmpty = s.clearBottom(nonEmpty)
+ for i = 0; i < nonEmpty; i++ {
+ _, ok := s.touch[i]
+ if ok {
+ s.transformLine(i)
+ changedLines++
+ }
+ }
+ }
+
+ // Sync windows and screen
+ s.touch = make(map[int]lineData, s.newbuf.Height())
+
+ if s.curbuf.Width() != s.newbuf.Width() || s.curbuf.Height() != s.newbuf.Height() {
+ // Resize the old buffer to match the new buffer.
+ _, oldh := s.curbuf.Width(), s.curbuf.Height()
+ s.curbuf.Resize(s.newbuf.Width(), s.newbuf.Height())
+ // Sync new lines to old lines
+ for i := oldh - 1; i < s.newbuf.Height(); i++ {
+ copy(s.curbuf.Line(i), s.newbuf.Line(i))
+ }
+ }
+
+ s.updatePen(nil) // nil indicates a blank cell with no styles
+
+ // Do we have enough changes to justify toggling the cursor?
+ if s.buf.Len() > 1 && s.opts.ShowCursor && !s.cursorHidden && s.queuedText {
+ nb := new(bytes.Buffer)
+ nb.Grow(s.buf.Len() + len(ansi.HideCursor) + len(ansi.ShowCursor))
+ nb.WriteString(ansi.HideCursor)
+ nb.Write(s.buf.Bytes())
+ nb.WriteString(ansi.ShowCursor)
+ *s.buf = *nb
+ }
+
+ s.queuedText = false
+}
+
+// Close writes the final screen update and resets the screen.
+func (s *Screen) Close() (err error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ s.render()
+ s.updatePen(nil)
+ // Go to the bottom of the screen
+ s.move(0, s.newbuf.Height()-1)
+
+ if s.altScreenMode {
+ s.buf.WriteString(ansi.ResetAltScreenSaveCursorMode)
+ s.altScreenMode = false
+ }
+
+ if s.cursorHidden {
+ s.buf.WriteString(ansi.ShowCursor)
+ s.cursorHidden = false
+ }
+
+ // Write the buffer
+ err = s.flush()
+ if err != nil {
+ return
+ }
+
+ s.reset()
+ return
+}
+
+// reset resets the screen to its initial state.
+func (s *Screen) reset() {
+ s.scrollHeight = 0
+ s.cursorHidden = false
+ s.altScreenMode = false
+ s.touch = make(map[int]lineData, s.newbuf.Height())
+ if s.curbuf != nil {
+ s.curbuf.Clear()
+ }
+ if s.newbuf != nil {
+ s.newbuf.Clear()
+ }
+ s.buf.Reset()
+ s.tabs = DefaultTabStops(s.newbuf.Width())
+ s.oldhash, s.newhash = nil, nil
+
+ // We always disable HardTabs when termtype is "linux".
+ if strings.HasPrefix(s.opts.Term, "linux") {
+ s.opts.HardTabs = false
+ }
+}
+
+// Resize resizes the screen.
+func (s *Screen) Resize(width, height int) bool {
+ oldw := s.newbuf.Width()
+ oldh := s.newbuf.Height()
+
+ if s.opts.AltScreen || width != oldw {
+ // We only clear the whole screen if the width changes. Adding/removing
+ // rows is handled by the [Screen.render] and [Screen.transformLine]
+ // methods.
+ s.clear = true
+ }
+
+ // Clear new columns and lines
+ if width > oldh {
+ s.ClearRect(Rect(max(oldw-1, 0), 0, width-oldw, height))
+ } else if width < oldw {
+ s.ClearRect(Rect(max(width-1, 0), 0, oldw-width, height))
+ }
+
+ if height > oldh {
+ s.ClearRect(Rect(0, max(oldh-1, 0), width, height-oldh))
+ } else if height < oldh {
+ s.ClearRect(Rect(0, max(height-1, 0), width, oldh-height))
+ }
+
+ s.mu.Lock()
+ s.newbuf.Resize(width, height)
+ s.tabs.Resize(width)
+ s.oldhash, s.newhash = nil, nil
+ s.scrollHeight = 0 // reset scroll lines
+ s.mu.Unlock()
+
+ return true
+}
+
+// MoveTo moves the cursor to the given position.
+func (s *Screen) MoveTo(x, y int) {
+ s.mu.Lock()
+ s.move(x, y)
+ s.mu.Unlock()
+}
+
+// InsertAbove inserts string above the screen. The inserted string is not
+// managed by the screen. This does nothing when alternate screen mode is
+// enabled.
+func (s *Screen) InsertAbove(str string) {
+ if s.opts.AltScreen {
+ return
+ }
+ s.mu.Lock()
+ for _, line := range strings.Split(str, "\n") {
+ s.queueAbove = append(s.queueAbove, s.method.Truncate(line, s.Width(), ""))
+ }
+ s.mu.Unlock()
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/sequence.go b/vendor/github.com/charmbracelet/x/cellbuf/sequence.go
new file mode 100644
index 0000000..613eefe
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/sequence.go
@@ -0,0 +1,131 @@
+package cellbuf
+
+import (
+ "bytes"
+ "image/color"
+
+ "github.com/charmbracelet/x/ansi"
+)
+
+// ReadStyle reads a Select Graphic Rendition (SGR) escape sequences from a
+// list of parameters.
+func ReadStyle(params ansi.Params, pen *Style) {
+ if len(params) == 0 {
+ pen.Reset()
+ return
+ }
+
+ for i := 0; i < len(params); i++ {
+ param, hasMore, _ := params.Param(i, 0)
+ 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
+ nextParam, _, ok := params.Param(i+1, 0)
+ if hasMore && ok { // Only accept subparameters i.e. separated by ":"
+ switch nextParam {
+ case 0, 1, 2, 3, 4, 5:
+ i++
+ 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
+ var c color.Color
+ n := ReadStyleColor(params[i:], &c)
+ if n > 0 {
+ pen.Foreground(c)
+ i += n - 1
+ }
+ 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
+ var c color.Color
+ n := ReadStyleColor(params[i:], &c)
+ if n > 0 {
+ pen.Background(c)
+ i += n - 1
+ }
+ case 49: // Default Background
+ pen.Background(nil)
+ case 58: // Set underline color
+ var c color.Color
+ n := ReadStyleColor(params[i:], &c)
+ if n > 0 {
+ pen.UnderlineColor(c)
+ i += n - 1
+ }
+ 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
+ }
+ }
+}
+
+// ReadLink reads a hyperlink escape sequence from a data buffer.
+func ReadLink(p []byte, link *Link) {
+ params := bytes.Split(p, []byte{';'})
+ if len(params) != 3 {
+ return
+ }
+ link.Params = string(params[1])
+ link.URL = string(params[2])
+}
+
+// ReadStyleColor reads a color from a list of parameters.
+// See [ansi.ReadStyleColor] for more information.
+func ReadStyleColor(params ansi.Params, c *color.Color) int {
+ return ansi.ReadStyleColor(params, c)
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/style.go b/vendor/github.com/charmbracelet/x/cellbuf/style.go
new file mode 100644
index 0000000..82c4afb
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/style.go
@@ -0,0 +1,31 @@
+package cellbuf
+
+import (
+ "github.com/charmbracelet/colorprofile"
+)
+
+// Convert converts a style to respect the given color profile.
+func ConvertStyle(s Style, p colorprofile.Profile) Style {
+ switch p {
+ case colorprofile.TrueColor:
+ return s
+ case colorprofile.Ascii:
+ s.Fg = nil
+ s.Bg = nil
+ s.Ul = nil
+ case colorprofile.NoTTY:
+ return Style{}
+ }
+
+ if s.Fg != nil {
+ s.Fg = p.Convert(s.Fg)
+ }
+ if s.Bg != nil {
+ s.Bg = p.Convert(s.Bg)
+ }
+ if s.Ul != nil {
+ s.Ul = p.Convert(s.Ul)
+ }
+
+ return s
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/tabstop.go b/vendor/github.com/charmbracelet/x/cellbuf/tabstop.go
new file mode 100644
index 0000000..24eec44
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/tabstop.go
@@ -0,0 +1,137 @@
+package cellbuf
+
+// DefaultTabInterval is the default tab interval.
+const DefaultTabInterval = 8
+
+// TabStops represents horizontal line tab stops.
+type TabStops struct {
+ stops []int
+ interval int
+ width int
+}
+
+// NewTabStops creates a new set of tab stops from a number of columns and an
+// interval.
+func NewTabStops(width, interval int) *TabStops {
+ ts := new(TabStops)
+ ts.interval = interval
+ ts.width = width
+ ts.stops = make([]int, (width+(interval-1))/interval)
+ ts.init(0, width)
+ return ts
+}
+
+// DefaultTabStops creates a new set of tab stops with the default interval.
+func DefaultTabStops(cols int) *TabStops {
+ return NewTabStops(cols, DefaultTabInterval)
+}
+
+// Resize resizes the tab stops to the given width.
+func (ts *TabStops) Resize(width int) {
+ if width == ts.width {
+ return
+ }
+
+ if width < ts.width {
+ size := (width + (ts.interval - 1)) / ts.interval
+ ts.stops = ts.stops[:size]
+ } else {
+ size := (width - ts.width + (ts.interval - 1)) / ts.interval
+ ts.stops = append(ts.stops, make([]int, size)...)
+ }
+
+ ts.init(ts.width, width)
+ ts.width = width
+}
+
+// IsStop returns true if the given column is a tab stop.
+func (ts TabStops) IsStop(col int) bool {
+ mask := ts.mask(col)
+ i := col >> 3
+ if i < 0 || i >= len(ts.stops) {
+ return false
+ }
+ return ts.stops[i]&mask != 0
+}
+
+// Next returns the next tab stop after the given column.
+func (ts TabStops) Next(col int) int {
+ return ts.Find(col, 1)
+}
+
+// Prev returns the previous tab stop before the given column.
+func (ts TabStops) Prev(col int) int {
+ return ts.Find(col, -1)
+}
+
+// Find returns the prev/next tab stop before/after the given column and delta.
+// If delta is positive, it returns the next tab stop after the given column.
+// If delta is negative, it returns the previous tab stop before the given column.
+// If delta is zero, it returns the given column.
+func (ts TabStops) Find(col, delta int) int {
+ if delta == 0 {
+ return col
+ }
+
+ var prev bool
+ count := delta
+ if count < 0 {
+ count = -count
+ prev = true
+ }
+
+ for count > 0 {
+ if !prev {
+ if col >= ts.width-1 {
+ return col
+ }
+
+ col++
+ } else {
+ if col < 1 {
+ return col
+ }
+
+ col--
+ }
+
+ if ts.IsStop(col) {
+ count--
+ }
+ }
+
+ return col
+}
+
+// Set adds a tab stop at the given column.
+func (ts *TabStops) Set(col int) {
+ mask := ts.mask(col)
+ ts.stops[col>>3] |= mask
+}
+
+// Reset removes the tab stop at the given column.
+func (ts *TabStops) Reset(col int) {
+ mask := ts.mask(col)
+ ts.stops[col>>3] &= ^mask
+}
+
+// Clear removes all tab stops.
+func (ts *TabStops) Clear() {
+ ts.stops = make([]int, len(ts.stops))
+}
+
+// mask returns the mask for the given column.
+func (ts *TabStops) mask(col int) int {
+ return 1 << (col & (ts.interval - 1))
+}
+
+// init initializes the tab stops starting from col until width.
+func (ts *TabStops) init(col, width int) {
+ for x := col; x < width; x++ {
+ if x%ts.interval == 0 {
+ ts.Set(x)
+ } else {
+ ts.Reset(x)
+ }
+ }
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/utils.go b/vendor/github.com/charmbracelet/x/cellbuf/utils.go
new file mode 100644
index 0000000..b0452fa
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/utils.go
@@ -0,0 +1,38 @@
+package cellbuf
+
+import (
+ "strings"
+)
+
+// Height returns the height of a string.
+func Height(s string) int {
+ return strings.Count(s, "\n") + 1
+}
+
+func min(a, b int) int { //nolint:predeclared
+ if a > b {
+ return b
+ }
+ return a
+}
+
+func max(a, b int) int { //nolint:predeclared
+ if a > b {
+ return a
+ }
+ return b
+}
+
+func clamp(v, low, high int) int {
+ if high < low {
+ low, high = high, low
+ }
+ return min(high, max(low, v))
+}
+
+func abs(a int) int {
+ if a < 0 {
+ return -a
+ }
+ return a
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/wrap.go b/vendor/github.com/charmbracelet/x/cellbuf/wrap.go
new file mode 100644
index 0000000..59a2a33
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/wrap.go
@@ -0,0 +1,178 @@
+package cellbuf
+
+import (
+ "bytes"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/charmbracelet/x/ansi"
+)
+
+// Wrap returns a string that is wrapped to the specified limit applying any
+// ANSI escape sequences in the string. It tries to wrap the string at word
+// boundaries, but will break words if necessary.
+//
+// The breakpoints string is a list of characters that are considered
+// breakpoints for word wrapping. A hyphen (-) is always considered a
+// breakpoint.
+//
+// Note: breakpoints must be a string of 1-cell wide rune characters.
+func Wrap(s string, limit int, breakpoints string) string {
+ if len(s) == 0 {
+ return ""
+ }
+
+ if limit < 1 {
+ return s
+ }
+
+ p := ansi.GetParser()
+ defer ansi.PutParser(p)
+
+ var (
+ buf bytes.Buffer
+ word bytes.Buffer
+ space bytes.Buffer
+ style, curStyle Style
+ link, curLink Link
+ curWidth int
+ wordLen int
+ )
+
+ addSpace := func() {
+ curWidth += space.Len()
+ buf.Write(space.Bytes())
+ space.Reset()
+ }
+
+ addWord := func() {
+ if word.Len() == 0 {
+ return
+ }
+
+ curLink = link
+ curStyle = style
+
+ addSpace()
+ curWidth += wordLen
+ buf.Write(word.Bytes())
+ word.Reset()
+ wordLen = 0
+ }
+
+ addNewline := func() {
+ if !curStyle.Empty() {
+ buf.WriteString(ansi.ResetStyle)
+ }
+ if !curLink.Empty() {
+ buf.WriteString(ansi.ResetHyperlink())
+ }
+ buf.WriteByte('\n')
+ if !curLink.Empty() {
+ buf.WriteString(ansi.SetHyperlink(curLink.URL, curLink.Params))
+ }
+ if !curStyle.Empty() {
+ buf.WriteString(curStyle.Sequence())
+ }
+ curWidth = 0
+ space.Reset()
+ }
+
+ var state byte
+ for len(s) > 0 {
+ seq, width, n, newState := ansi.DecodeSequence(s, state, p)
+ switch width {
+ case 0:
+ if ansi.Equal(seq, "\t") {
+ addWord()
+ space.WriteString(seq)
+ break
+ } else if ansi.Equal(seq, "\n") {
+ if wordLen == 0 {
+ if curWidth+space.Len() > limit {
+ curWidth = 0
+ } else {
+ // preserve whitespaces
+ buf.Write(space.Bytes())
+ }
+ space.Reset()
+ }
+
+ addWord()
+ addNewline()
+ break
+ } else if ansi.HasCsiPrefix(seq) && p.Command() == 'm' {
+ // SGR style sequence [ansi.SGR]
+ ReadStyle(p.Params(), &style)
+ } else if ansi.HasOscPrefix(seq) && p.Command() == 8 {
+ // Hyperlink sequence [ansi.SetHyperlink]
+ ReadLink(p.Data(), &link)
+ }
+
+ word.WriteString(seq)
+ default:
+ if len(seq) == 1 {
+ // ASCII
+ r, _ := utf8.DecodeRuneInString(seq)
+ if unicode.IsSpace(r) {
+ addWord()
+ space.WriteRune(r)
+ break
+ } else if r == '-' || runeContainsAny(r, breakpoints) {
+ addSpace()
+ if curWidth+wordLen+width <= limit {
+ addWord()
+ buf.WriteString(seq)
+ curWidth += width
+ break
+ }
+ }
+ }
+
+ if wordLen+width > limit {
+ // Hardwrap the word if it's too long
+ addWord()
+ }
+
+ word.WriteString(seq)
+ wordLen += width
+
+ if curWidth+wordLen+space.Len() > limit {
+ addNewline()
+ }
+ }
+
+ s = s[n:]
+ state = newState
+ }
+
+ if wordLen == 0 {
+ if curWidth+space.Len() > limit {
+ curWidth = 0
+ } else {
+ // preserve whitespaces
+ buf.Write(space.Bytes())
+ }
+ space.Reset()
+ }
+
+ addWord()
+
+ if !curLink.Empty() {
+ buf.WriteString(ansi.ResetHyperlink())
+ }
+ if !curStyle.Empty() {
+ buf.WriteString(ansi.ResetStyle)
+ }
+
+ return buf.String()
+}
+
+func runeContainsAny[T string | []rune](r rune, s T) bool {
+ for _, c := range []rune(s) {
+ if c == r {
+ return true
+ }
+ }
+ return false
+}
diff --git a/vendor/github.com/charmbracelet/x/cellbuf/writer.go b/vendor/github.com/charmbracelet/x/cellbuf/writer.go
new file mode 100644
index 0000000..ae8b2a8
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/cellbuf/writer.go
@@ -0,0 +1,339 @@
+package cellbuf
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+
+ "github.com/charmbracelet/x/ansi"
+)
+
+// CellBuffer is a cell buffer that represents a set of cells in a screen or a
+// grid.
+type CellBuffer interface {
+ // Cell returns the cell at the given position.
+ Cell(x, y int) *Cell
+ // SetCell sets the cell at the given position to the given cell. It
+ // returns whether the cell was set successfully.
+ SetCell(x, y int, c *Cell) bool
+ // Bounds returns the bounds of the cell buffer.
+ Bounds() Rectangle
+}
+
+// FillRect fills the rectangle within the cell buffer with the given cell.
+// This will not fill cells outside the bounds of the cell buffer.
+func FillRect(s CellBuffer, c *Cell, rect Rectangle) {
+ for y := rect.Min.Y; y < rect.Max.Y; y++ {
+ for x := rect.Min.X; x < rect.Max.X; x++ {
+ s.SetCell(x, y, c) //nolint:errcheck
+ }
+ }
+}
+
+// Fill fills the cell buffer with the given cell.
+func Fill(s CellBuffer, c *Cell) {
+ FillRect(s, c, s.Bounds())
+}
+
+// ClearRect clears the rectangle within the cell buffer with blank cells.
+func ClearRect(s CellBuffer, rect Rectangle) {
+ FillRect(s, nil, rect)
+}
+
+// Clear clears the cell buffer with blank cells.
+func Clear(s CellBuffer) {
+ Fill(s, nil)
+}
+
+// SetContentRect clears the rectangle within the cell buffer with blank cells,
+// and sets the given string as its content. If the height or width of the
+// string exceeds the height or width of the cell buffer, it will be truncated.
+func SetContentRect(s CellBuffer, str string, rect Rectangle) {
+ // Replace all "\n" with "\r\n" to ensure the cursor is reset to the start
+ // of the line. Make sure we don't replace "\r\n" with "\r\r\n".
+ str = strings.ReplaceAll(str, "\r\n", "\n")
+ str = strings.ReplaceAll(str, "\n", "\r\n")
+ ClearRect(s, rect)
+ printString(s, ansi.GraphemeWidth, rect.Min.X, rect.Min.Y, rect, str, true, "")
+}
+
+// SetContent clears the cell buffer with blank cells, and sets the given string
+// as its content. If the height or width of the string exceeds the height or
+// width of the cell buffer, it will be truncated.
+func SetContent(s CellBuffer, str string) {
+ SetContentRect(s, str, s.Bounds())
+}
+
+// Render returns a string representation of the grid with ANSI escape sequences.
+func Render(d CellBuffer) string {
+ var buf bytes.Buffer
+ height := d.Bounds().Dy()
+ for y := 0; y < height; y++ {
+ _, line := RenderLine(d, y)
+ buf.WriteString(line)
+ if y < height-1 {
+ buf.WriteString("\r\n")
+ }
+ }
+ return buf.String()
+}
+
+// RenderLine returns a string representation of the yth line of the grid along
+// with the width of the line.
+func RenderLine(d CellBuffer, n int) (w int, line string) {
+ var pen Style
+ var link Link
+ 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 < d.Bounds().Dx(); x++ {
+ if cell := d.Cell(x, n); cell != nil && cell.Width > 0 {
+ // Convert the cell's style and link to the given color profile.
+ cellStyle := cell.Style
+ cellLink := cell.Link
+ if cellStyle.Empty() && !pen.Empty() {
+ writePending()
+ buf.WriteString(ansi.ResetStyle) //nolint:errcheck
+ pen.Reset()
+ }
+ if !cellStyle.Equal(&pen) {
+ writePending()
+ seq := cellStyle.DiffSequence(pen)
+ buf.WriteString(seq) // nolint:errcheck
+ pen = cellStyle
+ }
+
+ // Write the URL escape sequence
+ if cellLink != link && link.URL != "" {
+ writePending()
+ buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck
+ link.Reset()
+ }
+ if cellLink != link {
+ writePending()
+ buf.WriteString(ansi.SetHyperlink(cellLink.URL, cellLink.Params)) //nolint:errcheck
+ link = cellLink
+ }
+
+ // 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.Equal(&BlankCell) {
+ pendingLine += cell.String()
+ pendingWidth += cell.Width
+ } else {
+ writePending()
+ buf.WriteString(cell.String())
+ w += cell.Width
+ }
+ }
+ }
+ if link.URL != "" {
+ buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck
+ }
+ if !pen.Empty() {
+ buf.WriteString(ansi.ResetStyle) //nolint:errcheck
+ }
+ return w, strings.TrimRight(buf.String(), " ") // Trim trailing spaces
+}
+
+// ScreenWriter represents a writer that writes to a [Screen] parsing ANSI
+// escape sequences and Unicode characters and converting them into cells that
+// can be written to a cell [Buffer].
+type ScreenWriter struct {
+ *Screen
+}
+
+// NewScreenWriter creates a new ScreenWriter that writes to the given Screen.
+// This is a convenience function for creating a ScreenWriter.
+func NewScreenWriter(s *Screen) *ScreenWriter {
+ return &ScreenWriter{s}
+}
+
+// Write writes the given bytes to the screen.
+// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
+// sequences.
+func (s *ScreenWriter) Write(p []byte) (n int, err error) {
+ printString(s.Screen, s.method,
+ s.cur.X, s.cur.Y, s.Bounds(),
+ p, false, "")
+ return len(p), nil
+}
+
+// SetContent clears the screen with blank cells, and sets the given string as
+// its content. If the height or width of the string exceeds the height or
+// width of the screen, it will be truncated.
+//
+// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape sequences.
+func (s *ScreenWriter) SetContent(str string) {
+ s.SetContentRect(str, s.Bounds())
+}
+
+// SetContentRect clears the rectangle within the screen with blank cells, and
+// sets the given string as its content. If the height or width of the string
+// exceeds the height or width of the screen, it will be truncated.
+//
+// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
+// sequences.
+func (s *ScreenWriter) SetContentRect(str string, rect Rectangle) {
+ // Replace all "\n" with "\r\n" to ensure the cursor is reset to the start
+ // of the line. Make sure we don't replace "\r\n" with "\r\r\n".
+ str = strings.ReplaceAll(str, "\r\n", "\n")
+ str = strings.ReplaceAll(str, "\n", "\r\n")
+ s.ClearRect(rect)
+ printString(s.Screen, s.method,
+ rect.Min.X, rect.Min.Y, rect,
+ str, true, "")
+}
+
+// Print prints the string at the current cursor position. It will wrap the
+// string to the width of the screen if it exceeds the width of the screen.
+// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
+// sequences.
+func (s *ScreenWriter) Print(str string, v ...interface{}) {
+ if len(v) > 0 {
+ str = fmt.Sprintf(str, v...)
+ }
+ printString(s.Screen, s.method,
+ s.cur.X, s.cur.Y, s.Bounds(),
+ str, false, "")
+}
+
+// PrintAt prints the string at the given position. It will wrap the string to
+// the width of the screen if it exceeds the width of the screen.
+// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
+// sequences.
+func (s *ScreenWriter) PrintAt(x, y int, str string, v ...interface{}) {
+ if len(v) > 0 {
+ str = fmt.Sprintf(str, v...)
+ }
+ printString(s.Screen, s.method,
+ x, y, s.Bounds(),
+ str, false, "")
+}
+
+// PrintCrop prints the string at the current cursor position and truncates the
+// text if it exceeds the width of the screen. Use tail to specify a string to
+// append if the string is truncated.
+// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
+// sequences.
+func (s *ScreenWriter) PrintCrop(str string, tail string) {
+ printString(s.Screen, s.method,
+ s.cur.X, s.cur.Y, s.Bounds(),
+ str, true, tail)
+}
+
+// PrintCropAt prints the string at the given position and truncates the text
+// if it exceeds the width of the screen. Use tail to specify a string to append
+// if the string is truncated.
+// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
+// sequences.
+func (s *ScreenWriter) PrintCropAt(x, y int, str string, tail string) {
+ printString(s.Screen, s.method,
+ x, y, s.Bounds(),
+ str, true, tail)
+}
+
+// printString draws a string starting at the given position.
+func printString[T []byte | string](
+ s CellBuffer,
+ m ansi.Method,
+ x, y int,
+ bounds Rectangle, str T,
+ truncate bool, tail string,
+) {
+ p := ansi.GetParser()
+ defer ansi.PutParser(p)
+
+ var tailc Cell
+ if truncate && len(tail) > 0 {
+ if m == ansi.WcWidth {
+ tailc = *NewCellString(tail)
+ } else {
+ tailc = *NewGraphemeCell(tail)
+ }
+ }
+
+ decoder := ansi.DecodeSequenceWc[T]
+ if m == ansi.GraphemeWidth {
+ decoder = ansi.DecodeSequence[T]
+ }
+
+ var cell Cell
+ var style Style
+ var link Link
+ var state byte
+ for len(str) > 0 {
+ seq, width, n, newState := decoder(str, state, p)
+
+ switch width {
+ case 1, 2, 3, 4: // wide cells can go up to 4 cells wide
+ cell.Width += width
+ cell.Append([]rune(string(seq))...)
+
+ if !truncate && x+cell.Width > bounds.Max.X && y+1 < bounds.Max.Y {
+ // Wrap the string to the width of the window
+ x = bounds.Min.X
+ y++
+ }
+ if Pos(x, y).In(bounds) {
+ if truncate && tailc.Width > 0 && x+cell.Width > bounds.Max.X-tailc.Width {
+ // Truncate the string and append the tail if any.
+ cell := tailc
+ cell.Style = style
+ cell.Link = link
+ s.SetCell(x, y, &cell)
+ x += tailc.Width
+ } else {
+ // Print the cell to the screen
+ cell.Style = style
+ cell.Link = link
+ s.SetCell(x, y, &cell) //nolint:errcheck
+ x += width
+ }
+ }
+
+ // String is too long for the line, truncate it.
+ // Make sure we reset the cell for the next iteration.
+ cell.Reset()
+ default:
+ // Valid sequences always have a non-zero Cmd.
+ // TODO: Handle cursor movement and other sequences
+ switch {
+ case ansi.HasCsiPrefix(seq) && p.Command() == 'm':
+ // SGR - Select Graphic Rendition
+ ReadStyle(p.Params(), &style)
+ case ansi.HasOscPrefix(seq) && p.Command() == 8:
+ // Hyperlinks
+ ReadLink(p.Data(), &link)
+ case ansi.Equal(seq, T("\n")):
+ y++
+ case ansi.Equal(seq, T("\r")):
+ x = bounds.Min.X
+ default:
+ cell.Append([]rune(string(seq))...)
+ }
+ }
+
+ // Advance the state and data
+ state = newState
+ str = str[n:]
+ }
+
+ // Make sure to set the last cell if it's not empty.
+ if !cell.Empty() {
+ s.SetCell(x, y, &cell) //nolint:errcheck
+ cell.Reset()
+ }
+}