okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
6d7982b
upstream
1package uv 2 3import ( 4"bytes" 5"image/color" 6"io" 7"log" 8"os" 9"strings" 10"sync" 11 12"github.com/charmbracelet/colorprofile" 13"github.com/charmbracelet/x/ansi" 14"github.com/charmbracelet/x/term" 15) 16 17// TerminalScreen represents a terminal screen, providing methods for managing 18// the screen state and rendering content. 19type TerminalScreen struct { 20win * Window 21w io.Writer 22buf * bytes.Buffer 23rend * TerminalRenderer 24rbuf * RenderBuffer 25env Environ 26profile colorprofile.Profile 27 28// Terminal state 29altScreen bool 30keyboardEnhancements * KeyboardEnhancements 31bracketedPaste bool 32mouseMode MouseMode 33mouseEncoding MouseEncoding 34cursor * Cursor // initial state is cursor hidden 35backgroundColor color.Color 36foregroundColor color.Color 37progressBar * ProgressBar 38windowTitle string 39syncUpdates bool // mode 2026 40resetTabs bool // DECST8C - reset terminal tabs on start 41 42// mu serializes access to the render buffer, output buffer, and 43// width-method state so the terminal event loop (which negotiates 44// grapheme-cluster width) cannot race with the application's own 45// Render/Flush goroutine. 46mu sync.Mutex 47// widthMethodOverride is set when the application explicitly calls 48// [TerminalScreen.SetWidthMethod]. An explicit override suppresses the 49// automatic mode-2027 width-method negotiation. 50widthMethodOverride bool 51} 52 53var _ Screen = (* TerminalScreen )(nil ) 54 55// NewTerminalScreen creates a new [TerminalScreen] with the given writer and environment. 56func NewTerminalScreen (w io.Writer ,env Environ )* TerminalScreen { 57s := & TerminalScreen {} 58s .buf = & bytes.Buffer {} 59s .win = NewWindow (0 ,0 ,nil ) 60s .w = w 61s .profile = colorprofile .Detect (w ,env ) 62s .rend = NewTerminalRenderer (s .buf ,env ) 63s .rend .SetFullscreen (false )// by default, we start in inline mode 64s .rend .SetRelativeCursor (true )// by default, we start in inline mode 65s .rend .SetColorProfile (s .profile ) 66s .rbuf = NewRenderBuffer (0 ,0 ) 67s .env = env 68 69if debugFile := env .Getenv ("UV_DEBUG" );debugFile != "" { 70if f ,err := os .OpenFile (debugFile ,os .O_CREATE | os .O_WRONLY | os .O_APPEND ,0o644 );err == nil { 71log .SetOutput (f ) 72s .rend .SetLogger (log .Default ()) 73 } 74 } 75 76// Configure renderer optimizations based on console settings. 77f ,ok := w .(term.File ) 78if ok { 79state ,err := term .GetState (f .Fd ()) 80if err == nil || isWindows {// Windows supports tabs and backspace by default, so we can ignore errors here. 81useTabs ,useBspace := optimizeMovements (state ) 82if useTabs { 83s .rend .SetTabStops (0 )// the width will be set after calling [TerminalScreen.Resize] 84 }else { 85s .rend .SetTabStops (- 1 ) 86 } 87s .rend .SetBackspace (useBspace ) 88s .resetTabs = useTabs 89 } 90 } 91// XXX: Do we still need map nl to crlf handling in the renderer? 92s .rend .SetMapNewline (false ) 93return s 94} 95 96// CellAt returns the cell at the specified x and y coordinates. 97func (s * TerminalScreen )CellAt (x ,y int )* Cell { 98return s .win .CellAt (x ,y ) 99} 100 101// SetCell sets the cell at the specified x and y coordinates. 102func (s * TerminalScreen )SetCell (x ,y int ,cell * Cell ) { 103s .win .SetCell (x ,y ,cell ) 104} 105 106// Bounds returns the bounds of the terminal screen as a rectangle. 107func (s * TerminalScreen )Bounds ()Rectangle { 108return s .win .Bounds () 109} 110 111// Width returns the width of the terminal screen. 112// 113// Note that this is not the actual width of the terminal window, but rather 114// the width of the screen we're managing. The actual width of the terminal 115// window can be obtained using [Terminal.GetSize] or by reading the "COLUMNS" 116// environment variable. 117func (s * TerminalScreen )Width ()int { 118return s .win .Width () 119} 120 121// Height returns the height of the terminal screen. 122// 123// Note that this is not the actual height of the terminal window, but rather 124// the height of the screen we're managing. The actual height of the terminal 125// window can be obtained using [Terminal.GetSize] or by reading the "LINES" 126// environment variable. 127func (s * TerminalScreen )Height ()int { 128return s .win .Height () 129} 130 131// StringWidth returns the cell width of the given string using the terminal 132// screen's width method. This accounts for the configured [WidthMethod] 133// (e.g. wcwidth vs grapheme width) so callers don't need to import ansi 134// directly. 135func (s * TerminalScreen )StringWidth (str string )int { 136return s .win .WidthMethod ().StringWidth (str ) 137} 138 139// WidthMethod returns the width method used by the terminal screen. 140func (s * TerminalScreen )WidthMethod ()WidthMethod { 141return s .win .WidthMethod () 142} 143 144// SetWidthMethod sets the width method for the terminal screen. This is an 145// override that propagates to the window/buffer and the renderer so that all 146// width measurements use the same method. Calling this marks the width method 147// as explicitly overridden, which disables automatic mode-2027 negotiation. 148func (s * TerminalScreen )SetWidthMethod (method ansi.Method ) { 149s .mu .Lock () 150defer s .mu .Unlock () 151s .widthMethodOverride = true 152s .setWidthMethod (method ) 153} 154 155// setWidthMethod propagates the width method to the window/buffer and the 156// renderer. Callers must hold s.mu. 157func (s * TerminalScreen )setWidthMethod (method ansi.Method ) { 158s .win .SetWidthMethod (method ) 159s .rend .SetWidthMethod (method ) 160} 161 162// requestGraphemeWidth queues a DECRQM request for Unicode core mode (DEC mode 163// 2027) so the terminal reports whether it measures cell width using grapheme 164// clustering. The response arrives as a [ModeReportEvent]. 165func (s * TerminalScreen )requestGraphemeWidth () { 166s .mu .Lock () 167defer s .mu .Unlock () 168s .buf .WriteString (ansi .RequestMode (ansi .ModeUnicodeCore )) 169} 170 171// enableGraphemeWidth enables Unicode core mode (DEC mode 2027) on the terminal 172// and switches the screen's width method to [ansi.GraphemeWidth] so wide-glyph 173// measurement matches the terminal. The change propagates to the 174// window/buffer and renderer. 175// 176// If the application has explicitly set a width method via 177// [TerminalScreen.SetWidthMethod], the explicit choice is preserved and this is 178// a no-op. The change is committed to the underlying writer through the 179// screen's normal locked write path so it cannot race with Render/Flush. 180func (s * TerminalScreen )enableGraphemeWidth () { 181s .mu .Lock () 182if s .widthMethodOverride { 183s .mu .Unlock () 184return 185 } 186s .buf .WriteString (ansi .SetMode (ansi .ModeUnicodeCore )) 187s .setWidthMethod (ansi .GraphemeWidth ) 188s .rend .SetGraphemeWidth (true ) 189s .mu .Unlock () 190_ = s .Flush () 191} 192 193// SetColorProfile sets the color profile for the terminal screen. 194// This is automatically detected when creating the terminal screen. However, 195// you can override it using this method. 196func (s * TerminalScreen )SetColorProfile (profile colorprofile.Profile ) { 197s .profile = profile 198s .rend .SetColorProfile (profile ) 199} 200 201// Resize resizes the terminal screen to the specified width and height, 202// updating the render buffer and renderer accordingly. 203func (s * TerminalScreen )Resize (width ,height int ) { 204s .win .Resize (width ,height ) 205s .rbuf .Resize (width ,height ) 206s .rend .Resize (width ,height ) 207s .rend .Erase () 208} 209 210// Display clears the screen and draws the given [Drawable] onto the terminal 211// screen and flushes the changes to the underlying writer. 212// 213// This is a convenience method that combines [TerminalScreen.Render] and 214// [TerminalScreen.Flush]. 215func (s * TerminalScreen )Display (d Drawable )error { 216if d != nil { 217s .win .Clear () 218d .Draw (s ,s .win .Bounds ()) 219 } 220s .Render () 221return s .Flush () 222} 223 224// Render renders changes that transform the screen from its current state to 225// the state represented by the [TerminalScreen]. 226// 227// The changes can be committed to the underlying writer by calling the 228// [TerminalScreen.Flush] method. 229func (s * TerminalScreen )Render () { 230s .mu .Lock () 231defer s .mu .Unlock () 232for y := 0 ;y < s .win .Height ();y ++ { 233for x := 0 ;x < s .win .Width (); { 234cell := s .win .CellAt (x ,y ) 235if cell == nil || cell .IsZero () { 236x ++ 237continue 238 } 239s .rbuf .SetCell (x ,y ,cell ) 240width := cell .Width 241if width <= 0 { 242width = 1 243 } 244x += width 245 } 246 } 247s .rend .Render (s .rbuf ) 248_ = s .rend .Flush () 249} 250 251// Flush writes any pending output to the underlying writer. 252func (s * TerminalScreen )Flush ()error { 253s .mu .Lock () 254defer s .mu .Unlock () 255if s .cursor != nil && ! s .cursor .Hidden && s .cursor .X >= 0 && s .cursor .Y >= 0 { 256s .rend .MoveTo (s .cursor .X ,s .cursor .Y ) 257 }else if ! s .altScreen { 258// We don't want the cursor to be dangling at the end of the line in 259// inline mode because it can cause unwanted line wraps in some 260// terminals. So we move it to the beginning of the next line if 261// necessary. 262// This is only needed when the cursor is hidden because when it's 263// visible, we already set its position above. 264x ,y := s .rend .Position () 265if x >= s .win .Width ()- 1 { 266s .rend .MoveTo (0 ,y ) 267 } 268 } 269 270var buf bytes.Buffer 271buf .Grow (s .buf .Len ()) 272 273if s .buf .Len ()> 0 { 274if s .syncUpdates { 275buf .Grow (len (ansi .SetModeSynchronizedOutput )+ len (ansi .ResetModeSynchronizedOutput )) 276 277// If synchronized updates are enabled, we need to wrap the output in 278// the appropriate control sequences to ensure that the terminal treats 279// it as a single atomic update. This is necessary to prevent flickering 280// and other visual artifacts that can occur when multiple updates are sent 281// separately. 282buf .WriteString (ansi .SetModeSynchronizedOutput ) 283 }else if s .cursor != nil && ! s .cursor .Hidden { 284buf .Grow (len (ansi .HideCursor )+ len (ansi .ShowCursor )) 285 286// If synchronized updates are not enabled, we need to ensure that 287// the cursor is hidden before writing any output to prevent 288// unwanted cursor visual artifacts. 289buf .WriteString (ansi .HideCursor ) 290 } 291 292buf .Write (s .buf .Bytes ()) 293 294if s .syncUpdates { 295buf .WriteString (ansi .ResetModeSynchronizedOutput ) 296 }else if s .cursor != nil && ! s .cursor .Hidden { 297buf .WriteString (ansi .ShowCursor ) 298 } 299 } 300 301_ ,err := s .w .Write (buf .Bytes ()) 302if err != nil { 303return err 304 } 305s .buf .Reset () 306return nil 307} 308 309// EnterAltScreen switches the terminal to the alternate screen buffer, allowing 310// applications to use a separate screen for their output without affecting the 311// main screen. 312// 313// The changes can be committed to the underlying writer by calling the 314// [TerminalScreen.Flush] method. 315func (s * TerminalScreen )EnterAltScreen () { 316var sb strings.Builder 317sb .WriteString (ansi .SetModeAltScreenSaveCursor ) 318if s .cursor == nil || s .cursor .Hidden { 319sb .WriteString (ansi .HideCursor ) 320 }else if s .cursor != nil && ! s .cursor .Hidden { 321sb .WriteString (ansi .ShowCursor ) 322 } 323if s .keyboardEnhancements != nil { 324EncodeKeyboardEnhancements (& sb ,s .keyboardEnhancements ) 325 } 326s .buf .WriteString (sb .String ()) 327 328if ! s .altScreen { 329s .rend .SaveCursor () 330s .rend .Erase () 331s .rend .SetFullscreen (true ) 332s .rend .SetRelativeCursor (false ) 333s .altScreen = true 334 } 335} 336 337// ExitAltScreen switches the terminal back to the main screen buffer, restoring 338// the previous screen state. 339// 340// The changes can be committed to the underlying writer by calling the 341// [TerminalScreen.Flush] method. 342func (s * TerminalScreen )ExitAltScreen () { 343var sb strings.Builder 344sb .WriteString (ansi .ResetModeAltScreenSaveCursor ) 345if s .cursor == nil || s .cursor .Hidden { 346sb .WriteString (ansi .HideCursor ) 347 }else if s .cursor != nil && ! s .cursor .Hidden { 348sb .WriteString (ansi .ShowCursor ) 349 } 350if s .keyboardEnhancements != nil { 351EncodeKeyboardEnhancements (& sb ,s .keyboardEnhancements ) 352 } 353s .buf .WriteString (sb .String ()) 354 355if s .altScreen { 356s .rend .RestoreCursor () 357s .rend .Erase () 358s .rend .SetFullscreen (false ) 359s .rend .SetRelativeCursor (true ) 360s .altScreen = false 361 } 362} 363 364// AltScreen returns whether the terminal is currently in the alternate screen 365// buffer. 366func (s * TerminalScreen )AltScreen ()bool { 367return s .altScreen 368} 369 370// HideCursor hides the terminal cursor. 371// 372// The changes can be committed to the underlying writer by calling the 373// [TerminalScreen.Flush] method. 374func (s * TerminalScreen )HideCursor () { 375s .buf .WriteString (ansi .HideCursor ) 376if s .cursor != nil { 377s .cursor .Hidden = true 378 } 379} 380 381// ShowCursor shows the terminal cursor. 382// 383// The changes can be committed to the underlying writer by calling the 384// [TerminalScreen.Flush] method. 385func (s * TerminalScreen )ShowCursor () { 386s .buf .WriteString (ansi .ShowCursor ) 387if s .cursor != nil { 388s .cursor .Hidden = false 389 }else { 390s .cursor = NewCursor (- 1 ,- 1 ) 391 } 392} 393 394// CursorVisible returns whether the terminal cursor is currently visible. 395func (s * TerminalScreen )CursorVisible ()bool { 396return s .cursor != nil && ! s .cursor .Hidden 397} 398 399// SetCursorPosition sets the position of the terminal cursor to the specified 400// coordinates. 401// 402// The changes can be committed to the underlying writer by calling the 403// [TerminalScreen.Flush] method. 404func (s * TerminalScreen )SetCursorPosition (x ,y int ) { 405if s .cursor == nil { 406s .cursor = NewCursor (x ,y ) 407s .cursor .Hidden = true 408 }else { 409s .cursor .X = x 410s .cursor .Y = y 411 } 412} 413 414// CursorPosition returns the last set cursor position of the terminal. If the 415// cursor position is not set, it returns (-1, -1). 416// 417// This can be affected by [TerminalScreen.Render] and 418// [TerminalScreen.SetCursorPosition] calls. 419func (s * TerminalScreen )CursorPosition () (x ,y int ) { 420if s .cursor != nil { 421return s .cursor .X ,s .cursor .Y 422 } 423return - 1 ,- 1 424} 425 426// SetCursorStyle sets the style of the terminal cursor. 427// 428// The changes can be committed to the underlying writer by calling the 429// [TerminalScreen.Flush] method. 430func (s * TerminalScreen )SetCursorStyle (shape CursorShape ,blink bool ) { 431EncodeCursorStyle (s .buf ,shape ,blink ) 432if s .cursor == nil { 433s .cursor = NewCursor (- 1 ,- 1 ) 434 } 435s .cursor .Shape = shape 436s .cursor .Blink = blink 437} 438 439// CursorStyle returns the current style of the terminal cursor. 440func (s * TerminalScreen )CursorStyle () (shape CursorShape ,blink bool ) { 441if s .cursor != nil { 442return s .cursor .Shape ,s .cursor .Blink 443 } 444return CursorBlock ,true 445} 446 447// SetCursorColor sets the color of the terminal cursor. 448// 449// The changes can be committed to the underlying writer by calling the 450// [TerminalScreen.Flush] method. 451func (s * TerminalScreen )SetCursorColor (c color.Color ) { 452EncodeCursorColor (s .buf ,c ) 453if s .cursor == nil { 454s .cursor = NewCursor (- 1 ,- 1 ) 455 } 456s .cursor .Color = c 457} 458 459// CursorColor returns the current color of the terminal cursor. 460// 461// A nil color indicates that the cursor color is the default terminal cursor 462// color. 463func (s * TerminalScreen )CursorColor () color.Color { 464if s .cursor != nil { 465return s .cursor .Color 466 } 467return nil 468} 469 470// SetBackgroundColor sets the background color of the terminal. 471// 472// The changes can be committed to the underlying writer by calling the 473// [TerminalScreen.Flush] method. 474func (s * TerminalScreen )SetBackgroundColor (c color.Color ) { 475EncodeBackgroundColor (s .buf ,c ) 476s .backgroundColor = c 477} 478 479// BackgroundColor returns the current background color of the terminal. 480// 481// A nil color indicates that the background color is the default terminal 482// background color. 483func (s * TerminalScreen )BackgroundColor () color.Color { 484return s .backgroundColor 485} 486 487// SetForegroundColor sets the foreground color of the terminal. 488// 489// The changes can be committed to the underlying writer by calling the 490// [TerminalScreen.Flush] method. 491func (s * TerminalScreen )SetForegroundColor (c color.Color ) { 492EncodeForegroundColor (s .buf ,c ) 493s .foregroundColor = c 494} 495 496// ForegroundColor returns the current foreground color of the terminal. 497// 498// A nil color indicates that the foreground color is the default terminal 499// foreground color. 500func (s * TerminalScreen )ForegroundColor () color.Color { 501return s .foregroundColor 502} 503 504// EnableBracketedPaste enables bracketed paste mode, allowing the terminal to 505// distinguish between pasted content and user input. 506// 507// The changes can be committed to the underlying writer by calling the 508// [TerminalScreen.Flush] method. 509func (s * TerminalScreen )EnableBracketedPaste () { 510s .buf .WriteString (ansi .SetModeBracketedPaste ) 511s .bracketedPaste = true 512} 513 514// DisableBracketedPaste disables bracketed paste mode. 515// 516// The changes can be committed to the underlying writer by calling the 517// [TerminalScreen.Flush] method. 518func (s * TerminalScreen )DisableBracketedPaste () { 519s .buf .WriteString (ansi .ResetModeBracketedPaste ) 520s .bracketedPaste = false 521} 522 523// BracketedPaste returns whether bracketed paste mode is currently enabled. 524func (s * TerminalScreen )BracketedPaste ()bool { 525return s .bracketedPaste 526} 527 528// SetSynchronizedUpdates sets whether to use synchronized updates (mode 2026), 529// which allows applications to batch updates to the terminal screen and flush 530// them all at once for improved performance. 531// 532// The changes can be committed to the underlying writer by calling the 533// [TerminalScreen.Flush] method. 534func (s * TerminalScreen )SetSynchronizedUpdates (enabled bool ) { 535s .syncUpdates = enabled 536} 537 538// SynchronizedUpdates returns whether synchronized updates (mode 2026) are 539// currently enabled. 540func (s * TerminalScreen )SynchronizedUpdates ()bool { 541return s .syncUpdates 542} 543 544// SetMouseMode sets the mouse tracking mode for the terminal, allowing 545// applications to receive mouse events. 546// 547// The changes can be committed to the underlying writer by calling the 548// [TerminalScreen.Flush] method. 549func (s * TerminalScreen )SetMouseMode (mode MouseMode ) { 550EncodeMouseMode (s .buf ,mode ) 551s .mouseMode = mode 552} 553 554// MouseMode returns the current mouse tracking mode of the terminal. 555func (s * TerminalScreen )MouseMode ()MouseMode { 556return s .mouseMode 557} 558 559// SetMouseEncoding sets the mouse encoding for the terminal. 560// The encoding determines how mouse coordinates and buttons are reported. 561// This is only meaningful when mouse tracking is enabled via [TerminalScreen.SetMouseMode]. 562// 563// The changes can be committed to the underlying writer by calling the 564// [TerminalScreen.Flush] method. 565func (s * TerminalScreen )SetMouseEncoding (enc MouseEncoding ) { 566EncodeMouseEncoding (s .buf ,enc ) 567s .mouseEncoding = enc 568} 569 570// MouseEncoding returns the current mouse encoding of the terminal. 571func (s * TerminalScreen )MouseEncoding ()MouseEncoding { 572return s .mouseEncoding 573} 574 575// SetWindowTitle sets the title of the terminal window. 576// 577// The changes can be committed to the underlying writer by calling the 578// [TerminalScreen.Flush] method. 579func (s * TerminalScreen )SetWindowTitle (title string ) { 580EncodeWindowTitle (s .buf ,title ) 581s .windowTitle = title 582} 583 584// WindowTitle returns the current title of the terminal window. 585func (s * TerminalScreen )WindowTitle ()string { 586return s .windowTitle 587} 588 589// SetKeyboardEnhancements sets the keyboard enhancements for the terminal, 590// allowing applications to receive enhanced keyboard input. 591// 592// The changes can be committed to the underlying writer by calling the 593// [TerminalScreen.Flush] method. 594func (s * TerminalScreen )SetKeyboardEnhancements (enh * KeyboardEnhancements ) { 595EncodeKeyboardEnhancements (s .buf ,enh ) 596s .keyboardEnhancements = enh 597} 598 599// KeyboardEnhancements returns the current keyboard enhancements of the terminal. 600// 601// A nil value indicates that no keyboard enhancements are currently enabled. 602func (s * TerminalScreen )KeyboardEnhancements ()* KeyboardEnhancements { 603return s .keyboardEnhancements 604} 605 606// SetProgressBar sets the progress bar for the terminal, allowing applications 607// to display progress information. 608// 609// The changes can be committed to the underlying writer by calling the 610// [TerminalScreen.Flush] method. 611func (s * TerminalScreen )SetProgressBar (pb * ProgressBar ) { 612EncodeProgressBar (s .buf ,pb ) 613s .progressBar = pb 614} 615 616// ProgressBar returns the current progress bar of the terminal. 617// 618// A nil value indicates that no progress bar is currently set. 619func (s * TerminalScreen )ProgressBar ()* ProgressBar { 620return s .progressBar 621} 622 623// Reset resets the terminal screen to its default state, clearing the screen, 624// switching back to the main screen buffer if necessary, and resetting all 625// terminal settings to their defaults. 626// 627// The changes can be committed to the underlying writer by calling the 628// [TerminalScreen.Flush] method. 629func (s * TerminalScreen )Reset () { 630var sb strings.Builder 631 632hasKeyboardEnhancements := s .keyboardEnhancements != nil 633 634if s .altScreen { 635if hasKeyboardEnhancements { 636sb .WriteString (ansi .KittyKeyboard (0 ,1 )) 637 } 638sb .WriteString (ansi .ResetModeAltScreenSaveCursor ) 639 } 640if hasKeyboardEnhancements { 641sb .WriteString (ansi .KittyKeyboard (0 ,1 )) 642 } 643if s .mouseMode != MouseModeNone { 644EncodeMouseMode (& sb ,MouseModeNone ) 645 } 646if s .mouseEncoding != MouseEncodingLegacy { 647EncodeMouseEncoding (& sb ,MouseEncodingLegacy ) 648 } 649 650if s .cursor == nil || ! s .cursor .Hidden { 651sb .WriteString (ansi .ShowCursor ) 652 } 653if s .cursor != nil { 654if s .cursor .Shape != CursorBlock || ! s .cursor .Blink { 655sb .WriteString (ansi .SetCursorStyle (0 )) 656 } 657if s .cursor .Color != nil { 658sb .WriteString (ansi .ResetCursorColor ) 659 } 660 } 661if s .backgroundColor != nil { 662sb .WriteString (ansi .ResetBackgroundColor ) 663 } 664if s .foregroundColor != nil { 665sb .WriteString (ansi .ResetForegroundColor ) 666 } 667if s .bracketedPaste { 668sb .WriteString (ansi .ResetModeBracketedPaste ) 669 } 670if s .windowTitle != "" { 671sb .WriteString (ansi .SetWindowTitle ("" )) 672 } 673if s .progressBar != nil && s .progressBar .State != ProgressBarNone { 674sb .WriteString (ansi .ResetProgressBar ) 675 } 676 677s .buf .WriteString (sb .String ()) 678 679// Go to the bottom of the screen. 680// We need to go to the bottom of the screen regardless of whether 681// we're in alt screen mode or not to avoid leaving the cursor in the 682// middle in terminals that don't support alt screen mode. 683// 684// This comes after resetting the screen state to ensure that moving the 685// cursor is the last thing we do, preventing any unwanted cursor movements 686// after resetting the screen. 687// 688// Note that both [TerminalScreen.rend] writes to [TerminalScreen.buf]. 689s .rend .MoveTo (0 ,s .win .Height ()- 1 ) 690} 691 692// Restore restores the terminal screen to its previous state, applying any 693// previous settings and state that were reset by the [TerminalScreen.Reset] method. 694// 695// The changes can be committed to the underlying writer by calling the 696// [TerminalScreen.Flush] method. 697func (s * TerminalScreen )Restore () { 698var sb strings.Builder 699 700if s .resetTabs { 701sb .WriteString (ansi .SetTabEvery8Columns ) 702 } 703if s .altScreen { 704sb .WriteString (ansi .SetModeAltScreenSaveCursor ) 705 } 706if s .cursor != nil && ! s .cursor .Hidden { 707sb .WriteString (ansi .ShowCursor ) 708 }else { 709// Hide the cursor by default. 710sb .WriteString (ansi .HideCursor ) 711 } 712if s .keyboardEnhancements != nil { 713EncodeKeyboardEnhancements (& sb ,s .keyboardEnhancements ) 714 } 715if s .mouseMode != MouseModeNone { 716EncodeMouseMode (& sb ,s .mouseMode ) 717 } 718if s .mouseEncoding != MouseEncodingLegacy { 719EncodeMouseEncoding (& sb ,s .mouseEncoding ) 720 } 721if s .cursor != nil { 722if s .cursor .Shape != CursorBlock || ! s .cursor .Blink { 723EncodeCursorStyle (& sb ,s .cursor .Shape ,s .cursor .Blink ) 724 } 725if s .cursor .Color != nil { 726EncodeCursorColor (& sb ,s .cursor .Color ) 727 } 728 } 729if s .backgroundColor != nil { 730EncodeBackgroundColor (& sb ,s .backgroundColor ) 731 } 732if s .foregroundColor != nil { 733EncodeForegroundColor (& sb ,s .foregroundColor ) 734 } 735if s .bracketedPaste { 736sb .WriteString (ansi .SetModeBracketedPaste ) 737 } 738if s .windowTitle != "" { 739EncodeWindowTitle (& sb ,s .windowTitle ) 740 } 741if s .progressBar != nil && s .progressBar .State != ProgressBarNone { 742EncodeProgressBar (& sb ,s .progressBar ) 743 } 744 745s .buf .WriteString (sb .String ()) 746 747// This needs to be called after restoring the screen state and writing to 748// the buffer. 749// 750// [TerminalScreen.Render] will write to [TerminalScreen.buf], so we need 751// to call it after writing the restore commands to the buffer to ensure 752// that the restore commands are included in the render output. This 753// ensures that the screen is properly restored before rendering any 754// changes. 755s .Render () 756 757// Cursor position will be restored by the caller after calling 758// [TerminalScreen.Flush]. 759} 760 761// Write writes data to the underlying buffer queuing it for output. 762// 763// The changes can be committed to the underlying writer by calling the 764// [TerminalScreen.Flush] method. 765func (s * TerminalScreen )Write (p []byte ) (n int ,err error ) { 766return s .buf .Write (p ) 767} 768 769// WriteString writes a string to the underlying buffer queuing it for output. 770// 771// The changes can be committed to the underlying writer by calling the 772// [TerminalScreen.Flush] method. 773func (s * TerminalScreen )WriteString (str string ) (n int ,err error ) { 774return s .buf .WriteString (str ) 775} 776 777// InsertAbove inserts content above the screen pushing the current content 778// down. 779// 780// This is useful for inserting content above the current screen content 781// without affecting the current cursor position or screen state. 782// 783// Note that this won't have any visible effect if the screen is in alt screen 784// mode, as the content will be inserted above the alt screen buffer, which is 785// not visible. However, if the screen is in inline mode, the content will be 786// inserted above and will not be managed by the renderer. 787// 788// Unlike other methods that modify the screen state, this method writes 789// directly to the underlying writer, so there is no need to call 790// [TerminalScreen.Flush] after calling this method. 791func (s * TerminalScreen )InsertAbove (content string )error { 792if len (content )== 0 { 793return nil 794 } 795 796var sb strings.Builder 797w ,h := s .win .Width (),s .win .Height () 798_ ,y := s .rend .Position () 799 800// We need to scroll the screen up by the number of lines in the queue. 801sb .WriteByte ('\r' ) 802down := h - y - 1 803if down > 0 { 804sb .WriteString (ansi .CursorDown (down )) 805 } 806 807lines := strings .Split (content ,"\n" ) 808offset := len (lines ) 809for _ ,line := range lines { 810lineWidth := s .win .WidthMethod ().StringWidth (line ) 811if w > 0 && lineWidth > w { 812offset += (lineWidth / w ) 813 } 814 } 815 816// Scroll the screen up by the offset to make room for the new lines. 817sb .WriteString (strings .Repeat ("\n" ,offset )) 818 819// XXX: Now go to the top of the screen, insert new lines, and write 820// the queued strings. It is important to use [Screen.moveCursor] 821// instead of [Screen.move] because we don't want to perform any checks 822// on the cursor position. 823up := offset + h - 1 824sb .WriteString (ansi .CursorUp (up )) 825sb .WriteString (ansi .InsertLine (offset )) 826for _ ,line := range lines { 827sb .WriteString (line ) 828sb .WriteString (ansi .EraseLineRight ) 829sb .WriteString ("\r\n" ) 830 } 831 832s .rend .SetPosition (0 ,0 ) 833 834_ ,err := io .WriteString (s .w ,sb .String ()) 835return err 836}