okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
8786532
upstream
1package uv 2 3import ( 4"bytes" 5"errors" 6"hash/maphash" 7"io" 8"strings" 9"unicode/utf8" 10 11"github.com/charmbracelet/colorprofile" 12"github.com/charmbracelet/x/ansi" 13) 14 15// ErrInvalidDimensions is returned when the dimensions of a window are invalid 16// for the operation. 17var ErrInvalidDimensions = errors .New ("invalid dimensions" ) 18 19// capabilities represents a mask of supported ANSI escape sequences. 20type capabilities uint 21 22const ( 23// Vertical Position Absolute [ansi.VPA]. 24capVPA capabilities = 1 << iota 25// Horizontal Position Absolute [ansi.HPA]. 26capHPA 27// Cursor Horizontal Absolute [ansi.CHA]. 28capCHA 29// Cursor Horizontal Tab [ansi.CHT]. 30capCHT 31// Cursor Backward Tab [ansi.CBT]. 32capCBT 33// Repeat Previous Character [ansi.REP]. 34capREP 35// Erase Character [ansi.ECH]. 36capECH 37// Insert Character [ansi.ICH]. 38capICH 39// Scroll Down [ansi.SD]. 40capSD 41// Scroll Up [ansi.SU]. 42capSU 43// These capabilities depend on the tty termios settings and are not 44// enabled by default. 45// Tabulation [ansi.HT]. 46capHT 47// Backspace [ansi.BS]. 48capBS 49 50noCaps capabilities = 0 51allCaps = capVPA | capHPA | capCHA | capCHT | capCBT | capREP | capECH | capICH | capSD | capSU 52) 53 54// Set sets the given capabilities. 55func (v * capabilities )Set (c capabilities ) { 56* v |= c 57} 58 59// Reset resets the given capabilities. 60func (v * capabilities )Reset (c capabilities ) { 61* v &^=c 62} 63 64// Contains returns whether the capabilities contains the given capability. 65func (v capabilities )Contains (c capabilities )bool { 66return v & c == c 67} 68 69// cursor represents a terminal cursor. 70type cursor struct { 71Cell 72Position 73} 74 75// LineData represents the metadata for a line. 76type LineData struct { 77// First and last changed cell indices. 78FirstCell ,LastCell int 79// Old index used for scrolling 80oldIndex int //nolint:unused 81} 82 83// tFlag is a bitmask of terminal flags. 84type tFlag uint 85 86// Terminal writer flags. 87const ( 88tRelativeCursor tFlag = 1 << iota 89tFullscreen 90tMapNewline 91tScrollOptim 92// tGraphemeWidth indicates the terminal measures cell width using Unicode 93// grapheme clustering (DEC mode 2027 / Unicode core). 94tGraphemeWidth 95) 96 97// Set sets the given flags. 98func (v * tFlag )Set (c tFlag ) { 99* v |= c 100} 101 102// Reset resets the given flags. 103func (v * tFlag )Reset (c tFlag ) { 104* v &^=c 105} 106 107// Contains returns whether the terminal flags contains the given flags. 108func (v tFlag )Contains (c tFlag )bool { 109return v & c == c 110} 111 112// TerminalRenderer is a terminal screen render and lazy writer that buffers 113// the output until it is flushed. It handles rendering a screen from a 114// [Buffer] to the terminal with the minimal necessary escape sequences to 115// transition the terminal to the new buffer state. It uses various escape 116// sequence optimizations to reduce the number of bytes sent to the terminal. 117// It's designed to be lazy and only flush the output when necessary by calling 118// the [TerminalRenderer.Flush] method. 119// 120// The renderer handles the terminal's alternate screen and cursor visibility 121// via the [TerminalRenderer.EnterAltScreen], [TerminalRenderer.ExitAltScreen], 122// [TerminalRenderer.ShowCursor] and [TerminalRenderer.HideCursor] methods. 123// Using these methods will queue the appropriate escape sequences to enter or 124// exit the alternate screen and show or hide the cursor respectively to be 125// flushed to the terminal. 126// 127// Use the [io.Writer] and [io.StringWriter] interfaces to queue custom 128// commands the renderer. 129// 130// The renderer is not thread-safe, the caller must protect the renderer when 131// using it from multiple goroutines. 132type TerminalRenderer struct { 133w io.Writer 134buf * bytes.Buffer // buffer for writing to the screen 135curbuf * RenderBuffer // the current buffer 136tabs * TabStops 137hasher maphash.Hash 138oldhash ,newhash []uint64 // the old and new hash values for each line 139hashtab []hashmap // the hashmap table 140oldnum []int // old indices from previous hash 141cur ,saved cursor // the current and saved cursors 142flags tFlag // terminal writer flags. 143method ansi.Method // the width method used to measure cell width 144term string // the terminal type 145clear bool // whether to force clear the screen 146caps capabilities // terminal control sequence capabilities 147atPhantom bool // whether the cursor is out of bounds and at a phantom cell 148noWrapLine bool // whether autowrap is off for the line currently being repainted 149driftRows []bool // rows holding a cell the terminal may measure differently 150termW ,termH int // the size [TerminalRenderer.Resize] was last told 151termResized bool // whether the terminal changed size since the last render 152damaged []bool // rows this render disturbed itself, see [TerminalRenderer.damage] 153logger Logger // The logger used for debugging. 154 155// profile is the color profile to use when downsampling colors. This is 156// used to determine the appropriate color the terminal can display. 157profile colorprofile.Profile 158} 159 160// NewTerminalRenderer returns a new [TerminalRenderer] that uses the given 161// writer, terminal type, and initializes the width of the terminal. The 162// terminal type is used to determine the capabilities of the terminal and 163// should be set to the value of the TERM environment variable. 164// 165// The renderer will try to detect the color profile from the output and the 166// given environment variables. Use [TerminalRenderer.SetColorProfile] method 167// to set a specific color profile for downsampling. 168// 169// See [TerminalRenderer] for more information on how to use the renderer. 170func NewTerminalRenderer (w io.Writer ,env []string ) (s * TerminalRenderer ) { 171s = new (TerminalRenderer ) 172s .w = w 173s .profile = colorprofile .Detect (w ,env ) 174s .buf = new (bytes.Buffer ) 175s .term = Environ (env ).Getenv ("TERM" ) 176s .caps = xtermCaps (s .term ) 177s .cur = cursor {Cell :EmptyCell ,Position :Pos (- 1 ,- 1 )}// start at -1 to force a move 178s .saved = s .cur 179s .oldhash ,s .newhash = nil ,nil 180return 181} 182 183// SetLogger sets the logger to use for debugging. If nil, no logging will be 184// performed. 185func (s * TerminalRenderer )SetLogger (logger Logger ) { 186s .logger = logger 187} 188 189// SetColorProfile sets the color profile to use for downsampling colors. This 190// is used to determine the appropriate color the terminal can display. 191func (s * TerminalRenderer )SetColorProfile (profile colorprofile.Profile ) { 192s .profile = profile 193} 194 195// SetScrollOptim sets whether to use hard scroll optimizations. 196func (s * TerminalRenderer )SetScrollOptim (v bool ) { 197if v { 198s .flags .Set (tScrollOptim ) 199 }else { 200s .flags .Reset (tScrollOptim ) 201 } 202} 203 204// SetMapNewline sets whether the terminal is currently mapping newlines to 205// CRLF or carriage return and line feed. This is used to correctly determine 206// how to move the cursor when writing to the screen. 207func (s * TerminalRenderer )SetMapNewline (v bool ) { 208if v { 209s .flags .Set (tMapNewline ) 210 }else { 211s .flags .Reset (tMapNewline ) 212 } 213} 214 215// SetWidthMethod sets the width method the renderer uses to measure the 216// display width of strings. This should match the width method configured on 217// the screen so that the renderer doesn't disagree with the cell widths it is 218// asked to draw. 219func (s * TerminalRenderer )SetWidthMethod (method ansi.Method ) { 220s .method = method 221} 222 223// SetGraphemeWidth sets whether the terminal measures cell width using Unicode 224// grapheme clustering (DEC mode 2027 / Unicode core). When enabled, the 225// renderer measures string width using [ansi.GraphemeWidth]; otherwise it 226// falls back to [ansi.WcWidth]. 227func (s * TerminalRenderer )SetGraphemeWidth (v bool ) { 228if v { 229s .flags .Set (tGraphemeWidth ) 230s .method = ansi .GraphemeWidth 231 }else { 232s .flags .Reset (tGraphemeWidth ) 233s .method = ansi .WcWidth 234 } 235} 236 237// SetBackspace sets whether to use backspace as a movement optimization. 238func (s * TerminalRenderer )SetBackspace (v bool ) { 239if v { 240s .caps .Set (capBS ) 241 }else { 242s .caps .Reset (capBS ) 243 } 244} 245 246// SetTabStops sets the tab stops for the terminal and enables hard tabs 247// movement optimizations. Use -1 to disable hard tabs. This option is ignored 248// when the terminal type is "linux" as it does not support hard tabs. 249func (s * TerminalRenderer )SetTabStops (width int ) { 250if width < 0 || strings .HasPrefix (s .term ,"linux" ) { 251// Linux terminal does not support hard tabs. 252s .caps .Reset (capHT ) 253 }else { 254s .caps .Set (capHT ) 255s .tabs = DefaultTabStops (width ) 256 } 257} 258 259// SetFullscreen sets whether whole screen is being used. This is usually 260// paired with the alternate screen mode, and used to control cursor movements 261// and optimizations when the terminal is occupying the whole screen. 262func (s * TerminalRenderer )SetFullscreen (v bool ) { 263if v { 264s .flags .Set (tFullscreen ) 265 }else { 266s .flags .Reset (tFullscreen ) 267 } 268} 269 270// Fullscreen returns whether whole screen is being used. This is usually 271// paired with the alternate screen mode, and used to control cursor movements 272// and optimizations when the terminal is occupying the whole screen. 273func (s * TerminalRenderer )Fullscreen ()bool { 274return s .flags .Contains (tFullscreen ) 275} 276 277// SetRelativeCursor sets whether to use relative cursor movements. 278func (s * TerminalRenderer )SetRelativeCursor (v bool ) { 279if v { 280s .flags .Set (tRelativeCursor ) 281 }else { 282s .flags .Reset (tRelativeCursor ) 283 } 284} 285 286// SaveCursor saves the current cursor position and styles. This can be used 287// when a program enters the alternate screen buffer mode using DECSET 1049 288// [ansi.ModeAltScreenSaveCursor] and wants to restore the cursor position and 289// styles later. 290func (s * TerminalRenderer )SaveCursor () { 291s .saved = s .cur 292} 293 294// RestoreCursor restores the saved cursor position and styles. This can be 295// used when a program exits the alternate screen buffer mode using DECRST 1049 296// [ansi.ModeAltScreenSaveCursor] and wants to restore the cursor position and 297// styles saved earlier. 298func (s * TerminalRenderer )RestoreCursor () { 299s .cur = s .saved 300} 301 302// EnterAltScreen is a helper that queues the [ansi.ModeAltScreenSaveCursor] 303// escape sequence to enter the alternate screen buffer and save cursor mode. 304// 305// It saves the current cursor properties, enables 306// [TerminalRenderer.SetFullscreen] flag, and disables 307// [TerminalRenderer.SetRelativeCursor] flag. On the next call to 308// [TerminalRenderer.Render], it will move the cursor home and clear the screen 309// to prepare for rendering the new buffer. 310// 311// Note: you might want to reapply the cursor visibility state after calling 312// this method, as some terminals reset the cursor visibility when switching to 313// the alternate screen. 314func (s * TerminalRenderer )EnterAltScreen () { 315s .SaveCursor () 316s .buf .WriteString (ansi .SetModeAltScreenSaveCursor ) 317s .SetFullscreen (true ) 318s .SetRelativeCursor (false ) 319s .Erase () 320} 321 322// ExitAltScreen is a helper that queues the [ansi.ModeAltScreenSaveCursor] 323// escape sequence to exit the alternate screen buffer and restore cursor mode. 324// 325// It restores the saved cursor properties, disables 326// [TerminalRenderer.SetFullscreen] flag, and enables 327// [TerminalRenderer.SetRelativeCursor] flag. On the next call to 328// [TerminalRenderer.Render], it will move the cursor to the first line and 329// clear everything below the cursor to prepare for rendering the new buffer. 330// 331// Note: you might want to reapply the cursor visibility state after calling 332// this method, as some terminals reset the cursor visibility when switching to 333// the alternate screen. 334func (s * TerminalRenderer )ExitAltScreen () { 335s .Erase () 336s .SetRelativeCursor (true ) 337s .SetFullscreen (false ) 338s .buf .WriteString (ansi .ResetModeAltScreenSaveCursor ) 339s .RestoreCursor () 340} 341 342// PrependString adds the lines of the given string to the top of the terminal 343// screen. The lines prepended are not managed by the renderer and will not be 344// cleared or updated by the renderer. 345// 346// Using this when the terminal is using the alternate screen or when occupying 347// the whole screen may not produce any visible effects. This is because 348// once the terminal writes the prepended lines, they will get overwritten 349// by the next frame. 350func (s * TerminalRenderer )PrependString (newbuf * RenderBuffer ,str string ) { 351if len (str )== 0 { 352return 353 } 354 355// TODO: Use scrolling region if available. 356// TODO: Use [Screen.Write] [io.Writer] interface. 357 358// We need to scroll the screen up by the number of lines in the queue. 359// We can't use [ansi.SU] because we want the cursor to move down until 360// it reaches the bottom of the screen. 361w ,h := newbuf .Width (),newbuf .Height () 362s .move (newbuf ,0 ,h - 1 ) 363lines := strings .Split (str ,"\n" ) 364offset := 0 365for _ ,line := range lines { 366lineWidth := s .method .StringWidth (line ) 367if w > 0 && lineWidth > w { 368offset += (lineWidth / w ) 369 } 370if lineWidth == 0 || lineWidth % w != 0 { 371offset ++ 372 } 373 } 374 375s .buf .WriteString (strings .Repeat ("\n" ,offset )) 376s .cur .Y += offset 377// XXX: Now go to the top of the screen, insert new lines, and write 378// the queued strings. It is important to use [Screen.moveCursor] 379// instead of [Screen.move] because we don't want to perform any checks 380// on the cursor position. 381s .moveCursor (newbuf ,0 ,0 ,false ) 382s .buf .WriteString (ansi .InsertLine (offset )) 383for _ ,line := range lines { 384s .buf .WriteString (line ) 385s .buf .WriteString ("\r\n" ) 386 } 387} 388 389// moveCursor moves the cursor to the specified position. 390// 391// It is safe to call this function with a nil [Buffer], in that case, it won't 392// be using any optimizations that depend on the buffer. 393func (s * TerminalRenderer )moveCursor (newbuf * RenderBuffer ,x ,y int ,overwrite bool ) { 394if ! s .flags .Contains (tFullscreen )&& s .flags .Contains (tRelativeCursor )&& 395s .cur .X == - 1 && s .cur .Y == - 1 { 396// First cursor movement in inline mode, move the cursor to the first 397// column before moving to the target position. 398_ = s .buf .WriteByte ('\r' ) 399s .cur .X ,s .cur .Y = 0 ,0 400 } 401seq := moveCursor (s ,newbuf ,x ,y ,overwrite ) 402_ ,_ = s .buf .WriteString (seq ) 403s .cur .X ,s .cur .Y = x ,y 404} 405 406// move moves the cursor to the specified position in the buffer. 407// 408// It is safe to call this function with a nil [Buffer], in that case, it won't 409// be using any optimizations that depend on the buffer. 410func (s * TerminalRenderer )move (newbuf * RenderBuffer ,x ,y int ) { 411// XXX: Make sure we use the max height and width of the buffer in case 412// we're in the middle of a resize operation. 413var width ,height int 414if s .curbuf != nil { 415width ,height = s .curbuf .Width (),s .curbuf .Height () 416 } 417if newbuf != nil { 418width = max (newbuf .Width (),width ) 419height = max (newbuf .Height (),height ) 420 } 421 422if width > 0 && x >= width { 423// Handle autowrap 424y += (x / width ) 425x %= width 426 } 427 428// XXX: Disable styles if there's any 429// Some move operations such as [ansi.LF] can apply styles to the new 430// cursor position, thus, we need to reset the styles before moving the 431// cursor. 432blank := s .clearBlank () 433resetPen := y != s .cur .Y && ! blank .Equal (& EmptyCell ) 434if resetPen { 435s .updatePen (nil ) 436 } 437 438// Reset wrap around (phantom cursor) state 439if s .atPhantom { 440s .cur .X = 0 441_ = s .buf .WriteByte ('\r' ) 442s .atPhantom = false // reset phantom cell state 443 } 444 445// TODO: Investigate if we need to handle this case and/or if we need the 446// following code. 447// 448// if width > 0 && s.cur.X >= width { 449// l := (s.cur.X + 1) / width 450// 451// s.cur.Y += l 452// if height > 0 && s.cur.Y >= height { 453// l -= s.cur.Y - height - 1 454// } 455// 456// if l > 0 { 457// s.cur.X = 0 458// s.buf.WriteString("\r" + strings.Repeat("\n", l)) //nolint:errcheck 459// } 460// } 461 462if height > 0 { 463// Only clamp the remembered cursor row in fullscreen mode, where the 464// buffer covers the whole screen. In relative mode the terminal 465// screen extends beyond the frame buffer: after a shrink the 466// real cursor can legitimately sit below the new frame's last row, 467// and clamping the model here would skip the cursor-up move and leave 468// the old frame's top lines on screen. 469if ! s .flags .Contains (tRelativeCursor )&& s .cur .Y > height - 1 { 470s .cur .Y = height - 1 471 } 472if y > height - 1 { 473y = height - 1 474 } 475 } 476 477if x == s .cur .X && y == s .cur .Y { 478// We give up later because we need to run checks for the phantom cell 479// and others before we can determine if we can give up. 480return 481 } 482 483// We set the new cursor in tscreen.moveCursor]. 484s .moveCursor (newbuf ,x ,y ,true )// Overwrite cells if possible 485} 486 487// cellEqual returns whether the two cells are equal. A nil cell is considered 488// a [EmptyCell]. 489func cellEqual (a ,b * Cell )bool { 490if a == b { 491return true 492 } 493// TODO: This is needed to handle empty lines correctly when scroll 494// optimizations are enabled. Instead, a nil check should be equivalent to 495// checking for an [EmptyCell], should it? 496// Investigate why when we assign the pointers to &[EmptyCell], this causes 497// scroll optimization related artifacts where excess lines are left behind 498// in empty lines after scrolling. 499if a == nil || b == nil { 500return false 501 } 502return a .Equal (b ) 503} 504 505// putCell draws a cell at the current cursor position. 506// 507// The lower right corner is written with autowrap off, since a pending wrap 508// there scrolls the screen as soon as anything else prints. A multi-codepoint 509// cluster is the exception: with autowrap off the terminal never advances past 510// the margin and reads the combining codepoints as belonging to the cell to the 511// left, so the cluster keeps autowrap on and the wrap is cancelled after. 512func (s * TerminalRenderer )putCell (newbuf * RenderBuffer ,cell * Cell ) { 513width ,height := newbuf .Width (),newbuf .Height () 514atMargin := s .cur .X == width - 1 && ! s .noWrapLine 515lowerRight := atMargin && s .flags .Contains (tFullscreen )&& s .cur .Y == height - 1 516 517// Only the corner cell can care, so the cluster is measured only there. 518if lowerRight && cell != nil && utf8 .RuneCountInString (cell .Content )> 1 { 519s .putAttrCell (newbuf ,cell ) 520// The cluster kept autowrap on so it landed whole, which leaves the 521// cursor pending wrap. On the last row there is no next row to wrap 522// onto, so the next print scrolls the screen instead. Cancel it here 523// rather than leave the frame ending in a state the model denies. 524if s .atPhantom { 525_ = s .buf .WriteByte ('\r' ) 526s .cur .X = 0 527s .atPhantom = false 528 } 529return 530 } 531 532if lowerRight { 533s .putCellLR (newbuf ,cell ) 534 }else { 535s .putAttrCell (newbuf ,cell ) 536 } 537} 538 539// wrapCursor wraps the cursor to the next line. 540func (s * TerminalRenderer )wrapCursor () { 541const autoRightMargin = true 542if autoRightMargin { 543// Assume we have auto wrap mode enabled. 544s .cur .X = 0 545s .cur .Y ++ 546 }else { 547s .cur .X -- 548 } 549} 550 551func (s * TerminalRenderer )putAttrCell (newbuf * RenderBuffer ,cell * Cell ) { 552if cell != nil && cell .Width == 0 { 553// XXX: Zero width cells are special and should not be written to the 554// screen no matter what other attributes they have. 555// Zero width cells are used for wide characters that are split into 556// multiple cells. 557return 558 } 559 560// We're at pending wrap state (phantom cell), incoming cell should 561// wrap. 562if s .atPhantom { 563s .wrapCursor () 564s .atPhantom = false 565 } 566 567s .updatePen (cell ) 568cellWidth := 1 569if cell == nil { 570_ = s .buf .WriteByte (' ' ) 571 }else { 572_ ,_ = s .buf .WriteString (cell .Content ) 573cellWidth = cell .Width 574 } 575 576s .cur .X += cellWidth 577if s .cur .X >= newbuf .Width () { 578s .atPhantom = true 579 } 580} 581 582// putCellLR draws a cell at the lower right corner of the screen. 583func (s * TerminalRenderer )putCellLR (newbuf * RenderBuffer ,cell * Cell ) { 584// Optimize for the lower right corner cell. 585curX := s .cur .X 586if ! cell .isWidePlaceholder () { 587_ ,_ = s .buf .WriteString (ansi .ResetModeAutoWrap ) 588s .putAttrCell (newbuf ,cell ) 589// Writing to lower-right corner cell should not wrap. 590s .atPhantom = false 591s .cur .X = curX 592_ ,_ = s .buf .WriteString (ansi .SetModeAutoWrap ) 593 } 594} 595 596// updatePen updates the cursor pen styles. 597func (s * TerminalRenderer )updatePen (cell * Cell ) { 598if cell == nil { 599cell = & EmptyCell 600 } 601 602// Downsample pen when we don't have a [colorprofile.TrueColor], 603// otherwise, use the original style. 604newStyle := ConvertStyle (cell .Style ,s .profile ) 605newLink := ConvertLink (cell .Link ,s .profile ) 606oldStyle := ConvertStyle (s .cur .Style ,s .profile ) 607oldLink := ConvertLink (s .cur .Link ,s .profile ) 608 609if ! newStyle .Equal (& oldStyle ) { 610seq := newStyle .Diff (& oldStyle ) 611if newStyle .IsZero ()&& len (seq )> len (ansi .ResetStyle ) { 612seq = ansi .ResetStyle 613 } 614_ ,_ = s .buf .WriteString (seq ) 615s .cur .Style = cell .Style // Copy the original style 616 } 617if ! newLink .Equal (& oldLink ) { 618_ ,_ = s .buf .WriteString (ansi .SetHyperlink (newLink .URL ,newLink .Params )) 619s .cur .Link = cell .Link // Copy the original link 620 } 621} 622 623// canClearWith checks whether the given cell can be used by clearing commands 624// like [ansi.EL] to clear the screen. It tests if a cell is empty i.e. space 625// or blank and doesn't include any bad style attributes such as [AttrReverse]. 626func canClearWith (c * Cell )bool { 627if c == nil { 628return true 629 } 630if c .Width != 1 || len (c .Content )!= 1 || c .Content != " " { 631return false 632 } 633// NOTE: This assumes that the terminal supports bce terminfo capability 634// which all xterm-compatible terminals and terminals that use xterm* 635// terminal types do. 636return c .Style .Underline == UnderlineNone && 637c .Style .Attrs &^(AttrBold | AttrFaint | AttrItalic | AttrBlink | AttrRapidBlink )== 0 && 638c .Link .IsZero () 639} 640 641// emitRange emits a range of cells to the buffer. It it equivalent to calling 642// tscreen.putCell] for each cell in the range. This is optimized to use 643// [ansi.ECH] and [ansi.REP]. 644// Returns whether the cursor is at the end of interval or somewhere in the 645// middle. 646func (s * TerminalRenderer )emitRange (newbuf * RenderBuffer ,line Line ,n int ) (eoi bool ) { 647hasECH := s .caps .Contains (capECH ) 648hasREP := s .caps .Contains (capREP ) 649if hasECH || hasREP {//nolint:nestif 650for n > 0 { 651var count int 652for n > 1 && ! cellEqual (line .At (0 ),line .At (1 )) { 653s .putCell (newbuf ,line .At (0 )) 654line = line [1 :] 655n -- 656 } 657 658cell0 := line [0 ] 659if n == 1 { 660s .putCell (newbuf ,& cell0 ) 661return false 662 } 663 664count = 2 665for count < n && cellEqual (line .At (count ),& cell0 ) { 666count ++ 667 } 668 669ech := ansi .EraseCharacter (count ) 670cup := ansi .CursorPosition (s .cur .X + count ,s .cur .Y ) 671rep := ansi .RepeatPreviousCharacter (count ) 672if hasECH && count > len (ech )+ len (cup )&& canClearWith (& cell0 ) { 673s .updatePen (& cell0 ) 674_ ,_ = s .buf .WriteString (ech ) 675 676// If this is the last cell, we don't need to move the cursor. 677if count < n { 678s .move (newbuf ,s .cur .X + count ,s .cur .Y ) 679 }else { 680return true // cursor in the middle 681 } 682 }else if hasREP && count > len (rep )&& 683 (len (cell0 .Content )== 1 && cell0 .Content [0 ]>= ansi .US && cell0 .Content [0 ]< ansi .DEL ) { 684// We only support ASCII characters. Most terminals will handle 685// non-ASCII characters correctly, but some might not, ahem xterm. 686// 687// NOTE: [ansi.REP] only repeats the last rune and won't work 688// if the last cell contains multiple runes. 689 690wrapPossible := s .cur .X + count >= newbuf .Width () 691repCount := count 692if wrapPossible { 693repCount -- 694 } 695 696s .updatePen (& cell0 ) 697s .putCell (newbuf ,& cell0 ) 698repCount -- // cell0 is a single width cell ASCII character 699 700_ ,_ = s .buf .WriteString (ansi .RepeatPreviousCharacter (repCount )) 701s .cur .X += repCount 702if wrapPossible { 703s .putCell (newbuf ,& cell0 ) 704 } 705 }else { 706for i := 0 ;i < count ;i ++ { 707s .putCell (newbuf ,line .At (i )) 708 } 709 } 710 711line = line [clamp (count ,0 ,len (line )):] 712n -= count 713 } 714 715return false 716 } 717 718for i := 0 ;i < n ;i ++ { 719s .putCell (newbuf ,line .At (i )) 720 } 721 722return false 723} 724 725// putRange puts a range of cells from the old line to the new line. 726// Returns whether the cursor is at the end of interval or somewhere in the 727// middle. 728func (s * TerminalRenderer )putRange (newbuf * RenderBuffer ,oldLine ,newLine Line ,y ,start ,end int ) (eoi bool ) { 729inline := min (len (ansi .CursorPosition (start + 1 ,y + 1 )), 730min (len (ansi .HorizontalPositionAbsolute (start + 1 )), 731len (ansi .CursorForward (start + 1 )))) 732if (end - start + 1 )> inline {//nolint:nestif 733var j ,same int 734for j ,same = start ,0 ;j <= end ;j ++ { 735oldCell ,newCell := oldLine .At (j ),newLine .At (j ) 736if same == 0 && oldCell .isWidePlaceholder ()&& newCell .isWidePlaceholder () { 737continue 738 } 739if cellEqual (oldCell ,newCell ) { 740same ++ 741 }else { 742if same > end - start { 743s .emitRange (newbuf ,newLine [start :],j - same - start ) 744s .move (newbuf ,j ,y ) 745start = j 746 } 747same = 0 748 } 749 } 750 751i := s .emitRange (newbuf ,newLine [start :],j - same - start ) 752 753// Always return 1 for the next [tScreen.move] after a 754// [tScreen.putRange] if we found identical characters at end of 755// interval. 756if same == 0 { 757return i 758 } 759return true 760 } 761 762return s .emitRange (newbuf ,newLine [start :],end - start + 1 ) 763} 764 765// clearToEnd clears the screen from the current cursor position to the end of 766// line. 767func (s * TerminalRenderer )clearToEnd (newbuf * RenderBuffer ,blank * Cell ,force bool ) { 768if s .cur .Y >= 0 { 769curline := s .curbuf .Line (s .cur .Y ) 770// We use the newbuf width because the current buffer might be smaller 771// than the new buffer during a resize operation and we want to detect 772// that. 773for j := s .cur .X ;j < newbuf .Width ();j ++ { 774if j >= 0 { 775c := curline .At (j ) 776if ! cellEqual (c ,blank ) { 777curline .Set (j ,blank ) 778force = true 779 } 780 } 781 } 782 } 783 784if force { 785s .updatePen (blank ) 786count := newbuf .Width ()- s .cur .X 787if s .el0Cost ()<= count { 788_ ,_ = s .buf .WriteString (ansi .EraseLineRight ) 789 }else { 790for i := 0 ;i < count ;i ++ { 791s .putCell (newbuf ,blank ) 792 } 793 } 794 } 795} 796 797// clearBlank returns a blank cell based on the current cursor background color. 798func (s * TerminalRenderer )clearBlank ()* Cell { 799return & s .cur .Cell 800} 801 802// insertCells inserts the count cells pointed by the given line at the current 803// cursor position. 804func (s * TerminalRenderer )insertCells (newbuf * RenderBuffer ,line Line ,count int ) { 805supportsICH := s .caps .Contains (capICH ) 806if supportsICH { 807// Use [ansi.ICH] as an optimization. 808_ ,_ = s .buf .WriteString (ansi .InsertCharacter (count )) 809 }else { 810// Otherwise, use [ansi.IRM] mode. 811_ ,_ = s .buf .WriteString (ansi .SetModeInsertReplace ) 812 } 813 814for i := 0 ;count > 0 ;i ++ { 815s .putAttrCell (newbuf ,line .At (i )) 816count -- 817 } 818 819if ! supportsICH { 820_ ,_ = s .buf .WriteString (ansi .ResetModeInsertReplace ) 821 } 822} 823 824// el0Cost returns the cost of using [ansi.EL] 0 i.e. [ansi.EraseLineRight]. If 825// this terminal supports background color erase, it can be cheaper to use 826// [ansi.EL] 0 i.e. [ansi.EraseLineRight] to clear 827// trailing spaces. 828func (s * TerminalRenderer )el0Cost ()int { 829if s .caps != noCaps { 830return 0 831 } 832return len (ansi .EraseLineRight ) 833} 834 835// lineHasDrift reports whether the line contains a cell that a cell-level 836// diff cannot safely reposition across: a wide cell (width > 1), or a cell 837// whose width the terminal may measure differently than the model. Wide cells 838// occupy several columns, so a diff that lands on a continuation column splits 839// the character; and a width disagreement means the model cannot know which 840// column each glyph landed on. 841func lineHasDrift (m ansi.Method ,line Line )bool { 842for i := 0 ;i < len (line );i ++ { 843c := line .At (i ) 844if c == nil || c .Width == 0 || len (c .Content )== 0 { 845continue 846 } 847if c .Width > 1 { 848return true 849 } 850// One printable ASCII byte is one column under every width model, so 851// it can never be a cell the models disagree about. Worth saying out 852// loud because working the width out means segmenting the content, 853// and this runs for every cell of every line the renderer diffs. 854if len (c .Content )== 1 && c .Content [0 ]>= 0x20 && c .Content [0 ]< 0x7f { 855continue 856 } 857if m .StringWidth (c .Content )!= ansi .StringWidth (c .Content ) { 858return true 859 } 860 } 861return false 862} 863 864// paintLine brings row y of the screen in line with newbuf. A drift-prone row, 865// or any row when force is set, is repainted; every other row is diffed. 866func (s * TerminalRenderer )paintLine (newbuf * RenderBuffer ,y int ,force bool ) { 867drift := lineHasDrift (s .method ,s .curbuf .Line (y ))|| lineHasDrift (s .method ,newbuf .Line (y )) 868s .markDrift (y ,newbuf .Height (),drift ) 869switch { 870case drift && ! s .flags .Contains (tGraphemeWidth ): 871// Clip a cluster the terminal measures wider than the model instead of 872// letting it spill onto the next row, then put the cursor back. 873s .noWrapLine = true 874_ ,_ = s .buf .WriteString (ansi .ResetModeAutoWrap ) 875s .repaintLine (newbuf ,y ) 876s .noWrapLine = false 877_ ,_ = s .buf .WriteString (ansi .SetModeAutoWrap ) 878s .reanchorWideLine (newbuf ) 879case drift || force : 880s .repaintLine (newbuf ,y ) 881default : 882s .transformLine (newbuf ,y ) 883 } 884} 885 886// repaintLine erases row y from column 0 and writes newbuf's cells, so the 887// result does not depend on what the model believes is on screen. 888func (s * TerminalRenderer )repaintLine (newbuf * RenderBuffer ,y int ) { 889oldLine := s .curbuf .Line (y ) 890newLine := newbuf .Line (y ) 891 892s .move (newbuf ,0 ,y ) 893blank := s .clearBlank () 894s .updatePen (blank ) 895_ ,_ = s .buf .WriteString (ansi .EraseLineRight ) 896s .cur .X = 0 897 898// A cell whose width runs past the right margin cannot be shown, so stop 899// there. The buffer can hold one after a resize narrowed the line around 900// it, and painting it would put a clipped half-glyph on the screen. 901width := newbuf .Width () 902clipped := width 903for x := 0 ;x < width ;x ++ { 904if c := newLine .At (x );c != nil && ! c .isWidePlaceholder ()&& x + c .Width > width { 905clipped = x 906break 907 } 908 } 909 910// Write the content cells. The line was just erased, so trailing blanks 911// need no write, and writing them would risk wrapping past the right 912// margin if the terminal painted a wide cell wider than the model 913// measured. 914last := - 1 915for x := 0 ;x < clipped ;x ++ { 916if c := newLine .At (x );c != nil && ! c .isWidePlaceholder ()&& ! cellEqual (c ,blank ) { 917last = x 918 } 919 } 920for x := 0 ;x <= last ;x ++ { 921s .putCell (newbuf ,newLine .At (x )) 922 } 923 924// Adopt the new line as the model of what is on screen. The clipped tail 925// was never painted, so the model records it as blank: claiming otherwise 926// would let a later frame skip the cell, and a resize that widens the line 927// would leave the half-glyph on screen forever. 928if len (oldLine )== len (newLine ) { 929copy (oldLine ,newLine ) 930for x := clipped ;x < width && x < len (oldLine );x ++ { 931oldLine [x ]= Cell {} 932 } 933 } 934} 935 936// markDrift records whether a row holds a cell the terminal may measure 937// differently than the model, so a later width change knows which rows to 938// repaint. Recorded here rather than rescanned later, because the diff already 939// had to work it out. 940func (s * TerminalRenderer )markDrift (y ,height int ,drift bool ) { 941if y < 0 { 942return 943 } 944if n := max (height ,y + 1 );len (s .driftRows )< n { 945s .driftRows = append (s .driftRows ,make ([]bool ,n - len (s .driftRows ))... ) 946 } 947s .driftRows [y ]= drift 948} 949 950// transformLine diffs row y against the model and writes only what changed, 951// using [ansi.ICH] and [ansi.DCH] to shift cells. It must not see a drift-prone 952// row; [TerminalRenderer.paintLine] routes those to a repaint. 953func (s * TerminalRenderer )transformLine (newbuf * RenderBuffer ,y int ) { 954var firstCell ,oLastCell ,nLastCell int // first, old last, new last index 955oldLine := s .curbuf .Line (y ) 956newLine := newbuf .Line (y ) 957 958// Find the first changed cell in the line 959blank := newLine .At (0 ) 960 961// It might be cheaper to clear leading spaces with [ansi.EL] 1 i.e. 962// [ansi.EraseLineLeft]. 963if canClearWith (blank ) {//nolint:nestif 964var oFirstCell ,nFirstCell int 965for oFirstCell = 0 ;oFirstCell < s .curbuf .Width ();oFirstCell ++ { 966if ! cellEqual (oldLine .At (oFirstCell ),blank ) { 967break 968 } 969 } 970for nFirstCell = 0 ;nFirstCell < newbuf .Width ();nFirstCell ++ { 971if ! cellEqual (newLine .At (nFirstCell ),blank ) { 972break 973 } 974 } 975 976if nFirstCell == oFirstCell { 977firstCell = nFirstCell 978 979// Find the first differing cell 980for firstCell < newbuf .Width ()&& 981cellEqual (oldLine .At (firstCell ),newLine .At (firstCell )) { 982firstCell ++ 983 } 984 }else if oFirstCell > nFirstCell { 985firstCell = nFirstCell 986 }else if oFirstCell < nFirstCell { 987firstCell = oFirstCell 988el1Cost := len (ansi .EraseLineLeft ) 989if el1Cost < nFirstCell - oFirstCell { 990if nFirstCell >= newbuf .Width () { 991s .move (newbuf ,0 ,y ) 992s .updatePen (blank ) 993_ ,_ = s .buf .WriteString (ansi .EraseLineRight ) 994 }else { 995s .move (newbuf ,nFirstCell - 1 ,y ) 996s .updatePen (blank ) 997_ ,_ = s .buf .WriteString (ansi .EraseLineLeft ) 998 } 999 1000for firstCell < nFirstCell { 1001oldLine .Set (firstCell ,blank ) 1002firstCell ++ 1003 } 1004 } 1005 } 1006 }else { 1007// Find the first differing cell 1008for firstCell < newbuf .Width ()&& cellEqual (newLine .At (firstCell ),oldLine .At (firstCell )) { 1009firstCell ++ 1010 } 1011 } 1012 1013// If we didn't find one, we're done 1014if firstCell >= newbuf .Width () { 1015return 1016 } 1017 1018blank = newLine .At (newbuf .Width ()- 1 ) 1019if blank != nil && ! canClearWith (blank ) { 1020// Find the last differing cell 1021nLastCell = newbuf .Width ()- 1 1022for nLastCell > firstCell && cellEqual (newLine .At (nLastCell ),oldLine .At (nLastCell )) { 1023nLastCell -- 1024 } 1025 1026if nLastCell >= firstCell { 1027s .move (newbuf ,firstCell ,y ) 1028s .putRange (newbuf ,oldLine ,newLine ,y ,firstCell ,nLastCell ) 1029if firstCell < len (oldLine )&& firstCell < len (newLine ) { 1030copy (oldLine [firstCell :],newLine [firstCell :]) 1031 }else { 1032copy (oldLine ,newLine ) 1033 } 1034 } 1035 1036return 1037 } 1038 1039// Find last non-blank cell in the old line. 1040// We always use the newbuf width to detect new cell changes. 1041oLastCell = newbuf .Width ()- 1 1042for oLastCell > firstCell && cellEqual (oldLine .At (oLastCell ),blank ) { 1043oLastCell -- 1044 } 1045 1046// Find last non-blank cell in the new line. 1047// We always use the newbuf width to detect new cell changes. 1048nLastCell = newbuf .Width ()- 1 1049for nLastCell > firstCell && cellEqual (newLine .At (nLastCell ),blank ) { 1050nLastCell -- 1051 } 1052 1053if nLastCell == firstCell && s .el0Cost ()< oLastCell - nLastCell {//nolint:nestif 1054s .move (newbuf ,firstCell ,y ) 1055if ! cellEqual (newLine .At (firstCell ),blank ) { 1056s .putCell (newbuf ,newLine .At (firstCell )) 1057 } 1058s .clearToEnd (newbuf ,blank ,false ) 1059 }else if nLastCell != oLastCell && 1060! cellEqual (newLine .At (nLastCell ),oldLine .At (oLastCell )) { 1061s .move (newbuf ,firstCell ,y ) 1062if oLastCell - nLastCell > s .el0Cost () { 1063if s .putRange (newbuf ,oldLine ,newLine ,y ,firstCell ,nLastCell ) { 1064s .move (newbuf ,nLastCell + 1 ,y ) 1065 } 1066s .clearToEnd (newbuf ,blank ,false ) 1067 }else { 1068n := max (nLastCell ,oLastCell ) 1069s .putRange (newbuf ,oldLine ,newLine ,y ,firstCell ,n ) 1070 } 1071 }else { 1072nLastNonBlank := nLastCell 1073oLastNonBlank := oLastCell 1074 1075// Find the last cells that really differ. 1076// Can be -1 if no cells differ. 1077for cellEqual (newLine .At (nLastCell ),oldLine .At (oLastCell )) { 1078if ! cellEqual (newLine .At (nLastCell - 1 ),oldLine .At (oLastCell - 1 )) { 1079break 1080 } 1081nLastCell -- 1082oLastCell -- 1083if nLastCell == - 1 || oLastCell == - 1 { 1084break 1085 } 1086 } 1087 1088n := min (oLastCell ,nLastCell ) 1089if n >= firstCell { 1090s .move (newbuf ,firstCell ,y ) 1091s .putRange (newbuf ,oldLine ,newLine ,y ,firstCell ,n ) 1092 } 1093 1094if oLastCell < nLastCell { 1095m := max (nLastNonBlank ,oLastNonBlank ) 1096if n != 0 { 1097for n > 0 { 1098wide := newLine .At (n + 1 ) 1099if ! wide .isWidePlaceholder () { 1100break 1101 } 1102n -- 1103oLastCell -- 1104 } 1105 }else if n >= firstCell && newLine .At (n )!= nil && newLine .At (n ).Width > 1 { 1106next := newLine .At (n + 1 ) 1107for next .isWidePlaceholder () { 1108n ++ 1109oLastCell ++ 1110next = newLine .At (n + 1 ) 1111 } 1112 } 1113 1114// TODO: This can sometimes send unnecessary cursor movements with 1115// negative or zero ranges. This could happen on a screen resize 1116// where oLastCell < nLastCell and oLastCell is -1 or less. 1117// Investigate and fix. 1118s .move (newbuf ,n + 1 ,y ) 1119ichCost := 3 + nLastCell - oLastCell 1120if s .caps .Contains (capICH )&& (nLastCell < nLastNonBlank || ichCost > (m - n )) { 1121s .putRange (newbuf ,oldLine ,newLine ,y ,n + 1 ,m ) 1122 }else { 1123s .insertCells (newbuf ,newLine [n + 1 :],nLastCell - oLastCell ) 1124 } 1125 }else if oLastCell > nLastCell { 1126s .move (newbuf ,n + 1 ,y ) 1127dchCost := 3 + oLastCell - nLastCell 1128if dchCost > len (ansi .EraseLineRight )+ nLastNonBlank - (n + 1 ) { 1129if s .putRange (newbuf ,oldLine ,newLine ,y ,n + 1 ,nLastNonBlank ) { 1130s .move (newbuf ,nLastNonBlank + 1 ,y ) 1131 } 1132s .clearToEnd (newbuf ,blank ,false ) 1133 }else { 1134s .updatePen (blank ) 1135s .deleteCells (oLastCell - nLastCell ) 1136 } 1137 } 1138 } 1139 1140// Update the old line with the new line 1141if firstCell < len (oldLine )&& firstCell < len (newLine ) { 1142copy (oldLine [firstCell :],newLine [firstCell :]) 1143 }else { 1144copy (oldLine ,newLine ) 1145 } 1146} 1147 1148// reanchorWideLine re-anchors the cursor after a line that may have left it 1149// adrift. This is a best-effort fallback that bounds cursor desync to one line 1150// on terminals whose width model disagrees with ours. When the terminal 1151// negotiated Unicode grapheme width (mode 2027) the models agree, so no 1152// re-anchor is needed. 1153// 1154// The row is re-anchored along with the column. A terminal that measures a 1155// cluster wider than the model does runs past the right margin and wraps, so a 1156// line the model believes fits can leave the real cursor a row further down. 1157// Recovering the column alone would then leave every following line painted one 1158// row off, which is the drift this re-anchor exists to contain. 1159func (s * TerminalRenderer )reanchorWideLine (newbuf * RenderBuffer ) { 1160// In relative cursor mode there is no absolute row to move to, so the 1161// column is all that can be recovered. 1162if s .flags .Contains (tRelativeCursor ) { 1163if s .atPhantom || s .cur .X < 0 || s .cur .X >= newbuf .Width () { 1164return 1165 } 1166_ ,_ = s .buf .WriteString (ansi .CursorHorizontalAbsolute (s .cur .X + 1 )) 1167return 1168 } 1169 1170if s .cur .X < 0 || s .cur .Y < 0 { 1171return 1172 } 1173 1174// A pending wrap is as untrustworthy as the column: the terminal may have 1175// already taken it. An absolute move cancels it and lands on a column the 1176// model can name, so clamp into the line and record that. 1177x := min (s .cur .X ,newbuf .Width ()- 1 ) 1178_ ,_ = s .buf .WriteString (ansi .CursorPosition (x + 1 ,s .cur .Y + 1 )) 1179s .cur .X = x 1180s .atPhantom = false 1181} 1182 1183// deleteCells deletes the count cells at the current cursor position and moves 1184// the rest of the line to the left. This is equivalent to [ansi.DCH]. 1185func (s * TerminalRenderer )deleteCells (count int ) { 1186// [ansi.DCH] will shift in cells from the right margin so we need to 1187// ensure that they are the right style. 1188_ ,_ = s .buf .WriteString (ansi .DeleteCharacter (count )) 1189} 1190 1191// clearToBottom clears the screen from the current cursor position to the end 1192// of the screen. 1193func (s * TerminalRenderer )clearToBottom (blank * Cell ) { 1194row ,col := s .cur .Y ,s .cur .X 1195if row < 0 { 1196row = 0 1197 } 1198 1199s .updatePen (blank ) 1200_ ,_ = s .buf .WriteString (ansi .EraseScreenBelow ) 1201// Clear the rest of the current line 1202s .curbuf .ClearArea (Rect (col ,row ,s .curbuf .Width ()- col ,1 )) 1203// Clear everything below the current line 1204s .curbuf .ClearArea (Rect (0 ,row + 1 ,s .curbuf .Width (),s .curbuf .Height ()- row - 1 )) 1205} 1206 1207// clearBottom tests if clearing the end of the screen would satisfy part of 1208// the screen update. Scan backwards through lines in the screen checking if 1209// each is blank and one or more are changed. 1210// It returns the top line. 1211func (s * TerminalRenderer )clearBottom (newbuf * RenderBuffer ,total int ) (top int ) { 1212if total <= 0 { 1213return 0 1214 } 1215 1216top = total 1217last := min (s .curbuf .Width (),newbuf .Width ()) 1218blank := s .clearBlank () 1219canClearWithBlank := canClearWith (blank ) 1220 1221if canClearWithBlank {//nolint:nestif 1222var row int 1223for row = total - 1 ;row >= 0 ;row -- { 1224oldLine := s .curbuf .Line (row ) 1225newLine := newbuf .Line (row ) 1226 1227var col int 1228ok := true 1229for col = 0 ;ok && col < last ;col ++ { 1230ok = cellEqual (newLine .At (col ),blank ) 1231 } 1232if ! ok { 1233break 1234 } 1235 1236for col = 0 ;ok && col < last ;col ++ { 1237ok = cellEqual (oldLine .At (col ),blank ) 1238 } 1239if ! ok { 1240top = row 1241 } 1242 } 1243 1244if top < total { 1245// top is the first row of the trailing blank region, so the 1246// erase starts there. Starting a row earlier would wipe a row 1247// this frame did not touch, and the loop that repaints only 1248// touched rows would never put it back. 1249s .move (newbuf ,0 ,top ) 1250s .clearToBottom (blank ) 1251if s .oldhash != nil && s .newhash != nil && 1252row < len (s .oldhash )&& row < len (s .newhash ) { 1253for row := top ;row < newbuf .Height ();row ++ { 1254s .oldhash [row ]= s .newhash [row ] 1255 } 1256 } 1257 } 1258 } 1259 1260return top 1261} 1262 1263// clearScreen clears the screen and put cursor at home. 1264func (s * TerminalRenderer )clearScreen (blank * Cell ) { 1265s .updatePen (blank ) 1266_ ,_ = s .buf .WriteString (ansi .CursorHomePosition ) 1267_ ,_ = s .buf .WriteString (ansi .EraseEntireScreen ) 1268s .cur .X ,s .cur .Y = 0 ,0 1269s .curbuf .Fill (blank ) 1270} 1271 1272// clearBelow clears everything below and including the row. 1273func (s * TerminalRenderer )clearBelow (newbuf * RenderBuffer ,blank * Cell ,row int ) { 1274s .move (newbuf ,0 ,row ) 1275s .clearToBottom (blank ) 1276} 1277 1278// clearUpdate forces a screen redraw. 1279func (s * TerminalRenderer )clearUpdate (newbuf * RenderBuffer ) { 1280blank := s .clearBlank () 1281var nonEmpty int 1282if s .flags .Contains (tFullscreen ) { 1283// XXX: We're using the maximum height of the two buffers to ensure we 1284// write newly added lines to the screen in 1285// [terminalWriter.transformLine]. 1286nonEmpty = max (s .curbuf .Height (),newbuf .Height ()) 1287s .clearScreen (blank ) 1288 }else { 1289nonEmpty = newbuf .Height () 1290// FIXME: Investigate the double [ansi.ClearScreenBelow] call. 1291// Commenting the line below out seems to work but it might cause other 1292// bugs. 1293s .clearBelow (newbuf ,blank ,0 ) 1294 } 1295nonEmpty = s .clearBottom (newbuf ,nonEmpty ) 1296for i := 0 ;i < nonEmpty && i < newbuf .Height ();i ++ { 1297s .paintLine (newbuf ,i ,false ) 1298 } 1299} 1300 1301func (s * TerminalRenderer )logf (format string ,args ... any ) { 1302if s .logger == nil { 1303return 1304 } 1305s .logger .Printf (format ,args ... ) 1306} 1307 1308// Buffered returns the number of bytes buffered for the next flush. 1309func (s * TerminalRenderer )Buffered ()int { 1310return s .buf .Len () 1311} 1312 1313// Flush flushes the buffer to the screen. 1314func (s * TerminalRenderer )Flush () (err error ) { 1315// Write the buffer 1316if n := s .buf .Len ();n > 0 { 1317bts := s .buf .Bytes () 1318if s .logger != nil { 1319s .logf ("output: %q" ,bts ) 1320 } 1321_ ,err = s .w .Write (bts ) 1322s .buf .Reset () 1323 } 1324return 1325} 1326 1327// sizeChange is what a render owes to the screen or the frame having changed 1328// shape since the last one. 1329type sizeChange struct { 1330// frameResized reports that the frame is a different shape than the model, 1331// so the model has to be resized before anything is diffed against it. 1332frameResized bool 1333 1334// repaintAll requires every row to be painted whatever the model says. 1335repaintAll bool 1336} 1337 1338// reconcileSize decides what a change of size costs, and is the only place that 1339// decides it. 1340// 1341// A terminal that changed size rewrapped what was on it, and a frame that 1342// changed shape leaves no row holding its old meaning. Either way the model 1343// cannot be trusted. What differs is how much of the screen is the renderer's to 1344// put right: fullscreen owns every cell and clears it, while an inline frame 1345// shares the screen with whatever came before and so repaints its own rows and 1346// leaves the rest alone. 1347func (s * TerminalRenderer )reconcileSize (newbuf * RenderBuffer )sizeChange { 1348newWidth ,newHeight := newbuf .Width (),newbuf .Height () 1349curWidth ,curHeight := s .curbuf .Width (),s .curbuf .Height () 1350fullscreen := s .flags .Contains (tFullscreen ) 1351 1352change := sizeChange { 1353frameResized :curWidth != newWidth || curHeight != newHeight , 1354 } 1355 1356if s .termResized { 1357if fullscreen { 1358s .clear = true 1359 }else { 1360change .repaintAll = true 1361 } 1362s .termResized = false 1363 } 1364 1365if change .frameResized { 1366// No row keeps its meaning, so the hashes that describe them are worth 1367// nothing. Inline uses the narrower partial clear in Render instead. 1368s .oldhash ,s .newhash = nil ,nil 1369if fullscreen { 1370s .clear = true 1371 } 1372// Nor does the application's touch list, which never saw rows that 1373// vanished and came back between two frames. 1374s .damage (0 ,newHeight ) 1375 } 1376 1377// The terminal clips a row it measures wider than the model does, and the 1378// model believes it painted the whole row. Changing the width changes what 1379// fits, so a row holding a cell the two might measure differently has to be 1380// painted again. Rows of plain cells are measured the same by everyone, so an 1381// ASCII screen pays nothing. 1382if ! fullscreen && curWidth != newWidth { 1383for y ,drifts := range s .driftRows { 1384if drifts { 1385s .damage (y ,1 ) 1386 } 1387 } 1388 } 1389 1390return change 1391} 1392 1393// damage marks n rows from y as rows this render disturbed on its own: rows it 1394// scrolled, rows it erased, rows the terminal is about to measure differently 1395// than the model does. The diff loop paints a damaged row whether or not the 1396// application drew into it, because the application has no reason to redraw a 1397// row it did not change and no way to know the renderer moved it. 1398func (s * TerminalRenderer )damage (y ,n int ) { 1399for i := max (y ,0 );i < min (y + n ,len (s .damaged ));i ++ { 1400s .damaged [i ]= true 1401 } 1402} 1403 1404// beginDamage starts a fresh account, sized to the screen this render is about 1405// to paint. A row outside it cannot be painted, so it cannot be damaged, and 1406// clamping here is what lets every reader index the record without checking. 1407// Reset on the way in rather than the way out, so no future exit path has to 1408// remember to do it. The slice is kept when the size holds, so a steady stream 1409// of frames allocates nothing. 1410func (s * TerminalRenderer )beginDamage (height int ) { 1411if len (s .damaged )!= height { 1412s .damaged = make ([]bool ,height ) 1413return 1414 } 1415clear (s .damaged ) 1416} 1417 1418// Redraw forces a full redraw of the screen. It's equivalent to calling 1419// [TerminalRenderer.Erase] and [TerminalRenderer.Render]. 1420func (s * TerminalRenderer )Redraw (newbuf * RenderBuffer ) { 1421s .clear = true 1422s .Render (newbuf ) 1423} 1424 1425// Render renders changes of the screen to the internal buffer. Call 1426// [terminalWriter.Flush] to flush pending changes to the screen. 1427func (s * TerminalRenderer )Render (newbuf * RenderBuffer ) { 1428// Do we need to render anything? A latched resize counts: the terminal moved 1429// content the application cannot know about, so a frame it considers 1430// unchanged still has to be put back on screen. 1431touchedLines := newbuf .TouchedLines () 1432if ! s .clear && ! s .termResized && touchedLines == 0 { 1433return 1434 } 1435 1436// Every reader below indexes the touch list by screen row, so the list has to 1437// cover the screen before any of them run. Growing here is what lets them do 1438// it without a length check each. 1439newbuf .growTouched () 1440 1441if s .curbuf == nil || s .curbuf .Bounds ().Empty () { 1442// Initialize the current buffer 1443s .curbuf = NewRenderBuffer (newbuf .Width (),newbuf .Height ()) 1444 } 1445 1446newWidth ,newHeight := newbuf .Width (),newbuf .Height () 1447curHeight := s .curbuf .Height () 1448 1449s .beginDamage (newHeight ) 1450 1451size := s .reconcileSize (newbuf ) 1452 1453// TODO: Investigate whether this is necessary. Theoretically, terminals 1454// can add/remove tab stops and we should be able to handle that. We could 1455// use [ansi.DECTABSR] to read the tab stops, but that's not implemented in 1456// most terminals :/ 1457// // Are we using hard tabs? If so, ensure tabs are using the 1458// // default interval using [ansi.DECST8C]. 1459// if s.opts.HardTabs && !s.initTabs { 1460// s.buf.WriteString(ansi.SetTabEvery8Columns) 1461// s.initTabs = true 1462// } 1463 1464var nonEmpty int 1465 1466// An inline frame that gives up rows has to erase what it no longer covers, 1467// or the tail of the taller frame stays on screen below the shorter one. 1468// Fullscreen has no such rows: it repaints the whole screen instead. 1469// 1470// The erase runs from the new last row to the bottom of the screen, so it 1471// reaches that residue wherever the terminal moved it. A width change at 1472// the same time is therefore a reason to erase rather than a reason to skip 1473// it, since a terminal that rewrapped those rows has spread them further 1474// than the model can account for. 1475partialClear := ! s .flags .Contains (tFullscreen )&& s .cur .X != - 1 && s .cur .Y != - 1 && 1476curHeight > 0 && 1477curHeight > newHeight 1478 1479if ! s .clear && partialClear { 1480// From the first row the frame gave up, so the frame's own rows are left 1481// alone: nothing it still owns needs putting back, and a frame that 1482// collapsed to nothing starts at its own first row rather than the one 1483// above, which belongs to whatever shared the screen first. 1484s .clearBelow (newbuf ,nil ,newHeight ) 1485 } 1486 1487// Resize the model before diffing so the loop below walks every row 1488// of the new screen, including rows added by a grow. 1489if size .frameResized { 1490s .curbuf .Resize (newWidth ,newHeight ) 1491 } 1492 1493if s .clear {//nolint:nestif 1494s .clearUpdate (newbuf ) 1495s .clear = false 1496 }else if touchedLines > 0 || size .repaintAll { 1497// On Windows, there's a bug with Windows Terminal where [ansi.DECSTBM] 1498// misbehaves and moves the cursor outside of the scrolling region. For 1499// now, we disable the optimizations completely on Windows. 1500// See https://github.com/microsoft/terminal/issues/19016 1501if s .flags .Contains (tScrollOptim )&& s .flags .Contains (tFullscreen ) { 1502// Optimize scrolling for the alternate screen buffer. 1503// TODO: Should we optimize for inline mode as well? If so, we need 1504// to know the actual cursor position to use [ansi.DECSTBM]. 1505s .scrollOptimize (newbuf ) 1506 } 1507 1508var changedLines int 1509var i int 1510 1511nonEmpty = newHeight 1512 1513nonEmpty = s .clearBottom (newbuf ,nonEmpty ) 1514for i = 0 ;i < nonEmpty ;i ++ { 1515// A repaintAll row has to be put back whatever the model says. 1516if size .repaintAll || s .damaged [i ]|| (newbuf .Touched [i ]!= nil && 1517 (newbuf .Touched [i ].FirstCell != - 1 || newbuf .Touched [i ].LastCell != - 1 )) { 1518s .paintLine (newbuf ,i ,size .repaintAll ) 1519changedLines ++ 1520 } 1521 } 1522 } 1523 1524if ! s .flags .Contains (tFullscreen )&& size .frameResized { 1525s .move (newbuf ,0 ,max (newHeight - 1 ,0 )) 1526 } 1527 1528// Sync windows and screen 1529newbuf .growTouched () 1530resetTouched (newbuf .Touched ) 1531 1532s .updatePen (nil ) 1533} 1534 1535// Erase marks the screen to be fully erased on the next render. 1536func (s * TerminalRenderer )Erase () { 1537s .clear = true 1538} 1539 1540// Resize updates the terminal screen tab stops. This is used to calculate 1541// terminal tab stops for hard tab optimizations. 1542// 1543// Resize also invalidates the cursor model when the renderer can recover 1544// from it: a terminal may move the cursor on any resize, so the remembered 1545// position no longer matches reality, and the next move will be absolute. 1546// 1547// In relative cursor mode there is no absolute move to fall back on, and 1548// -1 there means "first move, assume the origin" rather than "unknown", so 1549// invalidating would assert a position instead of forgetting one. Keep the 1550// old model in that mode and let the next render diff against it. 1551// 1552// A resize also forces the next render to repaint, since the screen moves 1553// content around to fit and the model cannot see where it went. Whether that 1554// means the whole screen or only the frame's own rows is decided in 1555// [TerminalRenderer.Render]. 1556func (s * TerminalRenderer )Resize (width ,height int ) { 1557if s .tabs != nil { 1558s .tabs .Resize (width ) 1559 } 1560 1561// Latched against the size last reported, not against the model, because 1562// Render sees only the size the application draws at: a screen that shrinks 1563// and grows back between two frames looks unchanged by the time it gets 1564// there. What the resize costs is decided in Render, which knows the mode. 1565if width > 0 && height > 0 { 1566s .termResized = s .termResized || (s .termW > 0 && (width != s .termW || height != s .termH )) 1567s .termW ,s .termH = width ,height 1568 } 1569 1570if ! s .flags .Contains (tRelativeCursor ) { 1571s .cur .X ,s .cur .Y = - 1 ,- 1 1572 } 1573} 1574 1575// Position returns the cursor position in the screen buffer after applying any 1576// pending transformations from the underlying buffer. 1577func (s * TerminalRenderer )Position () (x ,y int ) { 1578return s .cur .X ,s .cur .Y 1579} 1580 1581// SetPosition changes the logical cursor position. This can be used when we 1582// change the cursor position outside of the screen and need to update the 1583// screen cursor position. 1584// This changes the cursor position for both normal and alternate screen 1585// buffers. 1586func (s * TerminalRenderer )SetPosition (x ,y int ) { 1587s .cur .X ,s .cur .Y = x ,y 1588} 1589 1590// WriteString writes the given string to the underlying buffer. 1591func (s * TerminalRenderer )WriteString (str string ) (int ,error ) { 1592return s .buf .WriteString (str )//nolint:wrapcheck 1593} 1594 1595// Write writes the given bytes to the underlying buffer. 1596func (s * TerminalRenderer )Write (b []byte ) (int ,error ) { 1597return s .buf .Write (b )//nolint:wrapcheck 1598} 1599 1600// MoveTo calculates and writes the shortest sequence to move the cursor to the 1601// given position. It uses the current cursor position and the new position to 1602// calculate the shortest amount of sequences to move the cursor. 1603func (s * TerminalRenderer )MoveTo (x ,y int ) { 1604s .move (nil ,x ,y ) 1605} 1606 1607// notLocal returns whether the coordinates are not considered local movement 1608// using the defined thresholds. 1609// This takes the number of columns, and the coordinates of the current and 1610// target positions. 1611func notLocal (cols ,fx ,fy ,tx ,ty int )bool { 1612// The typical distance for a [ansi.CUP] sequence. Anything less than this 1613// is considered local movement. 1614const longDist = 8 - 1 1615return (tx > longDist )&& 1616 (tx < cols - 1 - longDist )&& 1617 (abs (ty - fy )+ abs (tx - fx )> longDist ) 1618} 1619 1620// relativeCursorMove returns the relative cursor movement sequence using one or two 1621// of the following sequences [ansi.CUU], [ansi.CUD], [ansi.CUF], [ansi.CUB], 1622// [ansi.VPA], [ansi.HPA]. 1623// When overwrite is true, this will try to optimize the sequence by using the 1624// screen cells values to move the cursor instead of using escape sequences. 1625// 1626// It is safe to call this function with a nil [Buffer]. In that case, it won't 1627// use any optimizations that require the new buffer such as overwrite. 1628func relativeCursorMove (s * TerminalRenderer ,newbuf * RenderBuffer ,fx ,fy ,tx ,ty int ,overwrite ,useTabs ,useBackspace bool )string { 1629var seq strings.Builder 1630if newbuf == nil { 1631overwrite = false // We can't overwrite the current buffer. 1632 } 1633 1634if ty != fy {//nolint:nestif 1635var yseq string 1636if s .caps .Contains (capVPA )&& ! s .flags .Contains (tRelativeCursor ) { 1637yseq = ansi .VerticalPositionAbsolute (ty + 1 ) 1638 } 1639 1640if ty > fy { 1641n := ty - fy 1642if cud := ansi .CursorDown (n );yseq == "" || len (cud )< len (yseq ) { 1643yseq = cud 1644 } 1645if ! s .flags .Contains (tFullscreen )|| n < len (yseq ) {// n is the cost of using newline characters 1646yseq = strings .Repeat ("\n" ,n ) 1647if s .flags .Contains (tMapNewline ) { 1648fx = 0 1649 } 1650 } 1651 }else if ty < fy { 1652n := fy - ty 1653if cuu := ansi .CursorUp (n );yseq == "" || len (cuu )< len (yseq ) { 1654yseq = cuu 1655 } 1656if n == 1 && fy - 1 > 0 { 1657// TODO: Ensure we're not unintentionally scrolling the screen up. 1658yseq = ansi .ReverseIndex 1659 } 1660 } 1661 1662seq .WriteString (yseq ) 1663 } 1664 1665if tx != fx {//nolint:nestif 1666var xseq string 1667if ! s .flags .Contains (tRelativeCursor ) { 1668if s .caps .Contains (capHPA ) { 1669xseq = ansi .HorizontalPositionAbsolute (tx + 1 ) 1670 }else if s .caps .Contains (capCHA ) { 1671xseq = ansi .CursorHorizontalAbsolute (tx + 1 ) 1672 } 1673 } 1674 1675if tx > fx { 1676n := tx - fx 1677if useTabs && s .tabs != nil { 1678var tabs int 1679var col int 1680for col = fx ;s .tabs .Next (col )<= tx ;col = s .tabs .Next (col ) { 1681tabs ++ 1682if col == s .tabs .Next (col )|| col >= s .tabs .Width ()- 1 { 1683break 1684 } 1685 } 1686 1687if tabs > 0 { 1688cht := ansi .CursorHorizontalForwardTab (tabs ) 1689tab := strings .Repeat ("\t" ,tabs ) 1690if false && s .caps .Contains (capCHT )&& len (cht )< len (tab ) { 1691// TODO: The linux console and some terminals such as 1692// Alacritty don't support [ansi.CHT]. Enable this when 1693// we have a way to detect this, or after 5 years when 1694// we're sure everyone has updated their terminals :P 1695seq .WriteString (cht ) 1696 }else { 1697seq .WriteString (tab ) 1698 } 1699 1700n = tx - col 1701fx = col 1702 } 1703 } 1704 1705if cuf := ansi .CursorForward (n );xseq == "" || len (cuf )< len (xseq ) { 1706xseq = cuf 1707 } 1708 1709// If we have no attribute and style changes, overwrite is cheaper. 1710var ovw string 1711if overwrite && ty >= 0 { 1712for i := 0 ;i < n ;i ++ { 1713cell := newbuf .CellAt (fx + i ,ty ) 1714if cell != nil && cell .Width > 0 { 1715i += cell .Width - 1 1716if ! cell .Style .Equal (& s .cur .Style )|| ! cell .Link .Equal (& s .cur .Link ) { 1717overwrite = false 1718break 1719 } 1720 } 1721 } 1722 } 1723 1724if overwrite && ty >= 0 { 1725for i := 0 ;i < n ;i ++ { 1726cell := newbuf .CellAt (fx + i ,ty ) 1727if cell != nil && cell .Width > 0 { 1728ovw += cell .String () 1729i += cell .Width - 1 1730 } 1731 } 1732 } 1733 1734if overwrite && len (ovw )< len (xseq ) { 1735xseq = ovw 1736 } 1737 }else if tx < fx { 1738n := fx - tx 1739if useTabs && s .tabs != nil && s .caps .Contains (capCBT ) { 1740// VT100 does not support backward tabs [ansi.CBT]. 1741 1742col := fx 1743 1744var cbt int // cursor backward tabs count 1745for s .tabs .Prev (col )>= tx { 1746col = s .tabs .Prev (col ) 1747cbt ++ 1748if col == s .tabs .Prev (col )|| col <= 0 { 1749break 1750 } 1751 } 1752 1753if cbt > 0 { 1754seq .WriteString (ansi .CursorBackwardTab (cbt )) 1755n = col - tx 1756 } 1757 } 1758 1759if cub := ansi .CursorBackward (n );xseq == "" || len (cub )< len (xseq ) { 1760xseq = cub 1761 } 1762 1763if useBackspace && n < len (xseq ) { 1764xseq = strings .Repeat ("\b" ,n ) 1765 } 1766 } 1767 1768seq .WriteString (xseq ) 1769 } 1770 1771return seq .String () 1772} 1773 1774// moveCursor moves and returns the cursor movement sequence to move the cursor 1775// to the specified position. 1776// When overwrite is true, this will try to optimize the sequence by using the 1777// screen cells values to move the cursor instead of using escape sequences. 1778// 1779// It is safe to call this function with a nil [Buffer]. In that case, it won't 1780// use any optimizations that require the new buffer such as overwrite. 1781func moveCursor (s * TerminalRenderer ,newbuf * RenderBuffer ,x ,y int ,overwrite bool ) (seq string ) { 1782fx ,fy := s .cur .X ,s .cur .Y 1783 1784if ! s .flags .Contains (tRelativeCursor ) { 1785width := - 1 // Use -1 to indicate that we don't know the width of the screen. 1786if s .tabs != nil { 1787width = s .tabs .Width () 1788 } 1789if newbuf != nil && width == - 1 { 1790// Even though this might not be accurate, we can use the new 1791// buffer width as a fallback. Technically, if the new buffer 1792// didn't have the width of the terminal, this would give us a 1793// wrong result from [notLocal]. 1794width = newbuf .Width () 1795 } 1796// Method #0: Use [ansi.CUP] if the distance is long. 1797seq = ansi .CursorPosition (x + 1 ,y + 1 ) 1798if fx == - 1 || fy == - 1 || width == - 1 || notLocal (width ,fx ,fy ,x ,y ) { 1799return seq 1800 } 1801 } 1802 1803// Optimize based on options. 1804trials := 0 1805if s .caps .Contains (capHT ) { 1806trials |= 2 // 0b10 in binary 1807 } 1808if s .caps .Contains (capBS ) { 1809trials |= 1 // 0b01 in binary 1810 } 1811 1812// Try all possible combinations of hard tabs and backspace optimizations. 1813for i := 0 ;i <= trials ;i ++ { 1814// Skip combinations that are not enabled. 1815if i & ^ trials != 0 { 1816continue 1817 } 1818 1819useHardTabs := i & 2 != 0 1820useBackspace := i & 1 != 0 1821 1822// Method #1: Use local movement sequences. 1823nseq1 := relativeCursorMove (s ,newbuf ,fx ,fy ,x ,y ,overwrite ,useHardTabs ,useBackspace ) 1824if (i == 0 && len (seq )== 0 )|| len (nseq1 )< len (seq ) { 1825seq = nseq1 1826 } 1827 1828// Method #2: Use [ansi.CR] and local movement sequences. 1829nseq2 := relativeCursorMove (s ,newbuf ,0 ,fy ,x ,y ,overwrite ,useHardTabs ,useBackspace ) 1830nseq2 = "\r" + nseq2 1831if len (nseq2 )< len (seq ) { 1832seq = nseq2 1833 } 1834 1835if ! s .flags .Contains (tRelativeCursor ) { 1836// Method #3: Use [ansi.CursorHomePosition] and local movement sequences. 1837nseq3 := relativeCursorMove (s ,newbuf ,0 ,0 ,x ,y ,overwrite ,useHardTabs ,useBackspace ) 1838nseq3 = ansi .CursorHomePosition + nseq3 1839if len (nseq3 )< len (seq ) { 1840seq = nseq3 1841 } 1842 } 1843 } 1844 1845return seq 1846} 1847 1848// xtermCaps returns whether the terminal is xterm-like. This means that the 1849// terminal supports ECMA-48 and ANSI X3.64 escape sequences. 1850// xtermCaps returns a list of control sequence capabilities for the given 1851// terminal type. This only supports a subset of sequences that can 1852// be different among terminals. 1853// NOTE: A hybrid approach would be to support Terminfo databases for a full 1854// set of capabilities. 1855func xtermCaps (termtype string ) (v capabilities ) { 1856parts := strings .Split (termtype ,"-" ) 1857if len (parts )== 0 { 1858return v 1859 } 1860 1861switch parts [0 ] { 1862case 1863"contour" , 1864"foot" , 1865"ghostty" , 1866"kitty" , 1867"rio" , 1868"st" , 1869"tmux" , 1870"wezterm" : 1871v = allCaps 1872case "xterm" : 1873switch { 1874case len (parts )> 1 && parts [1 ]== "ghostty" : 1875fallthrough 1876case len (parts )> 1 && parts [1 ]== "kitty" : 1877fallthrough 1878case len (parts )> 1 && parts [1 ]== "rio" : 1879// These terminals can be defined as xterm- variants for 1880// compatibility with applications that check for xterm. 1881v = allCaps 1882default : 1883// NOTE: We exclude capHPA from allCaps because terminals like 1884// Konsole don't support it. Xterm terminfo defines HPA as CHA 1885// which means we can use CHA instead of HPA. 1886v = allCaps 1887v &^=capHPA 1888v &^=capCHT 1889v &^=capREP 1890 } 1891case "alacritty" : 1892v = allCaps 1893v &^=capCHT // NOTE: alacritty added support for [ansi.CHT] in 2024-12-28 #62d5b13. 1894case "screen" : 1895// See https://www.gnu.org/software/screen/manual/screen.html#Control-Sequences-1 1896v = allCaps 1897v &^=capREP 1898case "linux" : 1899// See https://man7.org/linux/man-pages/man4/console_codes.4.html 1900v = capVPA | capCHA | capHPA | capECH | capICH 1901 } 1902 1903return v 1904} 1905 1906// resetTouched marks every line as untouched, reusing the records already there 1907// rather than allocating a new one per line on every render. 1908func resetTouched (touched []* LineData ) { 1909for i ,ld := range touched { 1910if ld == nil { 1911touched [i ]= & LineData {FirstCell :- 1 ,LastCell :- 1 } 1912continue 1913 } 1914ld .FirstCell ,ld .LastCell = - 1 ,- 1 1915 } 1916}