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