1package uv
2
3import (
4 "bytes"
5 "errors"
6 "io"
7 "strings"
8
9 "github.com/charmbracelet/colorprofile"
10 "github.com/charmbracelet/x/ansi"
11)
12
13// ErrInvalidDimensions is returned when the dimensions of a window are invalid
14// for the operation.
15var ErrInvalidDimensions = errors.New("invalid dimensions")
16
17// capabilities represents a mask of supported ANSI escape sequences.
18type capabilities uint
19
20const (
21 // Vertical Position Absolute [ansi.VPA].
22 capVPA capabilities = 1 << iota
23 // Horizontal Position Absolute [ansi.HPA].
24 capHPA
25 // Cursor Horizontal Tab [ansi.CHT].
26 capCHT
27 // Cursor Backward Tab [ansi.CBT].
28 capCBT
29 // Repeat Previous Character [ansi.REP].
30 capREP
31 // Erase Character [ansi.ECH].
32 capECH
33 // Insert Character [ansi.ICH].
34 capICH
35 // Scroll Down [ansi.SD].
36 capSD
37 // Scroll Up [ansi.SU].
38 capSU
39
40 // These capabilities depend on the tty termios settings and are not
41 // enabled by default.
42
43 // Tabulation [ansi.HT].
44 capHT
45 // Backspace [ansi.BS].
46 capBS
47
48 noCaps capabilities = 0
49 allCaps = capVPA | capHPA | capCHT | capCBT | capREP | capECH | capICH |
50 capSD | capSU
51)
52
53// Set sets the given capabilities.
54func (v *capabilities) Set(c capabilities) {
55 *v |= c
56}
57
58// Reset resets the given capabilities.
59func (v *capabilities) Reset(c capabilities) {
60 *v &^= c
61}
62
63// Contains returns whether the capabilities contains the given capability.
64func (v capabilities) Contains(c capabilities) bool {
65 return v&c == c
66}
67
68// cursor represents a terminal cursor.
69type cursor struct {
70 Style
71 Link
72 Position
73}
74
75// LineData represents the metadata for a line.
76type LineData struct {
77 // First and last changed cell indices.
78 FirstCell, LastCell int
79 // Old index used for scrolling
80 oldIndex int //nolint:unused
81}
82
83// tFlag is a bitmask of terminal flags.
84type tFlag uint
85
86// Terminal writer flags.
87const (
88 tRelativeCursor tFlag = 1 << iota
89 tCursorHidden
90 tAltScreen
91 tMapNewline
92)
93
94// Set sets the given flags.
95func (v *tFlag) Set(c tFlag) {
96 *v |= c
97}
98
99// Reset resets the given flags.
100func (v *tFlag) Reset(c tFlag) {
101 *v &^= c
102}
103
104// Contains returns whether the terminal flags contains the given flags.
105func (v tFlag) Contains(c tFlag) bool {
106 return v&c == c
107}
108
109// TerminalRenderer is a terminal screen render and lazy writer that buffers
110// the output until it is flushed. It handles rendering a screen from a
111// [Buffer] to the terminal with the minimal necessary escape sequences to
112// transition the terminal to the new buffer state. It uses various escape
113// sequence optimizations to reduce the number of bytes sent to the terminal.
114// It's designed to be lazy and only flush the output when necessary by calling
115// the [TerminalRenderer.Flush] method.
116//
117// The renderer handles the terminal's alternate screen and cursor visibility
118// via the [TerminalRenderer.EnterAltScreen], [TerminalRenderer.ExitAltScreen],
119// [TerminalRenderer.ShowCursor] and [TerminalRenderer.HideCursor] methods.
120// Using these methods will queue the appropriate escape sequences to enter or
121// exit the alternate screen and show or hide the cursor respectively to be
122// flushed to the terminal.
123//
124// Use the [io.Writer] and [io.StringWriter] interfaces to queue custom
125// commands the renderer.
126//
127// The renderer is not thread-safe, the caller must protect the renderer when
128// using it from multiple goroutines.
129type TerminalRenderer struct {
130 w io.Writer
131 buf *bytes.Buffer // buffer for writing to the screen
132 curbuf *Buffer // the current buffer
133 tabs *TabStops
134 oldhash, newhash []uint64 // the old and new hash values for each line
135 hashtab []hashmap // the hashmap table
136 oldnum []int // old indices from previous hash
137 cur, saved cursor // the current and saved cursors
138 flags tFlag // terminal writer flags.
139 term string // the terminal type
140 scrollHeight int // keeps track of how many lines we've scrolled down (inline mode)
141 clear bool // whether to force clear the screen
142 caps capabilities // terminal control sequence capabilities
143 atPhantom bool // whether the cursor is out of bounds and at a phantom cell
144 logger Logger // The logger used for debugging.
145 laterFlush bool // whether this is the first flush of the renderer
146
147 // profile is the color profile to use when downsampling colors. This is
148 // used to determine the appropriate color the terminal can display.
149 profile colorprofile.Profile
150}
151
152// NewTerminalRenderer returns a new [TerminalRenderer] that uses the given
153// writer, terminal type, and initializes the width of the terminal. The
154// terminal type is used to determine the capabilities of the terminal and
155// should be set to the value of the TERM environment variable.
156//
157// The renderer will try to detect the color profile from the output and the
158// given environment variables. Use [TerminalRenderer.SetColorProfile] method
159// to set a specific color profile for downsampling.
160//
161// See [TerminalRenderer] for more information on how to use the renderer.
162func NewTerminalRenderer(w io.Writer, env []string) (s *TerminalRenderer) {
163 s = new(TerminalRenderer)
164 s.w = w
165 s.profile = colorprofile.Detect(w, env)
166 s.buf = new(bytes.Buffer)
167 s.curbuf = NewBuffer(0, 0)
168 s.term = Environ(env).Getenv("TERM")
169 s.caps = xtermCaps(s.term)
170 s.cur = cursor{Position: Pos(-1, -1)} // start at -1 to force a move
171 s.saved = s.cur
172 s.scrollHeight = 0
173 s.oldhash, s.newhash = nil, nil
174 return
175}
176
177// SetLogger sets the logger to use for debugging. If nil, no logging will be
178// performed.
179func (s *TerminalRenderer) SetLogger(logger Logger) {
180 s.logger = logger
181}
182
183// SetColorProfile sets the color profile to use for downsampling colors. This
184// is used to determine the appropriate color the terminal can display.
185func (s *TerminalRenderer) SetColorProfile(profile colorprofile.Profile) {
186 s.profile = profile
187}
188
189// SetMapNewline sets whether the terminal is currently mapping newlines to
190// CRLF or carriage return and line feed. This is used to correctly determine
191// how to move the cursor when writing to the screen.
192func (s *TerminalRenderer) SetMapNewline(v bool) {
193 if v {
194 s.flags.Set(tMapNewline)
195 } else {
196 s.flags.Reset(tMapNewline)
197 }
198}
199
200// SetBackspace sets whether to use backspace as a movement optimization.
201func (s *TerminalRenderer) SetBackspace(v bool) {
202 if v {
203 s.caps.Set(capBS)
204 } else {
205 s.caps.Reset(capBS)
206 }
207}
208
209// SetTabStops sets the tab stops for the terminal and enables hard tabs
210// movement optimizations. Use -1 to disable hard tabs. This option is ignored
211// when the terminal type is "linux" as it does not support hard tabs.
212func (s *TerminalRenderer) SetTabStops(width int) {
213 if width < 0 || strings.HasPrefix(s.term, "linux") {
214 // Linux terminal does not support hard tabs.
215 s.caps.Reset(capHT)
216 } else {
217 s.caps.Set(capHT)
218 s.tabs = DefaultTabStops(width)
219 }
220}
221
222// SetAltScreen sets whether the alternate screen is enabled. This is used to
223// control the internal alternate screen state when it was changed outside the
224// renderer.
225func (s *TerminalRenderer) SetAltScreen(v bool) {
226 if v {
227 s.flags.Set(tAltScreen)
228 } else {
229 s.flags.Reset(tAltScreen)
230 }
231}
232
233// AltScreen returns whether the alternate screen is enabled. This returns the
234// state of the renderer's alternate screen which does not necessarily reflect
235// the actual alternate screen state of the terminal if it was changed outside
236// the renderer.
237func (s *TerminalRenderer) AltScreen() bool {
238 return s.flags.Contains(tAltScreen)
239}
240
241// SetCursorHidden sets whether the cursor is hidden. This is used to control
242// the internal cursor visibility state when it was changed outside the
243// renderer.
244func (s *TerminalRenderer) SetCursorHidden(v bool) {
245 if v {
246 s.flags.Set(tCursorHidden)
247 } else {
248 s.flags.Reset(tCursorHidden)
249 }
250}
251
252// CursorHidden returns the current cursor visibility state. This returns the
253// state of the renderer's cursor visibility which does not necessarily reflect
254// the actual cursor visibility state of the terminal if it was changed outside
255// the renderer.
256func (s *TerminalRenderer) CursorHidden() bool {
257 return s.flags.Contains(tCursorHidden)
258}
259
260// SetRelativeCursor sets whether to use relative cursor movements.
261func (s *TerminalRenderer) SetRelativeCursor(v bool) {
262 if v {
263 s.flags.Set(tRelativeCursor)
264 } else {
265 s.flags.Reset(tRelativeCursor)
266 }
267}
268
269// EnterAltScreen enters the alternate screen. This is used to switch to a
270// different screen buffer.
271// This queues the escape sequence command to enter the alternate screen, the
272// current cursor visibility state, saves the current cursor properties, and
273// queues a screen clear command.
274func (s *TerminalRenderer) EnterAltScreen() {
275 if !s.flags.Contains(tAltScreen) {
276 s.buf.WriteString(ansi.SetAltScreenSaveCursorMode) //nolint:errcheck
277 s.saved = s.cur
278 s.clear = true
279 }
280 s.flags.Set(tAltScreen)
281}
282
283// ExitAltScreen exits the alternate screen. This is used to switch back to the
284// main screen buffer.
285// This queues the escape sequence command to exit the alternate screen, , the
286// last cursor visibility state, restores the cursor properties, and queues a
287// screen clear command.
288func (s *TerminalRenderer) ExitAltScreen() {
289 if s.flags.Contains(tAltScreen) {
290 s.buf.WriteString(ansi.ResetAltScreenSaveCursorMode) //nolint:errcheck
291 s.cur = s.saved
292 s.clear = true
293 }
294 s.flags.Reset(tAltScreen)
295}
296
297// ShowCursor queues the escape sequence to show the cursor. This is used to
298// make the text cursor visible on the screen.
299func (s *TerminalRenderer) ShowCursor() {
300 if s.flags.Contains(tCursorHidden) {
301 s.buf.WriteString(ansi.ShowCursor) //nolint:errcheck
302 }
303 s.flags.Reset(tCursorHidden)
304}
305
306// HideCursor queues the escape sequence to hide the cursor. This is used to
307// make the text cursor invisible on the screen.
308func (s *TerminalRenderer) HideCursor() {
309 if !s.flags.Contains(tCursorHidden) {
310 s.buf.WriteString(ansi.HideCursor) //nolint:errcheck
311 }
312 s.flags.Set(tCursorHidden)
313}
314
315// PrependString adds the lines of the given string to the top of the terminal
316// screen. The lines prepended are not managed by the renderer and will not be
317// cleared or updated by the renderer.
318//
319// Using this when the terminal is using the alternate screen or when occupying
320// the whole screen may not produce any visible effects. This is because
321// once the terminal writes the prepended lines, they will get overwritten
322// by the next frame.
323func (s *TerminalRenderer) PrependString(str string) {
324 s.prependStringLines(strings.Split(str, "\n")...)
325}
326
327func (s *TerminalRenderer) prependStringLines(lines ...string) {
328 if len(lines) == 0 {
329 return
330 }
331
332 // TODO: Use scrolling region if available.
333 // TODO: Use [Screen.Write] [io.Writer] interface.
334
335 // We need to scroll the screen up by the number of lines in the queue.
336 // We can't use [ansi.SU] because we want the cursor to move down until
337 // it reaches the bottom of the screen.
338 s.move(s.curbuf, 0, s.curbuf.Height()-1)
339 s.buf.WriteString(strings.Repeat("\n", len(lines)))
340 s.cur.Y += len(lines)
341 // XXX: Now go to the top of the screen, insert new lines, and write
342 // the queued strings. It is important to use [Screen.moveCursor]
343 // instead of [Screen.move] because we don't want to perform any checks
344 // on the cursor position.
345 s.moveCursor(s.curbuf, 0, 0, false)
346 s.buf.WriteString(ansi.InsertLine(len(lines)))
347 for _, line := range lines {
348 s.buf.WriteString(line)
349 s.buf.WriteString("\r\n")
350 }
351}
352
353// PrependLines adds lines of cells to the top of the terminal screen. The
354// added lines are unmanaged and will not be cleared or updated by the
355// renderer.
356//
357// Using this when the terminal is using the alternate screen or when occupying
358// the whole screen may not produce any visible effects. This is because once
359// the terminal writes the prepended lines, they will get overwritten by the
360// next frame.
361func (s *TerminalRenderer) PrependLines(lines ...Line) {
362 strLines := make([]string, len(lines))
363 for i, line := range lines {
364 strLines[i] = line.Render()
365 }
366 s.prependStringLines(strLines...)
367}
368
369// moveCursor moves the cursor to the specified position.
370//
371// It is safe to call this function with a nil [Buffer], in that case, it won't
372// be using any optimizations that depend on the buffer.
373func (s *TerminalRenderer) moveCursor(newbuf *Buffer, x, y int, overwrite bool) {
374 if !s.flags.Contains(tAltScreen) && s.flags.Contains(tRelativeCursor) &&
375 s.cur.X == -1 && s.cur.Y == -1 {
376 // First cursor movement in inline mode, move the cursor to the first
377 // column before moving to the target position.
378 s.buf.WriteByte('\r') //nolint:errcheck
379 s.cur.X, s.cur.Y = 0, 0
380 }
381 seq, scrollHeight := moveCursor(s, newbuf, x, y, overwrite)
382 // If we scrolled the screen, we need to update the scroll height.
383 s.scrollHeight = max(s.scrollHeight, scrollHeight)
384 s.buf.WriteString(seq) //nolint:errcheck
385 s.cur.X, s.cur.Y = x, y
386}
387
388// move moves the cursor to the specified position in the buffer.
389//
390// It is safe to call this function with a nil [Buffer], in that case, it won't
391// be using any optimizations that depend on the buffer.
392func (s *TerminalRenderer) move(newbuf *Buffer, x, y int) {
393 // XXX: Make sure we use the max height and width of the buffer in case
394 // we're in the middle of a resize operation.
395 width, height := s.curbuf.Width(), s.curbuf.Height()
396 if newbuf != nil {
397 width = max(newbuf.Width(), width)
398 height = max(newbuf.Height(), height)
399 }
400
401 if width > 0 && x >= width {
402 // Handle autowrap
403 y += (x / width)
404 x %= width
405 }
406
407 // XXX: Disable styles if there's any
408 // Some move operations such as [ansi.LF] can apply styles to the new
409 // cursor position, thus, we need to reset the styles before moving the
410 // cursor.
411 blank := s.clearBlank()
412 resetPen := y != s.cur.Y && !blank.Equal(&EmptyCell)
413 if resetPen {
414 s.updatePen(nil)
415 }
416
417 // Reset wrap around (phantom cursor) state
418 if s.atPhantom {
419 s.cur.X = 0
420 s.buf.WriteByte('\r') //nolint:errcheck
421 s.atPhantom = false // reset phantom cell state
422 }
423
424 // TODO: Investigate if we need to handle this case and/or if we need the
425 // following code.
426 //
427 // if width > 0 && s.cur.X >= width {
428 // l := (s.cur.X + 1) / width
429 //
430 // s.cur.Y += l
431 // if height > 0 && s.cur.Y >= height {
432 // l -= s.cur.Y - height - 1
433 // }
434 //
435 // if l > 0 {
436 // s.cur.X = 0
437 // s.buf.WriteString("\r" + strings.Repeat("\n", l)) //nolint:errcheck
438 // }
439 // }
440
441 if height > 0 {
442 if s.cur.Y > height-1 {
443 s.cur.Y = height - 1
444 }
445 if y > height-1 {
446 y = height - 1
447 }
448 }
449
450 if x == s.cur.X && y == s.cur.Y {
451 // We give up later because we need to run checks for the phantom cell
452 // and others before we can determine if we can give up.
453 return
454 }
455
456 // We set the new cursor in tscreen.moveCursor].
457 s.moveCursor(newbuf, x, y, true) // Overwrite cells if possible
458}
459
460// cellEqual returns whether the two cells are equal. A nil cell is considered
461// a [EmptyCell].
462func cellEqual(a, b *Cell) bool {
463 if a == b {
464 return true
465 }
466 if a == nil || b == nil {
467 return false
468 }
469 return a.Equal(b)
470}
471
472// putCell draws a cell at the current cursor position.
473func (s *TerminalRenderer) putCell(newbuf *Buffer, cell *Cell) {
474 width, height := newbuf.Width(), newbuf.Height()
475 if s.flags.Contains(tAltScreen) && s.cur.X == width-1 && s.cur.Y == height-1 {
476 s.putCellLR(newbuf, cell)
477 } else {
478 s.putAttrCell(newbuf, cell)
479 }
480}
481
482// wrapCursor wraps the cursor to the next line.
483//
484//nolint:unused
485func (s *TerminalRenderer) wrapCursor() {
486 const autoRightMargin = true
487 if autoRightMargin {
488 // Assume we have auto wrap mode enabled.
489 s.cur.X = 0
490 s.cur.Y++
491 } else {
492 s.cur.X--
493 }
494}
495
496func (s *TerminalRenderer) putAttrCell(newbuf *Buffer, cell *Cell) {
497 if cell != nil && cell.IsZero() {
498 // XXX: Zero width cells are special and should not be written to the
499 // screen no matter what other attributes they have.
500 // Zero width cells are used for wide characters that are split into
501 // multiple cells.
502 return
503 }
504
505 if cell == nil {
506 cell = s.clearBlank()
507 }
508
509 // We're at pending wrap state (phantom cell), incoming cell should
510 // wrap.
511 if s.atPhantom {
512 s.wrapCursor()
513 s.atPhantom = false
514 }
515
516 s.updatePen(cell)
517 s.buf.WriteString(cell.Content) //nolint:errcheck
518
519 s.cur.X += cell.Width
520 if s.cur.X >= newbuf.Width() {
521 s.atPhantom = true
522 }
523}
524
525// putCellLR draws a cell at the lower right corner of the screen.
526func (s *TerminalRenderer) putCellLR(newbuf *Buffer, cell *Cell) {
527 // Optimize for the lower right corner cell.
528 curX := s.cur.X
529 if cell == nil || !cell.IsZero() {
530 s.buf.WriteString(ansi.ResetAutoWrapMode) //nolint:errcheck
531 s.putAttrCell(newbuf, cell)
532 // Writing to lower-right corner cell should not wrap.
533 s.atPhantom = false
534 s.cur.X = curX
535 s.buf.WriteString(ansi.SetAutoWrapMode) //nolint:errcheck
536 }
537}
538
539// updatePen updates the cursor pen styles.
540func (s *TerminalRenderer) updatePen(cell *Cell) {
541 if cell == nil {
542 cell = &EmptyCell
543 }
544
545 if s.profile != 0 {
546 // Downsample colors to the given color profile.
547 cell.Style = ConvertStyle(cell.Style, s.profile)
548 cell.Link = ConvertLink(cell.Link, s.profile)
549 }
550
551 if !cell.Style.Equal(&s.cur.Style) {
552 seq := cell.Style.DiffSequence(s.cur.Style)
553 if cell.Style.IsZero() && len(seq) > len(ansi.ResetStyle) {
554 seq = ansi.ResetStyle
555 }
556 s.buf.WriteString(seq) //nolint:errcheck
557 s.cur.Style = cell.Style
558 }
559 if !cell.Link.Equal(&s.cur.Link) {
560 s.buf.WriteString(ansi.SetHyperlink(cell.Link.URL, cell.Link.Params)) //nolint:errcheck
561 s.cur.Link = cell.Link
562 }
563}
564
565// emitRange emits a range of cells to the buffer. It it equivalent to calling
566// tscreen.putCell] for each cell in the range. This is optimized to use
567// [ansi.ECH] and [ansi.REP].
568// Returns whether the cursor is at the end of interval or somewhere in the
569// middle.
570func (s *TerminalRenderer) emitRange(newbuf *Buffer, line Line, n int) (eoi bool) {
571 hasECH := s.caps.Contains(capECH)
572 hasREP := s.caps.Contains(capREP)
573 if hasECH || hasREP {
574 for n > 0 {
575 var count int
576 for n > 1 && !cellEqual(line.At(0), line.At(1)) {
577 s.putCell(newbuf, line.At(0))
578 line = line[1:]
579 n--
580 }
581
582 cell0 := line[0]
583 if n == 1 {
584 s.putCell(newbuf, &cell0)
585 return false
586 }
587
588 count = 2
589 for count < n && cellEqual(line.At(count), &cell0) {
590 count++
591 }
592
593 ech := ansi.EraseCharacter(count)
594 cup := ansi.CursorPosition(s.cur.X+count, s.cur.Y)
595 rep := ansi.RepeatPreviousCharacter(count)
596 if hasECH && count > len(ech)+len(cup) && cell0.IsBlank() {
597 s.updatePen(&cell0)
598 s.buf.WriteString(ech) //nolint:errcheck
599
600 // If this is the last cell, we don't need to move the cursor.
601 if count < n {
602 s.move(newbuf, s.cur.X+count, s.cur.Y)
603 } else {
604 return true // cursor in the middle
605 }
606 } else if hasREP && count > len(rep) &&
607 (len(cell0.Content) == 1 && cell0.Content[0] >= ansi.US && cell0.Content[0] < ansi.DEL) {
608 // We only support ASCII characters. Most terminals will handle
609 // non-ASCII characters correctly, but some might not, ahem xterm.
610 //
611 // NOTE: [ansi.REP] only repeats the last rune and won't work
612 // if the last cell contains multiple runes.
613
614 wrapPossible := s.cur.X+count >= newbuf.Width()
615 repCount := count
616 if wrapPossible {
617 repCount--
618 }
619
620 s.updatePen(&cell0)
621 s.putCell(newbuf, &cell0)
622 repCount-- // cell0 is a single width cell ASCII character
623
624 s.buf.WriteString(ansi.RepeatPreviousCharacter(repCount)) //nolint:errcheck
625 s.cur.X += repCount
626 if wrapPossible {
627 s.putCell(newbuf, &cell0)
628 }
629 } else {
630 for i := 0; i < count; i++ {
631 s.putCell(newbuf, line.At(i))
632 }
633 }
634
635 line = line[clamp(count, 0, len(line)):]
636 n -= count
637 }
638
639 return false
640 }
641
642 for i := 0; i < n; i++ {
643 s.putCell(newbuf, line.At(i))
644 }
645
646 return false
647}
648
649// putRange puts a range of cells from the old line to the new line.
650// Returns whether the cursor is at the end of interval or somewhere in the
651// middle.
652func (s *TerminalRenderer) putRange(newbuf *Buffer, oldLine, newLine Line, y, start, end int) (eoi bool) {
653 inline := min(len(ansi.CursorPosition(start+1, y+1)),
654 min(len(ansi.HorizontalPositionAbsolute(start+1)),
655 len(ansi.CursorForward(start+1))))
656 if (end - start + 1) > inline {
657 var j, same int
658 for j, same = start, 0; j <= end; j++ {
659 oldCell, newCell := oldLine.At(j), newLine.At(j)
660 if same == 0 && oldCell != nil && oldCell.IsZero() {
661 continue
662 }
663 if cellEqual(oldCell, newCell) {
664 same++
665 } else {
666 if same > end-start {
667 s.emitRange(newbuf, newLine[start:], j-same-start)
668 s.move(newbuf, j, y)
669 start = j
670 }
671 same = 0
672 }
673 }
674
675 i := s.emitRange(newbuf, newLine[start:], j-same-start)
676
677 // Always return 1 for the next [tScreen.move] after a
678 // [tScreen.putRange] if we found identical characters at end of
679 // interval.
680 if same == 0 {
681 return i
682 }
683 return true
684 }
685
686 return s.emitRange(newbuf, newLine[start:], end-start+1)
687}
688
689// clearToEnd clears the screen from the current cursor position to the end of
690// line.
691func (s *TerminalRenderer) clearToEnd(newbuf *Buffer, blank *Cell, force bool) { //nolint:unparam
692 if s.cur.Y >= 0 {
693 curline := s.curbuf.Line(s.cur.Y)
694 for j := s.cur.X; j < s.curbuf.Width(); j++ {
695 if j >= 0 {
696 c := curline.At(j)
697 if !cellEqual(c, blank) {
698 curline.Set(j, blank)
699 force = true
700 }
701 }
702 }
703 }
704
705 if force {
706 s.updatePen(blank)
707 count := newbuf.Width() - s.cur.X
708 if s.el0Cost() <= count {
709 s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck
710 } else {
711 for i := 0; i < count; i++ {
712 s.putCell(newbuf, blank)
713 }
714 }
715 }
716}
717
718// clearBlank returns a blank cell based on the current cursor background color.
719func (s *TerminalRenderer) clearBlank() *Cell {
720 c := EmptyCell
721 if !s.cur.Style.IsZero() || !s.cur.Link.IsZero() {
722 c.Style = s.cur.Style
723 c.Link = s.cur.Link
724 }
725 return &c
726}
727
728// insertCells inserts the count cells pointed by the given line at the current
729// cursor position.
730func (s *TerminalRenderer) insertCells(newbuf *Buffer, line Line, count int) {
731 supportsICH := s.caps.Contains(capICH)
732 if supportsICH {
733 // Use [ansi.ICH] as an optimization.
734 s.buf.WriteString(ansi.InsertCharacter(count)) //nolint:errcheck
735 } else {
736 // Otherwise, use [ansi.IRM] mode.
737 s.buf.WriteString(ansi.SetInsertReplaceMode) //nolint:errcheck
738 }
739
740 for i := 0; count > 0; i++ {
741 s.putAttrCell(newbuf, line.At(i))
742 count--
743 }
744
745 if !supportsICH {
746 s.buf.WriteString(ansi.ResetInsertReplaceMode) //nolint:errcheck
747 }
748}
749
750// el0Cost returns the cost of using [ansi.EL] 0 i.e. [ansi.EraseLineRight]. If
751// this terminal supports background color erase, it can be cheaper to use
752// [ansi.EL] 0 i.e. [ansi.EraseLineRight] to clear
753// trailing spaces.
754func (s *TerminalRenderer) el0Cost() int {
755 if s.caps != noCaps {
756 return 0
757 }
758 return len(ansi.EraseLineRight)
759}
760
761// transformLine transforms the given line in the current window to the
762// corresponding line in the new window. It uses [ansi.ICH] and [ansi.DCH] to
763// insert or delete characters.
764func (s *TerminalRenderer) transformLine(newbuf *Buffer, y int) {
765 var firstCell, oLastCell, nLastCell int // first, old last, new last index
766 oldLine := s.curbuf.Line(y)
767 newLine := newbuf.Line(y)
768
769 // Find the first changed cell in the line
770 var lineChanged bool
771 for i := 0; i < newbuf.Width(); i++ {
772 if !cellEqual(newLine.At(i), oldLine.At(i)) {
773 lineChanged = true
774 break
775 }
776 }
777
778 const ceolStandoutGlitch = false
779 if ceolStandoutGlitch && lineChanged {
780 s.move(newbuf, 0, y)
781 s.clearToEnd(newbuf, nil, false)
782 s.putRange(newbuf, oldLine, newLine, y, 0, newbuf.Width()-1)
783 } else {
784 blank := newLine.At(0)
785
786 // It might be cheaper to clear leading spaces with [ansi.EL] 1 i.e.
787 // [ansi.EraseLineLeft].
788 if blank == nil || blank.IsBlank() {
789 var oFirstCell, nFirstCell int
790 for oFirstCell = 0; oFirstCell < s.curbuf.Width(); oFirstCell++ {
791 if !cellEqual(oldLine.At(oFirstCell), blank) {
792 break
793 }
794 }
795 for nFirstCell = 0; nFirstCell < newbuf.Width(); nFirstCell++ {
796 if !cellEqual(newLine.At(nFirstCell), blank) {
797 break
798 }
799 }
800
801 if nFirstCell == oFirstCell {
802 firstCell = nFirstCell
803
804 // Find the first differing cell
805 for firstCell < newbuf.Width() &&
806 cellEqual(oldLine.At(firstCell), newLine.At(firstCell)) {
807 firstCell++
808 }
809 } else if oFirstCell > nFirstCell {
810 firstCell = nFirstCell
811 } else if oFirstCell < nFirstCell {
812 firstCell = oFirstCell
813 el1Cost := len(ansi.EraseLineLeft)
814 if el1Cost < nFirstCell-oFirstCell {
815 if nFirstCell >= newbuf.Width() {
816 s.move(newbuf, 0, y)
817 s.updatePen(blank)
818 s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck
819 } else {
820 s.move(newbuf, nFirstCell-1, y)
821 s.updatePen(blank)
822 s.buf.WriteString(ansi.EraseLineLeft) //nolint:errcheck
823 }
824
825 for firstCell < nFirstCell {
826 oldLine.Set(firstCell, blank)
827 firstCell++
828 }
829 }
830 }
831 } else {
832 // Find the first differing cell
833 for firstCell < newbuf.Width() && cellEqual(newLine.At(firstCell), oldLine.At(firstCell)) {
834 firstCell++
835 }
836 }
837
838 // If we didn't find one, we're done
839 if firstCell >= newbuf.Width() {
840 return
841 }
842
843 blank = newLine.At(newbuf.Width() - 1)
844 if blank != nil && !blank.IsBlank() {
845 // Find the last differing cell
846 nLastCell = newbuf.Width() - 1
847 for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), oldLine.At(nLastCell)) {
848 nLastCell--
849 }
850
851 if nLastCell >= firstCell {
852 s.move(newbuf, firstCell, y)
853 s.putRange(newbuf, oldLine, newLine, y, firstCell, nLastCell)
854 if firstCell < len(oldLine) && firstCell < len(newLine) {
855 copy(oldLine[firstCell:], newLine[firstCell:])
856 } else {
857 copy(oldLine, newLine)
858 }
859 }
860
861 return
862 }
863
864 // Find last non-blank cell in the old line.
865 oLastCell = s.curbuf.Width() - 1
866 for oLastCell > firstCell && cellEqual(oldLine.At(oLastCell), blank) {
867 oLastCell--
868 }
869
870 // Find last non-blank cell in the new line.
871 nLastCell = newbuf.Width() - 1
872 for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), blank) {
873 nLastCell--
874 }
875
876 if nLastCell == firstCell && s.el0Cost() < oLastCell-nLastCell {
877 s.move(newbuf, firstCell, y)
878 if !cellEqual(newLine.At(firstCell), blank) {
879 s.putCell(newbuf, newLine.At(firstCell))
880 }
881 s.clearToEnd(newbuf, blank, false)
882 } else if nLastCell != oLastCell &&
883 !cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) {
884 s.move(newbuf, firstCell, y)
885 if oLastCell-nLastCell > s.el0Cost() {
886 if s.putRange(newbuf, oldLine, newLine, y, firstCell, nLastCell) {
887 s.move(newbuf, nLastCell+1, y)
888 }
889 s.clearToEnd(newbuf, blank, false)
890 } else {
891 n := max(nLastCell, oLastCell)
892 s.putRange(newbuf, oldLine, newLine, y, firstCell, n)
893 }
894 } else {
895 nLastNonBlank := nLastCell
896 oLastNonBlank := oLastCell
897
898 // Find the last cells that really differ.
899 // Can be -1 if no cells differ.
900 for cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) {
901 if !cellEqual(newLine.At(nLastCell-1), oldLine.At(oLastCell-1)) {
902 break
903 }
904 nLastCell--
905 oLastCell--
906 if nLastCell == -1 || oLastCell == -1 {
907 break
908 }
909 }
910
911 n := min(oLastCell, nLastCell)
912 if n >= firstCell {
913 s.move(newbuf, firstCell, y)
914 s.putRange(newbuf, oldLine, newLine, y, firstCell, n)
915 }
916
917 if oLastCell < nLastCell {
918 m := max(nLastNonBlank, oLastNonBlank)
919 if n != 0 {
920 for n > 0 {
921 wide := newLine.At(n + 1)
922 if wide == nil || !wide.IsZero() {
923 break
924 }
925 n--
926 oLastCell--
927 }
928 } else if n >= firstCell && newLine.At(n) != nil && newLine.At(n).Width > 1 {
929 next := newLine.At(n + 1)
930 for next != nil && next.IsZero() {
931 n++
932 oLastCell++
933 }
934 }
935
936 s.move(newbuf, n+1, y)
937 ichCost := 3 + nLastCell - oLastCell
938 if s.caps.Contains(capICH) && (nLastCell < nLastNonBlank || ichCost > (m-n)) {
939 s.putRange(newbuf, oldLine, newLine, y, n+1, m)
940 } else {
941 s.insertCells(newbuf, newLine[n+1:], nLastCell-oLastCell)
942 }
943 } else if oLastCell > nLastCell {
944 s.move(newbuf, n+1, y)
945 dchCost := 3 + oLastCell - nLastCell
946 if dchCost > len(ansi.EraseLineRight)+nLastNonBlank-(n+1) {
947 if s.putRange(newbuf, oldLine, newLine, y, n+1, nLastNonBlank) {
948 s.move(newbuf, nLastNonBlank+1, y)
949 }
950 s.clearToEnd(newbuf, blank, false)
951 } else {
952 s.updatePen(blank)
953 s.deleteCells(oLastCell - nLastCell)
954 }
955 }
956 }
957 }
958
959 // Update the old line with the new line
960 if firstCell < len(oldLine) && firstCell < len(newLine) {
961 copy(oldLine[firstCell:], newLine[firstCell:])
962 } else {
963 copy(oldLine, newLine)
964 }
965}
966
967// deleteCells deletes the count cells at the current cursor position and moves
968// the rest of the line to the left. This is equivalent to [ansi.DCH].
969func (s *TerminalRenderer) deleteCells(count int) {
970 // [ansi.DCH] will shift in cells from the right margin so we need to
971 // ensure that they are the right style.
972 s.buf.WriteString(ansi.DeleteCharacter(count)) //nolint:errcheck
973}
974
975// clearToBottom clears the screen from the current cursor position to the end
976// of the screen.
977func (s *TerminalRenderer) clearToBottom(blank *Cell) {
978 row, col := s.cur.Y, s.cur.X
979 if row < 0 {
980 row = 0
981 }
982
983 s.updatePen(blank)
984 s.buf.WriteString(ansi.EraseScreenBelow) //nolint:errcheck
985 // Clear the rest of the current line
986 s.curbuf.ClearArea(Rect(col, row, s.curbuf.Width()-col, 1))
987 // Clear everything below the current line
988 s.curbuf.ClearArea(Rect(0, row+1, s.curbuf.Width(), s.curbuf.Height()-row-1))
989}
990
991// clearBottom tests if clearing the end of the screen would satisfy part of
992// the screen update. Scan backwards through lines in the screen checking if
993// each is blank and one or more are changed.
994// It returns the top line.
995func (s *TerminalRenderer) clearBottom(newbuf *Buffer, total int) (top int) {
996 if total <= 0 {
997 return
998 }
999
1000 top = total
1001 last := min(s.curbuf.Width(), newbuf.Width())
1002 blank := s.clearBlank()
1003 canClearWithBlank := blank == nil || blank.IsBlank()
1004
1005 if canClearWithBlank {
1006 var row int
1007 for row = total - 1; row >= 0; row-- {
1008 oldLine := s.curbuf.Line(row)
1009 newLine := newbuf.Line(row)
1010
1011 var col int
1012 ok := true
1013 for col = 0; ok && col < last; col++ {
1014 ok = cellEqual(newLine.At(col), blank)
1015 }
1016 if !ok {
1017 break
1018 }
1019
1020 for col = 0; ok && col < last; col++ {
1021 ok = cellEqual(oldLine.At(col), blank)
1022 }
1023 if !ok {
1024 top = row
1025 }
1026 }
1027
1028 if top < total {
1029 s.move(newbuf, 0, max(0, top-1)) // top is 1-based
1030 s.clearToBottom(blank)
1031 if s.oldhash != nil && s.newhash != nil &&
1032 row < len(s.oldhash) && row < len(s.newhash) {
1033 for row := top; row < newbuf.Height(); row++ {
1034 s.oldhash[row] = s.newhash[row]
1035 }
1036 }
1037 }
1038 }
1039
1040 return
1041}
1042
1043// clearScreen clears the screen and put cursor at home.
1044func (s *TerminalRenderer) clearScreen(blank *Cell) {
1045 s.updatePen(blank)
1046 s.buf.WriteString(ansi.CursorHomePosition) //nolint:errcheck
1047 s.buf.WriteString(ansi.EraseEntireScreen) //nolint:errcheck
1048 s.cur.X, s.cur.Y = 0, 0
1049 s.curbuf.Fill(blank)
1050}
1051
1052// clearBelow clears everything below and including the row.
1053func (s *TerminalRenderer) clearBelow(newbuf *Buffer, blank *Cell, row int) {
1054 s.move(newbuf, 0, row)
1055 s.clearToBottom(blank)
1056}
1057
1058// clearUpdate forces a screen redraw.
1059func (s *TerminalRenderer) clearUpdate(newbuf *Buffer) {
1060 blank := s.clearBlank()
1061 var nonEmpty int
1062 if s.flags.Contains(tAltScreen) {
1063 // XXX: We're using the maximum height of the two buffers to ensure we
1064 // write newly added lines to the screen in
1065 // [terminalWriter.transformLine].
1066 nonEmpty = max(s.curbuf.Height(), newbuf.Height())
1067 s.clearScreen(blank)
1068 } else {
1069 nonEmpty = newbuf.Height()
1070 // FIXME: Investigate the double [ansi.ClearScreenBelow] call.
1071 // Commenting the line below out seems to work but it might cause other
1072 // bugs.
1073 s.clearBelow(newbuf, blank, 0)
1074 }
1075 nonEmpty = s.clearBottom(newbuf, nonEmpty)
1076 for i := 0; i < nonEmpty && i < newbuf.Height(); i++ {
1077 s.transformLine(newbuf, i)
1078 }
1079}
1080
1081func (s *TerminalRenderer) logf(format string, args ...any) {
1082 if s.logger == nil {
1083 return
1084 }
1085 s.logger.Printf(format, args...)
1086}
1087
1088// Buffered returns the number of bytes buffered for the next flush.
1089func (s *TerminalRenderer) Buffered() int {
1090 return s.buf.Len()
1091}
1092
1093// Flush flushes the buffer to the screen.
1094func (s *TerminalRenderer) Flush() (err error) {
1095 // Write the buffer
1096 if n := s.buf.Len(); n > 0 {
1097 bts := s.buf.Bytes()
1098 if !s.flags.Contains(tCursorHidden) {
1099 // Hide the cursor during the flush operation.
1100 buf := make([]byte, len(bts)+len(ansi.HideCursor)+len(ansi.ShowCursor))
1101 copy(buf, ansi.HideCursor)
1102 copy(buf[len(ansi.HideCursor):], bts)
1103 copy(buf[len(ansi.HideCursor)+len(bts):], ansi.ShowCursor)
1104 bts = buf
1105 }
1106 if s.logger != nil {
1107 s.logf("output: %q", bts)
1108 }
1109 _, err = s.w.Write(bts)
1110 s.buf.Reset()
1111 s.laterFlush = true
1112 }
1113 return
1114}
1115
1116// Touched returns the number of lines that have been touched or changed.
1117func (s *TerminalRenderer) Touched(buf *Buffer) (n int) {
1118 if buf.Touched == nil {
1119 return buf.Height()
1120 }
1121 for _, ch := range buf.Touched {
1122 if ch != nil {
1123 n++
1124 }
1125 }
1126 return
1127}
1128
1129// Redraw forces a full redraw of the screen. It's equivalent to calling
1130// [TerminalRenderer.Erase] and [TerminalRenderer.Render].
1131func (s *TerminalRenderer) Redraw(newbuf *Buffer) {
1132 s.clear = true
1133 s.Render(newbuf)
1134}
1135
1136// Render renders changes of the screen to the internal buffer. Call
1137// [terminalWriter.Flush] to flush pending changes to the screen.
1138func (s *TerminalRenderer) Render(newbuf *Buffer) {
1139 // Do we need to render anything?
1140 touchedLines := s.Touched(newbuf)
1141 if !s.clear && touchedLines == 0 {
1142 return
1143 }
1144
1145 newWidth, newHeight := newbuf.Width(), newbuf.Height()
1146 curWidth, curHeight := s.curbuf.Width(), s.curbuf.Height()
1147
1148 // Do we have a buffer to compare to?
1149 if s.curbuf == nil || s.curbuf.Bounds().Empty() {
1150 s.curbuf = NewBuffer(newWidth, newHeight)
1151 }
1152
1153 if curWidth != newWidth || curHeight != newHeight {
1154 s.oldhash, s.newhash = nil, nil
1155 }
1156
1157 // TODO: Investigate whether this is necessary. Theoretically, terminals
1158 // can add/remove tab stops and we should be able to handle that. We could
1159 // use [ansi.DECTABSR] to read the tab stops, but that's not implemented in
1160 // most terminals :/
1161 // // Are we using hard tabs? If so, ensure tabs are using the
1162 // // default interval using [ansi.DECST8C].
1163 // if s.opts.HardTabs && !s.initTabs {
1164 // s.buf.WriteString(ansi.SetTabEvery8Columns)
1165 // s.initTabs = true
1166 // }
1167
1168 var nonEmpty int
1169
1170 // XXX: In inline mode, after a screen resize, we need to clear the extra
1171 // lines at the bottom of the screen. This is because in inline mode, we
1172 // don't use the full screen height and the current buffer size might be
1173 // larger than the new buffer size.
1174 partialClear := !s.flags.Contains(tAltScreen) && s.cur.X != -1 && s.cur.Y != -1 &&
1175 curWidth == newWidth &&
1176 curHeight > 0 &&
1177 curHeight > newHeight
1178
1179 if !s.clear && partialClear {
1180 s.clearBelow(newbuf, nil, newHeight-1)
1181 }
1182
1183 if s.clear {
1184 s.clearUpdate(newbuf)
1185 s.clear = false
1186 } else if touchedLines > 0 {
1187 if s.flags.Contains(tAltScreen) {
1188 // Optimize scrolling for the alternate screen buffer.
1189 // TODO: Should we optimize for inline mode as well? If so, we need
1190 // to know the actual cursor position to use [ansi.DECSTBM].
1191 s.scrollOptimize(newbuf)
1192 }
1193
1194 var changedLines int
1195 var i int
1196
1197 if s.flags.Contains(tAltScreen) {
1198 nonEmpty = min(curHeight, newHeight)
1199 } else {
1200 nonEmpty = newHeight
1201 }
1202
1203 nonEmpty = s.clearBottom(newbuf, nonEmpty)
1204 for i = 0; i < nonEmpty && i < newHeight; i++ {
1205 if newbuf.Touched == nil || i >= len(newbuf.Touched) || (newbuf.Touched[i] != nil &&
1206 (newbuf.Touched[i].FirstCell != -1 || newbuf.Touched[i].LastCell != -1)) {
1207 s.transformLine(newbuf, i)
1208 changedLines++
1209 }
1210
1211 // Mark line changed successfully.
1212 if i < len(newbuf.Touched) && i <= newbuf.Height()-1 {
1213 newbuf.Touched[i] = &LineData{
1214 FirstCell: -1, LastCell: -1,
1215 }
1216 }
1217 if i < len(s.curbuf.Touched) && i < s.curbuf.Height()-1 {
1218 s.curbuf.Touched[i] = &LineData{
1219 FirstCell: -1, LastCell: -1,
1220 }
1221 }
1222 }
1223 }
1224
1225 if !s.laterFlush && !s.flags.Contains(tAltScreen) && s.scrollHeight < newHeight-1 {
1226 s.move(newbuf, 0, newHeight-1)
1227 }
1228
1229 // Sync windows and screen
1230 newbuf.Touched = make([]*LineData, newHeight)
1231 for i := range newbuf.Touched {
1232 newbuf.Touched[i] = &LineData{
1233 FirstCell: -1, LastCell: -1,
1234 }
1235 }
1236 for i := range s.curbuf.Touched {
1237 s.curbuf.Touched[i] = &LineData{
1238 FirstCell: -1, LastCell: -1,
1239 }
1240 }
1241
1242 if curWidth != newWidth || curHeight != newHeight {
1243 // Resize the old buffer to match the new buffer.
1244 s.curbuf.Resize(newWidth, newHeight)
1245 // Sync new lines to old lines
1246 for i := curHeight - 1; i < newHeight; i++ {
1247 copy(s.curbuf.Line(i), newbuf.Line(i))
1248 }
1249 }
1250
1251 s.updatePen(nil) // nil indicates a blank cell with no styles
1252}
1253
1254// Erase marks the screen to be fully erased on the next render.
1255func (s *TerminalRenderer) Erase() {
1256 s.clear = true
1257}
1258
1259// Resize updates the terminal screen tab stops. This is used to calculate
1260// terminal tab stops for hard tab optimizations.
1261func (s *TerminalRenderer) Resize(width, _ int) {
1262 if s.tabs != nil {
1263 s.tabs.Resize(width)
1264 }
1265 s.scrollHeight = 0
1266}
1267
1268// Position returns the cursor position in the screen buffer after applying any
1269// pending transformations from the underlying buffer.
1270func (s *TerminalRenderer) Position() (x, y int) {
1271 return s.cur.X, s.cur.Y
1272}
1273
1274// SetPosition changes the logical cursor position. This can be used when we
1275// change the cursor position outside of the screen and need to update the
1276// screen cursor position.
1277// This changes the cursor position for both normal and alternate screen
1278// buffers.
1279func (s *TerminalRenderer) SetPosition(x, y int) {
1280 s.cur.X, s.cur.Y = x, y
1281 s.saved.X, s.saved.Y = x, y
1282}
1283
1284// WriteString writes the given string to the underlying buffer.
1285func (s *TerminalRenderer) WriteString(str string) (int, error) {
1286 return s.buf.WriteString(str)
1287}
1288
1289// Write writes the given bytes to the underlying buffer.
1290func (s *TerminalRenderer) Write(b []byte) (int, error) {
1291 return s.buf.Write(b)
1292}
1293
1294// MoveTo calculates and writes the shortest sequence to move the cursor to the
1295// given position. It uses the current cursor position and the new position to
1296// calculate the shortest amount of sequences to move the cursor.
1297func (s *TerminalRenderer) MoveTo(x, y int) {
1298 s.move(nil, x, y)
1299}
1300
1301// notLocal returns whether the coordinates are not considered local movement
1302// using the defined thresholds.
1303// This takes the number of columns, and the coordinates of the current and
1304// target positions.
1305func notLocal(cols, fx, fy, tx, ty int) bool {
1306 // The typical distance for a [ansi.CUP] sequence. Anything less than this
1307 // is considered local movement.
1308 const longDist = 8 - 1
1309 return (tx > longDist) &&
1310 (tx < cols-1-longDist) &&
1311 (abs(ty-fy)+abs(tx-fx) > longDist)
1312}
1313
1314// relativeCursorMove returns the relative cursor movement sequence using one or two
1315// of the following sequences [ansi.CUU], [ansi.CUD], [ansi.CUF], [ansi.CUB],
1316// [ansi.VPA], [ansi.HPA].
1317// When overwrite is true, this will try to optimize the sequence by using the
1318// screen cells values to move the cursor instead of using escape sequences.
1319//
1320// It is safe to call this function with a nil [Buffer]. In that case, it won't
1321// use any optimizations that require the new buffer such as overwrite.
1322func relativeCursorMove(s *TerminalRenderer, newbuf *Buffer, fx, fy, tx, ty int, overwrite, useTabs, useBackspace bool) (string, int) {
1323 var seq strings.Builder
1324 var scrollHeight int
1325 if newbuf == nil {
1326 overwrite = false // We can't overwrite the current buffer.
1327 }
1328
1329 if ty != fy {
1330 var yseq string
1331 if s.caps.Contains(capVPA) && !s.flags.Contains(tRelativeCursor) {
1332 yseq = ansi.VerticalPositionAbsolute(ty + 1)
1333 }
1334
1335 if ty > fy {
1336 n := ty - fy
1337 if cud := ansi.CursorDown(n); yseq == "" || len(cud) < len(yseq) {
1338 yseq = cud
1339 }
1340 shouldScroll := !s.flags.Contains(tAltScreen) && ty > s.scrollHeight
1341 if lf := strings.Repeat("\n", n); shouldScroll || len(lf) < len(yseq) {
1342 yseq = lf
1343 scrollHeight = ty
1344 if s.flags.Contains(tMapNewline) {
1345 fx = 0
1346 }
1347 }
1348 } else if ty < fy {
1349 n := fy - ty
1350 if cuu := ansi.CursorUp(n); yseq == "" || len(cuu) < len(yseq) {
1351 yseq = cuu
1352 }
1353 if n == 1 && fy-1 > 0 {
1354 // TODO: Ensure we're not unintentionally scrolling the screen up.
1355 yseq = ansi.ReverseIndex
1356 }
1357 }
1358
1359 seq.WriteString(yseq)
1360 }
1361
1362 if tx != fx {
1363 var xseq string
1364 if s.caps.Contains(capHPA) && !s.flags.Contains(tRelativeCursor) {
1365 xseq = ansi.HorizontalPositionAbsolute(tx + 1)
1366 }
1367
1368 if tx > fx {
1369 n := tx - fx
1370 if useTabs && s.tabs != nil {
1371 var tabs int
1372 var col int
1373 for col = fx; s.tabs.Next(col) <= tx; col = s.tabs.Next(col) {
1374 tabs++
1375 if col == s.tabs.Next(col) || col >= s.tabs.Width()-1 {
1376 break
1377 }
1378 }
1379
1380 if tabs > 0 {
1381 cht := ansi.CursorHorizontalForwardTab(tabs)
1382 tab := strings.Repeat("\t", tabs)
1383 if false && s.caps.Contains(capCHT) && len(cht) < len(tab) {
1384 // TODO: The linux console and some terminals such as
1385 // Alacritty don't support [ansi.CHT]. Enable this when
1386 // we have a way to detect this, or after 5 years when
1387 // we're sure everyone has updated their terminals :P
1388 seq.WriteString(cht)
1389 } else {
1390 seq.WriteString(tab)
1391 }
1392
1393 n = tx - col
1394 fx = col
1395 }
1396 }
1397
1398 if cuf := ansi.CursorForward(n); xseq == "" || len(cuf) < len(xseq) {
1399 xseq = cuf
1400 }
1401
1402 // If we have no attribute and style changes, overwrite is cheaper.
1403 var ovw string
1404 if overwrite && ty >= 0 {
1405 for i := 0; i < n; i++ {
1406 cell := newbuf.CellAt(fx+i, ty)
1407 if cell != nil && cell.Width > 0 {
1408 i += cell.Width - 1
1409 if !cell.Style.Equal(&s.cur.Style) || !cell.Link.Equal(&s.cur.Link) {
1410 overwrite = false
1411 break
1412 }
1413 }
1414 }
1415 }
1416
1417 if overwrite && ty >= 0 {
1418 for i := 0; i < n; i++ {
1419 cell := newbuf.CellAt(fx+i, ty)
1420 if cell != nil && cell.Width > 0 {
1421 ovw += cell.String()
1422 i += cell.Width - 1
1423 } else {
1424 ovw += " "
1425 }
1426 }
1427 }
1428
1429 if overwrite && len(ovw) < len(xseq) {
1430 xseq = ovw
1431 }
1432 } else if tx < fx {
1433 n := fx - tx
1434 if useTabs && s.tabs != nil && s.caps.Contains(capCBT) {
1435 // VT100 does not support backward tabs [ansi.CBT].
1436
1437 col := fx
1438
1439 var cbt int // cursor backward tabs count
1440 for s.tabs.Prev(col) >= tx {
1441 col = s.tabs.Prev(col)
1442 cbt++
1443 if col == s.tabs.Prev(col) || col <= 0 {
1444 break
1445 }
1446 }
1447
1448 if cbt > 0 {
1449 seq.WriteString(ansi.CursorBackwardTab(cbt))
1450 n = col - tx
1451 }
1452 }
1453
1454 if cub := ansi.CursorBackward(n); xseq == "" || len(cub) < len(xseq) {
1455 xseq = cub
1456 }
1457
1458 if useBackspace && n < len(xseq) {
1459 xseq = strings.Repeat("\b", n)
1460 }
1461 }
1462
1463 seq.WriteString(xseq)
1464 }
1465
1466 return seq.String(), scrollHeight
1467}
1468
1469// moveCursor moves and returns the cursor movement sequence to move the cursor
1470// to the specified position.
1471// When overwrite is true, this will try to optimize the sequence by using the
1472// screen cells values to move the cursor instead of using escape sequences.
1473//
1474// It is safe to call this function with a nil [Buffer]. In that case, it won't
1475// use any optimizations that require the new buffer such as overwrite.
1476func moveCursor(s *TerminalRenderer, newbuf *Buffer, x, y int, overwrite bool) (seq string, scrollHeight int) {
1477 fx, fy := s.cur.X, s.cur.Y
1478
1479 if !s.flags.Contains(tRelativeCursor) {
1480 width := -1 // Use -1 to indicate that we don't know the width of the screen.
1481 if s.tabs != nil {
1482 width = s.tabs.Width()
1483 }
1484 if newbuf != nil && width == -1 {
1485 // Even though this might not be accurate, we can use the new
1486 // buffer width as a fallback. Technically, if the new buffer
1487 // didn't have the width of the terminal, this would give us a
1488 // wrong result from [notLocal].
1489 width = newbuf.Width()
1490 }
1491 // Method #0: Use [ansi.CUP] if the distance is long.
1492 seq = ansi.CursorPosition(x+1, y+1)
1493 if fx == -1 || fy == -1 || width == -1 || notLocal(width, fx, fy, x, y) {
1494 return seq, 0
1495 }
1496 }
1497
1498 // Optimize based on options.
1499 trials := 0
1500 if s.caps.Contains(capHT) {
1501 trials |= 2 // 0b10 in binary
1502 }
1503 if s.caps.Contains(capBS) {
1504 trials |= 1 // 0b01 in binary
1505 }
1506
1507 // Try all possible combinations of hard tabs and backspace optimizations.
1508 for i := 0; i <= trials; i++ {
1509 // Skip combinations that are not enabled.
1510 if i & ^trials != 0 {
1511 continue
1512 }
1513
1514 useHardTabs := i&2 != 0
1515 useBackspace := i&1 != 0
1516
1517 // Method #1: Use local movement sequences.
1518 nseq1, nscrollHeight1 := relativeCursorMove(s, newbuf, fx, fy, x, y, overwrite, useHardTabs, useBackspace)
1519 if (i == 0 && len(seq) == 0) || len(nseq1) < len(seq) {
1520 seq = nseq1
1521 scrollHeight = max(scrollHeight, nscrollHeight1)
1522 }
1523
1524 // Method #2: Use [ansi.CR] and local movement sequences.
1525 nseq2, nscrollHeight2 := relativeCursorMove(s, newbuf, 0, fy, x, y, overwrite, useHardTabs, useBackspace)
1526 nseq2 = "\r" + nseq2
1527 if len(nseq2) < len(seq) {
1528 seq = nseq2
1529 scrollHeight = max(scrollHeight, nscrollHeight2)
1530 }
1531
1532 if !s.flags.Contains(tRelativeCursor) {
1533 // Method #3: Use [ansi.CursorHomePosition] and local movement sequences.
1534 nseq3, nscrollHeight3 := relativeCursorMove(s, newbuf, 0, 0, x, y, overwrite, useHardTabs, useBackspace)
1535 nseq3 = ansi.CursorHomePosition + nseq3
1536 if len(nseq3) < len(seq) {
1537 seq = nseq3
1538 scrollHeight = max(scrollHeight, nscrollHeight3)
1539 }
1540 }
1541 }
1542
1543 return seq, scrollHeight
1544}
1545
1546// xtermCaps returns whether the terminal is xterm-like. This means that the
1547// terminal supports ECMA-48 and ANSI X3.64 escape sequences.
1548// xtermCaps returns a list of control sequence capabilities for the given
1549// terminal type. This only supports a subset of sequences that can
1550// be different among terminals.
1551// NOTE: A hybrid approach would be to support Terminfo databases for a full
1552// set of capabilities.
1553func xtermCaps(termtype string) (v capabilities) {
1554 parts := strings.Split(termtype, "-")
1555 if len(parts) == 0 {
1556 return
1557 }
1558
1559 switch parts[0] {
1560 case
1561 "contour",
1562 "foot",
1563 "ghostty",
1564 "kitty",
1565 "rio",
1566 "st",
1567 "tmux",
1568 "wezterm",
1569 "xterm":
1570 v = allCaps
1571 case "alacritty":
1572 v = allCaps
1573 v &^= capCHT // NOTE: alacritty added support for [ansi.CHT] in 2024-12-28 #62d5b13.
1574 case "screen":
1575 // See https://www.gnu.org/software/screen/manual/screen.html#Control-Sequences-1
1576 v = allCaps
1577 v &^= capREP
1578 case "linux":
1579 // See https://man7.org/linux/man-pages/man4/console_codes.4.html
1580 v = capVPA | capHPA | capECH | capICH
1581 }
1582
1583 return
1584}