forked from toolshed/abra
We were running behind and there were quite some deprecations to update. This was mostly in the upstream copy/pasta package but seems quite minimal.
340 lines
10 KiB
Go
340 lines
10 KiB
Go
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()
|
|
}
|
|
}
|