buffer.go

  1package uv
  2
  3import (
  4	"image"
  5	"io"
  6	"strings"
  7
  8	"github.com/charmbracelet/x/ansi"
  9)
 10
 11// Position represents a position in a coordinate system.
 12type Position = image.Point
 13
 14// Pos is a shorthand for creating a new [Position].
 15func Pos(x, y int) Position {
 16	return Position{X: x, Y: y}
 17}
 18
 19// Rectangle represents a rectangular area.
 20type Rectangle = image.Rectangle
 21
 22// Rect is a shorthand for creating a new [Rectangle].
 23func Rect(x, y, w, h int) Rectangle {
 24	return Rectangle{Min: image.Point{X: x, Y: y}, Max: image.Point{X: x + w, Y: y + h}}
 25}
 26
 27// Line represents cells in a line.
 28type Line []Cell
 29
 30// Set sets the cell at the given x position.
 31func (l Line) Set(x int, c *Cell) {
 32	// maxCellWidth is the maximum width a terminal cell is expected to have.
 33	const maxCellWidth = 5
 34
 35	lineWidth := len(l)
 36	if x < 0 || x >= lineWidth {
 37		return
 38	}
 39
 40	// When a wide cell is partially overwritten, we need
 41	// to fill the rest of the cell with space cells to
 42	// avoid rendering issues.
 43	var prev *Cell
 44	if prev = l.At(x); prev != nil {
 45		if pw := prev.Width; pw > 1 {
 46			// Writing to the first wide cell
 47			for j := 0; j < pw && x+j < lineWidth; j++ {
 48				l[x+j] = *prev
 49				l[x+j].Empty()
 50			}
 51		} else if pw == 0 {
 52			// Writing to wide cell placeholders
 53			for j := 1; j < maxCellWidth && x-j >= 0; j++ {
 54				if wide := l.At(x - j); wide != nil {
 55					if ww := wide.Width; ww > 1 && j < ww {
 56						for k := 0; k < ww; k++ {
 57							l[x-j+k] = *wide
 58							l[x-j+k].Empty()
 59						}
 60						break
 61					}
 62				}
 63			}
 64		}
 65	}
 66
 67	if c == nil {
 68		// Nil cells are treated as blank empty cells.
 69		l[x] = EmptyCell
 70		return
 71	}
 72
 73	l[x] = *c
 74	cw := c.Width
 75	if x+cw > lineWidth {
 76		// If the cell is too wide, we write blanks with the same style.
 77		for i := 0; i < cw && x+i < lineWidth; i++ {
 78			l[x+i] = *c
 79			l[x+i].Empty()
 80		}
 81		return
 82	}
 83
 84	if cw > 1 {
 85		// Mark wide cells with an zero cells.
 86		// We set the wide cell down below
 87		for j := 1; j < cw && x+j < lineWidth; j++ {
 88			l[x+j] = Cell{}
 89		}
 90	}
 91}
 92
 93// At returns the cell at the given x position.
 94// If the cell does not exist, it returns nil.
 95func (l Line) At(x int) *Cell {
 96	if x < 0 || x >= len(l) {
 97		return nil
 98	}
 99
100	return &l[x]
101}
102
103// String returns the string representation of the line. Any trailing spaces
104// are removed.
105func (l Line) String() (s string) {
106	for _, c := range l {
107		if c.IsZero() {
108			continue
109		} else {
110			s += c.String()
111		}
112	}
113	s = strings.TrimRight(s, " ")
114	return
115}
116
117// Render renders the line to a string with all the required attributes and
118// styles.
119func (l Line) Render() string {
120	var buf strings.Builder
121	renderLine(&buf, l)
122	return strings.TrimRight(buf.String(), " ") // Trim trailing spaces
123}
124
125func renderLine(buf io.StringWriter, l Line) {
126	var pen Style
127	var link Link
128	var pendingLine string
129	var pendingWidth int // this ignores space cells until we hit a non-space cell
130
131	writePending := func() {
132		// If there's no pending line, we don't need to do anything.
133		if len(pendingLine) == 0 {
134			return
135		}
136		buf.WriteString(pendingLine)
137		pendingWidth = 0
138		pendingLine = ""
139	}
140
141	for _, cell := range l {
142		if cell.Width > 0 {
143			// Convert the cell's style and link to the given color profile.
144			cellStyle := cell.Style
145			cellLink := cell.Link
146			if cellStyle.IsZero() && !pen.IsZero() {
147				writePending()
148				buf.WriteString(ansi.ResetStyle) //nolint:errcheck
149				pen = Style{}
150			}
151			if !cellStyle.Equal(&pen) {
152				writePending()
153				seq := cellStyle.DiffSequence(pen)
154				buf.WriteString(seq) // nolint:errcheck
155				pen = cellStyle
156			}
157
158			// Write the URL escape sequence
159			if cellLink != link && link.URL != "" {
160				writePending()
161				buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck
162				link = Link{}
163			}
164			if cellLink != link {
165				writePending()
166				buf.WriteString(ansi.SetHyperlink(cellLink.URL, cellLink.Params)) //nolint:errcheck
167				link = cellLink
168			}
169
170			// We only write the cell content if it's not empty. If it is, we
171			// append it to the pending line and width to be evaluated later.
172			if cell.Equal(&EmptyCell) {
173				pendingLine += cell.String()
174				pendingWidth += cell.Width
175			} else {
176				writePending()
177				buf.WriteString(cell.String()) //nolint:errcheck
178			}
179		}
180	}
181	if link.URL != "" {
182		buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck
183	}
184	if !pen.IsZero() {
185		buf.WriteString(ansi.ResetStyle) //nolint:errcheck
186	}
187}
188
189// Buffer represents a cell buffer that contains the contents of a screen.
190type Buffer struct {
191	// Lines is a slice of lines that make up the cells of the buffer.
192	Lines []Line
193	// Touched represents the lines that have been modified or touched. It is
194	// used to track which lines need to be redrawn.
195	Touched []*LineData
196}
197
198// NewBuffer creates a new buffer with the given width and height.
199// This is a convenience function that initializes a new buffer and resizes it.
200func NewBuffer(width int, height int) *Buffer {
201	b := new(Buffer)
202	b.Lines = make([]Line, height)
203	for i := range b.Lines {
204		b.Lines[i] = make(Line, width)
205		for j := range b.Lines[i] {
206			b.Lines[i][j] = EmptyCell
207		}
208	}
209	b.Touched = make([]*LineData, height)
210	b.Resize(width, height)
211	return b
212}
213
214// String returns the string representation of the buffer.
215func (b *Buffer) String() string {
216	var buf strings.Builder
217	for i, l := range b.Lines {
218		buf.WriteString(l.String())
219		if i < len(b.Lines)-1 {
220			buf.WriteString("\r\n") //nolint:errcheck
221		}
222	}
223	return buf.String()
224}
225
226// Render renders the buffer to a string with all the required attributes and
227// styles.
228func (b *Buffer) Render() string {
229	var buf strings.Builder
230	for i, l := range b.Lines {
231		renderLine(&buf, l)
232		if i < len(b.Lines)-1 {
233			buf.WriteString("\r\n") //nolint:errcheck
234		}
235	}
236	return strings.TrimRight(buf.String(), " ") // Trim trailing spaces
237}
238
239// Line returns a pointer to the line at the given y position.
240// If the line does not exist, it returns nil.
241func (b *Buffer) Line(y int) Line {
242	if y < 0 || y >= len(b.Lines) {
243		return nil
244	}
245	return b.Lines[y]
246}
247
248// CellAt returns the cell at the given position. It returns nil if the
249// position is out of bounds.
250func (b *Buffer) CellAt(x int, y int) *Cell {
251	if y < 0 || y >= len(b.Lines) {
252		return nil
253	}
254	return b.Lines[y].At(x)
255}
256
257// SetCell sets the cell at the given x, y position.
258func (b *Buffer) SetCell(x, y int, c *Cell) {
259	if y < 0 || y >= len(b.Lines) {
260		return
261	}
262
263	if !cellEqual(b.CellAt(x, y), c) {
264		width := 1
265		if c != nil && c.Width > 0 {
266			width = c.Width
267		}
268		b.TouchLine(x, y, width)
269	}
270	b.Lines[y].Set(x, c)
271}
272
273// Touch marks the cell at the given x, y position as touched.
274func (b *Buffer) Touch(x, y int) {
275	b.TouchLine(x, y, 0)
276}
277
278// TouchLine marks a line n times starting at the given x position as touched.
279func (b *Buffer) TouchLine(x, y, n int) {
280	if y < 0 || y >= len(b.Lines) {
281		return
282	}
283
284	if y >= len(b.Touched) {
285		b.Touched = append(b.Touched, make([]*LineData, y-len(b.Touched)+1)...)
286	}
287
288	ch := b.Touched[y]
289	if ch == nil {
290		ch = &LineData{FirstCell: x, LastCell: x + n}
291	} else {
292		ch.FirstCell = min(ch.FirstCell, x)
293		ch.LastCell = max(ch.LastCell, x+n)
294	}
295	b.Touched[y] = ch
296}
297
298// Height implements Screen.
299func (b *Buffer) Height() int {
300	return len(b.Lines)
301}
302
303// Width implements Screen.
304func (b *Buffer) Width() int {
305	if len(b.Lines) == 0 {
306		return 0
307	}
308	w := len(b.Lines[0])
309	for _, l := range b.Lines {
310		if len(l) > w {
311			w = len(l)
312		}
313	}
314	return w
315}
316
317// Bounds returns the bounds of the buffer.
318func (b *Buffer) Bounds() Rectangle {
319	return Rect(0, 0, b.Width(), b.Height())
320}
321
322// Resize resizes the buffer to the given width and height.
323func (b *Buffer) Resize(width int, height int) {
324	curWidth, curHeight := b.Width(), b.Height()
325	if curWidth == width && curHeight == height {
326		// No need to resize if the dimensions are the same.
327		return
328	}
329
330	if width > curWidth {
331		line := make(Line, width-curWidth)
332		for i := range line {
333			line[i] = EmptyCell
334		}
335		for i := range b.Lines {
336			b.Lines[i] = append(b.Lines[i], line...)
337		}
338	} else if width < curWidth {
339		for i := range b.Lines {
340			b.Lines[i] = b.Lines[i][:width]
341		}
342	}
343
344	if height > len(b.Lines) {
345		for i := len(b.Lines); i < height; i++ {
346			line := make(Line, width)
347			for j := range line {
348				line[j] = EmptyCell
349			}
350			b.Lines = append(b.Lines, line)
351		}
352	} else if height < len(b.Lines) {
353		b.Lines = b.Lines[:height]
354	}
355}
356
357// Fill fills the buffer with the given cell and rectangle.
358func (b *Buffer) Fill(c *Cell) {
359	b.FillArea(c, b.Bounds())
360}
361
362// FillArea fills the buffer with the given cell and rectangle.
363func (b *Buffer) FillArea(c *Cell, area Rectangle) {
364	cellWidth := 1
365	if c != nil && c.Width > 1 {
366		cellWidth = c.Width
367	}
368	for y := area.Min.Y; y < area.Max.Y; y++ {
369		for x := area.Min.X; x < area.Max.X; x += cellWidth {
370			b.SetCell(x, y, c)
371		}
372	}
373}
374
375// Clear clears the buffer with space cells and rectangle.
376func (b *Buffer) Clear() {
377	b.ClearArea(b.Bounds())
378}
379
380// ClearArea clears the buffer with space cells within the specified
381// rectangles. Only cells within the rectangle's bounds are affected.
382func (b *Buffer) ClearArea(area Rectangle) {
383	b.FillArea(nil, area)
384}
385
386// CloneArea clones the area of the buffer within the specified rectangle. If
387// the area is out of bounds, it returns nil.
388func (b *Buffer) CloneArea(area Rectangle) *Buffer {
389	bounds := b.Bounds()
390	if !area.In(bounds) {
391		return nil
392	}
393	n := NewBuffer(area.Dx(), area.Dy())
394	for y := area.Min.Y; y < area.Max.Y; y++ {
395		for x := area.Min.X; x < area.Max.X; x++ {
396			c := b.CellAt(x, y)
397			n.SetCell(x-area.Min.X, y-area.Min.Y, c)
398		}
399	}
400	return n
401}
402
403// Clone clones the entire buffer into a new buffer.
404func (b *Buffer) Clone() *Buffer {
405	return b.CloneArea(b.Bounds())
406}
407
408// Draw draws the buffer to the given screen at the specified area.
409// It implements the [Drawable] interface.
410func (b *Buffer) Draw(scr Screen, area Rectangle) {
411	if area.Empty() {
412		return
413	}
414
415	// Ensure the area is within the bounds of the screen.
416	bounds := scr.Bounds()
417	if !area.In(bounds) {
418		return
419	}
420
421	for y := area.Min.Y; y < area.Max.Y; y++ {
422		for x := area.Min.X; x < area.Max.X; x++ {
423			c := b.CellAt(x-area.Min.X, y-area.Min.Y)
424			if c == nil || c.IsZero() {
425				continue
426			}
427			scr.SetCell(x, y, c)
428		}
429	}
430}
431
432// InsertLine inserts n lines at the given line position, with the given
433// optional cell, within the specified rectangles. If no rectangles are
434// specified, it inserts lines in the entire buffer. Only cells within the
435// rectangle's horizontal bounds are affected. Lines are pushed out of the
436// rectangle bounds and lost. This follows terminal [ansi.IL] behavior.
437// It returns the pushed out lines.
438func (b *Buffer) InsertLine(y, n int, c *Cell) {
439	b.InsertLineArea(y, n, c, b.Bounds())
440}
441
442// InsertLineArea inserts new lines at the given line position, with the
443// given optional cell, within the rectangle bounds. Only cells within the
444// rectangle's horizontal bounds are affected. Lines are pushed out of the
445// rectangle bounds and lost. This follows terminal [ansi.IL] behavior.
446func (b *Buffer) InsertLineArea(y, n int, c *Cell, area Rectangle) {
447	if n <= 0 || y < area.Min.Y || y >= area.Max.Y || y >= b.Height() {
448		return
449	}
450
451	// Limit number of lines to insert to available space
452	if y+n > area.Max.Y {
453		n = area.Max.Y - y
454	}
455
456	// Move existing lines down within the bounds
457	for i := area.Max.Y - 1; i >= y+n; i-- {
458		for x := area.Min.X; x < area.Max.X; x++ {
459			// We don't need to clone c here because we're just moving lines down.
460			b.Lines[i][x] = b.Lines[i-n][x]
461		}
462		b.TouchLine(area.Min.X, i, area.Max.X-area.Min.X)
463		b.TouchLine(area.Min.X, i-n, area.Max.X-area.Min.X)
464	}
465
466	// Clear the newly inserted lines within bounds
467	for i := y; i < y+n; i++ {
468		for x := area.Min.X; x < area.Max.X; x++ {
469			b.SetCell(x, i, c)
470		}
471	}
472}
473
474// DeleteLineArea deletes lines at the given line position, with the given
475// optional cell, within the rectangle bounds. Only cells within the
476// rectangle's bounds are affected. Lines are shifted up within the bounds and
477// new blank lines are created at the bottom. This follows terminal [ansi.DL]
478// behavior.
479func (b *Buffer) DeleteLineArea(y, n int, c *Cell, area Rectangle) {
480	if n <= 0 || y < area.Min.Y || y >= area.Max.Y || y >= b.Height() {
481		return
482	}
483
484	// Limit deletion count to available space in scroll region
485	if n > area.Max.Y-y {
486		n = area.Max.Y - y
487	}
488
489	// Shift cells up within the bounds
490	for dst := y; dst < area.Max.Y-n; dst++ {
491		src := dst + n
492		for x := area.Min.X; x < area.Max.X; x++ {
493			// We don't need to clone c here because we're just moving cells up.
494			b.Lines[dst][x] = b.Lines[src][x]
495		}
496		b.TouchLine(area.Min.X, dst, area.Max.X-area.Min.X)
497		b.TouchLine(area.Min.X, src, area.Max.X-area.Min.X)
498	}
499
500	// Fill the bottom n lines with blank cells
501	for i := area.Max.Y - n; i < area.Max.Y; i++ {
502		for x := area.Min.X; x < area.Max.X; x++ {
503			b.SetCell(x, i, c)
504		}
505	}
506}
507
508// DeleteLine deletes n lines at the given line position, with the given
509// optional cell, within the specified rectangles. If no rectangles are
510// specified, it deletes lines in the entire buffer.
511func (b *Buffer) DeleteLine(y, n int, c *Cell) {
512	b.DeleteLineArea(y, n, c, b.Bounds())
513}
514
515// InsertCell inserts new cells at the given position, with the given optional
516// cell, within the specified rectangles. If no rectangles are specified, it
517// inserts cells in the entire buffer. This follows terminal [ansi.ICH]
518// behavior.
519func (b *Buffer) InsertCell(x, y, n int, c *Cell) {
520	b.InsertCellArea(x, y, n, c, b.Bounds())
521}
522
523// InsertCellArea inserts new cells at the given position, with the given
524// optional cell, within the rectangle bounds. Only cells within the
525// rectangle's bounds are affected, following terminal [ansi.ICH] behavior.
526func (b *Buffer) InsertCellArea(x, y, n int, c *Cell, area Rectangle) {
527	if n <= 0 || y < area.Min.Y || y >= area.Max.Y || y >= b.Height() ||
528		x < area.Min.X || x >= area.Max.X || x >= b.Width() {
529		return
530	}
531
532	// Limit number of cells to insert to available space
533	if x+n > area.Max.X {
534		n = area.Max.X - x
535	}
536
537	// Move existing cells within rectangle bounds to the right
538	for i := area.Max.X - 1; i >= x+n && i-n >= area.Min.X; i-- {
539		// We don't need to clone c here because we're just moving cells to the
540		// right.
541		b.Lines[y][i] = b.Lines[y][i-n]
542	}
543	// Touch the lines that were moved
544	b.TouchLine(x, y, n)
545
546	// Clear the newly inserted cells within rectangle bounds
547	for i := x; i < x+n && i < area.Max.X; i++ {
548		b.SetCell(i, y, c)
549	}
550}
551
552// DeleteCell deletes cells at the given position, with the given optional
553// cell, within the specified rectangles. If no rectangles are specified, it
554// deletes cells in the entire buffer. This follows terminal [ansi.DCH]
555// behavior.
556func (b *Buffer) DeleteCell(x, y, n int, c *Cell) {
557	b.DeleteCellArea(x, y, n, c, b.Bounds())
558}
559
560// DeleteCellArea deletes cells at the given position, with the given
561// optional cell, within the rectangle bounds. Only cells within the
562// rectangle's bounds are affected, following terminal [ansi.DCH] behavior.
563func (b *Buffer) DeleteCellArea(x, y, n int, c *Cell, area Rectangle) {
564	if n <= 0 || y < area.Min.Y || y >= area.Max.Y || y >= b.Height() ||
565		x < area.Min.X || x >= area.Max.X || x >= b.Width() {
566		return
567	}
568
569	// Calculate how many positions we can actually delete
570	remainingCells := area.Max.X - x
571	if n > remainingCells {
572		n = remainingCells
573	}
574
575	// Shift the remaining cells to the left
576	for i := x; i < area.Max.X-n; i++ {
577		if i+n < area.Max.X {
578			// We need to use SetCell here to ensure we blank out any wide
579			// cells we encounter.
580			b.SetCell(i, y, b.CellAt(i+n, y))
581		}
582	}
583	// Touch the line that was modified
584	b.TouchLine(x, y, n)
585
586	// Fill the vacated positions with the given cell
587	for i := area.Max.X - n; i < area.Max.X; i++ {
588		b.SetCell(i, y, c)
589	}
590}
591
592// ScreenBuffer is a buffer that can be used as a [Screen].
593type ScreenBuffer struct {
594	*Buffer
595	Method ansi.Method
596}
597
598var _ Screen = ScreenBuffer{}
599
600// NewScreenBuffer creates a new ScreenBuffer with the given width and height.
601func NewScreenBuffer(width, height int) ScreenBuffer {
602	return ScreenBuffer{
603		Buffer: NewBuffer(width, height),
604		Method: ansi.WcWidth,
605	}
606}
607
608// WidthMethod returns the width method used by the screen.
609// It defaults to [ansi.WcWidth].
610func (s ScreenBuffer) WidthMethod() WidthMethod {
611	return s.Method
612}