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