okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Kieran Klukasfix(renderer): erase rows a frame no longer draws after a resize8786532

upstream
60.3 KiB1916 linesraw
1package uv
2
3import (
4	"bytes"
5	"errors"
6	"hash/maphash"
7	"io"
8	"strings"
9	"unicode/utf8"
10
11	"github.com/charmbracelet/colorprofile"
12	"github.com/charmbracelet/x/ansi"
13)
14
15// ErrInvalidDimensions is returned when the dimensions of a window are invalid
16// for the operation.
17var ErrInvalidDimensions = errors.New("invalid dimensions")
18
19// capabilities represents a mask of supported ANSI escape sequences.
20type capabilities uint
21
22const (
23	// Vertical Position Absolute [ansi.VPA].
24	capVPA capabilities = 1 << iota
25	// Horizontal Position Absolute [ansi.HPA].
26	capHPA
27	// Cursor Horizontal Absolute [ansi.CHA].
28	capCHA
29	// Cursor Horizontal Tab [ansi.CHT].
30	capCHT
31	// Cursor Backward Tab [ansi.CBT].
32	capCBT
33	// Repeat Previous Character [ansi.REP].
34	capREP
35	// Erase Character [ansi.ECH].
36	capECH
37	// Insert Character [ansi.ICH].
38	capICH
39	// Scroll Down [ansi.SD].
40	capSD
41	// Scroll Up [ansi.SU].
42	capSU
43	// These capabilities depend on the tty termios settings and are not
44	// enabled by default.
45	// Tabulation [ansi.HT].
46	capHT
47	// Backspace [ansi.BS].
48	capBS
49
50	noCaps  capabilities = 0
51	allCaps              = capVPA | capHPA | capCHA | capCHT | capCBT | capREP | capECH | capICH | capSD | capSU
52)
53
54// Set sets the given capabilities.
55func (v *capabilities) Set(c capabilities) {
56	*v |= c
57}
58
59// Reset resets the given capabilities.
60func (v *capabilities) Reset(c capabilities) {
61	*v &^= c
62}
63
64// Contains returns whether the capabilities contains the given capability.
65func (v capabilities) Contains(c capabilities) bool {
66	return v&c == c
67}
68
69// cursor represents a terminal cursor.
70type cursor struct {
71	Cell
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	tFullscreen
90	tMapNewline
91	tScrollOptim
92	// tGraphemeWidth indicates the terminal measures cell width using Unicode
93	// grapheme clustering (DEC mode 2027 / Unicode core).
94	tGraphemeWidth
95)
96
97// Set sets the given flags.
98func (v *tFlag) Set(c tFlag) {
99	*v |= c
100}
101
102// Reset resets the given flags.
103func (v *tFlag) Reset(c tFlag) {
104	*v &^= c
105}
106
107// Contains returns whether the terminal flags contains the given flags.
108func (v tFlag) Contains(c tFlag) bool {
109	return v&c == c
110}
111
112// TerminalRenderer is a terminal screen render and lazy writer that buffers
113// the output until it is flushed. It handles rendering a screen from a
114// [Buffer] to the terminal with the minimal necessary escape sequences to
115// transition the terminal to the new buffer state. It uses various escape
116// sequence optimizations to reduce the number of bytes sent to the terminal.
117// It's designed to be lazy and only flush the output when necessary by calling
118// the [TerminalRenderer.Flush] method.
119//
120// The renderer handles the terminal's alternate screen and cursor visibility
121// via the [TerminalRenderer.EnterAltScreen], [TerminalRenderer.ExitAltScreen],
122// [TerminalRenderer.ShowCursor] and [TerminalRenderer.HideCursor] methods.
123// Using these methods will queue the appropriate escape sequences to enter or
124// exit the alternate screen and show or hide the cursor respectively to be
125// flushed to the terminal.
126//
127// Use the [io.Writer] and [io.StringWriter] interfaces to queue custom
128// commands the renderer.
129//
130// The renderer is not thread-safe, the caller must protect the renderer when
131// using it from multiple goroutines.
132type TerminalRenderer struct {
133	w                io.Writer
134	buf              *bytes.Buffer // buffer for writing to the screen
135	curbuf           *RenderBuffer // the current buffer
136	tabs             *TabStops
137	hasher           maphash.Hash
138	oldhash, newhash []uint64     // the old and new hash values for each line
139	hashtab          []hashmap    // the hashmap table
140	oldnum           []int        // old indices from previous hash
141	cur, saved       cursor       // the current and saved cursors
142	flags            tFlag        // terminal writer flags.
143	method           ansi.Method  // the width method used to measure cell width
144	term             string       // the terminal type
145	clear            bool         // whether to force clear the screen
146	caps             capabilities // terminal control sequence capabilities
147	atPhantom        bool         // whether the cursor is out of bounds and at a phantom cell
148	noWrapLine       bool         // whether autowrap is off for the line currently being repainted
149	driftRows        []bool       // rows holding a cell the terminal may measure differently
150	termW, termH     int          // the size [TerminalRenderer.Resize] was last told
151	termResized      bool         // whether the terminal changed size since the last render
152	damaged          []bool       // rows this render disturbed itself, see [TerminalRenderer.damage]
153	logger           Logger       // The logger used for debugging.
154
155	// profile is the color profile to use when downsampling colors. This is
156	// used to determine the appropriate color the terminal can display.
157	profile colorprofile.Profile
158}
159
160// NewTerminalRenderer returns a new [TerminalRenderer] that uses the given
161// writer, terminal type, and initializes the width of the terminal. The
162// terminal type is used to determine the capabilities of the terminal and
163// should be set to the value of the TERM environment variable.
164//
165// The renderer will try to detect the color profile from the output and the
166// given environment variables. Use [TerminalRenderer.SetColorProfile] method
167// to set a specific color profile for downsampling.
168//
169// See [TerminalRenderer] for more information on how to use the renderer.
170func NewTerminalRenderer(w io.Writer, env []string) (s *TerminalRenderer) {
171	s = new(TerminalRenderer)
172	s.w = w
173	s.profile = colorprofile.Detect(w, env)
174	s.buf = new(bytes.Buffer)
175	s.term = Environ(env).Getenv("TERM")
176	s.caps = xtermCaps(s.term)
177	s.cur = cursor{Cell: EmptyCell, Position: Pos(-1, -1)} // start at -1 to force a move
178	s.saved = s.cur
179	s.oldhash, s.newhash = nil, nil
180	return
181}
182
183// SetLogger sets the logger to use for debugging. If nil, no logging will be
184// performed.
185func (s *TerminalRenderer) SetLogger(logger Logger) {
186	s.logger = logger
187}
188
189// SetColorProfile sets the color profile to use for downsampling colors. This
190// is used to determine the appropriate color the terminal can display.
191func (s *TerminalRenderer) SetColorProfile(profile colorprofile.Profile) {
192	s.profile = profile
193}
194
195// SetScrollOptim sets whether to use hard scroll optimizations.
196func (s *TerminalRenderer) SetScrollOptim(v bool) {
197	if v {
198		s.flags.Set(tScrollOptim)
199	} else {
200		s.flags.Reset(tScrollOptim)
201	}
202}
203
204// SetMapNewline sets whether the terminal is currently mapping newlines to
205// CRLF or carriage return and line feed. This is used to correctly determine
206// how to move the cursor when writing to the screen.
207func (s *TerminalRenderer) SetMapNewline(v bool) {
208	if v {
209		s.flags.Set(tMapNewline)
210	} else {
211		s.flags.Reset(tMapNewline)
212	}
213}
214
215// SetWidthMethod sets the width method the renderer uses to measure the
216// display width of strings. This should match the width method configured on
217// the screen so that the renderer doesn't disagree with the cell widths it is
218// asked to draw.
219func (s *TerminalRenderer) SetWidthMethod(method ansi.Method) {
220	s.method = method
221}
222
223// SetGraphemeWidth sets whether the terminal measures cell width using Unicode
224// grapheme clustering (DEC mode 2027 / Unicode core). When enabled, the
225// renderer measures string width using [ansi.GraphemeWidth]; otherwise it
226// falls back to [ansi.WcWidth].
227func (s *TerminalRenderer) SetGraphemeWidth(v bool) {
228	if v {
229		s.flags.Set(tGraphemeWidth)
230		s.method = ansi.GraphemeWidth
231	} else {
232		s.flags.Reset(tGraphemeWidth)
233		s.method = ansi.WcWidth
234	}
235}
236
237// SetBackspace sets whether to use backspace as a movement optimization.
238func (s *TerminalRenderer) SetBackspace(v bool) {
239	if v {
240		s.caps.Set(capBS)
241	} else {
242		s.caps.Reset(capBS)
243	}
244}
245
246// SetTabStops sets the tab stops for the terminal and enables hard tabs
247// movement optimizations. Use -1 to disable hard tabs. This option is ignored
248// when the terminal type is "linux" as it does not support hard tabs.
249func (s *TerminalRenderer) SetTabStops(width int) {
250	if width < 0 || strings.HasPrefix(s.term, "linux") {
251		// Linux terminal does not support hard tabs.
252		s.caps.Reset(capHT)
253	} else {
254		s.caps.Set(capHT)
255		s.tabs = DefaultTabStops(width)
256	}
257}
258
259// SetFullscreen sets whether whole screen is being used. This is usually
260// paired with the alternate screen mode, and used to control cursor movements
261// and optimizations when the terminal is occupying the whole screen.
262func (s *TerminalRenderer) SetFullscreen(v bool) {
263	if v {
264		s.flags.Set(tFullscreen)
265	} else {
266		s.flags.Reset(tFullscreen)
267	}
268}
269
270// Fullscreen returns whether whole screen is being used. This is usually
271// paired with the alternate screen mode, and used to control cursor movements
272// and optimizations when the terminal is occupying the whole screen.
273func (s *TerminalRenderer) Fullscreen() bool {
274	return s.flags.Contains(tFullscreen)
275}
276
277// SetRelativeCursor sets whether to use relative cursor movements.
278func (s *TerminalRenderer) SetRelativeCursor(v bool) {
279	if v {
280		s.flags.Set(tRelativeCursor)
281	} else {
282		s.flags.Reset(tRelativeCursor)
283	}
284}
285
286// SaveCursor saves the current cursor position and styles. This can be used
287// when a program enters the alternate screen buffer mode using DECSET 1049
288// [ansi.ModeAltScreenSaveCursor] and wants to restore the cursor position and
289// styles later.
290func (s *TerminalRenderer) SaveCursor() {
291	s.saved = s.cur
292}
293
294// RestoreCursor restores the saved cursor position and styles. This can be
295// used when a program exits the alternate screen buffer mode using DECRST 1049
296// [ansi.ModeAltScreenSaveCursor] and wants to restore the cursor position and
297// styles saved earlier.
298func (s *TerminalRenderer) RestoreCursor() {
299	s.cur = s.saved
300}
301
302// EnterAltScreen is a helper that queues the [ansi.ModeAltScreenSaveCursor]
303// escape sequence to enter the alternate screen buffer and save cursor mode.
304//
305// It saves the current cursor properties, enables
306// [TerminalRenderer.SetFullscreen] flag, and disables
307// [TerminalRenderer.SetRelativeCursor] flag. On the next call to
308// [TerminalRenderer.Render], it will move the cursor home and clear the screen
309// to prepare for rendering the new buffer.
310//
311// Note: you might want to reapply the cursor visibility state after calling
312// this method, as some terminals reset the cursor visibility when switching to
313// the alternate screen.
314func (s *TerminalRenderer) EnterAltScreen() {
315	s.SaveCursor()
316	s.buf.WriteString(ansi.SetModeAltScreenSaveCursor)
317	s.SetFullscreen(true)
318	s.SetRelativeCursor(false)
319	s.Erase()
320}
321
322// ExitAltScreen is a helper that queues the [ansi.ModeAltScreenSaveCursor]
323// escape sequence to exit the alternate screen buffer and restore cursor mode.
324//
325// It restores the saved cursor properties, disables
326// [TerminalRenderer.SetFullscreen] flag, and enables
327// [TerminalRenderer.SetRelativeCursor] flag. On the next call to
328// [TerminalRenderer.Render], it will move the cursor to the first line and
329// clear everything below the cursor to prepare for rendering the new buffer.
330//
331// Note: you might want to reapply the cursor visibility state after calling
332// this method, as some terminals reset the cursor visibility when switching to
333// the alternate screen.
334func (s *TerminalRenderer) ExitAltScreen() {
335	s.Erase()
336	s.SetRelativeCursor(true)
337	s.SetFullscreen(false)
338	s.buf.WriteString(ansi.ResetModeAltScreenSaveCursor)
339	s.RestoreCursor()
340}
341
342// PrependString adds the lines of the given string to the top of the terminal
343// screen. The lines prepended are not managed by the renderer and will not be
344// cleared or updated by the renderer.
345//
346// Using this when the terminal is using the alternate screen or when occupying
347// the whole screen may not produce any visible effects. This is because
348// once the terminal writes the prepended lines, they will get overwritten
349// by the next frame.
350func (s *TerminalRenderer) PrependString(newbuf *RenderBuffer, str string) {
351	if len(str) == 0 {
352		return
353	}
354
355	// TODO: Use scrolling region if available.
356	// TODO: Use [Screen.Write] [io.Writer] interface.
357
358	// We need to scroll the screen up by the number of lines in the queue.
359	// We can't use [ansi.SU] because we want the cursor to move down until
360	// it reaches the bottom of the screen.
361	w, h := newbuf.Width(), newbuf.Height()
362	s.move(newbuf, 0, h-1)
363	lines := strings.Split(str, "\n")
364	offset := 0
365	for _, line := range lines {
366		lineWidth := s.method.StringWidth(line)
367		if w > 0 && lineWidth > w {
368			offset += (lineWidth / w)
369		}
370		if lineWidth == 0 || lineWidth%w != 0 {
371			offset++
372		}
373	}
374
375	s.buf.WriteString(strings.Repeat("\n", offset))
376	s.cur.Y += offset
377	// XXX: Now go to the top of the screen, insert new lines, and write
378	// the queued strings. It is important to use [Screen.moveCursor]
379	// instead of [Screen.move] because we don't want to perform any checks
380	// on the cursor position.
381	s.moveCursor(newbuf, 0, 0, false)
382	s.buf.WriteString(ansi.InsertLine(offset))
383	for _, line := range lines {
384		s.buf.WriteString(line)
385		s.buf.WriteString("\r\n")
386	}
387}
388
389// moveCursor moves the cursor to the specified position.
390//
391// It is safe to call this function with a nil [Buffer], in that case, it won't
392// be using any optimizations that depend on the buffer.
393func (s *TerminalRenderer) moveCursor(newbuf *RenderBuffer, x, y int, overwrite bool) {
394	if !s.flags.Contains(tFullscreen) && s.flags.Contains(tRelativeCursor) &&
395		s.cur.X == -1 && s.cur.Y == -1 {
396		// First cursor movement in inline mode, move the cursor to the first
397		// column before moving to the target position.
398		_ = s.buf.WriteByte('\r')
399		s.cur.X, s.cur.Y = 0, 0
400	}
401	seq := moveCursor(s, newbuf, x, y, overwrite)
402	_, _ = s.buf.WriteString(seq)
403	s.cur.X, s.cur.Y = x, y
404}
405
406// move moves the cursor to the specified position in the buffer.
407//
408// It is safe to call this function with a nil [Buffer], in that case, it won't
409// be using any optimizations that depend on the buffer.
410func (s *TerminalRenderer) move(newbuf *RenderBuffer, x, y int) {
411	// XXX: Make sure we use the max height and width of the buffer in case
412	// we're in the middle of a resize operation.
413	var width, height int
414	if s.curbuf != nil {
415		width, height = s.curbuf.Width(), s.curbuf.Height()
416	}
417	if newbuf != nil {
418		width = max(newbuf.Width(), width)
419		height = max(newbuf.Height(), height)
420	}
421
422	if width > 0 && x >= width {
423		// Handle autowrap
424		y += (x / width)
425		x %= width
426	}
427
428	// XXX: Disable styles if there's any
429	// Some move operations such as [ansi.LF] can apply styles to the new
430	// cursor position, thus, we need to reset the styles before moving the
431	// cursor.
432	blank := s.clearBlank()
433	resetPen := y != s.cur.Y && !blank.Equal(&EmptyCell)
434	if resetPen {
435		s.updatePen(nil)
436	}
437
438	// Reset wrap around (phantom cursor) state
439	if s.atPhantom {
440		s.cur.X = 0
441		_ = s.buf.WriteByte('\r')
442		s.atPhantom = false // reset phantom cell state
443	}
444
445	// TODO: Investigate if we need to handle this case and/or if we need the
446	// following code.
447	//
448	// if width > 0 && s.cur.X >= width {
449	// 	l := (s.cur.X + 1) / width
450	//
451	// 	s.cur.Y += l
452	// 	if height > 0 && s.cur.Y >= height {
453	// 		l -= s.cur.Y - height - 1
454	// 	}
455	//
456	// 	if l > 0 {
457	// 		s.cur.X = 0
458	// 		s.buf.WriteString("\r" + strings.Repeat("\n", l)) //nolint:errcheck
459	// 	}
460	// }
461
462	if height > 0 {
463		// Only clamp the remembered cursor row in fullscreen mode, where the
464		// buffer covers the whole screen. In relative mode the terminal
465		// screen extends beyond the frame buffer: after a shrink the
466		// real cursor can legitimately sit below the new frame's last row,
467		// and clamping the model here would skip the cursor-up move and leave
468		// the old frame's top lines on screen.
469		if !s.flags.Contains(tRelativeCursor) && s.cur.Y > height-1 {
470			s.cur.Y = height - 1
471		}
472		if y > height-1 {
473			y = height - 1
474		}
475	}
476
477	if x == s.cur.X && y == s.cur.Y {
478		// We give up later because we need to run checks for the phantom cell
479		// and others before we can determine if we can give up.
480		return
481	}
482
483	// We set the new cursor in tscreen.moveCursor].
484	s.moveCursor(newbuf, x, y, true) // Overwrite cells if possible
485}
486
487// cellEqual returns whether the two cells are equal. A nil cell is considered
488// a [EmptyCell].
489func cellEqual(a, b *Cell) bool {
490	if a == b {
491		return true
492	}
493	// TODO: This is needed to handle empty lines correctly when scroll
494	// optimizations are enabled. Instead, a nil check should be equivalent to
495	// checking for an [EmptyCell], should it?
496	// Investigate why when we assign the pointers to &[EmptyCell], this causes
497	// scroll optimization related artifacts where excess lines are left behind
498	// in empty lines after scrolling.
499	if a == nil || b == nil {
500		return false
501	}
502	return a.Equal(b)
503}
504
505// putCell draws a cell at the current cursor position.
506//
507// The lower right corner is written with autowrap off, since a pending wrap
508// there scrolls the screen as soon as anything else prints. A multi-codepoint
509// cluster is the exception: with autowrap off the terminal never advances past
510// the margin and reads the combining codepoints as belonging to the cell to the
511// left, so the cluster keeps autowrap on and the wrap is cancelled after.
512func (s *TerminalRenderer) putCell(newbuf *RenderBuffer, cell *Cell) {
513	width, height := newbuf.Width(), newbuf.Height()
514	atMargin := s.cur.X == width-1 && !s.noWrapLine
515	lowerRight := atMargin && s.flags.Contains(tFullscreen) && s.cur.Y == height-1
516
517	// Only the corner cell can care, so the cluster is measured only there.
518	if lowerRight && cell != nil && utf8.RuneCountInString(cell.Content) > 1 {
519		s.putAttrCell(newbuf, cell)
520		// The cluster kept autowrap on so it landed whole, which leaves the
521		// cursor pending wrap. On the last row there is no next row to wrap
522		// onto, so the next print scrolls the screen instead. Cancel it here
523		// rather than leave the frame ending in a state the model denies.
524		if s.atPhantom {
525			_ = s.buf.WriteByte('\r')
526			s.cur.X = 0
527			s.atPhantom = false
528		}
529		return
530	}
531
532	if lowerRight {
533		s.putCellLR(newbuf, cell)
534	} else {
535		s.putAttrCell(newbuf, cell)
536	}
537}
538
539// wrapCursor wraps the cursor to the next line.
540func (s *TerminalRenderer) wrapCursor() {
541	const autoRightMargin = true
542	if autoRightMargin {
543		// Assume we have auto wrap mode enabled.
544		s.cur.X = 0
545		s.cur.Y++
546	} else {
547		s.cur.X--
548	}
549}
550
551func (s *TerminalRenderer) putAttrCell(newbuf *RenderBuffer, cell *Cell) {
552	if cell != nil && cell.Width == 0 {
553		// XXX: Zero width cells are special and should not be written to the
554		// screen no matter what other attributes they have.
555		// Zero width cells are used for wide characters that are split into
556		// multiple cells.
557		return
558	}
559
560	// We're at pending wrap state (phantom cell), incoming cell should
561	// wrap.
562	if s.atPhantom {
563		s.wrapCursor()
564		s.atPhantom = false
565	}
566
567	s.updatePen(cell)
568	cellWidth := 1
569	if cell == nil {
570		_ = s.buf.WriteByte(' ')
571	} else {
572		_, _ = s.buf.WriteString(cell.Content)
573		cellWidth = cell.Width
574	}
575
576	s.cur.X += cellWidth
577	if s.cur.X >= newbuf.Width() {
578		s.atPhantom = true
579	}
580}
581
582// putCellLR draws a cell at the lower right corner of the screen.
583func (s *TerminalRenderer) putCellLR(newbuf *RenderBuffer, cell *Cell) {
584	// Optimize for the lower right corner cell.
585	curX := s.cur.X
586	if !cell.isWidePlaceholder() {
587		_, _ = s.buf.WriteString(ansi.ResetModeAutoWrap)
588		s.putAttrCell(newbuf, cell)
589		// Writing to lower-right corner cell should not wrap.
590		s.atPhantom = false
591		s.cur.X = curX
592		_, _ = s.buf.WriteString(ansi.SetModeAutoWrap)
593	}
594}
595
596// updatePen updates the cursor pen styles.
597func (s *TerminalRenderer) updatePen(cell *Cell) {
598	if cell == nil {
599		cell = &EmptyCell
600	}
601
602	// Downsample pen when we don't have a [colorprofile.TrueColor],
603	// otherwise, use the original style.
604	newStyle := ConvertStyle(cell.Style, s.profile)
605	newLink := ConvertLink(cell.Link, s.profile)
606	oldStyle := ConvertStyle(s.cur.Style, s.profile)
607	oldLink := ConvertLink(s.cur.Link, s.profile)
608
609	if !newStyle.Equal(&oldStyle) {
610		seq := newStyle.Diff(&oldStyle)
611		if newStyle.IsZero() && len(seq) > len(ansi.ResetStyle) {
612			seq = ansi.ResetStyle
613		}
614		_, _ = s.buf.WriteString(seq)
615		s.cur.Style = cell.Style // Copy the original style
616	}
617	if !newLink.Equal(&oldLink) {
618		_, _ = s.buf.WriteString(ansi.SetHyperlink(newLink.URL, newLink.Params))
619		s.cur.Link = cell.Link // Copy the original link
620	}
621}
622
623// canClearWith checks whether the given cell can be used by clearing commands
624// like [ansi.EL] to clear the screen. It tests if a cell is empty i.e. space
625// or blank and doesn't include any bad style attributes such as [AttrReverse].
626func canClearWith(c *Cell) bool {
627	if c == nil {
628		return true
629	}
630	if c.Width != 1 || len(c.Content) != 1 || c.Content != " " {
631		return false
632	}
633	// NOTE: This assumes that the terminal supports bce terminfo capability
634	// which all xterm-compatible terminals and terminals that use xterm*
635	// terminal types do.
636	return c.Style.Underline == UnderlineNone &&
637		c.Style.Attrs&^(AttrBold|AttrFaint|AttrItalic|AttrBlink|AttrRapidBlink) == 0 &&
638		c.Link.IsZero()
639}
640
641// emitRange emits a range of cells to the buffer. It it equivalent to calling
642// tscreen.putCell] for each cell in the range. This is optimized to use
643// [ansi.ECH] and [ansi.REP].
644// Returns whether the cursor is at the end of interval or somewhere in the
645// middle.
646func (s *TerminalRenderer) emitRange(newbuf *RenderBuffer, line Line, n int) (eoi bool) {
647	hasECH := s.caps.Contains(capECH)
648	hasREP := s.caps.Contains(capREP)
649	if hasECH || hasREP { //nolint:nestif
650		for n > 0 {
651			var count int
652			for n > 1 && !cellEqual(line.At(0), line.At(1)) {
653				s.putCell(newbuf, line.At(0))
654				line = line[1:]
655				n--
656			}
657
658			cell0 := line[0]
659			if n == 1 {
660				s.putCell(newbuf, &cell0)
661				return false
662			}
663
664			count = 2
665			for count < n && cellEqual(line.At(count), &cell0) {
666				count++
667			}
668
669			ech := ansi.EraseCharacter(count)
670			cup := ansi.CursorPosition(s.cur.X+count, s.cur.Y)
671			rep := ansi.RepeatPreviousCharacter(count)
672			if hasECH && count > len(ech)+len(cup) && canClearWith(&cell0) {
673				s.updatePen(&cell0)
674				_, _ = s.buf.WriteString(ech)
675
676				// If this is the last cell, we don't need to move the cursor.
677				if count < n {
678					s.move(newbuf, s.cur.X+count, s.cur.Y)
679				} else {
680					return true // cursor in the middle
681				}
682			} else if hasREP && count > len(rep) &&
683				(len(cell0.Content) == 1 && cell0.Content[0] >= ansi.US && cell0.Content[0] < ansi.DEL) {
684				// We only support ASCII characters. Most terminals will handle
685				// non-ASCII characters correctly, but some might not, ahem xterm.
686				//
687				// NOTE: [ansi.REP] only repeats the last rune and won't work
688				// if the last cell contains multiple runes.
689
690				wrapPossible := s.cur.X+count >= newbuf.Width()
691				repCount := count
692				if wrapPossible {
693					repCount--
694				}
695
696				s.updatePen(&cell0)
697				s.putCell(newbuf, &cell0)
698				repCount-- // cell0 is a single width cell ASCII character
699
700				_, _ = s.buf.WriteString(ansi.RepeatPreviousCharacter(repCount))
701				s.cur.X += repCount
702				if wrapPossible {
703					s.putCell(newbuf, &cell0)
704				}
705			} else {
706				for i := 0; i < count; i++ {
707					s.putCell(newbuf, line.At(i))
708				}
709			}
710
711			line = line[clamp(count, 0, len(line)):]
712			n -= count
713		}
714
715		return false
716	}
717
718	for i := 0; i < n; i++ {
719		s.putCell(newbuf, line.At(i))
720	}
721
722	return false
723}
724
725// putRange puts a range of cells from the old line to the new line.
726// Returns whether the cursor is at the end of interval or somewhere in the
727// middle.
728func (s *TerminalRenderer) putRange(newbuf *RenderBuffer, oldLine, newLine Line, y, start, end int) (eoi bool) {
729	inline := min(len(ansi.CursorPosition(start+1, y+1)),
730		min(len(ansi.HorizontalPositionAbsolute(start+1)),
731			len(ansi.CursorForward(start+1))))
732	if (end - start + 1) > inline { //nolint:nestif
733		var j, same int
734		for j, same = start, 0; j <= end; j++ {
735			oldCell, newCell := oldLine.At(j), newLine.At(j)
736			if same == 0 && oldCell.isWidePlaceholder() && newCell.isWidePlaceholder() {
737				continue
738			}
739			if cellEqual(oldCell, newCell) {
740				same++
741			} else {
742				if same > end-start {
743					s.emitRange(newbuf, newLine[start:], j-same-start)
744					s.move(newbuf, j, y)
745					start = j
746				}
747				same = 0
748			}
749		}
750
751		i := s.emitRange(newbuf, newLine[start:], j-same-start)
752
753		// Always return 1 for the next [tScreen.move] after a
754		// [tScreen.putRange] if we found identical characters at end of
755		// interval.
756		if same == 0 {
757			return i
758		}
759		return true
760	}
761
762	return s.emitRange(newbuf, newLine[start:], end-start+1)
763}
764
765// clearToEnd clears the screen from the current cursor position to the end of
766// line.
767func (s *TerminalRenderer) clearToEnd(newbuf *RenderBuffer, blank *Cell, force bool) {
768	if s.cur.Y >= 0 {
769		curline := s.curbuf.Line(s.cur.Y)
770		// We use the newbuf width because the current buffer might be smaller
771		// than the new buffer during a resize operation and we want to detect
772		// that.
773		for j := s.cur.X; j < newbuf.Width(); j++ {
774			if j >= 0 {
775				c := curline.At(j)
776				if !cellEqual(c, blank) {
777					curline.Set(j, blank)
778					force = true
779				}
780			}
781		}
782	}
783
784	if force {
785		s.updatePen(blank)
786		count := newbuf.Width() - s.cur.X
787		if s.el0Cost() <= count {
788			_, _ = s.buf.WriteString(ansi.EraseLineRight)
789		} else {
790			for i := 0; i < count; i++ {
791				s.putCell(newbuf, blank)
792			}
793		}
794	}
795}
796
797// clearBlank returns a blank cell based on the current cursor background color.
798func (s *TerminalRenderer) clearBlank() *Cell {
799	return &s.cur.Cell
800}
801
802// insertCells inserts the count cells pointed by the given line at the current
803// cursor position.
804func (s *TerminalRenderer) insertCells(newbuf *RenderBuffer, line Line, count int) {
805	supportsICH := s.caps.Contains(capICH)
806	if supportsICH {
807		// Use [ansi.ICH] as an optimization.
808		_, _ = s.buf.WriteString(ansi.InsertCharacter(count))
809	} else {
810		// Otherwise, use [ansi.IRM] mode.
811		_, _ = s.buf.WriteString(ansi.SetModeInsertReplace)
812	}
813
814	for i := 0; count > 0; i++ {
815		s.putAttrCell(newbuf, line.At(i))
816		count--
817	}
818
819	if !supportsICH {
820		_, _ = s.buf.WriteString(ansi.ResetModeInsertReplace)
821	}
822}
823
824// el0Cost returns the cost of using [ansi.EL] 0 i.e. [ansi.EraseLineRight]. If
825// this terminal supports background color erase, it can be cheaper to use
826// [ansi.EL] 0 i.e. [ansi.EraseLineRight] to clear
827// trailing spaces.
828func (s *TerminalRenderer) el0Cost() int {
829	if s.caps != noCaps {
830		return 0
831	}
832	return len(ansi.EraseLineRight)
833}
834
835// lineHasDrift reports whether the line contains a cell that a cell-level
836// diff cannot safely reposition across: a wide cell (width > 1), or a cell
837// whose width the terminal may measure differently than the model. Wide cells
838// occupy several columns, so a diff that lands on a continuation column splits
839// the character; and a width disagreement means the model cannot know which
840// column each glyph landed on.
841func lineHasDrift(m ansi.Method, line Line) bool {
842	for i := 0; i < len(line); i++ {
843		c := line.At(i)
844		if c == nil || c.Width == 0 || len(c.Content) == 0 {
845			continue
846		}
847		if c.Width > 1 {
848			return true
849		}
850		// One printable ASCII byte is one column under every width model, so
851		// it can never be a cell the models disagree about. Worth saying out
852		// loud because working the width out means segmenting the content,
853		// and this runs for every cell of every line the renderer diffs.
854		if len(c.Content) == 1 && c.Content[0] >= 0x20 && c.Content[0] < 0x7f {
855			continue
856		}
857		if m.StringWidth(c.Content) != ansi.StringWidth(c.Content) {
858			return true
859		}
860	}
861	return false
862}
863
864// paintLine brings row y of the screen in line with newbuf. A drift-prone row,
865// or any row when force is set, is repainted; every other row is diffed.
866func (s *TerminalRenderer) paintLine(newbuf *RenderBuffer, y int, force bool) {
867	drift := lineHasDrift(s.method, s.curbuf.Line(y)) || lineHasDrift(s.method, newbuf.Line(y))
868	s.markDrift(y, newbuf.Height(), drift)
869	switch {
870	case drift && !s.flags.Contains(tGraphemeWidth):
871		// Clip a cluster the terminal measures wider than the model instead of
872		// letting it spill onto the next row, then put the cursor back.
873		s.noWrapLine = true
874		_, _ = s.buf.WriteString(ansi.ResetModeAutoWrap)
875		s.repaintLine(newbuf, y)
876		s.noWrapLine = false
877		_, _ = s.buf.WriteString(ansi.SetModeAutoWrap)
878		s.reanchorWideLine(newbuf)
879	case drift || force:
880		s.repaintLine(newbuf, y)
881	default:
882		s.transformLine(newbuf, y)
883	}
884}
885
886// repaintLine erases row y from column 0 and writes newbuf's cells, so the
887// result does not depend on what the model believes is on screen.
888func (s *TerminalRenderer) repaintLine(newbuf *RenderBuffer, y int) {
889	oldLine := s.curbuf.Line(y)
890	newLine := newbuf.Line(y)
891
892	s.move(newbuf, 0, y)
893	blank := s.clearBlank()
894	s.updatePen(blank)
895	_, _ = s.buf.WriteString(ansi.EraseLineRight)
896	s.cur.X = 0
897
898	// A cell whose width runs past the right margin cannot be shown, so stop
899	// there. The buffer can hold one after a resize narrowed the line around
900	// it, and painting it would put a clipped half-glyph on the screen.
901	width := newbuf.Width()
902	clipped := width
903	for x := 0; x < width; x++ {
904		if c := newLine.At(x); c != nil && !c.isWidePlaceholder() && x+c.Width > width {
905			clipped = x
906			break
907		}
908	}
909
910	// Write the content cells. The line was just erased, so trailing blanks
911	// need no write, and writing them would risk wrapping past the right
912	// margin if the terminal painted a wide cell wider than the model
913	// measured.
914	last := -1
915	for x := 0; x < clipped; x++ {
916		if c := newLine.At(x); c != nil && !c.isWidePlaceholder() && !cellEqual(c, blank) {
917			last = x
918		}
919	}
920	for x := 0; x <= last; x++ {
921		s.putCell(newbuf, newLine.At(x))
922	}
923
924	// Adopt the new line as the model of what is on screen. The clipped tail
925	// was never painted, so the model records it as blank: claiming otherwise
926	// would let a later frame skip the cell, and a resize that widens the line
927	// would leave the half-glyph on screen forever.
928	if len(oldLine) == len(newLine) {
929		copy(oldLine, newLine)
930		for x := clipped; x < width && x < len(oldLine); x++ {
931			oldLine[x] = Cell{}
932		}
933	}
934}
935
936// markDrift records whether a row holds a cell the terminal may measure
937// differently than the model, so a later width change knows which rows to
938// repaint. Recorded here rather than rescanned later, because the diff already
939// had to work it out.
940func (s *TerminalRenderer) markDrift(y, height int, drift bool) {
941	if y < 0 {
942		return
943	}
944	if n := max(height, y+1); len(s.driftRows) < n {
945		s.driftRows = append(s.driftRows, make([]bool, n-len(s.driftRows))...)
946	}
947	s.driftRows[y] = drift
948}
949
950// transformLine diffs row y against the model and writes only what changed,
951// using [ansi.ICH] and [ansi.DCH] to shift cells. It must not see a drift-prone
952// row; [TerminalRenderer.paintLine] routes those to a repaint.
953func (s *TerminalRenderer) transformLine(newbuf *RenderBuffer, y int) {
954	var firstCell, oLastCell, nLastCell int // first, old last, new last index
955	oldLine := s.curbuf.Line(y)
956	newLine := newbuf.Line(y)
957
958	// Find the first changed cell in the line
959	blank := newLine.At(0)
960
961	// It might be cheaper to clear leading spaces with [ansi.EL] 1 i.e.
962	// [ansi.EraseLineLeft].
963	if canClearWith(blank) { //nolint:nestif
964		var oFirstCell, nFirstCell int
965		for oFirstCell = 0; oFirstCell < s.curbuf.Width(); oFirstCell++ {
966			if !cellEqual(oldLine.At(oFirstCell), blank) {
967				break
968			}
969		}
970		for nFirstCell = 0; nFirstCell < newbuf.Width(); nFirstCell++ {
971			if !cellEqual(newLine.At(nFirstCell), blank) {
972				break
973			}
974		}
975
976		if nFirstCell == oFirstCell {
977			firstCell = nFirstCell
978
979			// Find the first differing cell
980			for firstCell < newbuf.Width() &&
981				cellEqual(oldLine.At(firstCell), newLine.At(firstCell)) {
982				firstCell++
983			}
984		} else if oFirstCell > nFirstCell {
985			firstCell = nFirstCell
986		} else if oFirstCell < nFirstCell {
987			firstCell = oFirstCell
988			el1Cost := len(ansi.EraseLineLeft)
989			if el1Cost < nFirstCell-oFirstCell {
990				if nFirstCell >= newbuf.Width() {
991					s.move(newbuf, 0, y)
992					s.updatePen(blank)
993					_, _ = s.buf.WriteString(ansi.EraseLineRight)
994				} else {
995					s.move(newbuf, nFirstCell-1, y)
996					s.updatePen(blank)
997					_, _ = s.buf.WriteString(ansi.EraseLineLeft)
998				}
999
1000				for firstCell < nFirstCell {
1001					oldLine.Set(firstCell, blank)
1002					firstCell++
1003				}
1004			}
1005		}
1006	} else {
1007		// Find the first differing cell
1008		for firstCell < newbuf.Width() && cellEqual(newLine.At(firstCell), oldLine.At(firstCell)) {
1009			firstCell++
1010		}
1011	}
1012
1013	// If we didn't find one, we're done
1014	if firstCell >= newbuf.Width() {
1015		return
1016	}
1017
1018	blank = newLine.At(newbuf.Width() - 1)
1019	if blank != nil && !canClearWith(blank) {
1020		// Find the last differing cell
1021		nLastCell = newbuf.Width() - 1
1022		for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), oldLine.At(nLastCell)) {
1023			nLastCell--
1024		}
1025
1026		if nLastCell >= firstCell {
1027			s.move(newbuf, firstCell, y)
1028			s.putRange(newbuf, oldLine, newLine, y, firstCell, nLastCell)
1029			if firstCell < len(oldLine) && firstCell < len(newLine) {
1030				copy(oldLine[firstCell:], newLine[firstCell:])
1031			} else {
1032				copy(oldLine, newLine)
1033			}
1034		}
1035
1036		return
1037	}
1038
1039	// Find last non-blank cell in the old line.
1040	// We always use the newbuf width to detect new cell changes.
1041	oLastCell = newbuf.Width() - 1
1042	for oLastCell > firstCell && cellEqual(oldLine.At(oLastCell), blank) {
1043		oLastCell--
1044	}
1045
1046	// Find last non-blank cell in the new line.
1047	// We always use the newbuf width to detect new cell changes.
1048	nLastCell = newbuf.Width() - 1
1049	for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), blank) {
1050		nLastCell--
1051	}
1052
1053	if nLastCell == firstCell && s.el0Cost() < oLastCell-nLastCell { //nolint:nestif
1054		s.move(newbuf, firstCell, y)
1055		if !cellEqual(newLine.At(firstCell), blank) {
1056			s.putCell(newbuf, newLine.At(firstCell))
1057		}
1058		s.clearToEnd(newbuf, blank, false)
1059	} else if nLastCell != oLastCell &&
1060		!cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) {
1061		s.move(newbuf, firstCell, y)
1062		if oLastCell-nLastCell > s.el0Cost() {
1063			if s.putRange(newbuf, oldLine, newLine, y, firstCell, nLastCell) {
1064				s.move(newbuf, nLastCell+1, y)
1065			}
1066			s.clearToEnd(newbuf, blank, false)
1067		} else {
1068			n := max(nLastCell, oLastCell)
1069			s.putRange(newbuf, oldLine, newLine, y, firstCell, n)
1070		}
1071	} else {
1072		nLastNonBlank := nLastCell
1073		oLastNonBlank := oLastCell
1074
1075		// Find the last cells that really differ.
1076		// Can be -1 if no cells differ.
1077		for cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) {
1078			if !cellEqual(newLine.At(nLastCell-1), oldLine.At(oLastCell-1)) {
1079				break
1080			}
1081			nLastCell--
1082			oLastCell--
1083			if nLastCell == -1 || oLastCell == -1 {
1084				break
1085			}
1086		}
1087
1088		n := min(oLastCell, nLastCell)
1089		if n >= firstCell {
1090			s.move(newbuf, firstCell, y)
1091			s.putRange(newbuf, oldLine, newLine, y, firstCell, n)
1092		}
1093
1094		if oLastCell < nLastCell {
1095			m := max(nLastNonBlank, oLastNonBlank)
1096			if n != 0 {
1097				for n > 0 {
1098					wide := newLine.At(n + 1)
1099					if !wide.isWidePlaceholder() {
1100						break
1101					}
1102					n--
1103					oLastCell--
1104				}
1105			} else if n >= firstCell && newLine.At(n) != nil && newLine.At(n).Width > 1 {
1106				next := newLine.At(n + 1)
1107				for next.isWidePlaceholder() {
1108					n++
1109					oLastCell++
1110					next = newLine.At(n + 1)
1111				}
1112			}
1113
1114			// TODO: This can sometimes send unnecessary cursor movements with
1115			// negative or zero ranges. This could happen on a screen resize
1116			// where oLastCell < nLastCell and oLastCell is -1 or less.
1117			// Investigate and fix.
1118			s.move(newbuf, n+1, y)
1119			ichCost := 3 + nLastCell - oLastCell
1120			if s.caps.Contains(capICH) && (nLastCell < nLastNonBlank || ichCost > (m-n)) {
1121				s.putRange(newbuf, oldLine, newLine, y, n+1, m)
1122			} else {
1123				s.insertCells(newbuf, newLine[n+1:], nLastCell-oLastCell)
1124			}
1125		} else if oLastCell > nLastCell {
1126			s.move(newbuf, n+1, y)
1127			dchCost := 3 + oLastCell - nLastCell
1128			if dchCost > len(ansi.EraseLineRight)+nLastNonBlank-(n+1) {
1129				if s.putRange(newbuf, oldLine, newLine, y, n+1, nLastNonBlank) {
1130					s.move(newbuf, nLastNonBlank+1, y)
1131				}
1132				s.clearToEnd(newbuf, blank, false)
1133			} else {
1134				s.updatePen(blank)
1135				s.deleteCells(oLastCell - nLastCell)
1136			}
1137		}
1138	}
1139
1140	// Update the old line with the new line
1141	if firstCell < len(oldLine) && firstCell < len(newLine) {
1142		copy(oldLine[firstCell:], newLine[firstCell:])
1143	} else {
1144		copy(oldLine, newLine)
1145	}
1146}
1147
1148// reanchorWideLine re-anchors the cursor after a line that may have left it
1149// adrift. This is a best-effort fallback that bounds cursor desync to one line
1150// on terminals whose width model disagrees with ours. When the terminal
1151// negotiated Unicode grapheme width (mode 2027) the models agree, so no
1152// re-anchor is needed.
1153//
1154// The row is re-anchored along with the column. A terminal that measures a
1155// cluster wider than the model does runs past the right margin and wraps, so a
1156// line the model believes fits can leave the real cursor a row further down.
1157// Recovering the column alone would then leave every following line painted one
1158// row off, which is the drift this re-anchor exists to contain.
1159func (s *TerminalRenderer) reanchorWideLine(newbuf *RenderBuffer) {
1160	// In relative cursor mode there is no absolute row to move to, so the
1161	// column is all that can be recovered.
1162	if s.flags.Contains(tRelativeCursor) {
1163		if s.atPhantom || s.cur.X < 0 || s.cur.X >= newbuf.Width() {
1164			return
1165		}
1166		_, _ = s.buf.WriteString(ansi.CursorHorizontalAbsolute(s.cur.X + 1))
1167		return
1168	}
1169
1170	if s.cur.X < 0 || s.cur.Y < 0 {
1171		return
1172	}
1173
1174	// A pending wrap is as untrustworthy as the column: the terminal may have
1175	// already taken it. An absolute move cancels it and lands on a column the
1176	// model can name, so clamp into the line and record that.
1177	x := min(s.cur.X, newbuf.Width()-1)
1178	_, _ = s.buf.WriteString(ansi.CursorPosition(x+1, s.cur.Y+1))
1179	s.cur.X = x
1180	s.atPhantom = false
1181}
1182
1183// deleteCells deletes the count cells at the current cursor position and moves
1184// the rest of the line to the left. This is equivalent to [ansi.DCH].
1185func (s *TerminalRenderer) deleteCells(count int) {
1186	// [ansi.DCH] will shift in cells from the right margin so we need to
1187	// ensure that they are the right style.
1188	_, _ = s.buf.WriteString(ansi.DeleteCharacter(count))
1189}
1190
1191// clearToBottom clears the screen from the current cursor position to the end
1192// of the screen.
1193func (s *TerminalRenderer) clearToBottom(blank *Cell) {
1194	row, col := s.cur.Y, s.cur.X
1195	if row < 0 {
1196		row = 0
1197	}
1198
1199	s.updatePen(blank)
1200	_, _ = s.buf.WriteString(ansi.EraseScreenBelow)
1201	// Clear the rest of the current line
1202	s.curbuf.ClearArea(Rect(col, row, s.curbuf.Width()-col, 1))
1203	// Clear everything below the current line
1204	s.curbuf.ClearArea(Rect(0, row+1, s.curbuf.Width(), s.curbuf.Height()-row-1))
1205}
1206
1207// clearBottom tests if clearing the end of the screen would satisfy part of
1208// the screen update. Scan backwards through lines in the screen checking if
1209// each is blank and one or more are changed.
1210// It returns the top line.
1211func (s *TerminalRenderer) clearBottom(newbuf *RenderBuffer, total int) (top int) {
1212	if total <= 0 {
1213		return 0
1214	}
1215
1216	top = total
1217	last := min(s.curbuf.Width(), newbuf.Width())
1218	blank := s.clearBlank()
1219	canClearWithBlank := canClearWith(blank)
1220
1221	if canClearWithBlank { //nolint:nestif
1222		var row int
1223		for row = total - 1; row >= 0; row-- {
1224			oldLine := s.curbuf.Line(row)
1225			newLine := newbuf.Line(row)
1226
1227			var col int
1228			ok := true
1229			for col = 0; ok && col < last; col++ {
1230				ok = cellEqual(newLine.At(col), blank)
1231			}
1232			if !ok {
1233				break
1234			}
1235
1236			for col = 0; ok && col < last; col++ {
1237				ok = cellEqual(oldLine.At(col), blank)
1238			}
1239			if !ok {
1240				top = row
1241			}
1242		}
1243
1244		if top < total {
1245			// top is the first row of the trailing blank region, so the
1246			// erase starts there. Starting a row earlier would wipe a row
1247			// this frame did not touch, and the loop that repaints only
1248			// touched rows would never put it back.
1249			s.move(newbuf, 0, top)
1250			s.clearToBottom(blank)
1251			if s.oldhash != nil && s.newhash != nil &&
1252				row < len(s.oldhash) && row < len(s.newhash) {
1253				for row := top; row < newbuf.Height(); row++ {
1254					s.oldhash[row] = s.newhash[row]
1255				}
1256			}
1257		}
1258	}
1259
1260	return top
1261}
1262
1263// clearScreen clears the screen and put cursor at home.
1264func (s *TerminalRenderer) clearScreen(blank *Cell) {
1265	s.updatePen(blank)
1266	_, _ = s.buf.WriteString(ansi.CursorHomePosition)
1267	_, _ = s.buf.WriteString(ansi.EraseEntireScreen)
1268	s.cur.X, s.cur.Y = 0, 0
1269	s.curbuf.Fill(blank)
1270}
1271
1272// clearBelow clears everything below and including the row.
1273func (s *TerminalRenderer) clearBelow(newbuf *RenderBuffer, blank *Cell, row int) {
1274	s.move(newbuf, 0, row)
1275	s.clearToBottom(blank)
1276}
1277
1278// clearUpdate forces a screen redraw.
1279func (s *TerminalRenderer) clearUpdate(newbuf *RenderBuffer) {
1280	blank := s.clearBlank()
1281	var nonEmpty int
1282	if s.flags.Contains(tFullscreen) {
1283		// XXX: We're using the maximum height of the two buffers to ensure we
1284		// write newly added lines to the screen in
1285		// [terminalWriter.transformLine].
1286		nonEmpty = max(s.curbuf.Height(), newbuf.Height())
1287		s.clearScreen(blank)
1288	} else {
1289		nonEmpty = newbuf.Height()
1290		// FIXME: Investigate the double [ansi.ClearScreenBelow] call.
1291		// Commenting the line below out seems to work but it might cause other
1292		// bugs.
1293		s.clearBelow(newbuf, blank, 0)
1294	}
1295	nonEmpty = s.clearBottom(newbuf, nonEmpty)
1296	for i := 0; i < nonEmpty && i < newbuf.Height(); i++ {
1297		s.paintLine(newbuf, i, false)
1298	}
1299}
1300
1301func (s *TerminalRenderer) logf(format string, args ...any) {
1302	if s.logger == nil {
1303		return
1304	}
1305	s.logger.Printf(format, args...)
1306}
1307
1308// Buffered returns the number of bytes buffered for the next flush.
1309func (s *TerminalRenderer) Buffered() int {
1310	return s.buf.Len()
1311}
1312
1313// Flush flushes the buffer to the screen.
1314func (s *TerminalRenderer) Flush() (err error) {
1315	// Write the buffer
1316	if n := s.buf.Len(); n > 0 {
1317		bts := s.buf.Bytes()
1318		if s.logger != nil {
1319			s.logf("output: %q", bts)
1320		}
1321		_, err = s.w.Write(bts)
1322		s.buf.Reset()
1323	}
1324	return
1325}
1326
1327// sizeChange is what a render owes to the screen or the frame having changed
1328// shape since the last one.
1329type sizeChange struct {
1330	// frameResized reports that the frame is a different shape than the model,
1331	// so the model has to be resized before anything is diffed against it.
1332	frameResized bool
1333
1334	// repaintAll requires every row to be painted whatever the model says.
1335	repaintAll bool
1336}
1337
1338// reconcileSize decides what a change of size costs, and is the only place that
1339// decides it.
1340//
1341// A terminal that changed size rewrapped what was on it, and a frame that
1342// changed shape leaves no row holding its old meaning. Either way the model
1343// cannot be trusted. What differs is how much of the screen is the renderer's to
1344// put right: fullscreen owns every cell and clears it, while an inline frame
1345// shares the screen with whatever came before and so repaints its own rows and
1346// leaves the rest alone.
1347func (s *TerminalRenderer) reconcileSize(newbuf *RenderBuffer) sizeChange {
1348	newWidth, newHeight := newbuf.Width(), newbuf.Height()
1349	curWidth, curHeight := s.curbuf.Width(), s.curbuf.Height()
1350	fullscreen := s.flags.Contains(tFullscreen)
1351
1352	change := sizeChange{
1353		frameResized: curWidth != newWidth || curHeight != newHeight,
1354	}
1355
1356	if s.termResized {
1357		if fullscreen {
1358			s.clear = true
1359		} else {
1360			change.repaintAll = true
1361		}
1362		s.termResized = false
1363	}
1364
1365	if change.frameResized {
1366		// No row keeps its meaning, so the hashes that describe them are worth
1367		// nothing. Inline uses the narrower partial clear in Render instead.
1368		s.oldhash, s.newhash = nil, nil
1369		if fullscreen {
1370			s.clear = true
1371		}
1372		// Nor does the application's touch list, which never saw rows that
1373		// vanished and came back between two frames.
1374		s.damage(0, newHeight)
1375	}
1376
1377	// The terminal clips a row it measures wider than the model does, and the
1378	// model believes it painted the whole row. Changing the width changes what
1379	// fits, so a row holding a cell the two might measure differently has to be
1380	// painted again. Rows of plain cells are measured the same by everyone, so an
1381	// ASCII screen pays nothing.
1382	if !fullscreen && curWidth != newWidth {
1383		for y, drifts := range s.driftRows {
1384			if drifts {
1385				s.damage(y, 1)
1386			}
1387		}
1388	}
1389
1390	return change
1391}
1392
1393// damage marks n rows from y as rows this render disturbed on its own: rows it
1394// scrolled, rows it erased, rows the terminal is about to measure differently
1395// than the model does. The diff loop paints a damaged row whether or not the
1396// application drew into it, because the application has no reason to redraw a
1397// row it did not change and no way to know the renderer moved it.
1398func (s *TerminalRenderer) damage(y, n int) {
1399	for i := max(y, 0); i < min(y+n, len(s.damaged)); i++ {
1400		s.damaged[i] = true
1401	}
1402}
1403
1404// beginDamage starts a fresh account, sized to the screen this render is about
1405// to paint. A row outside it cannot be painted, so it cannot be damaged, and
1406// clamping here is what lets every reader index the record without checking.
1407// Reset on the way in rather than the way out, so no future exit path has to
1408// remember to do it. The slice is kept when the size holds, so a steady stream
1409// of frames allocates nothing.
1410func (s *TerminalRenderer) beginDamage(height int) {
1411	if len(s.damaged) != height {
1412		s.damaged = make([]bool, height)
1413		return
1414	}
1415	clear(s.damaged)
1416}
1417
1418// Redraw forces a full redraw of the screen. It's equivalent to calling
1419// [TerminalRenderer.Erase] and [TerminalRenderer.Render].
1420func (s *TerminalRenderer) Redraw(newbuf *RenderBuffer) {
1421	s.clear = true
1422	s.Render(newbuf)
1423}
1424
1425// Render renders changes of the screen to the internal buffer. Call
1426// [terminalWriter.Flush] to flush pending changes to the screen.
1427func (s *TerminalRenderer) Render(newbuf *RenderBuffer) {
1428	// Do we need to render anything? A latched resize counts: the terminal moved
1429	// content the application cannot know about, so a frame it considers
1430	// unchanged still has to be put back on screen.
1431	touchedLines := newbuf.TouchedLines()
1432	if !s.clear && !s.termResized && touchedLines == 0 {
1433		return
1434	}
1435
1436	// Every reader below indexes the touch list by screen row, so the list has to
1437	// cover the screen before any of them run. Growing here is what lets them do
1438	// it without a length check each.
1439	newbuf.growTouched()
1440
1441	if s.curbuf == nil || s.curbuf.Bounds().Empty() {
1442		// Initialize the current buffer
1443		s.curbuf = NewRenderBuffer(newbuf.Width(), newbuf.Height())
1444	}
1445
1446	newWidth, newHeight := newbuf.Width(), newbuf.Height()
1447	curHeight := s.curbuf.Height()
1448
1449	s.beginDamage(newHeight)
1450
1451	size := s.reconcileSize(newbuf)
1452
1453	// TODO: Investigate whether this is necessary. Theoretically, terminals
1454	// can add/remove tab stops and we should be able to handle that. We could
1455	// use [ansi.DECTABSR] to read the tab stops, but that's not implemented in
1456	// most terminals :/
1457	// // Are we using hard tabs? If so, ensure tabs are using the
1458	// // default interval using [ansi.DECST8C].
1459	// if s.opts.HardTabs && !s.initTabs {
1460	// 	s.buf.WriteString(ansi.SetTabEvery8Columns)
1461	// 	s.initTabs = true
1462	// }
1463
1464	var nonEmpty int
1465
1466	// An inline frame that gives up rows has to erase what it no longer covers,
1467	// or the tail of the taller frame stays on screen below the shorter one.
1468	// Fullscreen has no such rows: it repaints the whole screen instead.
1469	//
1470	// The erase runs from the new last row to the bottom of the screen, so it
1471	// reaches that residue wherever the terminal moved it. A width change at
1472	// the same time is therefore a reason to erase rather than a reason to skip
1473	// it, since a terminal that rewrapped those rows has spread them further
1474	// than the model can account for.
1475	partialClear := !s.flags.Contains(tFullscreen) && s.cur.X != -1 && s.cur.Y != -1 &&
1476		curHeight > 0 &&
1477		curHeight > newHeight
1478
1479	if !s.clear && partialClear {
1480		// From the first row the frame gave up, so the frame's own rows are left
1481		// alone: nothing it still owns needs putting back, and a frame that
1482		// collapsed to nothing starts at its own first row rather than the one
1483		// above, which belongs to whatever shared the screen first.
1484		s.clearBelow(newbuf, nil, newHeight)
1485	}
1486
1487	// Resize the model before diffing so the loop below walks every row
1488	// of the new screen, including rows added by a grow.
1489	if size.frameResized {
1490		s.curbuf.Resize(newWidth, newHeight)
1491	}
1492
1493	if s.clear { //nolint:nestif
1494		s.clearUpdate(newbuf)
1495		s.clear = false
1496	} else if touchedLines > 0 || size.repaintAll {
1497		// On Windows, there's a bug with Windows Terminal where [ansi.DECSTBM]
1498		// misbehaves and moves the cursor outside of the scrolling region. For
1499		// now, we disable the optimizations completely on Windows.
1500		// See https://github.com/microsoft/terminal/issues/19016
1501		if s.flags.Contains(tScrollOptim) && s.flags.Contains(tFullscreen) {
1502			// Optimize scrolling for the alternate screen buffer.
1503			// TODO: Should we optimize for inline mode as well? If so, we need
1504			// to know the actual cursor position to use [ansi.DECSTBM].
1505			s.scrollOptimize(newbuf)
1506		}
1507
1508		var changedLines int
1509		var i int
1510
1511		nonEmpty = newHeight
1512
1513		nonEmpty = s.clearBottom(newbuf, nonEmpty)
1514		for i = 0; i < nonEmpty; i++ {
1515			// A repaintAll row has to be put back whatever the model says.
1516			if size.repaintAll || s.damaged[i] || (newbuf.Touched[i] != nil &&
1517				(newbuf.Touched[i].FirstCell != -1 || newbuf.Touched[i].LastCell != -1)) {
1518				s.paintLine(newbuf, i, size.repaintAll)
1519				changedLines++
1520			}
1521		}
1522	}
1523
1524	if !s.flags.Contains(tFullscreen) && size.frameResized {
1525		s.move(newbuf, 0, max(newHeight-1, 0))
1526	}
1527
1528	// Sync windows and screen
1529	newbuf.growTouched()
1530	resetTouched(newbuf.Touched)
1531
1532	s.updatePen(nil)
1533}
1534
1535// Erase marks the screen to be fully erased on the next render.
1536func (s *TerminalRenderer) Erase() {
1537	s.clear = true
1538}
1539
1540// Resize updates the terminal screen tab stops. This is used to calculate
1541// terminal tab stops for hard tab optimizations.
1542//
1543// Resize also invalidates the cursor model when the renderer can recover
1544// from it: a terminal may move the cursor on any resize, so the remembered
1545// position no longer matches reality, and the next move will be absolute.
1546//
1547// In relative cursor mode there is no absolute move to fall back on, and
1548// -1 there means "first move, assume the origin" rather than "unknown", so
1549// invalidating would assert a position instead of forgetting one. Keep the
1550// old model in that mode and let the next render diff against it.
1551//
1552// A resize also forces the next render to repaint, since the screen moves
1553// content around to fit and the model cannot see where it went. Whether that
1554// means the whole screen or only the frame's own rows is decided in
1555// [TerminalRenderer.Render].
1556func (s *TerminalRenderer) Resize(width, height int) {
1557	if s.tabs != nil {
1558		s.tabs.Resize(width)
1559	}
1560
1561	// Latched against the size last reported, not against the model, because
1562	// Render sees only the size the application draws at: a screen that shrinks
1563	// and grows back between two frames looks unchanged by the time it gets
1564	// there. What the resize costs is decided in Render, which knows the mode.
1565	if width > 0 && height > 0 {
1566		s.termResized = s.termResized || (s.termW > 0 && (width != s.termW || height != s.termH))
1567		s.termW, s.termH = width, height
1568	}
1569
1570	if !s.flags.Contains(tRelativeCursor) {
1571		s.cur.X, s.cur.Y = -1, -1
1572	}
1573}
1574
1575// Position returns the cursor position in the screen buffer after applying any
1576// pending transformations from the underlying buffer.
1577func (s *TerminalRenderer) Position() (x, y int) {
1578	return s.cur.X, s.cur.Y
1579}
1580
1581// SetPosition changes the logical cursor position. This can be used when we
1582// change the cursor position outside of the screen and need to update the
1583// screen cursor position.
1584// This changes the cursor position for both normal and alternate screen
1585// buffers.
1586func (s *TerminalRenderer) SetPosition(x, y int) {
1587	s.cur.X, s.cur.Y = x, y
1588}
1589
1590// WriteString writes the given string to the underlying buffer.
1591func (s *TerminalRenderer) WriteString(str string) (int, error) {
1592	return s.buf.WriteString(str) //nolint:wrapcheck
1593}
1594
1595// Write writes the given bytes to the underlying buffer.
1596func (s *TerminalRenderer) Write(b []byte) (int, error) {
1597	return s.buf.Write(b) //nolint:wrapcheck
1598}
1599
1600// MoveTo calculates and writes the shortest sequence to move the cursor to the
1601// given position. It uses the current cursor position and the new position to
1602// calculate the shortest amount of sequences to move the cursor.
1603func (s *TerminalRenderer) MoveTo(x, y int) {
1604	s.move(nil, x, y)
1605}
1606
1607// notLocal returns whether the coordinates are not considered local movement
1608// using the defined thresholds.
1609// This takes the number of columns, and the coordinates of the current and
1610// target positions.
1611func notLocal(cols, fx, fy, tx, ty int) bool {
1612	// The typical distance for a [ansi.CUP] sequence. Anything less than this
1613	// is considered local movement.
1614	const longDist = 8 - 1
1615	return (tx > longDist) &&
1616		(tx < cols-1-longDist) &&
1617		(abs(ty-fy)+abs(tx-fx) > longDist)
1618}
1619
1620// relativeCursorMove returns the relative cursor movement sequence using one or two
1621// of the following sequences [ansi.CUU], [ansi.CUD], [ansi.CUF], [ansi.CUB],
1622// [ansi.VPA], [ansi.HPA].
1623// When overwrite is true, this will try to optimize the sequence by using the
1624// screen cells values to move the cursor instead of using escape sequences.
1625//
1626// It is safe to call this function with a nil [Buffer]. In that case, it won't
1627// use any optimizations that require the new buffer such as overwrite.
1628func relativeCursorMove(s *TerminalRenderer, newbuf *RenderBuffer, fx, fy, tx, ty int, overwrite, useTabs, useBackspace bool) string {
1629	var seq strings.Builder
1630	if newbuf == nil {
1631		overwrite = false // We can't overwrite the current buffer.
1632	}
1633
1634	if ty != fy { //nolint:nestif
1635		var yseq string
1636		if s.caps.Contains(capVPA) && !s.flags.Contains(tRelativeCursor) {
1637			yseq = ansi.VerticalPositionAbsolute(ty + 1)
1638		}
1639
1640		if ty > fy {
1641			n := ty - fy
1642			if cud := ansi.CursorDown(n); yseq == "" || len(cud) < len(yseq) {
1643				yseq = cud
1644			}
1645			if !s.flags.Contains(tFullscreen) || n < len(yseq) { // n is the cost of using newline characters
1646				yseq = strings.Repeat("\n", n)
1647				if s.flags.Contains(tMapNewline) {
1648					fx = 0
1649				}
1650			}
1651		} else if ty < fy {
1652			n := fy - ty
1653			if cuu := ansi.CursorUp(n); yseq == "" || len(cuu) < len(yseq) {
1654				yseq = cuu
1655			}
1656			if n == 1 && fy-1 > 0 {
1657				// TODO: Ensure we're not unintentionally scrolling the screen up.
1658				yseq = ansi.ReverseIndex
1659			}
1660		}
1661
1662		seq.WriteString(yseq)
1663	}
1664
1665	if tx != fx { //nolint:nestif
1666		var xseq string
1667		if !s.flags.Contains(tRelativeCursor) {
1668			if s.caps.Contains(capHPA) {
1669				xseq = ansi.HorizontalPositionAbsolute(tx + 1)
1670			} else if s.caps.Contains(capCHA) {
1671				xseq = ansi.CursorHorizontalAbsolute(tx + 1)
1672			}
1673		}
1674
1675		if tx > fx {
1676			n := tx - fx
1677			if useTabs && s.tabs != nil {
1678				var tabs int
1679				var col int
1680				for col = fx; s.tabs.Next(col) <= tx; col = s.tabs.Next(col) {
1681					tabs++
1682					if col == s.tabs.Next(col) || col >= s.tabs.Width()-1 {
1683						break
1684					}
1685				}
1686
1687				if tabs > 0 {
1688					cht := ansi.CursorHorizontalForwardTab(tabs)
1689					tab := strings.Repeat("\t", tabs)
1690					if false && s.caps.Contains(capCHT) && len(cht) < len(tab) {
1691						// TODO: The linux console and some terminals such as
1692						// Alacritty don't support [ansi.CHT]. Enable this when
1693						// we have a way to detect this, or after 5 years when
1694						// we're sure everyone has updated their terminals :P
1695						seq.WriteString(cht)
1696					} else {
1697						seq.WriteString(tab)
1698					}
1699
1700					n = tx - col
1701					fx = col
1702				}
1703			}
1704
1705			if cuf := ansi.CursorForward(n); xseq == "" || len(cuf) < len(xseq) {
1706				xseq = cuf
1707			}
1708
1709			// If we have no attribute and style changes, overwrite is cheaper.
1710			var ovw string
1711			if overwrite && ty >= 0 {
1712				for i := 0; i < n; i++ {
1713					cell := newbuf.CellAt(fx+i, ty)
1714					if cell != nil && cell.Width > 0 {
1715						i += cell.Width - 1
1716						if !cell.Style.Equal(&s.cur.Style) || !cell.Link.Equal(&s.cur.Link) {
1717							overwrite = false
1718							break
1719						}
1720					}
1721				}
1722			}
1723
1724			if overwrite && ty >= 0 {
1725				for i := 0; i < n; i++ {
1726					cell := newbuf.CellAt(fx+i, ty)
1727					if cell != nil && cell.Width > 0 {
1728						ovw += cell.String()
1729						i += cell.Width - 1
1730					}
1731				}
1732			}
1733
1734			if overwrite && len(ovw) < len(xseq) {
1735				xseq = ovw
1736			}
1737		} else if tx < fx {
1738			n := fx - tx
1739			if useTabs && s.tabs != nil && s.caps.Contains(capCBT) {
1740				// VT100 does not support backward tabs [ansi.CBT].
1741
1742				col := fx
1743
1744				var cbt int // cursor backward tabs count
1745				for s.tabs.Prev(col) >= tx {
1746					col = s.tabs.Prev(col)
1747					cbt++
1748					if col == s.tabs.Prev(col) || col <= 0 {
1749						break
1750					}
1751				}
1752
1753				if cbt > 0 {
1754					seq.WriteString(ansi.CursorBackwardTab(cbt))
1755					n = col - tx
1756				}
1757			}
1758
1759			if cub := ansi.CursorBackward(n); xseq == "" || len(cub) < len(xseq) {
1760				xseq = cub
1761			}
1762
1763			if useBackspace && n < len(xseq) {
1764				xseq = strings.Repeat("\b", n)
1765			}
1766		}
1767
1768		seq.WriteString(xseq)
1769	}
1770
1771	return seq.String()
1772}
1773
1774// moveCursor moves and returns the cursor movement sequence to move the cursor
1775// to the specified position.
1776// When overwrite is true, this will try to optimize the sequence by using the
1777// screen cells values to move the cursor instead of using escape sequences.
1778//
1779// It is safe to call this function with a nil [Buffer]. In that case, it won't
1780// use any optimizations that require the new buffer such as overwrite.
1781func moveCursor(s *TerminalRenderer, newbuf *RenderBuffer, x, y int, overwrite bool) (seq string) {
1782	fx, fy := s.cur.X, s.cur.Y
1783
1784	if !s.flags.Contains(tRelativeCursor) {
1785		width := -1 // Use -1 to indicate that we don't know the width of the screen.
1786		if s.tabs != nil {
1787			width = s.tabs.Width()
1788		}
1789		if newbuf != nil && width == -1 {
1790			// Even though this might not be accurate, we can use the new
1791			// buffer width as a fallback. Technically, if the new buffer
1792			// didn't have the width of the terminal, this would give us a
1793			// wrong result from [notLocal].
1794			width = newbuf.Width()
1795		}
1796		// Method #0: Use [ansi.CUP] if the distance is long.
1797		seq = ansi.CursorPosition(x+1, y+1)
1798		if fx == -1 || fy == -1 || width == -1 || notLocal(width, fx, fy, x, y) {
1799			return seq
1800		}
1801	}
1802
1803	// Optimize based on options.
1804	trials := 0
1805	if s.caps.Contains(capHT) {
1806		trials |= 2 // 0b10 in binary
1807	}
1808	if s.caps.Contains(capBS) {
1809		trials |= 1 // 0b01 in binary
1810	}
1811
1812	// Try all possible combinations of hard tabs and backspace optimizations.
1813	for i := 0; i <= trials; i++ {
1814		// Skip combinations that are not enabled.
1815		if i & ^trials != 0 {
1816			continue
1817		}
1818
1819		useHardTabs := i&2 != 0
1820		useBackspace := i&1 != 0
1821
1822		// Method #1: Use local movement sequences.
1823		nseq1 := relativeCursorMove(s, newbuf, fx, fy, x, y, overwrite, useHardTabs, useBackspace)
1824		if (i == 0 && len(seq) == 0) || len(nseq1) < len(seq) {
1825			seq = nseq1
1826		}
1827
1828		// Method #2: Use [ansi.CR] and local movement sequences.
1829		nseq2 := relativeCursorMove(s, newbuf, 0, fy, x, y, overwrite, useHardTabs, useBackspace)
1830		nseq2 = "\r" + nseq2
1831		if len(nseq2) < len(seq) {
1832			seq = nseq2
1833		}
1834
1835		if !s.flags.Contains(tRelativeCursor) {
1836			// Method #3: Use [ansi.CursorHomePosition] and local movement sequences.
1837			nseq3 := relativeCursorMove(s, newbuf, 0, 0, x, y, overwrite, useHardTabs, useBackspace)
1838			nseq3 = ansi.CursorHomePosition + nseq3
1839			if len(nseq3) < len(seq) {
1840				seq = nseq3
1841			}
1842		}
1843	}
1844
1845	return seq
1846}
1847
1848// xtermCaps returns whether the terminal is xterm-like. This means that the
1849// terminal supports ECMA-48 and ANSI X3.64 escape sequences.
1850// xtermCaps returns a list of control sequence capabilities for the given
1851// terminal type. This only supports a subset of sequences that can
1852// be different among terminals.
1853// NOTE: A hybrid approach would be to support Terminfo databases for a full
1854// set of capabilities.
1855func xtermCaps(termtype string) (v capabilities) {
1856	parts := strings.Split(termtype, "-")
1857	if len(parts) == 0 {
1858		return v
1859	}
1860
1861	switch parts[0] {
1862	case
1863		"contour",
1864		"foot",
1865		"ghostty",
1866		"kitty",
1867		"rio",
1868		"st",
1869		"tmux",
1870		"wezterm":
1871		v = allCaps
1872	case "xterm":
1873		switch {
1874		case len(parts) > 1 && parts[1] == "ghostty":
1875			fallthrough
1876		case len(parts) > 1 && parts[1] == "kitty":
1877			fallthrough
1878		case len(parts) > 1 && parts[1] == "rio":
1879			// These terminals can be defined as xterm- variants for
1880			// compatibility with applications that check for xterm.
1881			v = allCaps
1882		default:
1883			// NOTE: We exclude capHPA from allCaps because terminals like
1884			// Konsole don't support it. Xterm terminfo defines HPA as CHA
1885			// which means we can use CHA instead of HPA.
1886			v = allCaps
1887			v &^= capHPA
1888			v &^= capCHT
1889			v &^= capREP
1890		}
1891	case "alacritty":
1892		v = allCaps
1893		v &^= capCHT // NOTE: alacritty added support for [ansi.CHT] in 2024-12-28 #62d5b13.
1894	case "screen":
1895		// See https://www.gnu.org/software/screen/manual/screen.html#Control-Sequences-1
1896		v = allCaps
1897		v &^= capREP
1898	case "linux":
1899		// See https://man7.org/linux/man-pages/man4/console_codes.4.html
1900		v = capVPA | capCHA | capHPA | capECH | capICH
1901	}
1902
1903	return v
1904}
1905
1906// resetTouched marks every line as untouched, reusing the records already there
1907// rather than allocating a new one per line on every render.
1908func resetTouched(touched []*LineData) {
1909	for i, ld := range touched {
1910		if ld == nil {
1911			touched[i] = &LineData{FirstCell: -1, LastCell: -1}
1912			continue
1913		}
1914		ld.FirstCell, ld.LastCell = -1, -1
1915	}
1916}