forked from toolshed/abra
		
	We were running behind and there were quite some deprecations to update. This was mostly in the upstream copy/pasta package but seems quite minimal.
		
			
				
	
	
		
			1458 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1458 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package cellbuf
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"errors"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 
 | |
| 	"github.com/charmbracelet/colorprofile"
 | |
| 	"github.com/charmbracelet/x/ansi"
 | |
| 	"github.com/charmbracelet/x/term"
 | |
| )
 | |
| 
 | |
| // ErrInvalidDimensions is returned when the dimensions of a window are invalid
 | |
| // for the operation.
 | |
| var ErrInvalidDimensions = errors.New("invalid dimensions")
 | |
| 
 | |
| // notLocal returns whether the coordinates are not considered local movement
 | |
| // using the defined thresholds.
 | |
| // This takes the number of columns, and the coordinates of the current and
 | |
| // target positions.
 | |
| func notLocal(cols, fx, fy, tx, ty int) bool {
 | |
| 	// The typical distance for a [ansi.CUP] sequence. Anything less than this
 | |
| 	// is considered local movement.
 | |
| 	const longDist = 8 - 1
 | |
| 	return (tx > longDist) &&
 | |
| 		(tx < cols-1-longDist) &&
 | |
| 		(abs(ty-fy)+abs(tx-fx) > longDist)
 | |
| }
 | |
| 
 | |
| // relativeCursorMove returns the relative cursor movement sequence using one or two
 | |
| // of the following sequences [ansi.CUU], [ansi.CUD], [ansi.CUF], [ansi.CUB],
 | |
| // [ansi.VPA], [ansi.HPA].
 | |
| // When overwrite is true, this will try to optimize the sequence by using the
 | |
| // screen cells values to move the cursor instead of using escape sequences.
 | |
| func relativeCursorMove(s *Screen, fx, fy, tx, ty int, overwrite, useTabs, useBackspace bool) string {
 | |
| 	var seq strings.Builder
 | |
| 
 | |
| 	width, height := s.newbuf.Width(), s.newbuf.Height()
 | |
| 	if ty != fy {
 | |
| 		var yseq string
 | |
| 		if s.xtermLike && !s.opts.RelativeCursor {
 | |
| 			yseq = ansi.VerticalPositionAbsolute(ty + 1)
 | |
| 		}
 | |
| 
 | |
| 		// OPTIM: Use [ansi.LF] and [ansi.ReverseIndex] as optimizations.
 | |
| 
 | |
| 		if ty > fy {
 | |
| 			n := ty - fy
 | |
| 			if cud := ansi.CursorDown(n); yseq == "" || len(cud) < len(yseq) {
 | |
| 				yseq = cud
 | |
| 			}
 | |
| 			shouldScroll := !s.opts.AltScreen && fy+n >= s.scrollHeight
 | |
| 			if lf := strings.Repeat("\n", n); shouldScroll || (fy+n < height && len(lf) < len(yseq)) {
 | |
| 				// TODO: Ensure we're not unintentionally scrolling the screen down.
 | |
| 				yseq = lf
 | |
| 				s.scrollHeight = max(s.scrollHeight, fy+n)
 | |
| 			}
 | |
| 		} else if ty < fy {
 | |
| 			n := fy - ty
 | |
| 			if cuu := ansi.CursorUp(n); yseq == "" || len(cuu) < len(yseq) {
 | |
| 				yseq = cuu
 | |
| 			}
 | |
| 			if n == 1 && fy-1 > 0 {
 | |
| 				// TODO: Ensure we're not unintentionally scrolling the screen up.
 | |
| 				yseq = ansi.ReverseIndex
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		seq.WriteString(yseq)
 | |
| 	}
 | |
| 
 | |
| 	if tx != fx {
 | |
| 		var xseq string
 | |
| 		if s.xtermLike && !s.opts.RelativeCursor {
 | |
| 			xseq = ansi.HorizontalPositionAbsolute(tx + 1)
 | |
| 		}
 | |
| 
 | |
| 		if tx > fx {
 | |
| 			n := tx - fx
 | |
| 			if useTabs {
 | |
| 				var tabs int
 | |
| 				var col int
 | |
| 				for col = fx; s.tabs.Next(col) <= tx; col = s.tabs.Next(col) {
 | |
| 					tabs++
 | |
| 					if col == s.tabs.Next(col) || col >= width-1 {
 | |
| 						break
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				if tabs > 0 {
 | |
| 					cht := ansi.CursorHorizontalForwardTab(tabs)
 | |
| 					tab := strings.Repeat("\t", tabs)
 | |
| 					if false && s.xtermLike && len(cht) < len(tab) {
 | |
| 						// TODO: The linux console and some terminals such as
 | |
| 						// Alacritty don't support [ansi.CHT]. Enable this when
 | |
| 						// we have a way to detect this, or after 5 years when
 | |
| 						// we're sure everyone has updated their terminals :P
 | |
| 						seq.WriteString(cht)
 | |
| 					} else {
 | |
| 						seq.WriteString(tab)
 | |
| 					}
 | |
| 
 | |
| 					n = tx - col
 | |
| 					fx = col
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if cuf := ansi.CursorForward(n); xseq == "" || len(cuf) < len(xseq) {
 | |
| 				xseq = cuf
 | |
| 			}
 | |
| 
 | |
| 			// If we have no attribute and style changes, overwrite is cheaper.
 | |
| 			var ovw string
 | |
| 			if overwrite && ty >= 0 {
 | |
| 				for i := 0; i < n; i++ {
 | |
| 					cell := s.newbuf.Cell(fx+i, ty)
 | |
| 					if cell != nil && cell.Width > 0 {
 | |
| 						i += cell.Width - 1
 | |
| 						if !cell.Style.Equal(&s.cur.Style) || !cell.Link.Equal(&s.cur.Link) {
 | |
| 							overwrite = false
 | |
| 							break
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if overwrite && ty >= 0 {
 | |
| 				for i := 0; i < n; i++ {
 | |
| 					cell := s.newbuf.Cell(fx+i, ty)
 | |
| 					if cell != nil && cell.Width > 0 {
 | |
| 						ovw += cell.String()
 | |
| 						i += cell.Width - 1
 | |
| 					} else {
 | |
| 						ovw += " "
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if overwrite && len(ovw) < len(xseq) {
 | |
| 				xseq = ovw
 | |
| 			}
 | |
| 		} else if tx < fx {
 | |
| 			n := fx - tx
 | |
| 			if useTabs && s.xtermLike {
 | |
| 				// VT100 does not support backward tabs [ansi.CBT].
 | |
| 
 | |
| 				col := fx
 | |
| 
 | |
| 				var cbt int // cursor backward tabs count
 | |
| 				for s.tabs.Prev(col) >= tx {
 | |
| 					col = s.tabs.Prev(col)
 | |
| 					cbt++
 | |
| 					if col == s.tabs.Prev(col) || col <= 0 {
 | |
| 						break
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				if cbt > 0 {
 | |
| 					seq.WriteString(ansi.CursorBackwardTab(cbt))
 | |
| 					n = col - tx
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if cub := ansi.CursorBackward(n); xseq == "" || len(cub) < len(xseq) {
 | |
| 				xseq = cub
 | |
| 			}
 | |
| 
 | |
| 			if useBackspace && n < len(xseq) {
 | |
| 				xseq = strings.Repeat("\b", n)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		seq.WriteString(xseq)
 | |
| 	}
 | |
| 
 | |
| 	return seq.String()
 | |
| }
 | |
| 
 | |
| // moveCursor moves and returns the cursor movement sequence to move the cursor
 | |
| // to the specified position.
 | |
| // When overwrite is true, this will try to optimize the sequence by using the
 | |
| // screen cells values to move the cursor instead of using escape sequences.
 | |
| func moveCursor(s *Screen, x, y int, overwrite bool) (seq string) {
 | |
| 	fx, fy := s.cur.X, s.cur.Y
 | |
| 
 | |
| 	if !s.opts.RelativeCursor {
 | |
| 		// Method #0: Use [ansi.CUP] if the distance is long.
 | |
| 		seq = ansi.CursorPosition(x+1, y+1)
 | |
| 		if fx == -1 || fy == -1 || notLocal(s.newbuf.Width(), fx, fy, x, y) {
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Optimize based on options.
 | |
| 	trials := 0
 | |
| 	if s.opts.HardTabs {
 | |
| 		trials |= 2 // 0b10 in binary
 | |
| 	}
 | |
| 	if s.opts.Backspace {
 | |
| 		trials |= 1 // 0b01 in binary
 | |
| 	}
 | |
| 
 | |
| 	// Try all possible combinations of hard tabs and backspace optimizations.
 | |
| 	for i := 0; i <= trials; i++ {
 | |
| 		// Skip combinations that are not enabled.
 | |
| 		if i & ^trials != 0 {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		useHardTabs := i&2 != 0
 | |
| 		useBackspace := i&1 != 0
 | |
| 
 | |
| 		// Method #1: Use local movement sequences.
 | |
| 		nseq := relativeCursorMove(s, fx, fy, x, y, overwrite, useHardTabs, useBackspace)
 | |
| 		if (i == 0 && len(seq) == 0) || len(nseq) < len(seq) {
 | |
| 			seq = nseq
 | |
| 		}
 | |
| 
 | |
| 		// Method #2: Use [ansi.CR] and local movement sequences.
 | |
| 		nseq = "\r" + relativeCursorMove(s, 0, fy, x, y, overwrite, useHardTabs, useBackspace)
 | |
| 		if len(nseq) < len(seq) {
 | |
| 			seq = nseq
 | |
| 		}
 | |
| 
 | |
| 		if !s.opts.RelativeCursor {
 | |
| 			// Method #3: Use [ansi.CursorHomePosition] and local movement sequences.
 | |
| 			nseq = ansi.CursorHomePosition + relativeCursorMove(s, 0, 0, x, y, overwrite, useHardTabs, useBackspace)
 | |
| 			if len(nseq) < len(seq) {
 | |
| 				seq = nseq
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // moveCursor moves the cursor to the specified position.
 | |
| func (s *Screen) moveCursor(x, y int, overwrite bool) {
 | |
| 	if !s.opts.AltScreen && s.cur.X == -1 && s.cur.Y == -1 {
 | |
| 		// First cursor movement in inline mode, move the cursor to the first
 | |
| 		// column before moving to the target position.
 | |
| 		s.buf.WriteByte('\r') //nolint:errcheck
 | |
| 		s.cur.X, s.cur.Y = 0, 0
 | |
| 	}
 | |
| 	s.buf.WriteString(moveCursor(s, x, y, overwrite)) //nolint:errcheck
 | |
| 	s.cur.X, s.cur.Y = x, y
 | |
| }
 | |
| 
 | |
| func (s *Screen) move(x, y int) {
 | |
| 	// XXX: Make sure we use the max height and width of the buffer in case
 | |
| 	// we're in the middle of a resize operation.
 | |
| 	width := max(s.newbuf.Width(), s.curbuf.Width())
 | |
| 	height := max(s.newbuf.Height(), s.curbuf.Height())
 | |
| 
 | |
| 	if width > 0 && x >= width {
 | |
| 		// Handle autowrap
 | |
| 		y += (x / width)
 | |
| 		x %= width
 | |
| 	}
 | |
| 
 | |
| 	// XXX: Disable styles if there's any
 | |
| 	// Some move operations such as [ansi.LF] can apply styles to the new
 | |
| 	// cursor position, thus, we need to reset the styles before moving the
 | |
| 	// cursor.
 | |
| 	blank := s.clearBlank()
 | |
| 	resetPen := y != s.cur.Y && !blank.Equal(&BlankCell)
 | |
| 	if resetPen {
 | |
| 		s.updatePen(nil)
 | |
| 	}
 | |
| 
 | |
| 	// Reset wrap around (phantom cursor) state
 | |
| 	if s.atPhantom {
 | |
| 		s.cur.X = 0
 | |
| 		s.buf.WriteByte('\r') //nolint:errcheck
 | |
| 		s.atPhantom = false   // reset phantom cell state
 | |
| 	}
 | |
| 
 | |
| 	// TODO: Investigate if we need to handle this case and/or if we need the
 | |
| 	// following code.
 | |
| 	//
 | |
| 	// if width > 0 && s.cur.X >= width {
 | |
| 	// 	l := (s.cur.X + 1) / width
 | |
| 	//
 | |
| 	// 	s.cur.Y += l
 | |
| 	// 	if height > 0 && s.cur.Y >= height {
 | |
| 	// 		l -= s.cur.Y - height - 1
 | |
| 	// 	}
 | |
| 	//
 | |
| 	// 	if l > 0 {
 | |
| 	// 		s.cur.X = 0
 | |
| 	// 		s.buf.WriteString("\r" + strings.Repeat("\n", l)) //nolint:errcheck
 | |
| 	// 	}
 | |
| 	// }
 | |
| 
 | |
| 	if height > 0 {
 | |
| 		if s.cur.Y > height-1 {
 | |
| 			s.cur.Y = height - 1
 | |
| 		}
 | |
| 		if y > height-1 {
 | |
| 			y = height - 1
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if x == s.cur.X && y == s.cur.Y {
 | |
| 		// We give up later because we need to run checks for the phantom cell
 | |
| 		// and others before we can determine if we can give up.
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// We set the new cursor in [Screen.moveCursor].
 | |
| 	s.moveCursor(x, y, true) // Overwrite cells if possible
 | |
| }
 | |
| 
 | |
| // Cursor represents a terminal Cursor.
 | |
| type Cursor struct {
 | |
| 	Style
 | |
| 	Link
 | |
| 	Position
 | |
| }
 | |
| 
 | |
| // ScreenOptions are options for the screen.
 | |
| type ScreenOptions struct {
 | |
| 	// Term is the terminal type to use when writing to the screen. When empty,
 | |
| 	// `$TERM` is used from [os.Getenv].
 | |
| 	Term string
 | |
| 	// Profile is the color profile to use when writing to the screen.
 | |
| 	Profile colorprofile.Profile
 | |
| 	// RelativeCursor is whether to use relative cursor movements. This is
 | |
| 	// useful when alt-screen is not used or when using inline mode.
 | |
| 	RelativeCursor bool
 | |
| 	// AltScreen is whether to use the alternate screen buffer.
 | |
| 	AltScreen bool
 | |
| 	// ShowCursor is whether to show the cursor.
 | |
| 	ShowCursor bool
 | |
| 	// HardTabs is whether to use hard tabs to optimize cursor movements.
 | |
| 	HardTabs bool
 | |
| 	// Backspace is whether to use backspace characters to move the cursor.
 | |
| 	Backspace bool
 | |
| }
 | |
| 
 | |
| // lineData represents the metadata for a line.
 | |
| type lineData struct {
 | |
| 	// first and last changed cell indices
 | |
| 	firstCell, lastCell int
 | |
| 	// old index used for scrolling
 | |
| 	oldIndex int //nolint:unused
 | |
| }
 | |
| 
 | |
| // Screen represents the terminal screen.
 | |
| type Screen struct {
 | |
| 	w                io.Writer
 | |
| 	buf              *bytes.Buffer // buffer for writing to the screen
 | |
| 	curbuf           *Buffer       // the current buffer
 | |
| 	newbuf           *Buffer       // the new buffer
 | |
| 	tabs             *TabStops
 | |
| 	touch            map[int]lineData
 | |
| 	queueAbove       []string  // the queue of strings to write above the screen
 | |
| 	oldhash, newhash []uint64  // the old and new hash values for each line
 | |
| 	hashtab          []hashmap // the hashmap table
 | |
| 	oldnum           []int     // old indices from previous hash
 | |
| 	cur, saved       Cursor    // the current and saved cursors
 | |
| 	opts             ScreenOptions
 | |
| 	mu               sync.Mutex
 | |
| 	method           ansi.Method
 | |
| 	scrollHeight     int  // keeps track of how many lines we've scrolled down (inline mode)
 | |
| 	altScreenMode    bool // whether alternate screen mode is enabled
 | |
| 	cursorHidden     bool // whether text cursor mode is enabled
 | |
| 	clear            bool // whether to force clear the screen
 | |
| 	xtermLike        bool // whether to use xterm-like optimizations, otherwise, it uses vt100 only
 | |
| 	queuedText       bool // whether we have queued non-zero width text queued up
 | |
| 	atPhantom        bool // whether the cursor is out of bounds and at a phantom cell
 | |
| }
 | |
| 
 | |
| // SetMethod sets the method used to calculate the width of cells.
 | |
| func (s *Screen) SetMethod(method ansi.Method) {
 | |
| 	s.method = method
 | |
| }
 | |
| 
 | |
| // UseBackspaces sets whether to use backspace characters to move the cursor.
 | |
| func (s *Screen) UseBackspaces(v bool) {
 | |
| 	s.opts.Backspace = v
 | |
| }
 | |
| 
 | |
| // UseHardTabs sets whether to use hard tabs to optimize cursor movements.
 | |
| func (s *Screen) UseHardTabs(v bool) {
 | |
| 	s.opts.HardTabs = v
 | |
| }
 | |
| 
 | |
| // SetColorProfile sets the color profile to use when writing to the screen.
 | |
| func (s *Screen) SetColorProfile(p colorprofile.Profile) {
 | |
| 	s.opts.Profile = p
 | |
| }
 | |
| 
 | |
| // SetRelativeCursor sets whether to use relative cursor movements.
 | |
| func (s *Screen) SetRelativeCursor(v bool) {
 | |
| 	s.opts.RelativeCursor = v
 | |
| }
 | |
| 
 | |
| // EnterAltScreen enters the alternate screen buffer.
 | |
| func (s *Screen) EnterAltScreen() {
 | |
| 	s.opts.AltScreen = true
 | |
| 	s.clear = true
 | |
| 	s.saved = s.cur
 | |
| }
 | |
| 
 | |
| // ExitAltScreen exits the alternate screen buffer.
 | |
| func (s *Screen) ExitAltScreen() {
 | |
| 	s.opts.AltScreen = false
 | |
| 	s.clear = true
 | |
| 	s.cur = s.saved
 | |
| }
 | |
| 
 | |
| // ShowCursor shows the cursor.
 | |
| func (s *Screen) ShowCursor() {
 | |
| 	s.opts.ShowCursor = true
 | |
| }
 | |
| 
 | |
| // HideCursor hides the cursor.
 | |
| func (s *Screen) HideCursor() {
 | |
| 	s.opts.ShowCursor = false
 | |
| }
 | |
| 
 | |
| // Bounds implements Window.
 | |
| func (s *Screen) Bounds() Rectangle {
 | |
| 	// Always return the new buffer bounds.
 | |
| 	return s.newbuf.Bounds()
 | |
| }
 | |
| 
 | |
| // Cell implements Window.
 | |
| func (s *Screen) Cell(x int, y int) *Cell {
 | |
| 	return s.newbuf.Cell(x, y)
 | |
| }
 | |
| 
 | |
| // Redraw forces a full redraw of the screen.
 | |
| func (s *Screen) Redraw() {
 | |
| 	s.mu.Lock()
 | |
| 	s.clear = true
 | |
| 	s.mu.Unlock()
 | |
| }
 | |
| 
 | |
| // Clear clears the screen with blank cells. This is a convenience method for
 | |
| // [Screen.Fill] with a nil cell.
 | |
| func (s *Screen) Clear() bool {
 | |
| 	return s.ClearRect(s.newbuf.Bounds())
 | |
| }
 | |
| 
 | |
| // ClearRect clears the given rectangle with blank cells. This is a convenience
 | |
| // method for [Screen.FillRect] with a nil cell.
 | |
| func (s *Screen) ClearRect(r Rectangle) bool {
 | |
| 	return s.FillRect(nil, r)
 | |
| }
 | |
| 
 | |
| // SetCell implements Window.
 | |
| func (s *Screen) SetCell(x int, y int, cell *Cell) (v bool) {
 | |
| 	s.mu.Lock()
 | |
| 	defer s.mu.Unlock()
 | |
| 	cellWidth := 1
 | |
| 	if cell != nil {
 | |
| 		cellWidth = cell.Width
 | |
| 	}
 | |
| 	if prev := s.curbuf.Cell(x, y); !cellEqual(prev, cell) {
 | |
| 		chg, ok := s.touch[y]
 | |
| 		if !ok {
 | |
| 			chg = lineData{firstCell: x, lastCell: x + cellWidth}
 | |
| 		} else {
 | |
| 			chg.firstCell = min(chg.firstCell, x)
 | |
| 			chg.lastCell = max(chg.lastCell, x+cellWidth)
 | |
| 		}
 | |
| 		s.touch[y] = chg
 | |
| 	}
 | |
| 
 | |
| 	return s.newbuf.SetCell(x, y, cell)
 | |
| }
 | |
| 
 | |
| // Fill implements Window.
 | |
| func (s *Screen) Fill(cell *Cell) bool {
 | |
| 	return s.FillRect(cell, s.newbuf.Bounds())
 | |
| }
 | |
| 
 | |
| // FillRect implements Window.
 | |
| func (s *Screen) FillRect(cell *Cell, r Rectangle) bool {
 | |
| 	s.mu.Lock()
 | |
| 	defer s.mu.Unlock()
 | |
| 	s.newbuf.FillRect(cell, r)
 | |
| 	for i := r.Min.Y; i < r.Max.Y; i++ {
 | |
| 		s.touch[i] = lineData{firstCell: r.Min.X, lastCell: r.Max.X}
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // isXtermLike returns whether the terminal is xterm-like. This means that the
 | |
| // terminal supports ECMA-48 and ANSI X3.64 escape sequences.
 | |
| // TODO: Should this be a lookup table into each $TERM terminfo database? Like
 | |
| // we could keep a map of ANSI escape sequence to terminfo capability name and
 | |
| // check if the database supports the escape sequence. Instead of keeping a
 | |
| // list of terminal names here.
 | |
| func isXtermLike(termtype string) (v bool) {
 | |
| 	parts := strings.Split(termtype, "-")
 | |
| 	if len(parts) == 0 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	switch parts[0] {
 | |
| 	case
 | |
| 		"alacritty",
 | |
| 		"contour",
 | |
| 		"foot",
 | |
| 		"ghostty",
 | |
| 		"kitty",
 | |
| 		"linux",
 | |
| 		"rio",
 | |
| 		"screen",
 | |
| 		"st",
 | |
| 		"tmux",
 | |
| 		"wezterm",
 | |
| 		"xterm":
 | |
| 		v = true
 | |
| 	}
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // NewScreen creates a new Screen.
 | |
| func NewScreen(w io.Writer, width, height int, opts *ScreenOptions) (s *Screen) {
 | |
| 	s = new(Screen)
 | |
| 	s.w = w
 | |
| 	if opts != nil {
 | |
| 		s.opts = *opts
 | |
| 	}
 | |
| 
 | |
| 	if s.opts.Term == "" {
 | |
| 		s.opts.Term = os.Getenv("TERM")
 | |
| 	}
 | |
| 
 | |
| 	if width <= 0 || height <= 0 {
 | |
| 		if f, ok := w.(term.File); ok {
 | |
| 			width, height, _ = term.GetSize(f.Fd())
 | |
| 		}
 | |
| 	}
 | |
| 	if width < 0 {
 | |
| 		width = 0
 | |
| 	}
 | |
| 	if height < 0 {
 | |
| 		height = 0
 | |
| 	}
 | |
| 
 | |
| 	s.buf = new(bytes.Buffer)
 | |
| 	s.xtermLike = isXtermLike(s.opts.Term)
 | |
| 	s.curbuf = NewBuffer(width, height)
 | |
| 	s.newbuf = NewBuffer(width, height)
 | |
| 	s.cur = Cursor{Position: Pos(-1, -1)} // start at -1 to force a move
 | |
| 	s.saved = s.cur
 | |
| 	s.reset()
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // Width returns the width of the screen.
 | |
| func (s *Screen) Width() int {
 | |
| 	return s.newbuf.Width()
 | |
| }
 | |
| 
 | |
| // Height returns the height of the screen.
 | |
| func (s *Screen) Height() int {
 | |
| 	return s.newbuf.Height()
 | |
| }
 | |
| 
 | |
| // cellEqual returns whether the two cells are equal. A nil cell is considered
 | |
| // a [BlankCell].
 | |
| func cellEqual(a, b *Cell) bool {
 | |
| 	if a == b {
 | |
| 		return true
 | |
| 	}
 | |
| 	if a == nil {
 | |
| 		a = &BlankCell
 | |
| 	}
 | |
| 	if b == nil {
 | |
| 		b = &BlankCell
 | |
| 	}
 | |
| 	return a.Equal(b)
 | |
| }
 | |
| 
 | |
| // putCell draws a cell at the current cursor position.
 | |
| func (s *Screen) putCell(cell *Cell) {
 | |
| 	width, height := s.newbuf.Width(), s.newbuf.Height()
 | |
| 	if s.opts.AltScreen && s.cur.X == width-1 && s.cur.Y == height-1 {
 | |
| 		s.putCellLR(cell)
 | |
| 	} else {
 | |
| 		s.putAttrCell(cell)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // wrapCursor wraps the cursor to the next line.
 | |
| //
 | |
| //nolint:unused
 | |
| func (s *Screen) wrapCursor() {
 | |
| 	const autoRightMargin = true
 | |
| 	if autoRightMargin {
 | |
| 		// Assume we have auto wrap mode enabled.
 | |
| 		s.cur.X = 0
 | |
| 		s.cur.Y++
 | |
| 	} else {
 | |
| 		s.cur.X--
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *Screen) putAttrCell(cell *Cell) {
 | |
| 	if cell != nil && cell.Empty() {
 | |
| 		// XXX: Zero width cells are special and should not be written to the
 | |
| 		// screen no matter what other attributes they have.
 | |
| 		// Zero width cells are used for wide characters that are split into
 | |
| 		// multiple cells.
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if cell == nil {
 | |
| 		cell = s.clearBlank()
 | |
| 	}
 | |
| 
 | |
| 	// We're at pending wrap state (phantom cell), incoming cell should
 | |
| 	// wrap.
 | |
| 	if s.atPhantom {
 | |
| 		s.wrapCursor()
 | |
| 		s.atPhantom = false
 | |
| 	}
 | |
| 
 | |
| 	s.updatePen(cell)
 | |
| 	s.buf.WriteRune(cell.Rune) //nolint:errcheck
 | |
| 	for _, c := range cell.Comb {
 | |
| 		s.buf.WriteRune(c) //nolint:errcheck
 | |
| 	}
 | |
| 
 | |
| 	s.cur.X += cell.Width
 | |
| 
 | |
| 	if cell.Width > 0 {
 | |
| 		s.queuedText = true
 | |
| 	}
 | |
| 
 | |
| 	if s.cur.X >= s.newbuf.Width() {
 | |
| 		s.atPhantom = true
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // putCellLR draws a cell at the lower right corner of the screen.
 | |
| func (s *Screen) putCellLR(cell *Cell) {
 | |
| 	// Optimize for the lower right corner cell.
 | |
| 	curX := s.cur.X
 | |
| 	if cell == nil || !cell.Empty() {
 | |
| 		s.buf.WriteString(ansi.ResetAutoWrapMode) //nolint:errcheck
 | |
| 		s.putAttrCell(cell)
 | |
| 		// Writing to lower-right corner cell should not wrap.
 | |
| 		s.atPhantom = false
 | |
| 		s.cur.X = curX
 | |
| 		s.buf.WriteString(ansi.SetAutoWrapMode) //nolint:errcheck
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // updatePen updates the cursor pen styles.
 | |
| func (s *Screen) updatePen(cell *Cell) {
 | |
| 	if cell == nil {
 | |
| 		cell = &BlankCell
 | |
| 	}
 | |
| 
 | |
| 	if s.opts.Profile != 0 {
 | |
| 		// Downsample colors to the given color profile.
 | |
| 		cell.Style = ConvertStyle(cell.Style, s.opts.Profile)
 | |
| 		cell.Link = ConvertLink(cell.Link, s.opts.Profile)
 | |
| 	}
 | |
| 
 | |
| 	if !cell.Style.Equal(&s.cur.Style) {
 | |
| 		seq := cell.Style.DiffSequence(s.cur.Style)
 | |
| 		if cell.Style.Empty() && len(seq) > len(ansi.ResetStyle) {
 | |
| 			seq = ansi.ResetStyle
 | |
| 		}
 | |
| 		s.buf.WriteString(seq) //nolint:errcheck
 | |
| 		s.cur.Style = cell.Style
 | |
| 	}
 | |
| 	if !cell.Link.Equal(&s.cur.Link) {
 | |
| 		s.buf.WriteString(ansi.SetHyperlink(cell.Link.URL, cell.Link.Params)) //nolint:errcheck
 | |
| 		s.cur.Link = cell.Link
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // emitRange emits a range of cells to the buffer. It it equivalent to calling
 | |
| // [Screen.putCell] for each cell in the range. This is optimized to use
 | |
| // [ansi.ECH] and [ansi.REP].
 | |
| // Returns whether the cursor is at the end of interval or somewhere in the
 | |
| // middle.
 | |
| func (s *Screen) emitRange(line Line, n int) (eoi bool) {
 | |
| 	for n > 0 {
 | |
| 		var count int
 | |
| 		for n > 1 && !cellEqual(line.At(0), line.At(1)) {
 | |
| 			s.putCell(line.At(0))
 | |
| 			line = line[1:]
 | |
| 			n--
 | |
| 		}
 | |
| 
 | |
| 		cell0 := line[0]
 | |
| 		if n == 1 {
 | |
| 			s.putCell(cell0)
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		count = 2
 | |
| 		for count < n && cellEqual(line.At(count), cell0) {
 | |
| 			count++
 | |
| 		}
 | |
| 
 | |
| 		ech := ansi.EraseCharacter(count)
 | |
| 		cup := ansi.CursorPosition(s.cur.X+count, s.cur.Y)
 | |
| 		rep := ansi.RepeatPreviousCharacter(count)
 | |
| 		if s.xtermLike && count > len(ech)+len(cup) && cell0 != nil && cell0.Clear() {
 | |
| 			s.updatePen(cell0)
 | |
| 			s.buf.WriteString(ech) //nolint:errcheck
 | |
| 
 | |
| 			// If this is the last cell, we don't need to move the cursor.
 | |
| 			if count < n {
 | |
| 				s.move(s.cur.X+count, s.cur.Y)
 | |
| 			} else {
 | |
| 				return true // cursor in the middle
 | |
| 			}
 | |
| 		} else if s.xtermLike && count > len(rep) &&
 | |
| 			(cell0 == nil || (len(cell0.Comb) == 0 && cell0.Rune < 256)) {
 | |
| 			// We only support ASCII characters. Most terminals will handle
 | |
| 			// non-ASCII characters correctly, but some might not, ahem xterm.
 | |
| 			//
 | |
| 			// NOTE: [ansi.REP] only repeats the last rune and won't work
 | |
| 			// if the last cell contains multiple runes.
 | |
| 
 | |
| 			wrapPossible := s.cur.X+count >= s.newbuf.Width()
 | |
| 			repCount := count
 | |
| 			if wrapPossible {
 | |
| 				repCount--
 | |
| 			}
 | |
| 
 | |
| 			s.updatePen(cell0)
 | |
| 			s.putCell(cell0)
 | |
| 			repCount-- // cell0 is a single width cell ASCII character
 | |
| 
 | |
| 			s.buf.WriteString(ansi.RepeatPreviousCharacter(repCount)) //nolint:errcheck
 | |
| 			s.cur.X += repCount
 | |
| 			if wrapPossible {
 | |
| 				s.putCell(cell0)
 | |
| 			}
 | |
| 		} else {
 | |
| 			for i := 0; i < count; i++ {
 | |
| 				s.putCell(line.At(i))
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		line = line[clamp(count, 0, len(line)):]
 | |
| 		n -= count
 | |
| 	}
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // putRange puts a range of cells from the old line to the new line.
 | |
| // Returns whether the cursor is at the end of interval or somewhere in the
 | |
| // middle.
 | |
| func (s *Screen) putRange(oldLine, newLine Line, y, start, end int) (eoi bool) {
 | |
| 	inline := min(len(ansi.CursorPosition(start+1, y+1)),
 | |
| 		min(len(ansi.HorizontalPositionAbsolute(start+1)),
 | |
| 			len(ansi.CursorForward(start+1))))
 | |
| 	if (end - start + 1) > inline {
 | |
| 		var j, same int
 | |
| 		for j, same = start, 0; j <= end; j++ {
 | |
| 			oldCell, newCell := oldLine.At(j), newLine.At(j)
 | |
| 			if same == 0 && oldCell != nil && oldCell.Empty() {
 | |
| 				continue
 | |
| 			}
 | |
| 			if cellEqual(oldCell, newCell) {
 | |
| 				same++
 | |
| 			} else {
 | |
| 				if same > end-start {
 | |
| 					s.emitRange(newLine[start:], j-same-start)
 | |
| 					s.move(j, y)
 | |
| 					start = j
 | |
| 				}
 | |
| 				same = 0
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		i := s.emitRange(newLine[start:], j-same-start)
 | |
| 
 | |
| 		// Always return 1 for the next [Screen.move] after a [Screen.putRange] if
 | |
| 		// we found identical characters at end of interval.
 | |
| 		if same == 0 {
 | |
| 			return i
 | |
| 		}
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	return s.emitRange(newLine[start:], end-start+1)
 | |
| }
 | |
| 
 | |
| // clearToEnd clears the screen from the current cursor position to the end of
 | |
| // line.
 | |
| func (s *Screen) clearToEnd(blank *Cell, force bool) { //nolint:unparam
 | |
| 	if s.cur.Y >= 0 {
 | |
| 		curline := s.curbuf.Line(s.cur.Y)
 | |
| 		for j := s.cur.X; j < s.curbuf.Width(); j++ {
 | |
| 			if j >= 0 {
 | |
| 				c := curline.At(j)
 | |
| 				if !cellEqual(c, blank) {
 | |
| 					curline.Set(j, blank)
 | |
| 					force = true
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if force {
 | |
| 		s.updatePen(blank)
 | |
| 		count := s.newbuf.Width() - s.cur.X
 | |
| 		if s.el0Cost() <= count {
 | |
| 			s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck
 | |
| 		} else {
 | |
| 			for i := 0; i < count; i++ {
 | |
| 				s.putCell(blank)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // clearBlank returns a blank cell based on the current cursor background color.
 | |
| func (s *Screen) clearBlank() *Cell {
 | |
| 	c := BlankCell
 | |
| 	if !s.cur.Style.Empty() || !s.cur.Link.Empty() {
 | |
| 		c.Style = s.cur.Style
 | |
| 		c.Link = s.cur.Link
 | |
| 	}
 | |
| 	return &c
 | |
| }
 | |
| 
 | |
| // insertCells inserts the count cells pointed by the given line at the current
 | |
| // cursor position.
 | |
| func (s *Screen) insertCells(line Line, count int) {
 | |
| 	if s.xtermLike {
 | |
| 		// Use [ansi.ICH] as an optimization.
 | |
| 		s.buf.WriteString(ansi.InsertCharacter(count)) //nolint:errcheck
 | |
| 	} else {
 | |
| 		// Otherwise, use [ansi.IRM] mode.
 | |
| 		s.buf.WriteString(ansi.SetInsertReplaceMode) //nolint:errcheck
 | |
| 	}
 | |
| 
 | |
| 	for i := 0; count > 0; i++ {
 | |
| 		s.putAttrCell(line[i])
 | |
| 		count--
 | |
| 	}
 | |
| 
 | |
| 	if !s.xtermLike {
 | |
| 		s.buf.WriteString(ansi.ResetInsertReplaceMode) //nolint:errcheck
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // el0Cost returns the cost of using [ansi.EL] 0 i.e. [ansi.EraseLineRight]. If
 | |
| // this terminal supports background color erase, it can be cheaper to use
 | |
| // [ansi.EL] 0 i.e. [ansi.EraseLineRight] to clear
 | |
| // trailing spaces.
 | |
| func (s *Screen) el0Cost() int {
 | |
| 	if s.xtermLike {
 | |
| 		return 0
 | |
| 	}
 | |
| 	return len(ansi.EraseLineRight)
 | |
| }
 | |
| 
 | |
| // transformLine transforms the given line in the current window to the
 | |
| // corresponding line in the new window. It uses [ansi.ICH] and [ansi.DCH] to
 | |
| // insert or delete characters.
 | |
| func (s *Screen) transformLine(y int) {
 | |
| 	var firstCell, oLastCell, nLastCell int // first, old last, new last index
 | |
| 	oldLine := s.curbuf.Line(y)
 | |
| 	newLine := s.newbuf.Line(y)
 | |
| 
 | |
| 	// Find the first changed cell in the line
 | |
| 	var lineChanged bool
 | |
| 	for i := 0; i < s.newbuf.Width(); i++ {
 | |
| 		if !cellEqual(newLine.At(i), oldLine.At(i)) {
 | |
| 			lineChanged = true
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	const ceolStandoutGlitch = false
 | |
| 	if ceolStandoutGlitch && lineChanged {
 | |
| 		s.move(0, y)
 | |
| 		s.clearToEnd(nil, false)
 | |
| 		s.putRange(oldLine, newLine, y, 0, s.newbuf.Width()-1)
 | |
| 	} else {
 | |
| 		blank := newLine.At(0)
 | |
| 
 | |
| 		// It might be cheaper to clear leading spaces with [ansi.EL] 1 i.e.
 | |
| 		// [ansi.EraseLineLeft].
 | |
| 		if blank == nil || blank.Clear() {
 | |
| 			var oFirstCell, nFirstCell int
 | |
| 			for oFirstCell = 0; oFirstCell < s.curbuf.Width(); oFirstCell++ {
 | |
| 				if !cellEqual(oldLine.At(oFirstCell), blank) {
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 			for nFirstCell = 0; nFirstCell < s.newbuf.Width(); nFirstCell++ {
 | |
| 				if !cellEqual(newLine.At(nFirstCell), blank) {
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if nFirstCell == oFirstCell {
 | |
| 				firstCell = nFirstCell
 | |
| 
 | |
| 				// Find the first differing cell
 | |
| 				for firstCell < s.newbuf.Width() &&
 | |
| 					cellEqual(oldLine.At(firstCell), newLine.At(firstCell)) {
 | |
| 					firstCell++
 | |
| 				}
 | |
| 			} else if oFirstCell > nFirstCell {
 | |
| 				firstCell = nFirstCell
 | |
| 			} else if oFirstCell < nFirstCell {
 | |
| 				firstCell = oFirstCell
 | |
| 				el1Cost := len(ansi.EraseLineLeft)
 | |
| 				if el1Cost < nFirstCell-oFirstCell {
 | |
| 					if nFirstCell >= s.newbuf.Width() {
 | |
| 						s.move(0, y)
 | |
| 						s.updatePen(blank)
 | |
| 						s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck
 | |
| 					} else {
 | |
| 						s.move(nFirstCell-1, y)
 | |
| 						s.updatePen(blank)
 | |
| 						s.buf.WriteString(ansi.EraseLineLeft) //nolint:errcheck
 | |
| 					}
 | |
| 
 | |
| 					for firstCell < nFirstCell {
 | |
| 						oldLine.Set(firstCell, blank)
 | |
| 						firstCell++
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		} else {
 | |
| 			// Find the first differing cell
 | |
| 			for firstCell < s.newbuf.Width() && cellEqual(newLine.At(firstCell), oldLine.At(firstCell)) {
 | |
| 				firstCell++
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// If we didn't find one, we're done
 | |
| 		if firstCell >= s.newbuf.Width() {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		blank = newLine.At(s.newbuf.Width() - 1)
 | |
| 		if blank != nil && !blank.Clear() {
 | |
| 			// Find the last differing cell
 | |
| 			nLastCell = s.newbuf.Width() - 1
 | |
| 			for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), oldLine.At(nLastCell)) {
 | |
| 				nLastCell--
 | |
| 			}
 | |
| 
 | |
| 			if nLastCell >= firstCell {
 | |
| 				s.move(firstCell, y)
 | |
| 				s.putRange(oldLine, newLine, y, firstCell, nLastCell)
 | |
| 				if firstCell < len(oldLine) && firstCell < len(newLine) {
 | |
| 					copy(oldLine[firstCell:], newLine[firstCell:])
 | |
| 				} else {
 | |
| 					copy(oldLine, newLine)
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// Find last non-blank cell in the old line.
 | |
| 		oLastCell = s.curbuf.Width() - 1
 | |
| 		for oLastCell > firstCell && cellEqual(oldLine.At(oLastCell), blank) {
 | |
| 			oLastCell--
 | |
| 		}
 | |
| 
 | |
| 		// Find last non-blank cell in the new line.
 | |
| 		nLastCell = s.newbuf.Width() - 1
 | |
| 		for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), blank) {
 | |
| 			nLastCell--
 | |
| 		}
 | |
| 
 | |
| 		if nLastCell == firstCell && s.el0Cost() < oLastCell-nLastCell {
 | |
| 			s.move(firstCell, y)
 | |
| 			if !cellEqual(newLine.At(firstCell), blank) {
 | |
| 				s.putCell(newLine.At(firstCell))
 | |
| 			}
 | |
| 			s.clearToEnd(blank, false)
 | |
| 		} else if nLastCell != oLastCell &&
 | |
| 			!cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) {
 | |
| 			s.move(firstCell, y)
 | |
| 			if oLastCell-nLastCell > s.el0Cost() {
 | |
| 				if s.putRange(oldLine, newLine, y, firstCell, nLastCell) {
 | |
| 					s.move(nLastCell+1, y)
 | |
| 				}
 | |
| 				s.clearToEnd(blank, false)
 | |
| 			} else {
 | |
| 				n := max(nLastCell, oLastCell)
 | |
| 				s.putRange(oldLine, newLine, y, firstCell, n)
 | |
| 			}
 | |
| 		} else {
 | |
| 			nLastNonBlank := nLastCell
 | |
| 			oLastNonBlank := oLastCell
 | |
| 
 | |
| 			// Find the last cells that really differ.
 | |
| 			// Can be -1 if no cells differ.
 | |
| 			for cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) {
 | |
| 				if !cellEqual(newLine.At(nLastCell-1), oldLine.At(oLastCell-1)) {
 | |
| 					break
 | |
| 				}
 | |
| 				nLastCell--
 | |
| 				oLastCell--
 | |
| 				if nLastCell == -1 || oLastCell == -1 {
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			n := min(oLastCell, nLastCell)
 | |
| 			if n >= firstCell {
 | |
| 				s.move(firstCell, y)
 | |
| 				s.putRange(oldLine, newLine, y, firstCell, n)
 | |
| 			}
 | |
| 
 | |
| 			if oLastCell < nLastCell {
 | |
| 				m := max(nLastNonBlank, oLastNonBlank)
 | |
| 				if n != 0 {
 | |
| 					for n > 0 {
 | |
| 						wide := newLine.At(n + 1)
 | |
| 						if wide == nil || !wide.Empty() {
 | |
| 							break
 | |
| 						}
 | |
| 						n--
 | |
| 						oLastCell--
 | |
| 					}
 | |
| 				} else if n >= firstCell && newLine.At(n) != nil && newLine.At(n).Width > 1 {
 | |
| 					next := newLine.At(n + 1)
 | |
| 					for next != nil && next.Empty() {
 | |
| 						n++
 | |
| 						oLastCell++
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				s.move(n+1, y)
 | |
| 				ichCost := 3 + nLastCell - oLastCell
 | |
| 				if s.xtermLike && (nLastCell < nLastNonBlank || ichCost > (m-n)) {
 | |
| 					s.putRange(oldLine, newLine, y, n+1, m)
 | |
| 				} else {
 | |
| 					s.insertCells(newLine[n+1:], nLastCell-oLastCell)
 | |
| 				}
 | |
| 			} else if oLastCell > nLastCell {
 | |
| 				s.move(n+1, y)
 | |
| 				dchCost := 3 + oLastCell - nLastCell
 | |
| 				if dchCost > len(ansi.EraseLineRight)+nLastNonBlank-(n+1) {
 | |
| 					if s.putRange(oldLine, newLine, y, n+1, nLastNonBlank) {
 | |
| 						s.move(nLastNonBlank+1, y)
 | |
| 					}
 | |
| 					s.clearToEnd(blank, false)
 | |
| 				} else {
 | |
| 					s.updatePen(blank)
 | |
| 					s.deleteCells(oLastCell - nLastCell)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Update the old line with the new line
 | |
| 	if firstCell < len(oldLine) && firstCell < len(newLine) {
 | |
| 		copy(oldLine[firstCell:], newLine[firstCell:])
 | |
| 	} else {
 | |
| 		copy(oldLine, newLine)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // deleteCells deletes the count cells at the current cursor position and moves
 | |
| // the rest of the line to the left. This is equivalent to [ansi.DCH].
 | |
| func (s *Screen) deleteCells(count int) {
 | |
| 	// [ansi.DCH] will shift in cells from the right margin so we need to
 | |
| 	// ensure that they are the right style.
 | |
| 	s.buf.WriteString(ansi.DeleteCharacter(count)) //nolint:errcheck
 | |
| }
 | |
| 
 | |
| // clearToBottom clears the screen from the current cursor position to the end
 | |
| // of the screen.
 | |
| func (s *Screen) clearToBottom(blank *Cell) {
 | |
| 	row, col := s.cur.Y, s.cur.X
 | |
| 	if row < 0 {
 | |
| 		row = 0
 | |
| 	}
 | |
| 
 | |
| 	s.updatePen(blank)
 | |
| 	s.buf.WriteString(ansi.EraseScreenBelow) //nolint:errcheck
 | |
| 	// Clear the rest of the current line
 | |
| 	s.curbuf.ClearRect(Rect(col, row, s.curbuf.Width()-col, 1))
 | |
| 	// Clear everything below the current line
 | |
| 	s.curbuf.ClearRect(Rect(0, row+1, s.curbuf.Width(), s.curbuf.Height()-row-1))
 | |
| }
 | |
| 
 | |
| // clearBottom tests if clearing the end of the screen would satisfy part of
 | |
| // the screen update. Scan backwards through lines in the screen checking if
 | |
| // each is blank and one or more are changed.
 | |
| // It returns the top line.
 | |
| func (s *Screen) clearBottom(total int) (top int) {
 | |
| 	if total <= 0 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	top = total
 | |
| 	last := s.newbuf.Width()
 | |
| 	blank := s.clearBlank()
 | |
| 	canClearWithBlank := blank == nil || blank.Clear()
 | |
| 
 | |
| 	if canClearWithBlank {
 | |
| 		var row int
 | |
| 		for row = total - 1; row >= 0; row-- {
 | |
| 			oldLine := s.curbuf.Line(row)
 | |
| 			newLine := s.newbuf.Line(row)
 | |
| 
 | |
| 			var col int
 | |
| 			ok := true
 | |
| 			for col = 0; ok && col < last; col++ {
 | |
| 				ok = cellEqual(newLine.At(col), blank)
 | |
| 			}
 | |
| 			if !ok {
 | |
| 				break
 | |
| 			}
 | |
| 
 | |
| 			for col = 0; ok && col < last; col++ {
 | |
| 				ok = len(oldLine) == last && cellEqual(oldLine.At(col), blank)
 | |
| 			}
 | |
| 			if !ok {
 | |
| 				top = row
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if top < total {
 | |
| 			s.move(0, top-1) // top is 1-based
 | |
| 			s.clearToBottom(blank)
 | |
| 			if s.oldhash != nil && s.newhash != nil &&
 | |
| 				row < len(s.oldhash) && row < len(s.newhash) {
 | |
| 				for row := top; row < s.newbuf.Height(); row++ {
 | |
| 					s.oldhash[row] = s.newhash[row]
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // clearScreen clears the screen and put cursor at home.
 | |
| func (s *Screen) clearScreen(blank *Cell) {
 | |
| 	s.updatePen(blank)
 | |
| 	s.buf.WriteString(ansi.CursorHomePosition) //nolint:errcheck
 | |
| 	s.buf.WriteString(ansi.EraseEntireScreen)  //nolint:errcheck
 | |
| 	s.cur.X, s.cur.Y = 0, 0
 | |
| 	s.curbuf.Fill(blank)
 | |
| }
 | |
| 
 | |
| // clearBelow clears everything below and including the row.
 | |
| func (s *Screen) clearBelow(blank *Cell, row int) {
 | |
| 	s.move(0, row)
 | |
| 	s.clearToBottom(blank)
 | |
| }
 | |
| 
 | |
| // clearUpdate forces a screen redraw.
 | |
| func (s *Screen) clearUpdate() {
 | |
| 	blank := s.clearBlank()
 | |
| 	var nonEmpty int
 | |
| 	if s.opts.AltScreen {
 | |
| 		// XXX: We're using the maximum height of the two buffers to ensure
 | |
| 		// we write newly added lines to the screen in [Screen.transformLine].
 | |
| 		nonEmpty = max(s.curbuf.Height(), s.newbuf.Height())
 | |
| 		s.clearScreen(blank)
 | |
| 	} else {
 | |
| 		nonEmpty = s.newbuf.Height()
 | |
| 		s.clearBelow(blank, 0)
 | |
| 	}
 | |
| 	nonEmpty = s.clearBottom(nonEmpty)
 | |
| 	for i := 0; i < nonEmpty; i++ {
 | |
| 		s.transformLine(i)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Flush flushes the buffer to the screen.
 | |
| func (s *Screen) Flush() (err error) {
 | |
| 	s.mu.Lock()
 | |
| 	defer s.mu.Unlock()
 | |
| 	return s.flush()
 | |
| }
 | |
| 
 | |
| func (s *Screen) flush() (err error) {
 | |
| 	// Write the buffer
 | |
| 	if s.buf.Len() > 0 {
 | |
| 		_, err = s.w.Write(s.buf.Bytes()) //nolint:errcheck
 | |
| 		if err == nil {
 | |
| 			s.buf.Reset()
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // Render renders changes of the screen to the internal buffer. Call
 | |
| // [Screen.Flush] to flush pending changes to the screen.
 | |
| func (s *Screen) Render() {
 | |
| 	s.mu.Lock()
 | |
| 	s.render()
 | |
| 	s.mu.Unlock()
 | |
| }
 | |
| 
 | |
| func (s *Screen) render() {
 | |
| 	// Do we need to render anything?
 | |
| 	if s.opts.AltScreen == s.altScreenMode &&
 | |
| 		!s.opts.ShowCursor == s.cursorHidden &&
 | |
| 		!s.clear &&
 | |
| 		len(s.touch) == 0 &&
 | |
| 		len(s.queueAbove) == 0 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// TODO: Investigate whether this is necessary. Theoretically, terminals
 | |
| 	// can add/remove tab stops and we should be able to handle that. We could
 | |
| 	// use [ansi.DECTABSR] to read the tab stops, but that's not implemented in
 | |
| 	// most terminals :/
 | |
| 	// // Are we using hard tabs? If so, ensure tabs are using the
 | |
| 	// // default interval using [ansi.DECST8C].
 | |
| 	// if s.opts.HardTabs && !s.initTabs {
 | |
| 	// 	s.buf.WriteString(ansi.SetTabEvery8Columns)
 | |
| 	// 	s.initTabs = true
 | |
| 	// }
 | |
| 
 | |
| 	// Do we need alt-screen mode?
 | |
| 	if s.opts.AltScreen != s.altScreenMode {
 | |
| 		if s.opts.AltScreen {
 | |
| 			s.buf.WriteString(ansi.SetAltScreenSaveCursorMode)
 | |
| 		} else {
 | |
| 			s.buf.WriteString(ansi.ResetAltScreenSaveCursorMode)
 | |
| 		}
 | |
| 		s.altScreenMode = s.opts.AltScreen
 | |
| 	}
 | |
| 
 | |
| 	// Do we need text cursor mode?
 | |
| 	if !s.opts.ShowCursor != s.cursorHidden {
 | |
| 		s.cursorHidden = !s.opts.ShowCursor
 | |
| 		if s.cursorHidden {
 | |
| 			s.buf.WriteString(ansi.HideCursor)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Do we have queued strings to write above the screen?
 | |
| 	if len(s.queueAbove) > 0 {
 | |
| 		// TODO: Use scrolling region if available.
 | |
| 		// TODO: Use [Screen.Write] [io.Writer] interface.
 | |
| 
 | |
| 		// We need to scroll the screen up by the number of lines in the queue.
 | |
| 		// We can't use [ansi.SU] because we want the cursor to move down until
 | |
| 		// it reaches the bottom of the screen.
 | |
| 		s.move(0, s.newbuf.Height()-1)
 | |
| 		s.buf.WriteString(strings.Repeat("\n", len(s.queueAbove)))
 | |
| 		s.cur.Y += len(s.queueAbove)
 | |
| 		// XXX: Now go to the top of the screen, insert new lines, and write
 | |
| 		// the queued strings. It is important to use [Screen.moveCursor]
 | |
| 		// instead of [Screen.move] because we don't want to perform any checks
 | |
| 		// on the cursor position.
 | |
| 		s.moveCursor(0, 0, false)
 | |
| 		s.buf.WriteString(ansi.InsertLine(len(s.queueAbove)))
 | |
| 		for _, line := range s.queueAbove {
 | |
| 			s.buf.WriteString(line + "\r\n")
 | |
| 		}
 | |
| 
 | |
| 		// Clear the queue
 | |
| 		s.queueAbove = s.queueAbove[:0]
 | |
| 	}
 | |
| 
 | |
| 	var nonEmpty int
 | |
| 
 | |
| 	// XXX: In inline mode, after a screen resize, we need to clear the extra
 | |
| 	// lines at the bottom of the screen. This is because in inline mode, we
 | |
| 	// don't use the full screen height and the current buffer size might be
 | |
| 	// larger than the new buffer size.
 | |
| 	partialClear := !s.opts.AltScreen && s.cur.X != -1 && s.cur.Y != -1 &&
 | |
| 		s.curbuf.Width() == s.newbuf.Width() &&
 | |
| 		s.curbuf.Height() > 0 &&
 | |
| 		s.curbuf.Height() > s.newbuf.Height()
 | |
| 
 | |
| 	if !s.clear && partialClear {
 | |
| 		s.clearBelow(nil, s.newbuf.Height()-1)
 | |
| 	}
 | |
| 
 | |
| 	if s.clear {
 | |
| 		s.clearUpdate()
 | |
| 		s.clear = false
 | |
| 	} else if len(s.touch) > 0 {
 | |
| 		if s.opts.AltScreen {
 | |
| 			// Optimize scrolling for the alternate screen buffer.
 | |
| 			// TODO: Should we optimize for inline mode as well? If so, we need
 | |
| 			// to know the actual cursor position to use [ansi.DECSTBM].
 | |
| 			s.scrollOptimize()
 | |
| 		}
 | |
| 
 | |
| 		var changedLines int
 | |
| 		var i int
 | |
| 
 | |
| 		if s.opts.AltScreen {
 | |
| 			nonEmpty = min(s.curbuf.Height(), s.newbuf.Height())
 | |
| 		} else {
 | |
| 			nonEmpty = s.newbuf.Height()
 | |
| 		}
 | |
| 
 | |
| 		nonEmpty = s.clearBottom(nonEmpty)
 | |
| 		for i = 0; i < nonEmpty; i++ {
 | |
| 			_, ok := s.touch[i]
 | |
| 			if ok {
 | |
| 				s.transformLine(i)
 | |
| 				changedLines++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Sync windows and screen
 | |
| 	s.touch = make(map[int]lineData, s.newbuf.Height())
 | |
| 
 | |
| 	if s.curbuf.Width() != s.newbuf.Width() || s.curbuf.Height() != s.newbuf.Height() {
 | |
| 		// Resize the old buffer to match the new buffer.
 | |
| 		_, oldh := s.curbuf.Width(), s.curbuf.Height()
 | |
| 		s.curbuf.Resize(s.newbuf.Width(), s.newbuf.Height())
 | |
| 		// Sync new lines to old lines
 | |
| 		for i := oldh - 1; i < s.newbuf.Height(); i++ {
 | |
| 			copy(s.curbuf.Line(i), s.newbuf.Line(i))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	s.updatePen(nil) // nil indicates a blank cell with no styles
 | |
| 
 | |
| 	// Do we have enough changes to justify toggling the cursor?
 | |
| 	if s.buf.Len() > 1 && s.opts.ShowCursor && !s.cursorHidden && s.queuedText {
 | |
| 		nb := new(bytes.Buffer)
 | |
| 		nb.Grow(s.buf.Len() + len(ansi.HideCursor) + len(ansi.ShowCursor))
 | |
| 		nb.WriteString(ansi.HideCursor)
 | |
| 		nb.Write(s.buf.Bytes())
 | |
| 		nb.WriteString(ansi.ShowCursor)
 | |
| 		*s.buf = *nb
 | |
| 	}
 | |
| 
 | |
| 	s.queuedText = false
 | |
| }
 | |
| 
 | |
| // Close writes the final screen update and resets the screen.
 | |
| func (s *Screen) Close() (err error) {
 | |
| 	s.mu.Lock()
 | |
| 	defer s.mu.Unlock()
 | |
| 
 | |
| 	s.render()
 | |
| 	s.updatePen(nil)
 | |
| 	// Go to the bottom of the screen
 | |
| 	s.move(0, s.newbuf.Height()-1)
 | |
| 
 | |
| 	if s.altScreenMode {
 | |
| 		s.buf.WriteString(ansi.ResetAltScreenSaveCursorMode)
 | |
| 		s.altScreenMode = false
 | |
| 	}
 | |
| 
 | |
| 	if s.cursorHidden {
 | |
| 		s.buf.WriteString(ansi.ShowCursor)
 | |
| 		s.cursorHidden = false
 | |
| 	}
 | |
| 
 | |
| 	// Write the buffer
 | |
| 	err = s.flush()
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	s.reset()
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // reset resets the screen to its initial state.
 | |
| func (s *Screen) reset() {
 | |
| 	s.scrollHeight = 0
 | |
| 	s.cursorHidden = false
 | |
| 	s.altScreenMode = false
 | |
| 	s.touch = make(map[int]lineData, s.newbuf.Height())
 | |
| 	if s.curbuf != nil {
 | |
| 		s.curbuf.Clear()
 | |
| 	}
 | |
| 	if s.newbuf != nil {
 | |
| 		s.newbuf.Clear()
 | |
| 	}
 | |
| 	s.buf.Reset()
 | |
| 	s.tabs = DefaultTabStops(s.newbuf.Width())
 | |
| 	s.oldhash, s.newhash = nil, nil
 | |
| 
 | |
| 	// We always disable HardTabs when termtype is "linux".
 | |
| 	if strings.HasPrefix(s.opts.Term, "linux") {
 | |
| 		s.opts.HardTabs = false
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Resize resizes the screen.
 | |
| func (s *Screen) Resize(width, height int) bool {
 | |
| 	oldw := s.newbuf.Width()
 | |
| 	oldh := s.newbuf.Height()
 | |
| 
 | |
| 	if s.opts.AltScreen || width != oldw {
 | |
| 		// We only clear the whole screen if the width changes. Adding/removing
 | |
| 		// rows is handled by the [Screen.render] and [Screen.transformLine]
 | |
| 		// methods.
 | |
| 		s.clear = true
 | |
| 	}
 | |
| 
 | |
| 	// Clear new columns and lines
 | |
| 	if width > oldh {
 | |
| 		s.ClearRect(Rect(max(oldw-1, 0), 0, width-oldw, height))
 | |
| 	} else if width < oldw {
 | |
| 		s.ClearRect(Rect(max(width-1, 0), 0, oldw-width, height))
 | |
| 	}
 | |
| 
 | |
| 	if height > oldh {
 | |
| 		s.ClearRect(Rect(0, max(oldh-1, 0), width, height-oldh))
 | |
| 	} else if height < oldh {
 | |
| 		s.ClearRect(Rect(0, max(height-1, 0), width, oldh-height))
 | |
| 	}
 | |
| 
 | |
| 	s.mu.Lock()
 | |
| 	s.newbuf.Resize(width, height)
 | |
| 	s.tabs.Resize(width)
 | |
| 	s.oldhash, s.newhash = nil, nil
 | |
| 	s.scrollHeight = 0 // reset scroll lines
 | |
| 	s.mu.Unlock()
 | |
| 
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // MoveTo moves the cursor to the given position.
 | |
| func (s *Screen) MoveTo(x, y int) {
 | |
| 	s.mu.Lock()
 | |
| 	s.move(x, y)
 | |
| 	s.mu.Unlock()
 | |
| }
 | |
| 
 | |
| // InsertAbove inserts string above the screen. The inserted string is not
 | |
| // managed by the screen. This does nothing when alternate screen mode is
 | |
| // enabled.
 | |
| func (s *Screen) InsertAbove(str string) {
 | |
| 	if s.opts.AltScreen {
 | |
| 		return
 | |
| 	}
 | |
| 	s.mu.Lock()
 | |
| 	for _, line := range strings.Split(str, "\n") {
 | |
| 		s.queueAbove = append(s.queueAbove, s.method.Truncate(line, s.Width(), ""))
 | |
| 	}
 | |
| 	s.mu.Unlock()
 | |
| }
 |