buffer.go

  1package cellbuf
  2
  3import (
  4	"strings"
  5
  6	"github.com/mattn/go-runewidth"
  7	"github.com/rivo/uniseg"
  8)
  9
 10// NewCell returns a new cell. This is a convenience function that initializes a
 11// new cell with the given content. The cell's width is determined by the
 12// content using [runewidth.RuneWidth].
 13// This will only account for the first combined rune in the content. If the
 14// content is empty, it will return an empty cell with a width of 0.
 15func NewCell(r rune, comb ...rune) (c *Cell) {
 16	c = new(Cell)
 17	c.Rune = r
 18	c.Width = runewidth.RuneWidth(r)
 19	for _, r := range comb {
 20		if runewidth.RuneWidth(r) > 0 {
 21			break
 22		}
 23		c.Comb = append(c.Comb, r)
 24	}
 25	c.Comb = comb
 26	c.Width = runewidth.StringWidth(string(append([]rune{r}, comb...)))
 27	return
 28}
 29
 30// NewCellString returns a new cell with the given string content. This is a
 31// convenience function that initializes a new cell with the given content. The
 32// cell's width is determined by the content using [runewidth.StringWidth].
 33// This will only use the first combined rune in the string. If the string is
 34// empty, it will return an empty cell with a width of 0.
 35func NewCellString(s string) (c *Cell) {
 36	c = new(Cell)
 37	for i, r := range s {
 38		if i == 0 {
 39			c.Rune = r
 40			// We only care about the first rune's width
 41			c.Width = runewidth.RuneWidth(r)
 42		} else {
 43			if runewidth.RuneWidth(r) > 0 {
 44				break
 45			}
 46			c.Comb = append(c.Comb, r)
 47		}
 48	}
 49	return
 50}
 51
 52// NewGraphemeCell returns a new cell. This is a convenience function that
 53// initializes a new cell with the given content. The cell's width is determined
 54// by the content using [uniseg.FirstGraphemeClusterInString].
 55// This is used when the content is a grapheme cluster i.e. a sequence of runes
 56// that form a single visual unit.
 57// This will only return the first grapheme cluster in the string. If the
 58// string is empty, it will return an empty cell with a width of 0.
 59func NewGraphemeCell(s string) (c *Cell) {
 60	g, _, w, _ := uniseg.FirstGraphemeClusterInString(s, -1)
 61	return newGraphemeCell(g, w)
 62}
 63
 64func newGraphemeCell(s string, w int) (c *Cell) {
 65	c = new(Cell)
 66	c.Width = w
 67	for i, r := range s {
 68		if i == 0 {
 69			c.Rune = r
 70		} else {
 71			c.Comb = append(c.Comb, r)
 72		}
 73	}
 74	return
 75}
 76
 77// Line represents a line in the terminal.
 78// A nil cell represents an blank cell, a cell with a space character and a
 79// width of 1.
 80// If a cell has no content and a width of 0, it is a placeholder for a wide
 81// cell.
 82type Line []*Cell
 83
 84// Width returns the width of the line.
 85func (l Line) Width() int {
 86	return len(l)
 87}
 88
 89// Len returns the length of the line.
 90func (l Line) Len() int {
 91	return len(l)
 92}
 93
 94// String returns the string representation of the line. Any trailing spaces
 95// are removed.
 96func (l Line) String() (s string) {
 97	for _, c := range l {
 98		if c == nil {
 99			s += " "
100		} else if c.Empty() {
101			continue
102		} else {
103			s += c.String()
104		}
105	}
106	s = strings.TrimRight(s, " ")
107	return
108}
109
110// At returns the cell at the given x position.
111// If the cell does not exist, it returns nil.
112func (l Line) At(x int) *Cell {
113	if x < 0 || x >= len(l) {
114		return nil
115	}
116
117	c := l[x]
118	if c == nil {
119		newCell := BlankCell
120		return &newCell
121	}
122
123	return c
124}
125
126// Set sets the cell at the given x position. If a wide cell is given, it will
127// set the cell and the following cells to [EmptyCell]. It returns true if the
128// cell was set.
129func (l Line) Set(x int, c *Cell) bool {
130	return l.set(x, c, true)
131}
132
133func (l Line) set(x int, c *Cell, clone bool) bool {
134	width := l.Width()
135	if x < 0 || x >= width {
136		return false
137	}
138
139	// When a wide cell is partially overwritten, we need
140	// to fill the rest of the cell with space cells to
141	// avoid rendering issues.
142	prev := l.At(x)
143	if prev != nil && prev.Width > 1 {
144		// Writing to the first wide cell
145		for j := 0; j < prev.Width && x+j < l.Width(); j++ {
146			l[x+j] = prev.Clone().Blank()
147		}
148	} else if prev != nil && prev.Width == 0 {
149		// Writing to wide cell placeholders
150		for j := 1; j < maxCellWidth && x-j >= 0; j++ {
151			wide := l.At(x - j)
152			if wide != nil && wide.Width > 1 && j < wide.Width {
153				for k := 0; k < wide.Width; k++ {
154					l[x-j+k] = wide.Clone().Blank()
155				}
156				break
157			}
158		}
159	}
160
161	if clone && c != nil {
162		// Clone the cell if not nil.
163		c = c.Clone()
164	}
165
166	if c != nil && x+c.Width > width {
167		// If the cell is too wide, we write blanks with the same style.
168		for i := 0; i < c.Width && x+i < width; i++ {
169			l[x+i] = c.Clone().Blank()
170		}
171	} else {
172		l[x] = c
173
174		// Mark wide cells with an empty cell zero width
175		// We set the wide cell down below
176		if c != nil && c.Width > 1 {
177			for j := 1; j < c.Width && x+j < l.Width(); j++ {
178				var wide Cell
179				l[x+j] = &wide
180			}
181		}
182	}
183
184	return true
185}
186
187// Buffer is a 2D grid of cells representing a screen or terminal.
188type Buffer struct {
189	// Lines holds the lines of the buffer.
190	Lines []Line
191}
192
193// NewBuffer creates a new buffer with the given width and height.
194// This is a convenience function that initializes a new buffer and resizes it.
195func NewBuffer(width int, height int) *Buffer {
196	b := new(Buffer)
197	b.Resize(width, height)
198	return b
199}
200
201// String returns the string representation of the buffer.
202func (b *Buffer) String() (s string) {
203	for i, l := range b.Lines {
204		s += l.String()
205		if i < len(b.Lines)-1 {
206			s += "\r\n"
207		}
208	}
209	return
210}
211
212// Line returns a pointer to the line at the given y position.
213// If the line does not exist, it returns nil.
214func (b *Buffer) Line(y int) Line {
215	if y < 0 || y >= len(b.Lines) {
216		return nil
217	}
218	return b.Lines[y]
219}
220
221// Cell implements Screen.
222func (b *Buffer) Cell(x int, y int) *Cell {
223	if y < 0 || y >= len(b.Lines) {
224		return nil
225	}
226	return b.Lines[y].At(x)
227}
228
229// maxCellWidth is the maximum width a terminal cell can get.
230const maxCellWidth = 4
231
232// SetCell sets the cell at the given x, y position.
233func (b *Buffer) SetCell(x, y int, c *Cell) bool {
234	return b.setCell(x, y, c, true)
235}
236
237// setCell sets the cell at the given x, y position. This will always clone and
238// allocates a new cell if c is not nil.
239func (b *Buffer) setCell(x, y int, c *Cell, clone bool) bool {
240	if y < 0 || y >= len(b.Lines) {
241		return false
242	}
243	return b.Lines[y].set(x, c, clone)
244}
245
246// Height implements Screen.
247func (b *Buffer) Height() int {
248	return len(b.Lines)
249}
250
251// Width implements Screen.
252func (b *Buffer) Width() int {
253	if len(b.Lines) == 0 {
254		return 0
255	}
256	return b.Lines[0].Width()
257}
258
259// Bounds returns the bounds of the buffer.
260func (b *Buffer) Bounds() Rectangle {
261	return Rect(0, 0, b.Width(), b.Height())
262}
263
264// Resize resizes the buffer to the given width and height.
265func (b *Buffer) Resize(width int, height int) {
266	if width == 0 || height == 0 {
267		b.Lines = nil
268		return
269	}
270
271	if width > b.Width() {
272		line := make(Line, width-b.Width())
273		for i := range b.Lines {
274			b.Lines[i] = append(b.Lines[i], line...)
275		}
276	} else if width < b.Width() {
277		for i := range b.Lines {
278			b.Lines[i] = b.Lines[i][:width]
279		}
280	}
281
282	if height > len(b.Lines) {
283		for i := len(b.Lines); i < height; i++ {
284			b.Lines = append(b.Lines, make(Line, width))
285		}
286	} else if height < len(b.Lines) {
287		b.Lines = b.Lines[:height]
288	}
289}
290
291// FillRect fills the buffer with the given cell and rectangle.
292func (b *Buffer) FillRect(c *Cell, rect Rectangle) {
293	cellWidth := 1
294	if c != nil && c.Width > 1 {
295		cellWidth = c.Width
296	}
297	for y := rect.Min.Y; y < rect.Max.Y; y++ {
298		for x := rect.Min.X; x < rect.Max.X; x += cellWidth {
299			b.setCell(x, y, c, false) //nolint:errcheck
300		}
301	}
302}
303
304// Fill fills the buffer with the given cell and rectangle.
305func (b *Buffer) Fill(c *Cell) {
306	b.FillRect(c, b.Bounds())
307}
308
309// Clear clears the buffer with space cells and rectangle.
310func (b *Buffer) Clear() {
311	b.ClearRect(b.Bounds())
312}
313
314// ClearRect clears the buffer with space cells within the specified
315// rectangles. Only cells within the rectangle's bounds are affected.
316func (b *Buffer) ClearRect(rect Rectangle) {
317	b.FillRect(nil, rect)
318}
319
320// InsertLine inserts n lines at the given line position, with the given
321// optional cell, within the specified rectangles. If no rectangles are
322// specified, it inserts lines in the entire buffer. Only cells within the
323// rectangle's horizontal bounds are affected. Lines are pushed out of the
324// rectangle bounds and lost. This follows terminal [ansi.IL] behavior.
325// It returns the pushed out lines.
326func (b *Buffer) InsertLine(y, n int, c *Cell) {
327	b.InsertLineRect(y, n, c, b.Bounds())
328}
329
330// InsertLineRect inserts new lines at the given line position, with the
331// given optional cell, within the rectangle bounds. Only cells within the
332// rectangle's horizontal bounds are affected. Lines are pushed out of the
333// rectangle bounds and lost. This follows terminal [ansi.IL] behavior.
334func (b *Buffer) InsertLineRect(y, n int, c *Cell, rect Rectangle) {
335	if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() {
336		return
337	}
338
339	// Limit number of lines to insert to available space
340	if y+n > rect.Max.Y {
341		n = rect.Max.Y - y
342	}
343
344	// Move existing lines down within the bounds
345	for i := rect.Max.Y - 1; i >= y+n; i-- {
346		for x := rect.Min.X; x < rect.Max.X; x++ {
347			// We don't need to clone c here because we're just moving lines down.
348			b.setCell(x, i, b.Lines[i-n][x], false)
349		}
350	}
351
352	// Clear the newly inserted lines within bounds
353	for i := y; i < y+n; i++ {
354		for x := rect.Min.X; x < rect.Max.X; x++ {
355			b.setCell(x, i, c, true)
356		}
357	}
358}
359
360// DeleteLineRect deletes lines at the given line position, with the given
361// optional cell, within the rectangle bounds. Only cells within the
362// rectangle's bounds are affected. Lines are shifted up within the bounds and
363// new blank lines are created at the bottom. This follows terminal [ansi.DL]
364// behavior.
365func (b *Buffer) DeleteLineRect(y, n int, c *Cell, rect Rectangle) {
366	if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() {
367		return
368	}
369
370	// Limit deletion count to available space in scroll region
371	if n > rect.Max.Y-y {
372		n = rect.Max.Y - y
373	}
374
375	// Shift cells up within the bounds
376	for dst := y; dst < rect.Max.Y-n; dst++ {
377		src := dst + n
378		for x := rect.Min.X; x < rect.Max.X; x++ {
379			// We don't need to clone c here because we're just moving cells up.
380			// b.lines[dst][x] = b.lines[src][x]
381			b.setCell(x, dst, b.Lines[src][x], false)
382		}
383	}
384
385	// Fill the bottom n lines with blank cells
386	for i := rect.Max.Y - n; i < rect.Max.Y; i++ {
387		for x := rect.Min.X; x < rect.Max.X; x++ {
388			b.setCell(x, i, c, true)
389		}
390	}
391}
392
393// DeleteLine deletes n lines at the given line position, with the given
394// optional cell, within the specified rectangles. If no rectangles are
395// specified, it deletes lines in the entire buffer.
396func (b *Buffer) DeleteLine(y, n int, c *Cell) {
397	b.DeleteLineRect(y, n, c, b.Bounds())
398}
399
400// InsertCell inserts new cells at the given position, with the given optional
401// cell, within the specified rectangles. If no rectangles are specified, it
402// inserts cells in the entire buffer. This follows terminal [ansi.ICH]
403// behavior.
404func (b *Buffer) InsertCell(x, y, n int, c *Cell) {
405	b.InsertCellRect(x, y, n, c, b.Bounds())
406}
407
408// InsertCellRect inserts new cells at the given position, with the given
409// optional cell, within the rectangle bounds. Only cells within the
410// rectangle's bounds are affected, following terminal [ansi.ICH] behavior.
411func (b *Buffer) InsertCellRect(x, y, n int, c *Cell, rect Rectangle) {
412	if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() ||
413		x < rect.Min.X || x >= rect.Max.X || x >= b.Width() {
414		return
415	}
416
417	// Limit number of cells to insert to available space
418	if x+n > rect.Max.X {
419		n = rect.Max.X - x
420	}
421
422	// Move existing cells within rectangle bounds to the right
423	for i := rect.Max.X - 1; i >= x+n && i-n >= rect.Min.X; i-- {
424		// We don't need to clone c here because we're just moving cells to the
425		// right.
426		// b.lines[y][i] = b.lines[y][i-n]
427		b.setCell(i, y, b.Lines[y][i-n], false)
428	}
429
430	// Clear the newly inserted cells within rectangle bounds
431	for i := x; i < x+n && i < rect.Max.X; i++ {
432		b.setCell(i, y, c, true)
433	}
434}
435
436// DeleteCell deletes cells at the given position, with the given optional
437// cell, within the specified rectangles. If no rectangles are specified, it
438// deletes cells in the entire buffer. This follows terminal [ansi.DCH]
439// behavior.
440func (b *Buffer) DeleteCell(x, y, n int, c *Cell) {
441	b.DeleteCellRect(x, y, n, c, b.Bounds())
442}
443
444// DeleteCellRect deletes cells at the given position, with the given
445// optional cell, within the rectangle bounds. Only cells within the
446// rectangle's bounds are affected, following terminal [ansi.DCH] behavior.
447func (b *Buffer) DeleteCellRect(x, y, n int, c *Cell, rect Rectangle) {
448	if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() ||
449		x < rect.Min.X || x >= rect.Max.X || x >= b.Width() {
450		return
451	}
452
453	// Calculate how many positions we can actually delete
454	remainingCells := rect.Max.X - x
455	if n > remainingCells {
456		n = remainingCells
457	}
458
459	// Shift the remaining cells to the left
460	for i := x; i < rect.Max.X-n; i++ {
461		if i+n < rect.Max.X {
462			// We don't need to clone c here because we're just moving cells to
463			// the left.
464			// b.lines[y][i] = b.lines[y][i+n]
465			b.setCell(i, y, b.Lines[y][i+n], false)
466		}
467	}
468
469	// Fill the vacated positions with the given cell
470	for i := rect.Max.X - n; i < rect.Max.X; i++ {
471		b.setCell(i, y, c, true)
472	}
473}