terminal_renderer.go

   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	s.buf.WriteString(moveCursor(s, newbuf, x, y, overwrite)) //nolint:errcheck
 382	s.cur.X, s.cur.Y = x, y
 383}
 384
 385// move moves the cursor to the specified position in the buffer.
 386//
 387// It is safe to call this function with a nil [Buffer], in that case, it won't
 388// be using any optimizations that depend on the buffer.
 389func (s *TerminalRenderer) move(newbuf *Buffer, x, y int) {
 390	// XXX: Make sure we use the max height and width of the buffer in case
 391	// we're in the middle of a resize operation.
 392	width, height := s.curbuf.Width(), s.curbuf.Height()
 393	if newbuf != nil {
 394		width = max(newbuf.Width(), width)
 395		height = max(newbuf.Height(), height)
 396	}
 397
 398	if width > 0 && x >= width {
 399		// Handle autowrap
 400		y += (x / width)
 401		x %= width
 402	}
 403
 404	// XXX: Disable styles if there's any
 405	// Some move operations such as [ansi.LF] can apply styles to the new
 406	// cursor position, thus, we need to reset the styles before moving the
 407	// cursor.
 408	blank := s.clearBlank()
 409	resetPen := y != s.cur.Y && !blank.Equal(&EmptyCell)
 410	if resetPen {
 411		s.updatePen(nil)
 412	}
 413
 414	// Reset wrap around (phantom cursor) state
 415	if s.atPhantom {
 416		s.cur.X = 0
 417		s.buf.WriteByte('\r') //nolint:errcheck
 418		s.atPhantom = false   // reset phantom cell state
 419	}
 420
 421	// TODO: Investigate if we need to handle this case and/or if we need the
 422	// following code.
 423	//
 424	// if width > 0 && s.cur.X >= width {
 425	// 	l := (s.cur.X + 1) / width
 426	//
 427	// 	s.cur.Y += l
 428	// 	if height > 0 && s.cur.Y >= height {
 429	// 		l -= s.cur.Y - height - 1
 430	// 	}
 431	//
 432	// 	if l > 0 {
 433	// 		s.cur.X = 0
 434	// 		s.buf.WriteString("\r" + strings.Repeat("\n", l)) //nolint:errcheck
 435	// 	}
 436	// }
 437
 438	if height > 0 {
 439		if s.cur.Y > height-1 {
 440			s.cur.Y = height - 1
 441		}
 442		if y > height-1 {
 443			y = height - 1
 444		}
 445	}
 446
 447	if x == s.cur.X && y == s.cur.Y {
 448		// We give up later because we need to run checks for the phantom cell
 449		// and others before we can determine if we can give up.
 450		return
 451	}
 452
 453	// We set the new cursor in tscreen.moveCursor].
 454	s.moveCursor(newbuf, x, y, true) // Overwrite cells if possible
 455}
 456
 457// cellEqual returns whether the two cells are equal. A nil cell is considered
 458// a [EmptyCell].
 459func cellEqual(a, b *Cell) bool {
 460	if a == b {
 461		return true
 462	}
 463	if a == nil || b == nil {
 464		return false
 465	}
 466	return a.Equal(b)
 467}
 468
 469// putCell draws a cell at the current cursor position.
 470func (s *TerminalRenderer) putCell(newbuf *Buffer, cell *Cell) {
 471	width, height := newbuf.Width(), newbuf.Height()
 472	if s.flags.Contains(tAltScreen) && s.cur.X == width-1 && s.cur.Y == height-1 {
 473		s.putCellLR(newbuf, cell)
 474	} else {
 475		s.putAttrCell(newbuf, cell)
 476	}
 477}
 478
 479// wrapCursor wraps the cursor to the next line.
 480//
 481//nolint:unused
 482func (s *TerminalRenderer) wrapCursor() {
 483	const autoRightMargin = true
 484	if autoRightMargin {
 485		// Assume we have auto wrap mode enabled.
 486		s.cur.X = 0
 487		s.cur.Y++
 488	} else {
 489		s.cur.X--
 490	}
 491}
 492
 493func (s *TerminalRenderer) putAttrCell(newbuf *Buffer, cell *Cell) {
 494	if cell != nil && cell.IsZero() {
 495		// XXX: Zero width cells are special and should not be written to the
 496		// screen no matter what other attributes they have.
 497		// Zero width cells are used for wide characters that are split into
 498		// multiple cells.
 499		return
 500	}
 501
 502	if cell == nil {
 503		cell = s.clearBlank()
 504	}
 505
 506	// We're at pending wrap state (phantom cell), incoming cell should
 507	// wrap.
 508	if s.atPhantom {
 509		s.wrapCursor()
 510		s.atPhantom = false
 511	}
 512
 513	s.updatePen(cell)
 514	s.buf.WriteString(cell.Content) //nolint:errcheck
 515
 516	s.cur.X += cell.Width
 517	if s.cur.X >= newbuf.Width() {
 518		s.atPhantom = true
 519	}
 520}
 521
 522// putCellLR draws a cell at the lower right corner of the screen.
 523func (s *TerminalRenderer) putCellLR(newbuf *Buffer, cell *Cell) {
 524	// Optimize for the lower right corner cell.
 525	curX := s.cur.X
 526	if cell == nil || !cell.IsZero() {
 527		s.buf.WriteString(ansi.ResetAutoWrapMode) //nolint:errcheck
 528		s.putAttrCell(newbuf, cell)
 529		// Writing to lower-right corner cell should not wrap.
 530		s.atPhantom = false
 531		s.cur.X = curX
 532		s.buf.WriteString(ansi.SetAutoWrapMode) //nolint:errcheck
 533	}
 534}
 535
 536// updatePen updates the cursor pen styles.
 537func (s *TerminalRenderer) updatePen(cell *Cell) {
 538	if cell == nil {
 539		cell = &EmptyCell
 540	}
 541
 542	if s.profile != 0 {
 543		// Downsample colors to the given color profile.
 544		cell.Style = ConvertStyle(cell.Style, s.profile)
 545		cell.Link = ConvertLink(cell.Link, s.profile)
 546	}
 547
 548	if !cell.Style.Equal(&s.cur.Style) {
 549		seq := cell.Style.DiffSequence(s.cur.Style)
 550		if cell.Style.IsZero() && len(seq) > len(ansi.ResetStyle) {
 551			seq = ansi.ResetStyle
 552		}
 553		s.buf.WriteString(seq) //nolint:errcheck
 554		s.cur.Style = cell.Style
 555	}
 556	if !cell.Link.Equal(&s.cur.Link) {
 557		s.buf.WriteString(ansi.SetHyperlink(cell.Link.URL, cell.Link.Params)) //nolint:errcheck
 558		s.cur.Link = cell.Link
 559	}
 560}
 561
 562// emitRange emits a range of cells to the buffer. It it equivalent to calling
 563// tscreen.putCell] for each cell in the range. This is optimized to use
 564// [ansi.ECH] and [ansi.REP].
 565// Returns whether the cursor is at the end of interval or somewhere in the
 566// middle.
 567func (s *TerminalRenderer) emitRange(newbuf *Buffer, line Line, n int) (eoi bool) {
 568	hasECH := s.caps.Contains(capECH)
 569	hasREP := s.caps.Contains(capREP)
 570	if hasECH || hasREP {
 571		for n > 0 {
 572			var count int
 573			for n > 1 && !cellEqual(line.At(0), line.At(1)) {
 574				s.putCell(newbuf, line.At(0))
 575				line = line[1:]
 576				n--
 577			}
 578
 579			cell0 := line[0]
 580			if n == 1 {
 581				s.putCell(newbuf, &cell0)
 582				return false
 583			}
 584
 585			count = 2
 586			for count < n && cellEqual(line.At(count), &cell0) {
 587				count++
 588			}
 589
 590			ech := ansi.EraseCharacter(count)
 591			cup := ansi.CursorPosition(s.cur.X+count, s.cur.Y)
 592			rep := ansi.RepeatPreviousCharacter(count)
 593			if hasECH && count > len(ech)+len(cup) && cell0.IsBlank() {
 594				s.updatePen(&cell0)
 595				s.buf.WriteString(ech) //nolint:errcheck
 596
 597				// If this is the last cell, we don't need to move the cursor.
 598				if count < n {
 599					s.move(newbuf, s.cur.X+count, s.cur.Y)
 600				} else {
 601					return true // cursor in the middle
 602				}
 603			} else if hasREP && count > len(rep) &&
 604				(len(cell0.Content) == 1 && cell0.Content[0] >= ansi.US && cell0.Content[0] < ansi.DEL) {
 605				// We only support ASCII characters. Most terminals will handle
 606				// non-ASCII characters correctly, but some might not, ahem xterm.
 607				//
 608				// NOTE: [ansi.REP] only repeats the last rune and won't work
 609				// if the last cell contains multiple runes.
 610
 611				wrapPossible := s.cur.X+count >= newbuf.Width()
 612				repCount := count
 613				if wrapPossible {
 614					repCount--
 615				}
 616
 617				s.updatePen(&cell0)
 618				s.putCell(newbuf, &cell0)
 619				repCount-- // cell0 is a single width cell ASCII character
 620
 621				s.buf.WriteString(ansi.RepeatPreviousCharacter(repCount)) //nolint:errcheck
 622				s.cur.X += repCount
 623				if wrapPossible {
 624					s.putCell(newbuf, &cell0)
 625				}
 626			} else {
 627				for i := 0; i < count; i++ {
 628					s.putCell(newbuf, line.At(i))
 629				}
 630			}
 631
 632			line = line[clamp(count, 0, len(line)):]
 633			n -= count
 634		}
 635
 636		return false
 637	}
 638
 639	for i := 0; i < n; i++ {
 640		s.putCell(newbuf, line.At(i))
 641	}
 642
 643	return false
 644}
 645
 646// putRange puts a range of cells from the old line to the new line.
 647// Returns whether the cursor is at the end of interval or somewhere in the
 648// middle.
 649func (s *TerminalRenderer) putRange(newbuf *Buffer, oldLine, newLine Line, y, start, end int) (eoi bool) {
 650	inline := min(len(ansi.CursorPosition(start+1, y+1)),
 651		min(len(ansi.HorizontalPositionAbsolute(start+1)),
 652			len(ansi.CursorForward(start+1))))
 653	if (end - start + 1) > inline {
 654		var j, same int
 655		for j, same = start, 0; j <= end; j++ {
 656			oldCell, newCell := oldLine.At(j), newLine.At(j)
 657			if same == 0 && oldCell != nil && oldCell.IsZero() {
 658				continue
 659			}
 660			if cellEqual(oldCell, newCell) {
 661				same++
 662			} else {
 663				if same > end-start {
 664					s.emitRange(newbuf, newLine[start:], j-same-start)
 665					s.move(newbuf, j, y)
 666					start = j
 667				}
 668				same = 0
 669			}
 670		}
 671
 672		i := s.emitRange(newbuf, newLine[start:], j-same-start)
 673
 674		// Always return 1 for the next [tScreen.move] after a
 675		// [tScreen.putRange] if we found identical characters at end of
 676		// interval.
 677		if same == 0 {
 678			return i
 679		}
 680		return true
 681	}
 682
 683	return s.emitRange(newbuf, newLine[start:], end-start+1)
 684}
 685
 686// clearToEnd clears the screen from the current cursor position to the end of
 687// line.
 688func (s *TerminalRenderer) clearToEnd(newbuf *Buffer, blank *Cell, force bool) { //nolint:unparam
 689	if s.cur.Y >= 0 {
 690		curline := s.curbuf.Line(s.cur.Y)
 691		for j := s.cur.X; j < s.curbuf.Width(); j++ {
 692			if j >= 0 {
 693				c := curline.At(j)
 694				if !cellEqual(c, blank) {
 695					curline.Set(j, blank)
 696					force = true
 697				}
 698			}
 699		}
 700	}
 701
 702	if force {
 703		s.updatePen(blank)
 704		count := newbuf.Width() - s.cur.X
 705		if s.el0Cost() <= count {
 706			s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck
 707		} else {
 708			for i := 0; i < count; i++ {
 709				s.putCell(newbuf, blank)
 710			}
 711		}
 712	}
 713}
 714
 715// clearBlank returns a blank cell based on the current cursor background color.
 716func (s *TerminalRenderer) clearBlank() *Cell {
 717	c := EmptyCell
 718	if !s.cur.Style.IsZero() || !s.cur.Link.IsZero() {
 719		c.Style = s.cur.Style
 720		c.Link = s.cur.Link
 721	}
 722	return &c
 723}
 724
 725// insertCells inserts the count cells pointed by the given line at the current
 726// cursor position.
 727func (s *TerminalRenderer) insertCells(newbuf *Buffer, line Line, count int) {
 728	supportsICH := s.caps.Contains(capICH)
 729	if supportsICH {
 730		// Use [ansi.ICH] as an optimization.
 731		s.buf.WriteString(ansi.InsertCharacter(count)) //nolint:errcheck
 732	} else {
 733		// Otherwise, use [ansi.IRM] mode.
 734		s.buf.WriteString(ansi.SetInsertReplaceMode) //nolint:errcheck
 735	}
 736
 737	for i := 0; count > 0; i++ {
 738		s.putAttrCell(newbuf, line.At(i))
 739		count--
 740	}
 741
 742	if !supportsICH {
 743		s.buf.WriteString(ansi.ResetInsertReplaceMode) //nolint:errcheck
 744	}
 745}
 746
 747// el0Cost returns the cost of using [ansi.EL] 0 i.e. [ansi.EraseLineRight]. If
 748// this terminal supports background color erase, it can be cheaper to use
 749// [ansi.EL] 0 i.e. [ansi.EraseLineRight] to clear
 750// trailing spaces.
 751func (s *TerminalRenderer) el0Cost() int {
 752	if s.caps != noCaps {
 753		return 0
 754	}
 755	return len(ansi.EraseLineRight)
 756}
 757
 758// transformLine transforms the given line in the current window to the
 759// corresponding line in the new window. It uses [ansi.ICH] and [ansi.DCH] to
 760// insert or delete characters.
 761func (s *TerminalRenderer) transformLine(newbuf *Buffer, y int) {
 762	var firstCell, oLastCell, nLastCell int // first, old last, new last index
 763	oldLine := s.curbuf.Line(y)
 764	newLine := newbuf.Line(y)
 765
 766	// Find the first changed cell in the line
 767	var lineChanged bool
 768	for i := 0; i < newbuf.Width(); i++ {
 769		if !cellEqual(newLine.At(i), oldLine.At(i)) {
 770			lineChanged = true
 771			break
 772		}
 773	}
 774
 775	const ceolStandoutGlitch = false
 776	if ceolStandoutGlitch && lineChanged {
 777		s.move(newbuf, 0, y)
 778		s.clearToEnd(newbuf, nil, false)
 779		s.putRange(newbuf, oldLine, newLine, y, 0, newbuf.Width()-1)
 780	} else {
 781		blank := newLine.At(0)
 782
 783		// It might be cheaper to clear leading spaces with [ansi.EL] 1 i.e.
 784		// [ansi.EraseLineLeft].
 785		if blank == nil || blank.IsBlank() {
 786			var oFirstCell, nFirstCell int
 787			for oFirstCell = 0; oFirstCell < s.curbuf.Width(); oFirstCell++ {
 788				if !cellEqual(oldLine.At(oFirstCell), blank) {
 789					break
 790				}
 791			}
 792			for nFirstCell = 0; nFirstCell < newbuf.Width(); nFirstCell++ {
 793				if !cellEqual(newLine.At(nFirstCell), blank) {
 794					break
 795				}
 796			}
 797
 798			if nFirstCell == oFirstCell {
 799				firstCell = nFirstCell
 800
 801				// Find the first differing cell
 802				for firstCell < newbuf.Width() &&
 803					cellEqual(oldLine.At(firstCell), newLine.At(firstCell)) {
 804					firstCell++
 805				}
 806			} else if oFirstCell > nFirstCell {
 807				firstCell = nFirstCell
 808			} else if oFirstCell < nFirstCell {
 809				firstCell = oFirstCell
 810				el1Cost := len(ansi.EraseLineLeft)
 811				if el1Cost < nFirstCell-oFirstCell {
 812					if nFirstCell >= newbuf.Width() {
 813						s.move(newbuf, 0, y)
 814						s.updatePen(blank)
 815						s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck
 816					} else {
 817						s.move(newbuf, nFirstCell-1, y)
 818						s.updatePen(blank)
 819						s.buf.WriteString(ansi.EraseLineLeft) //nolint:errcheck
 820					}
 821
 822					for firstCell < nFirstCell {
 823						oldLine.Set(firstCell, blank)
 824						firstCell++
 825					}
 826				}
 827			}
 828		} else {
 829			// Find the first differing cell
 830			for firstCell < newbuf.Width() && cellEqual(newLine.At(firstCell), oldLine.At(firstCell)) {
 831				firstCell++
 832			}
 833		}
 834
 835		// If we didn't find one, we're done
 836		if firstCell >= newbuf.Width() {
 837			return
 838		}
 839
 840		blank = newLine.At(newbuf.Width() - 1)
 841		if blank != nil && !blank.IsBlank() {
 842			// Find the last differing cell
 843			nLastCell = newbuf.Width() - 1
 844			for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), oldLine.At(nLastCell)) {
 845				nLastCell--
 846			}
 847
 848			if nLastCell >= firstCell {
 849				s.move(newbuf, firstCell, y)
 850				s.putRange(newbuf, oldLine, newLine, y, firstCell, nLastCell)
 851				if firstCell < len(oldLine) && firstCell < len(newLine) {
 852					copy(oldLine[firstCell:], newLine[firstCell:])
 853				} else {
 854					copy(oldLine, newLine)
 855				}
 856			}
 857
 858			return
 859		}
 860
 861		// Find last non-blank cell in the old line.
 862		oLastCell = s.curbuf.Width() - 1
 863		for oLastCell > firstCell && cellEqual(oldLine.At(oLastCell), blank) {
 864			oLastCell--
 865		}
 866
 867		// Find last non-blank cell in the new line.
 868		nLastCell = newbuf.Width() - 1
 869		for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), blank) {
 870			nLastCell--
 871		}
 872
 873		if nLastCell == firstCell && s.el0Cost() < oLastCell-nLastCell {
 874			s.move(newbuf, firstCell, y)
 875			if !cellEqual(newLine.At(firstCell), blank) {
 876				s.putCell(newbuf, newLine.At(firstCell))
 877			}
 878			s.clearToEnd(newbuf, blank, false)
 879		} else if nLastCell != oLastCell &&
 880			!cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) {
 881			s.move(newbuf, firstCell, y)
 882			if oLastCell-nLastCell > s.el0Cost() {
 883				if s.putRange(newbuf, oldLine, newLine, y, firstCell, nLastCell) {
 884					s.move(newbuf, nLastCell+1, y)
 885				}
 886				s.clearToEnd(newbuf, blank, false)
 887			} else {
 888				n := max(nLastCell, oLastCell)
 889				s.putRange(newbuf, oldLine, newLine, y, firstCell, n)
 890			}
 891		} else {
 892			nLastNonBlank := nLastCell
 893			oLastNonBlank := oLastCell
 894
 895			// Find the last cells that really differ.
 896			// Can be -1 if no cells differ.
 897			for cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) {
 898				if !cellEqual(newLine.At(nLastCell-1), oldLine.At(oLastCell-1)) {
 899					break
 900				}
 901				nLastCell--
 902				oLastCell--
 903				if nLastCell == -1 || oLastCell == -1 {
 904					break
 905				}
 906			}
 907
 908			n := min(oLastCell, nLastCell)
 909			if n >= firstCell {
 910				s.move(newbuf, firstCell, y)
 911				s.putRange(newbuf, oldLine, newLine, y, firstCell, n)
 912			}
 913
 914			if oLastCell < nLastCell {
 915				m := max(nLastNonBlank, oLastNonBlank)
 916				if n != 0 {
 917					for n > 0 {
 918						wide := newLine.At(n + 1)
 919						if wide == nil || !wide.IsZero() {
 920							break
 921						}
 922						n--
 923						oLastCell--
 924					}
 925				} else if n >= firstCell && newLine.At(n) != nil && newLine.At(n).Width > 1 {
 926					next := newLine.At(n + 1)
 927					for next != nil && next.IsZero() {
 928						n++
 929						oLastCell++
 930					}
 931				}
 932
 933				s.move(newbuf, n+1, y)
 934				ichCost := 3 + nLastCell - oLastCell
 935				if s.caps.Contains(capICH) && (nLastCell < nLastNonBlank || ichCost > (m-n)) {
 936					s.putRange(newbuf, oldLine, newLine, y, n+1, m)
 937				} else {
 938					s.insertCells(newbuf, newLine[n+1:], nLastCell-oLastCell)
 939				}
 940			} else if oLastCell > nLastCell {
 941				s.move(newbuf, n+1, y)
 942				dchCost := 3 + oLastCell - nLastCell
 943				if dchCost > len(ansi.EraseLineRight)+nLastNonBlank-(n+1) {
 944					if s.putRange(newbuf, oldLine, newLine, y, n+1, nLastNonBlank) {
 945						s.move(newbuf, nLastNonBlank+1, y)
 946					}
 947					s.clearToEnd(newbuf, blank, false)
 948				} else {
 949					s.updatePen(blank)
 950					s.deleteCells(oLastCell - nLastCell)
 951				}
 952			}
 953		}
 954	}
 955
 956	// Update the old line with the new line
 957	if firstCell < len(oldLine) && firstCell < len(newLine) {
 958		copy(oldLine[firstCell:], newLine[firstCell:])
 959	} else {
 960		copy(oldLine, newLine)
 961	}
 962}
 963
 964// deleteCells deletes the count cells at the current cursor position and moves
 965// the rest of the line to the left. This is equivalent to [ansi.DCH].
 966func (s *TerminalRenderer) deleteCells(count int) {
 967	// [ansi.DCH] will shift in cells from the right margin so we need to
 968	// ensure that they are the right style.
 969	s.buf.WriteString(ansi.DeleteCharacter(count)) //nolint:errcheck
 970}
 971
 972// clearToBottom clears the screen from the current cursor position to the end
 973// of the screen.
 974func (s *TerminalRenderer) clearToBottom(blank *Cell) {
 975	row, col := s.cur.Y, s.cur.X
 976	if row < 0 {
 977		row = 0
 978	}
 979
 980	s.updatePen(blank)
 981	s.buf.WriteString(ansi.EraseScreenBelow) //nolint:errcheck
 982	// Clear the rest of the current line
 983	s.curbuf.ClearArea(Rect(col, row, s.curbuf.Width()-col, 1))
 984	// Clear everything below the current line
 985	s.curbuf.ClearArea(Rect(0, row+1, s.curbuf.Width(), s.curbuf.Height()-row-1))
 986}
 987
 988// clearBottom tests if clearing the end of the screen would satisfy part of
 989// the screen update. Scan backwards through lines in the screen checking if
 990// each is blank and one or more are changed.
 991// It returns the top line.
 992func (s *TerminalRenderer) clearBottom(newbuf *Buffer, total int) (top int) {
 993	if total <= 0 {
 994		return
 995	}
 996
 997	top = total
 998	last := min(s.curbuf.Width(), newbuf.Width())
 999	blank := s.clearBlank()
1000	canClearWithBlank := blank == nil || blank.IsBlank()
1001
1002	if canClearWithBlank {
1003		var row int
1004		for row = total - 1; row >= 0; row-- {
1005			oldLine := s.curbuf.Line(row)
1006			newLine := newbuf.Line(row)
1007
1008			var col int
1009			ok := true
1010			for col = 0; ok && col < last; col++ {
1011				ok = cellEqual(newLine.At(col), blank)
1012			}
1013			if !ok {
1014				break
1015			}
1016
1017			for col = 0; ok && col < last; col++ {
1018				ok = cellEqual(oldLine.At(col), blank)
1019			}
1020			if !ok {
1021				top = row
1022			}
1023		}
1024
1025		if top < total {
1026			s.move(newbuf, 0, max(0, top-1)) // top is 1-based
1027			s.clearToBottom(blank)
1028			if s.oldhash != nil && s.newhash != nil &&
1029				row < len(s.oldhash) && row < len(s.newhash) {
1030				for row := top; row < newbuf.Height(); row++ {
1031					s.oldhash[row] = s.newhash[row]
1032				}
1033			}
1034		}
1035	}
1036
1037	return
1038}
1039
1040// clearScreen clears the screen and put cursor at home.
1041func (s *TerminalRenderer) clearScreen(blank *Cell) {
1042	s.updatePen(blank)
1043	s.buf.WriteString(ansi.CursorHomePosition) //nolint:errcheck
1044	s.buf.WriteString(ansi.EraseEntireScreen)  //nolint:errcheck
1045	s.cur.X, s.cur.Y = 0, 0
1046	s.curbuf.Fill(blank)
1047}
1048
1049// clearBelow clears everything below and including the row.
1050func (s *TerminalRenderer) clearBelow(newbuf *Buffer, blank *Cell, row int) {
1051	s.move(newbuf, 0, row)
1052	s.clearToBottom(blank)
1053}
1054
1055// clearUpdate forces a screen redraw.
1056func (s *TerminalRenderer) clearUpdate(newbuf *Buffer) {
1057	blank := s.clearBlank()
1058	var nonEmpty int
1059	if s.flags.Contains(tAltScreen) {
1060		// XXX: We're using the maximum height of the two buffers to ensure we
1061		// write newly added lines to the screen in
1062		// [terminalWriter.transformLine].
1063		nonEmpty = max(s.curbuf.Height(), newbuf.Height())
1064		s.clearScreen(blank)
1065	} else {
1066		nonEmpty = newbuf.Height()
1067		// FIXME: Investigate the double [ansi.ClearScreenBelow] call.
1068		// Commenting the line below out seems to work but it might cause other
1069		// bugs.
1070		s.clearBelow(newbuf, blank, 0)
1071	}
1072	nonEmpty = s.clearBottom(newbuf, nonEmpty)
1073	for i := 0; i < nonEmpty && i < newbuf.Height(); i++ {
1074		s.transformLine(newbuf, i)
1075	}
1076}
1077
1078func (s *TerminalRenderer) logf(format string, args ...any) {
1079	if s.logger == nil {
1080		return
1081	}
1082	s.logger.Printf(format, args...)
1083}
1084
1085// Buffered returns the number of bytes buffered for the next flush.
1086func (s *TerminalRenderer) Buffered() int {
1087	return s.buf.Len()
1088}
1089
1090// Flush flushes the buffer to the screen.
1091func (s *TerminalRenderer) Flush() (err error) {
1092	// Write the buffer
1093	if n := s.buf.Len(); n > 0 {
1094		bts := s.buf.Bytes()
1095		if !s.flags.Contains(tCursorHidden) {
1096			// Hide the cursor during the flush operation.
1097			buf := make([]byte, len(bts)+len(ansi.HideCursor)+len(ansi.ShowCursor))
1098			copy(buf, ansi.HideCursor)
1099			copy(buf[len(ansi.HideCursor):], bts)
1100			copy(buf[len(ansi.HideCursor)+len(bts):], ansi.ShowCursor)
1101			bts = buf
1102		}
1103		if s.logger != nil {
1104			s.logf("output: %q", bts)
1105		}
1106		_, err = s.w.Write(bts)
1107		s.buf.Reset()
1108		s.laterFlush = true
1109	}
1110	return
1111}
1112
1113// Touched returns the number of lines that have been touched or changed.
1114func (s *TerminalRenderer) Touched(buf *Buffer) (n int) {
1115	if buf.Touched == nil {
1116		return buf.Height()
1117	}
1118	for _, ch := range buf.Touched {
1119		if ch != nil {
1120			n++
1121		}
1122	}
1123	return
1124}
1125
1126// Redraw forces a full redraw of the screen. It's equivalent to calling
1127// [TerminalRenderer.Erase] and [TerminalRenderer.Render].
1128func (s *TerminalRenderer) Redraw(newbuf *Buffer) {
1129	s.clear = true
1130	s.Render(newbuf)
1131}
1132
1133// Render renders changes of the screen to the internal buffer. Call
1134// [terminalWriter.Flush] to flush pending changes to the screen.
1135func (s *TerminalRenderer) Render(newbuf *Buffer) {
1136	// Do we need to render anything?
1137	touchedLines := s.Touched(newbuf)
1138	if !s.clear && touchedLines == 0 {
1139		return
1140	}
1141
1142	newWidth, newHeight := newbuf.Width(), newbuf.Height()
1143	curWidth, curHeight := s.curbuf.Width(), s.curbuf.Height()
1144
1145	// Do we have a buffer to compare to?
1146	if s.curbuf == nil || s.curbuf.Bounds().Empty() {
1147		s.curbuf = NewBuffer(newWidth, newHeight)
1148	}
1149
1150	if curWidth != newWidth || curHeight != newHeight {
1151		s.oldhash, s.newhash = nil, nil
1152	}
1153
1154	// TODO: Investigate whether this is necessary. Theoretically, terminals
1155	// can add/remove tab stops and we should be able to handle that. We could
1156	// use [ansi.DECTABSR] to read the tab stops, but that's not implemented in
1157	// most terminals :/
1158	// // Are we using hard tabs? If so, ensure tabs are using the
1159	// // default interval using [ansi.DECST8C].
1160	// if s.opts.HardTabs && !s.initTabs {
1161	// 	s.buf.WriteString(ansi.SetTabEvery8Columns)
1162	// 	s.initTabs = true
1163	// }
1164
1165	var nonEmpty int
1166
1167	// XXX: In inline mode, after a screen resize, we need to clear the extra
1168	// lines at the bottom of the screen. This is because in inline mode, we
1169	// don't use the full screen height and the current buffer size might be
1170	// larger than the new buffer size.
1171	partialClear := !s.flags.Contains(tAltScreen) && s.cur.X != -1 && s.cur.Y != -1 &&
1172		curWidth == newWidth &&
1173		curHeight > 0 &&
1174		curHeight > newHeight
1175
1176	if !s.clear && partialClear {
1177		s.clearBelow(newbuf, nil, newHeight-1)
1178	}
1179
1180	if s.clear {
1181		s.clearUpdate(newbuf)
1182		s.clear = false
1183	} else if touchedLines > 0 {
1184		if s.flags.Contains(tAltScreen) {
1185			// Optimize scrolling for the alternate screen buffer.
1186			// TODO: Should we optimize for inline mode as well? If so, we need
1187			// to know the actual cursor position to use [ansi.DECSTBM].
1188			s.scrollOptimize(newbuf)
1189		}
1190
1191		var changedLines int
1192		var i int
1193
1194		if s.flags.Contains(tAltScreen) {
1195			nonEmpty = min(curHeight, newHeight)
1196		} else {
1197			nonEmpty = newHeight
1198		}
1199
1200		nonEmpty = s.clearBottom(newbuf, nonEmpty)
1201		for i = 0; i < nonEmpty && i < newHeight; i++ {
1202			if newbuf.Touched == nil || i >= len(newbuf.Touched) || (newbuf.Touched[i] != nil &&
1203				(newbuf.Touched[i].FirstCell != -1 || newbuf.Touched[i].LastCell != -1)) {
1204				s.transformLine(newbuf, i)
1205				changedLines++
1206			}
1207
1208			// Mark line changed successfully.
1209			if i < len(newbuf.Touched) && i <= newbuf.Height()-1 {
1210				newbuf.Touched[i] = &LineData{
1211					FirstCell: -1, LastCell: -1,
1212				}
1213			}
1214			if i < len(s.curbuf.Touched) && i < s.curbuf.Height()-1 {
1215				s.curbuf.Touched[i] = &LineData{
1216					FirstCell: -1, LastCell: -1,
1217				}
1218			}
1219		}
1220	}
1221
1222	if !s.laterFlush && !s.flags.Contains(tAltScreen) && s.scrollHeight < newHeight-1 {
1223		s.move(newbuf, 0, newHeight-1)
1224	}
1225
1226	// Sync windows and screen
1227	newbuf.Touched = make([]*LineData, newHeight)
1228	for i := range newbuf.Touched {
1229		newbuf.Touched[i] = &LineData{
1230			FirstCell: -1, LastCell: -1,
1231		}
1232	}
1233	for i := range s.curbuf.Touched {
1234		s.curbuf.Touched[i] = &LineData{
1235			FirstCell: -1, LastCell: -1,
1236		}
1237	}
1238
1239	if curWidth != newWidth || curHeight != newHeight {
1240		// Resize the old buffer to match the new buffer.
1241		s.curbuf.Resize(newWidth, newHeight)
1242		// Sync new lines to old lines
1243		for i := curHeight - 1; i < newHeight; i++ {
1244			copy(s.curbuf.Line(i), newbuf.Line(i))
1245		}
1246	}
1247
1248	s.updatePen(nil) // nil indicates a blank cell with no styles
1249}
1250
1251// Erase marks the screen to be fully erased on the next render.
1252func (s *TerminalRenderer) Erase() {
1253	s.clear = true
1254}
1255
1256// Resize updates the terminal screen tab stops. This is used to calculate
1257// terminal tab stops for hard tab optimizations.
1258func (s *TerminalRenderer) Resize(width, _ int) {
1259	if s.tabs != nil {
1260		s.tabs.Resize(width)
1261	}
1262	s.scrollHeight = 0
1263}
1264
1265// Position returns the cursor position in the screen buffer after applying any
1266// pending transformations from the underlying buffer.
1267func (s *TerminalRenderer) Position() (x, y int) {
1268	return s.cur.X, s.cur.Y
1269}
1270
1271// SetPosition changes the logical cursor position. This can be used when we
1272// change the cursor position outside of the screen and need to update the
1273// screen cursor position.
1274// This changes the cursor position for both normal and alternate screen
1275// buffers.
1276func (s *TerminalRenderer) SetPosition(x, y int) {
1277	s.cur.X, s.cur.Y = x, y
1278	s.saved.X, s.saved.Y = x, y
1279}
1280
1281// WriteString writes the given string to the underlying buffer.
1282func (s *TerminalRenderer) WriteString(str string) (int, error) {
1283	return s.buf.WriteString(str)
1284}
1285
1286// Write writes the given bytes to the underlying buffer.
1287func (s *TerminalRenderer) Write(b []byte) (int, error) {
1288	return s.buf.Write(b)
1289}
1290
1291// MoveTo calculates and writes the shortest sequence to move the cursor to the
1292// given position. It uses the current cursor position and the new position to
1293// calculate the shortest amount of sequences to move the cursor.
1294func (s *TerminalRenderer) MoveTo(x, y int) {
1295	s.move(nil, x, y)
1296}
1297
1298// notLocal returns whether the coordinates are not considered local movement
1299// using the defined thresholds.
1300// This takes the number of columns, and the coordinates of the current and
1301// target positions.
1302func notLocal(cols, fx, fy, tx, ty int) bool {
1303	// The typical distance for a [ansi.CUP] sequence. Anything less than this
1304	// is considered local movement.
1305	const longDist = 8 - 1
1306	return (tx > longDist) &&
1307		(tx < cols-1-longDist) &&
1308		(abs(ty-fy)+abs(tx-fx) > longDist)
1309}
1310
1311// relativeCursorMove returns the relative cursor movement sequence using one or two
1312// of the following sequences [ansi.CUU], [ansi.CUD], [ansi.CUF], [ansi.CUB],
1313// [ansi.VPA], [ansi.HPA].
1314// When overwrite is true, this will try to optimize the sequence by using the
1315// screen cells values to move the cursor instead of using escape sequences.
1316//
1317// It is safe to call this function with a nil [Buffer]. In that case, it won't
1318// use any optimizations that require the new buffer such as overwrite.
1319func relativeCursorMove(s *TerminalRenderer, newbuf *Buffer, fx, fy, tx, ty int, overwrite, useTabs, useBackspace bool) string {
1320	var seq strings.Builder
1321	height := -1
1322	if newbuf == nil {
1323		overwrite = false // We can't overwrite the current buffer.
1324	} else {
1325		height = newbuf.Height()
1326	}
1327
1328	if ty != fy {
1329		var yseq string
1330		if s.caps.Contains(capVPA) && !s.flags.Contains(tRelativeCursor) {
1331			yseq = ansi.VerticalPositionAbsolute(ty + 1)
1332		}
1333
1334		// OPTIM: Use [ansi.LF] and [ansi.ReverseIndex] as optimizations.
1335
1336		if ty > fy {
1337			n := ty - fy
1338			if cud := ansi.CursorDown(n); yseq == "" || len(cud) < len(yseq) {
1339				yseq = cud
1340			}
1341			shouldScroll := !s.flags.Contains(tAltScreen) && ty > s.scrollHeight
1342			if shouldScroll && ty == s.scrollHeight && ty < height {
1343				n = min(n, height-1-ty)
1344			}
1345			if lf := strings.Repeat("\n", n); shouldScroll ||
1346				((ty < height || height == -1) && len(lf) < len(yseq)) {
1347				yseq = lf
1348				s.scrollHeight = max(s.scrollHeight, ty)
1349				if s.flags.Contains(tMapNewline) {
1350					fx = 0
1351				}
1352			}
1353		} else if ty < fy {
1354			n := fy - ty
1355			if cuu := ansi.CursorUp(n); yseq == "" || len(cuu) < len(yseq) {
1356				yseq = cuu
1357			}
1358			if n == 1 && fy-1 > 0 {
1359				// TODO: Ensure we're not unintentionally scrolling the screen up.
1360				yseq = ansi.ReverseIndex
1361			}
1362		}
1363
1364		seq.WriteString(yseq)
1365	}
1366
1367	if tx != fx {
1368		var xseq string
1369		if s.caps.Contains(capHPA) && !s.flags.Contains(tRelativeCursor) {
1370			xseq = ansi.HorizontalPositionAbsolute(tx + 1)
1371		}
1372
1373		if tx > fx {
1374			n := tx - fx
1375			if useTabs && s.tabs != nil {
1376				var tabs int
1377				var col int
1378				for col = fx; s.tabs.Next(col) <= tx; col = s.tabs.Next(col) {
1379					tabs++
1380					if col == s.tabs.Next(col) || col >= s.tabs.Width()-1 {
1381						break
1382					}
1383				}
1384
1385				if tabs > 0 {
1386					cht := ansi.CursorHorizontalForwardTab(tabs)
1387					tab := strings.Repeat("\t", tabs)
1388					if false && s.caps.Contains(capCHT) && len(cht) < len(tab) {
1389						// TODO: The linux console and some terminals such as
1390						// Alacritty don't support [ansi.CHT]. Enable this when
1391						// we have a way to detect this, or after 5 years when
1392						// we're sure everyone has updated their terminals :P
1393						seq.WriteString(cht)
1394					} else {
1395						seq.WriteString(tab)
1396					}
1397
1398					n = tx - col
1399					fx = col
1400				}
1401			}
1402
1403			if cuf := ansi.CursorForward(n); xseq == "" || len(cuf) < len(xseq) {
1404				xseq = cuf
1405			}
1406
1407			// If we have no attribute and style changes, overwrite is cheaper.
1408			var ovw string
1409			if overwrite && ty >= 0 {
1410				for i := 0; i < n; i++ {
1411					cell := newbuf.CellAt(fx+i, ty)
1412					if cell != nil && cell.Width > 0 {
1413						i += cell.Width - 1
1414						if !cell.Style.Equal(&s.cur.Style) || !cell.Link.Equal(&s.cur.Link) {
1415							overwrite = false
1416							break
1417						}
1418					}
1419				}
1420			}
1421
1422			if overwrite && ty >= 0 {
1423				for i := 0; i < n; i++ {
1424					cell := newbuf.CellAt(fx+i, ty)
1425					if cell != nil && cell.Width > 0 {
1426						ovw += cell.String()
1427						i += cell.Width - 1
1428					} else {
1429						ovw += " "
1430					}
1431				}
1432			}
1433
1434			if overwrite && len(ovw) < len(xseq) {
1435				xseq = ovw
1436			}
1437		} else if tx < fx {
1438			n := fx - tx
1439			if useTabs && s.tabs != nil && s.caps.Contains(capCBT) {
1440				// VT100 does not support backward tabs [ansi.CBT].
1441
1442				col := fx
1443
1444				var cbt int // cursor backward tabs count
1445				for s.tabs.Prev(col) >= tx {
1446					col = s.tabs.Prev(col)
1447					cbt++
1448					if col == s.tabs.Prev(col) || col <= 0 {
1449						break
1450					}
1451				}
1452
1453				if cbt > 0 {
1454					seq.WriteString(ansi.CursorBackwardTab(cbt))
1455					n = col - tx
1456				}
1457			}
1458
1459			if cub := ansi.CursorBackward(n); xseq == "" || len(cub) < len(xseq) {
1460				xseq = cub
1461			}
1462
1463			if useBackspace && n < len(xseq) {
1464				xseq = strings.Repeat("\b", n)
1465			}
1466		}
1467
1468		seq.WriteString(xseq)
1469	}
1470
1471	return seq.String()
1472}
1473
1474// moveCursor moves and returns the cursor movement sequence to move the cursor
1475// to the specified position.
1476// When overwrite is true, this will try to optimize the sequence by using the
1477// screen cells values to move the cursor instead of using escape sequences.
1478//
1479// It is safe to call this function with a nil [Buffer]. In that case, it won't
1480// use any optimizations that require the new buffer such as overwrite.
1481func moveCursor(s *TerminalRenderer, newbuf *Buffer, x, y int, overwrite bool) (seq string) {
1482	fx, fy := s.cur.X, s.cur.Y
1483
1484	if !s.flags.Contains(tRelativeCursor) {
1485		width := -1 // Use -1 to indicate that we don't know the width of the screen.
1486		if s.tabs != nil {
1487			width = s.tabs.Width()
1488		}
1489		if newbuf != nil && width == -1 {
1490			// Even though this might not be accurate, we can use the new
1491			// buffer width as a fallback. Technically, if the new buffer
1492			// didn't have the width of the terminal, this would give us a
1493			// wrong result from [notLocal].
1494			width = newbuf.Width()
1495		}
1496		// Method #0: Use [ansi.CUP] if the distance is long.
1497		seq = ansi.CursorPosition(x+1, y+1)
1498		if fx == -1 || fy == -1 || width == -1 || notLocal(width, fx, fy, x, y) {
1499			return
1500		}
1501	}
1502
1503	// Optimize based on options.
1504	trials := 0
1505	if s.caps.Contains(capHT) {
1506		trials |= 2 // 0b10 in binary
1507	}
1508	if s.caps.Contains(capBS) {
1509		trials |= 1 // 0b01 in binary
1510	}
1511
1512	// Try all possible combinations of hard tabs and backspace optimizations.
1513	for i := 0; i <= trials; i++ {
1514		// Skip combinations that are not enabled.
1515		if i & ^trials != 0 {
1516			continue
1517		}
1518
1519		useHardTabs := i&2 != 0
1520		useBackspace := i&1 != 0
1521
1522		// Method #1: Use local movement sequences.
1523		nseq := relativeCursorMove(s, newbuf, fx, fy, x, y, overwrite, useHardTabs, useBackspace)
1524		if (i == 0 && len(seq) == 0) || len(nseq) < len(seq) {
1525			seq = nseq
1526		}
1527
1528		// Method #2: Use [ansi.CR] and local movement sequences.
1529		nseq = "\r" + relativeCursorMove(s, newbuf, 0, fy, x, y, overwrite, useHardTabs, useBackspace)
1530		if len(nseq) < len(seq) {
1531			seq = nseq
1532		}
1533
1534		if !s.flags.Contains(tRelativeCursor) {
1535			// Method #3: Use [ansi.CursorHomePosition] and local movement sequences.
1536			nseq = ansi.CursorHomePosition + relativeCursorMove(s, newbuf, 0, 0, x, y, overwrite, useHardTabs, useBackspace)
1537			if len(nseq) < len(seq) {
1538				seq = nseq
1539			}
1540		}
1541	}
1542
1543	return
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}