diff options
| author | mo khan <mo@mokhan.ca> | 2025-07-22 17:35:49 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-07-22 17:35:49 -0600 |
| commit | 20ef0d92694465ac86b550df139e8366a0a2b4fa (patch) | |
| tree | 3f14589e1ce6eb9306a3af31c3a1f9e1af5ed637 /vendor/github.com/charmbracelet/x/cellbuf | |
| parent | 44e0d272c040cdc53a98b9f1dc58ae7da67752e6 (diff) | |
feat: connect to spicedb
Diffstat (limited to 'vendor/github.com/charmbracelet/x/cellbuf')
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/LICENSE | 21 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/buffer.go | 473 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/cell.go | 503 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/errors.go | 6 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/geom.go | 21 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/hardscroll.go | 272 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/hashmap.go | 301 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/link.go | 14 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/screen.go | 1457 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/sequence.go | 131 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/style.go | 31 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/tabstop.go | 137 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/utils.go | 38 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/wrap.go | 178 | ||||
| -rw-r--r-- | vendor/github.com/charmbracelet/x/cellbuf/writer.go | 339 |
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() + } +} |
