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}