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}