okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Kieran Klukasfix(renderer): keep the touch list covering the screen, once6d7982b

main
25.8 KiB836 linesraw
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 {
20	win     *Window
21	w       io.Writer
22	buf     *bytes.Buffer
23	rend    *TerminalRenderer
24	rbuf    *RenderBuffer
25	env     Environ
26	profile colorprofile.Profile
27
28	// Terminal state
29	altScreen            bool
30	keyboardEnhancements *KeyboardEnhancements
31	bracketedPaste       bool
32	mouseMode            MouseMode
33	mouseEncoding        MouseEncoding
34	cursor               *Cursor // initial state is cursor hidden
35	backgroundColor      color.Color
36	foregroundColor      color.Color
37	progressBar          *ProgressBar
38	windowTitle          string
39	syncUpdates          bool // mode 2026
40	resetTabs            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.
46	mu 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.
50	widthMethodOverride 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 {
57	s := &TerminalScreen{}
58	s.buf = &bytes.Buffer{}
59	s.win = NewWindow(0, 0, nil)
60	s.w = w
61	s.profile = colorprofile.Detect(w, env)
62	s.rend = NewTerminalRenderer(s.buf, env)
63	s.rend.SetFullscreen(false)    // by default, we start in inline mode
64	s.rend.SetRelativeCursor(true) // by default, we start in inline mode
65	s.rend.SetColorProfile(s.profile)
66	s.rbuf = NewRenderBuffer(0, 0)
67	s.env = env
68
69	if debugFile := env.Getenv("UV_DEBUG"); debugFile != "" {
70		if f, err := os.OpenFile(debugFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644); err == nil {
71			log.SetOutput(f)
72			s.rend.SetLogger(log.Default())
73		}
74	}
75
76	// Configure renderer optimizations based on console settings.
77	f, ok := w.(term.File)
78	if ok {
79		state, err := term.GetState(f.Fd())
80		if err == nil || isWindows { // Windows supports tabs and backspace by default, so we can ignore errors here.
81			useTabs, useBspace := optimizeMovements(state)
82			if useTabs {
83				s.rend.SetTabStops(0) // the width will be set after calling [TerminalScreen.Resize]
84			} else {
85				s.rend.SetTabStops(-1)
86			}
87			s.rend.SetBackspace(useBspace)
88			s.resetTabs = useTabs
89		}
90	}
91	// XXX: Do we still need map nl to crlf handling in the renderer?
92	s.rend.SetMapNewline(false)
93	return s
94}
95
96// CellAt returns the cell at the specified x and y coordinates.
97func (s *TerminalScreen) CellAt(x, y int) *Cell {
98	return 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) {
103	s.win.SetCell(x, y, cell)
104}
105
106// Bounds returns the bounds of the terminal screen as a rectangle.
107func (s *TerminalScreen) Bounds() Rectangle {
108	return 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 {
118	return 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 {
128	return 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 {
136	return s.win.WidthMethod().StringWidth(str)
137}
138
139// WidthMethod returns the width method used by the terminal screen.
140func (s *TerminalScreen) WidthMethod() WidthMethod {
141	return 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) {
149	s.mu.Lock()
150	defer s.mu.Unlock()
151	s.widthMethodOverride = true
152	s.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) {
158	s.win.SetWidthMethod(method)
159	s.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() {
166	s.mu.Lock()
167	defer s.mu.Unlock()
168	s.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() {
181	s.mu.Lock()
182	if s.widthMethodOverride {
183		s.mu.Unlock()
184		return
185	}
186	s.buf.WriteString(ansi.SetMode(ansi.ModeUnicodeCore))
187	s.setWidthMethod(ansi.GraphemeWidth)
188	s.rend.SetGraphemeWidth(true)
189	s.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) {
197	s.profile = profile
198	s.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) {
204	s.win.Resize(width, height)
205	s.rbuf.Resize(width, height)
206	s.rend.Resize(width, height)
207	s.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 {
216	if d != nil {
217		s.win.Clear()
218		d.Draw(s, s.win.Bounds())
219	}
220	s.Render()
221	return 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() {
230	s.mu.Lock()
231	defer s.mu.Unlock()
232	for y := 0; y < s.win.Height(); y++ {
233		for x := 0; x < s.win.Width(); {
234			cell := s.win.CellAt(x, y)
235			if cell == nil || cell.IsZero() {
236				x++
237				continue
238			}
239			s.rbuf.SetCell(x, y, cell)
240			width := cell.Width
241			if width <= 0 {
242				width = 1
243			}
244			x += width
245		}
246	}
247	s.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 {
253	s.mu.Lock()
254	defer s.mu.Unlock()
255	if s.cursor != nil && !s.cursor.Hidden && s.cursor.X >= 0 && s.cursor.Y >= 0 {
256		s.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.
264		x, y := s.rend.Position()
265		if x >= s.win.Width()-1 {
266			s.rend.MoveTo(0, y)
267		}
268	}
269
270	var buf bytes.Buffer
271	buf.Grow(s.buf.Len())
272
273	if s.buf.Len() > 0 {
274		if s.syncUpdates {
275			buf.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.
282			buf.WriteString(ansi.SetModeSynchronizedOutput)
283		} else if s.cursor != nil && !s.cursor.Hidden {
284			buf.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.
289			buf.WriteString(ansi.HideCursor)
290		}
291
292		buf.Write(s.buf.Bytes())
293
294		if s.syncUpdates {
295			buf.WriteString(ansi.ResetModeSynchronizedOutput)
296		} else if s.cursor != nil && !s.cursor.Hidden {
297			buf.WriteString(ansi.ShowCursor)
298		}
299	}
300
301	_, err := s.w.Write(buf.Bytes())
302	if err != nil {
303		return err
304	}
305	s.buf.Reset()
306	return 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() {
316	var sb strings.Builder
317	sb.WriteString(ansi.SetModeAltScreenSaveCursor)
318	if s.cursor == nil || s.cursor.Hidden {
319		sb.WriteString(ansi.HideCursor)
320	} else if s.cursor != nil && !s.cursor.Hidden {
321		sb.WriteString(ansi.ShowCursor)
322	}
323	if s.keyboardEnhancements != nil {
324		EncodeKeyboardEnhancements(&sb, s.keyboardEnhancements)
325	}
326	s.buf.WriteString(sb.String())
327
328	if !s.altScreen {
329		s.rend.SaveCursor()
330		s.rend.Erase()
331		s.rend.SetFullscreen(true)
332		s.rend.SetRelativeCursor(false)
333		s.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() {
343	var sb strings.Builder
344	sb.WriteString(ansi.ResetModeAltScreenSaveCursor)
345	if s.cursor == nil || s.cursor.Hidden {
346		sb.WriteString(ansi.HideCursor)
347	} else if s.cursor != nil && !s.cursor.Hidden {
348		sb.WriteString(ansi.ShowCursor)
349	}
350	if s.keyboardEnhancements != nil {
351		EncodeKeyboardEnhancements(&sb, s.keyboardEnhancements)
352	}
353	s.buf.WriteString(sb.String())
354
355	if s.altScreen {
356		s.rend.RestoreCursor()
357		s.rend.Erase()
358		s.rend.SetFullscreen(false)
359		s.rend.SetRelativeCursor(true)
360		s.altScreen = false
361	}
362}
363
364// AltScreen returns whether the terminal is currently in the alternate screen
365// buffer.
366func (s *TerminalScreen) AltScreen() bool {
367	return 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() {
375	s.buf.WriteString(ansi.HideCursor)
376	if s.cursor != nil {
377		s.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() {
386	s.buf.WriteString(ansi.ShowCursor)
387	if s.cursor != nil {
388		s.cursor.Hidden = false
389	} else {
390		s.cursor = NewCursor(-1, -1)
391	}
392}
393
394// CursorVisible returns whether the terminal cursor is currently visible.
395func (s *TerminalScreen) CursorVisible() bool {
396	return 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) {
405	if s.cursor == nil {
406		s.cursor = NewCursor(x, y)
407		s.cursor.Hidden = true
408	} else {
409		s.cursor.X = x
410		s.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) {
420	if s.cursor != nil {
421		return s.cursor.X, s.cursor.Y
422	}
423	return -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) {
431	EncodeCursorStyle(s.buf, shape, blink)
432	if s.cursor == nil {
433		s.cursor = NewCursor(-1, -1)
434	}
435	s.cursor.Shape = shape
436	s.cursor.Blink = blink
437}
438
439// CursorStyle returns the current style of the terminal cursor.
440func (s *TerminalScreen) CursorStyle() (shape CursorShape, blink bool) {
441	if s.cursor != nil {
442		return s.cursor.Shape, s.cursor.Blink
443	}
444	return 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) {
452	EncodeCursorColor(s.buf, c)
453	if s.cursor == nil {
454		s.cursor = NewCursor(-1, -1)
455	}
456	s.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 {
464	if s.cursor != nil {
465		return s.cursor.Color
466	}
467	return 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) {
475	EncodeBackgroundColor(s.buf, c)
476	s.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 {
484	return 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) {
492	EncodeForegroundColor(s.buf, c)
493	s.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 {
501	return 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() {
510	s.buf.WriteString(ansi.SetModeBracketedPaste)
511	s.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() {
519	s.buf.WriteString(ansi.ResetModeBracketedPaste)
520	s.bracketedPaste = false
521}
522
523// BracketedPaste returns whether bracketed paste mode is currently enabled.
524func (s *TerminalScreen) BracketedPaste() bool {
525	return 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) {
535	s.syncUpdates = enabled
536}
537
538// SynchronizedUpdates returns whether synchronized updates (mode 2026) are
539// currently enabled.
540func (s *TerminalScreen) SynchronizedUpdates() bool {
541	return 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) {
550	EncodeMouseMode(s.buf, mode)
551	s.mouseMode = mode
552}
553
554// MouseMode returns the current mouse tracking mode of the terminal.
555func (s *TerminalScreen) MouseMode() MouseMode {
556	return 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) {
566	EncodeMouseEncoding(s.buf, enc)
567	s.mouseEncoding = enc
568}
569
570// MouseEncoding returns the current mouse encoding of the terminal.
571func (s *TerminalScreen) MouseEncoding() MouseEncoding {
572	return 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) {
580	EncodeWindowTitle(s.buf, title)
581	s.windowTitle = title
582}
583
584// WindowTitle returns the current title of the terminal window.
585func (s *TerminalScreen) WindowTitle() string {
586	return 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) {
595	EncodeKeyboardEnhancements(s.buf, enh)
596	s.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 {
603	return 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) {
612	EncodeProgressBar(s.buf, pb)
613	s.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 {
620	return 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() {
630	var sb strings.Builder
631
632	hasKeyboardEnhancements := s.keyboardEnhancements != nil
633
634	if s.altScreen {
635		if hasKeyboardEnhancements {
636			sb.WriteString(ansi.KittyKeyboard(0, 1))
637		}
638		sb.WriteString(ansi.ResetModeAltScreenSaveCursor)
639	}
640	if hasKeyboardEnhancements {
641		sb.WriteString(ansi.KittyKeyboard(0, 1))
642	}
643	if s.mouseMode != MouseModeNone {
644		EncodeMouseMode(&sb, MouseModeNone)
645	}
646	if s.mouseEncoding != MouseEncodingLegacy {
647		EncodeMouseEncoding(&sb, MouseEncodingLegacy)
648	}
649
650	if s.cursor == nil || !s.cursor.Hidden {
651		sb.WriteString(ansi.ShowCursor)
652	}
653	if s.cursor != nil {
654		if s.cursor.Shape != CursorBlock || !s.cursor.Blink {
655			sb.WriteString(ansi.SetCursorStyle(0))
656		}
657		if s.cursor.Color != nil {
658			sb.WriteString(ansi.ResetCursorColor)
659		}
660	}
661	if s.backgroundColor != nil {
662		sb.WriteString(ansi.ResetBackgroundColor)
663	}
664	if s.foregroundColor != nil {
665		sb.WriteString(ansi.ResetForegroundColor)
666	}
667	if s.bracketedPaste {
668		sb.WriteString(ansi.ResetModeBracketedPaste)
669	}
670	if s.windowTitle != "" {
671		sb.WriteString(ansi.SetWindowTitle(""))
672	}
673	if s.progressBar != nil && s.progressBar.State != ProgressBarNone {
674		sb.WriteString(ansi.ResetProgressBar)
675	}
676
677	s.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].
689	s.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() {
698	var sb strings.Builder
699
700	if s.resetTabs {
701		sb.WriteString(ansi.SetTabEvery8Columns)
702	}
703	if s.altScreen {
704		sb.WriteString(ansi.SetModeAltScreenSaveCursor)
705	}
706	if s.cursor != nil && !s.cursor.Hidden {
707		sb.WriteString(ansi.ShowCursor)
708	} else {
709		// Hide the cursor by default.
710		sb.WriteString(ansi.HideCursor)
711	}
712	if s.keyboardEnhancements != nil {
713		EncodeKeyboardEnhancements(&sb, s.keyboardEnhancements)
714	}
715	if s.mouseMode != MouseModeNone {
716		EncodeMouseMode(&sb, s.mouseMode)
717	}
718	if s.mouseEncoding != MouseEncodingLegacy {
719		EncodeMouseEncoding(&sb, s.mouseEncoding)
720	}
721	if s.cursor != nil {
722		if s.cursor.Shape != CursorBlock || !s.cursor.Blink {
723			EncodeCursorStyle(&sb, s.cursor.Shape, s.cursor.Blink)
724		}
725		if s.cursor.Color != nil {
726			EncodeCursorColor(&sb, s.cursor.Color)
727		}
728	}
729	if s.backgroundColor != nil {
730		EncodeBackgroundColor(&sb, s.backgroundColor)
731	}
732	if s.foregroundColor != nil {
733		EncodeForegroundColor(&sb, s.foregroundColor)
734	}
735	if s.bracketedPaste {
736		sb.WriteString(ansi.SetModeBracketedPaste)
737	}
738	if s.windowTitle != "" {
739		EncodeWindowTitle(&sb, s.windowTitle)
740	}
741	if s.progressBar != nil && s.progressBar.State != ProgressBarNone {
742		EncodeProgressBar(&sb, s.progressBar)
743	}
744
745	s.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.
755	s.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) {
766	return 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) {
774	return 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 {
792	if len(content) == 0 {
793		return nil
794	}
795
796	var sb strings.Builder
797	w, 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.
801	sb.WriteByte('\r')
802	down := h - y - 1
803	if down > 0 {
804		sb.WriteString(ansi.CursorDown(down))
805	}
806
807	lines := strings.Split(content, "\n")
808	offset := len(lines)
809	for _, line := range lines {
810		lineWidth := s.win.WidthMethod().StringWidth(line)
811		if w > 0 && lineWidth > w {
812			offset += (lineWidth / w)
813		}
814	}
815
816	// Scroll the screen up by the offset to make room for the new lines.
817	sb.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.
823	up := offset + h - 1
824	sb.WriteString(ansi.CursorUp(up))
825	sb.WriteString(ansi.InsertLine(offset))
826	for _, line := range lines {
827		sb.WriteString(line)
828		sb.WriteString(ansi.EraseLineRight)
829		sb.WriteString("\r\n")
830	}
831
832	s.rend.SetPosition(0, 0)
833
834	_, err := io.WriteString(s.w, sb.String())
835	return err
836}