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}