screen.go

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