main
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}