terminal.go

   1package uv
   2
   3import (
   4	"bytes"
   5	"context"
   6	"errors"
   7	"fmt"
   8	"image/color"
   9	"io"
  10	"log"
  11	"os"
  12	"runtime"
  13	"strings"
  14	"sync"
  15
  16	"github.com/charmbracelet/colorprofile"
  17	"github.com/charmbracelet/x/ansi"
  18	"github.com/charmbracelet/x/term"
  19	"github.com/lucasb-eyer/go-colorful"
  20	"github.com/muesli/cancelreader"
  21)
  22
  23var (
  24	// ErrNotTerminal is returned when one of the I/O streams is not a terminal.
  25	ErrNotTerminal = fmt.Errorf("not a terminal")
  26
  27	// ErrPlatformNotSupported is returned when the platform is not supported.
  28	ErrPlatformNotSupported = fmt.Errorf("platform not supported")
  29
  30	// ErrStarted is returned when the terminal has already been started.
  31	ErrStarted = fmt.Errorf("terminal already started")
  32)
  33
  34// Terminal represents a terminal screen that can be manipulated and drawn to.
  35// It handles reading events from the terminal using [WinChReceiver],
  36// [SequenceReceiver], and [ConReceiver].
  37type Terminal struct {
  38	// Terminal I/O streams and state.
  39	in          io.Reader
  40	out         io.Writer
  41	inTty       term.File
  42	inTtyState  *term.State
  43	outTty      term.File
  44	outTtyState *term.State
  45	winchTty    term.File // The terminal to receive window size changes from.
  46	started     bool      // Indicates if the terminal has been started.
  47
  48	// Terminal type, screen and buffer.
  49	termtype            string            // The $TERM type.
  50	environ             Environ           // The environment variables.
  51	buf                 *Buffer           // Reference to the last buffer used.
  52	scr                 *TerminalRenderer // The actual screen to be drawn to.
  53	size                Size              // The last known full size of the terminal.
  54	method              ansi.Method       // The width method used by the terminal.
  55	profile             colorprofile.Profile
  56	modes               ansi.Modes  // Keep track of terminal modes.
  57	useTabs             bool        // Whether to use hard tabs or not.
  58	useBspace           bool        // Whether to use backspace or not.
  59	cursorHidden        bool        // The current cursor visibility state.
  60	setFg, setBg, setCc color.Color // The current set foreground, background, and cursor colors.
  61	curStyle            int         // The encoded cursor style.
  62
  63	// Terminal input stream.
  64	rd           *TerminalReader
  65	wrdr         *WinChReceiver
  66	im           *InputManager
  67	err          error
  68	evch         chan Event
  69	evOnce       sync.Once
  70	once         sync.Once
  71	mouseMode    MouseMode // The mouse mode for the terminal.
  72	readLoopDone chan struct{}
  73
  74	logger Logger // The debug logger for I/O.
  75}
  76
  77// DefaultTerminal returns a new default terminal instance that uses
  78// [os.Stdin], [os.Stdout], and [os.Environ].
  79func DefaultTerminal() *Terminal {
  80	return NewTerminal(os.Stdin, os.Stdout, os.Environ())
  81}
  82
  83var defaultModes = ansi.Modes{
  84	// These are modes we care about and want to track.
  85	ansi.TextCursorEnableMode:    ansi.ModeSet,
  86	ansi.AltScreenSaveCursorMode: ansi.ModeReset,
  87	ansi.ButtonEventMouseMode:    ansi.ModeReset,
  88	ansi.AnyEventMouseMode:       ansi.ModeReset,
  89	ansi.SgrExtMouseMode:         ansi.ModeReset,
  90	ansi.BracketedPasteMode:      ansi.ModeReset,
  91	ansi.FocusEventMode:          ansi.ModeReset,
  92}
  93
  94// NewTerminal creates a new Terminal instance with the given terminal size.
  95// Use [term.GetSize] to get the size of the output screen.
  96func NewTerminal(in io.Reader, out io.Writer, env []string) *Terminal {
  97	t := new(Terminal)
  98	t.in = in
  99	t.out = out
 100	if f, ok := in.(term.File); ok {
 101		t.inTty = f
 102	}
 103	if f, ok := out.(term.File); ok {
 104		t.outTty = f
 105	}
 106	t.modes = ansi.Modes{}
 107	// Initialize the default modes.
 108	for k, v := range defaultModes {
 109		t.modes[k] = v
 110	}
 111	t.environ = env
 112	t.termtype = t.environ.Getenv("TERM")
 113	t.scr = NewTerminalRenderer(t.out, t.environ)
 114	t.buf = NewBuffer(0, 0)
 115	t.method = ansi.WcWidth // Default width method.
 116	t.SetColorProfile(colorprofile.Detect(out, env))
 117	t.rd = NewTerminalReader(t.in, t.termtype)
 118	t.rd.MouseMode = &t.mouseMode
 119	t.readLoopDone = make(chan struct{})
 120	t.evch = make(chan Event, 1)
 121	t.once = sync.Once{}
 122
 123	// Handle debugging I/O.
 124	debug, ok := os.LookupEnv("UV_DEBUG")
 125	if ok && len(debug) > 0 {
 126		f, err := os.OpenFile(debug, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
 127		if err != nil {
 128			panic("failed to open debug file: " + err.Error())
 129		}
 130
 131		logger := log.New(f, "uv: ", log.LstdFlags|log.Lshortfile)
 132		t.SetLogger(logger)
 133	}
 134	return t
 135}
 136
 137// SetLogger sets the debug logger for the terminal. This is used to log debug
 138// information about the terminal I/O. By default, it is set to a no-op logger.
 139func (t *Terminal) SetLogger(logger Logger) {
 140	t.logger = logger
 141	t.rd.SetLogger(logger)
 142	t.scr.SetLogger(logger)
 143}
 144
 145// ColorProfile returns the currently used color profile for the terminal.
 146func (t *Terminal) ColorProfile() colorprofile.Profile {
 147	return t.profile
 148}
 149
 150// SetColorProfile sets a custom color profile for the terminal. This is useful
 151// for forcing a specific color output. By default, the terminal will use the
 152// system's color profile inferred by the environment variables.
 153func (t *Terminal) SetColorProfile(p colorprofile.Profile) {
 154	t.profile = p
 155	t.scr.SetColorProfile(p)
 156}
 157
 158// ColorModel returns the color model of the terminal screen.
 159func (t *Terminal) ColorModel() color.Model {
 160	return t.ColorProfile()
 161}
 162
 163// SetWidthMethod sets the width method used by the terminal. This is typically
 164// used to determine how the terminal calculates the width of a single
 165// grapheme.
 166// The default method is [ansi.WcWidth].
 167func (t *Terminal) SetWidthMethod(method ansi.Method) {
 168	t.method = method
 169}
 170
 171// WidthMethod returns the width method used by the terminal. This is typically
 172// used to determine how the terminal calculates the width of a single
 173// grapheme.
 174func (t *Terminal) WidthMethod() WidthMethod {
 175	return t.method
 176}
 177
 178var _ color.Model = (*Terminal)(nil)
 179
 180// Convert converts the given color to the terminal's color profile. This
 181// implements the [color.Model] interface, allowing you to convert any color to
 182// the terminal's preferred color model.
 183func (t *Terminal) Convert(c color.Color) color.Color {
 184	return t.profile.Convert(c)
 185}
 186
 187// GetSize returns the size of the terminal screen. It errors if the size
 188// cannot be determined.
 189func (t *Terminal) GetSize() (width, height int, err error) {
 190	w, h, err := t.getSize()
 191	if err != nil {
 192		return 0, 0, fmt.Errorf("error getting terminal size: %w", err)
 193	}
 194	// Cache the last known size.
 195	t.size.Width = w
 196	t.size.Height = h
 197	return w, h, nil
 198}
 199
 200// Bounds returns the bounds of the terminal screen buffer. This is the
 201// rectangle that contains start and end points of the screen buffer.
 202// This is different from [Terminal.GetSize] which queries the size of the
 203// terminal window. The screen buffer can occupy a portion or all of the
 204// terminal window. Use [Terminal.Resize] to change the size of the screen
 205// buffer.
 206func (t *Terminal) Bounds() Rectangle {
 207	return Rect(0, 0, t.buf.Width(), t.buf.Height())
 208}
 209
 210// SetCell sets the cell at the given x, y position in the terminal buffer.
 211func (t *Terminal) SetCell(x int, y int, c *Cell) {
 212	t.buf.SetCell(x, y, c)
 213}
 214
 215// CellAt returns the cell at the given x, y position in the terminal buffer.
 216func (t *Terminal) CellAt(x int, y int) *Cell {
 217	return t.buf.CellAt(x, y)
 218}
 219
 220var _ Screen = (*Terminal)(nil)
 221
 222// Clear fills the terminal screen with empty cells, and clears the
 223// terminal screen.
 224//
 225// This is different from [Terminal.Erase], which only fills the screen
 226// buffer with empty cells without erasing the terminal screen first.
 227func (t *Terminal) Clear() {
 228	t.buf.Clear()
 229}
 230
 231// ClearArea fills the given area of the terminal screen with empty cells.
 232func (t *Terminal) ClearArea(area Rectangle) {
 233	t.buf.ClearArea(area)
 234}
 235
 236// Fill fills the terminal screen with the given cell. If the cell is nil, it
 237// fills the screen with empty cells.
 238func (t *Terminal) Fill(c *Cell) {
 239	t.buf.Fill(c)
 240}
 241
 242// FillArea fills the given area of the terminal screen with the given cell.
 243// If the cell is nil, it fills the area with empty cells.
 244func (t *Terminal) FillArea(c *Cell, area Rectangle) {
 245	t.buf.FillArea(c, area)
 246}
 247
 248// Clone returns a copy of the terminal screen buffer. This is useful for
 249// creating a snapshot of the current terminal state without modifying the
 250// original buffer.
 251func (t *Terminal) Clone() *Buffer {
 252	return t.buf.Clone()
 253}
 254
 255// CloneArea clones the given area of the terminal screen and returns a new
 256// buffer with the same size as the area. The new buffer will contain the
 257// same cells as the area in the terminal screen.
 258func (t *Terminal) CloneArea(area Rectangle) *Buffer {
 259	return t.buf.CloneArea(area)
 260}
 261
 262// Position returns the last known position of the cursor in the terminal.
 263func (t *Terminal) Position() (int, int) {
 264	return t.scr.Position()
 265}
 266
 267// SetPosition sets the position of the cursor in the terminal. This is
 268// typically used when the cursor was moved manually outside of the [Terminal]
 269// context.
 270func (t *Terminal) SetPosition(x, y int) {
 271	t.scr.SetPosition(x, y)
 272}
 273
 274// MoveTo moves the cursor to the given x, y position in the terminal.
 275// This won't take any effect until the next [Terminal.Display] or
 276// [Terminal.Flush] call.
 277func (t *Terminal) MoveTo(x, y int) {
 278	t.scr.MoveTo(x, y)
 279}
 280
 281func (t *Terminal) configureRenderer() {
 282	t.scr.SetColorProfile(t.profile)
 283	if t.useTabs {
 284		t.scr.SetTabStops(t.size.Width)
 285	}
 286	t.scr.SetBackspace(t.useBspace)
 287	t.scr.SetRelativeCursor(true) // Initial state is relative cursor movements.
 288	if t.scr != nil {
 289		if t.scr.AltScreen() {
 290			t.scr.EnterAltScreen()
 291		} else {
 292			t.scr.ExitAltScreen()
 293		}
 294	}
 295	t.scr.SetLogger(t.logger)
 296}
 297
 298// Erase fills the screen buffer with empty cells, and wipe the terminal
 299// screen. This is different from [Terminal.Clear], which only fills the
 300// terminal with empty cells.
 301//
 302// This won't take any effect until the next [Terminal.Display] or
 303// [Terminal.Flush] call.
 304func (t *Terminal) Erase() {
 305	t.scr.Erase()
 306	t.Clear()
 307}
 308
 309// Display computes the necessary changes to the terminal screen and renders
 310// the current buffer to the terminal screen.
 311//
 312// Typically, you would call this after modifying the terminal buffer using
 313// [Terminal.SetCell] or [Terminal.PrependString].
 314func (t *Terminal) Display() error {
 315	t.scr.Render(t.buf)
 316	return t.scr.Flush()
 317}
 318
 319// Flush flushes any pending renders to the terminal screen. This is typically
 320// used to flush the underlying screen buffer to the terminal.
 321//
 322// Use [Terminal.Buffered] to check how many bytes pending to be flushed.
 323func (t *Terminal) Flush() error {
 324	return t.scr.Flush()
 325}
 326
 327// Buffered returns the number of bytes buffered for the flush operation.
 328func (t *Terminal) Buffered() int {
 329	return t.scr.Buffered()
 330}
 331
 332// Touched returns the number of touched lines in the terminal buffer.
 333func (t *Terminal) Touched() int {
 334	return t.scr.Touched(t.buf)
 335}
 336
 337// EnableMode enables the given modes on the terminal. This is typically used
 338// to enable mouse support, bracketed paste mode, and other terminal features.
 339//
 340// Note that this won't take any effect until the next [Terminal.Display] or
 341// [Terminal.Flush] call.
 342func (t *Terminal) EnableMode(modes ...ansi.Mode) {
 343	if len(modes) == 0 {
 344		return
 345	}
 346	for _, m := range modes {
 347		t.modes[m] = ansi.ModeSet
 348	}
 349	t.scr.WriteString(ansi.SetMode(modes...)) //nolint:errcheck
 350}
 351
 352// DisableMode disables the given modes on the terminal. This is typically
 353// used to disable mouse support, bracketed paste mode, and other terminal
 354// features.
 355//
 356// Note that this won't take any effect until the next [Terminal.Display] or
 357// [Terminal.Flush] call.
 358func (t *Terminal) DisableMode(modes ...ansi.Mode) {
 359	if len(modes) == 0 {
 360		return
 361	}
 362	for _, m := range modes {
 363		t.modes[m] = ansi.ModeReset
 364	}
 365	t.scr.WriteString(ansi.ResetMode(modes...)) //nolint:errcheck
 366}
 367
 368// RequestMode requests the current state of the given modes from the terminal.
 369// This is typically used to check if a specific mode is recognized, enabled,
 370// or disabled on the terminal.
 371//
 372// Note that this won't take any effect until the next [Terminal.Display] or
 373// [Terminal.Flush] call.
 374func (t *Terminal) RequestMode(mode ansi.Mode) {
 375	t.scr.WriteString(ansi.RequestMode(mode)) //nolint:errcheck
 376}
 377
 378// MouseMode represents the mouse mode for the terminal. It is used to enable
 379// or disable mouse support on the terminal.
 380type MouseMode byte
 381
 382const (
 383	// ButtonMouseMode enables basic mouse button clicks and releases.
 384	ButtonMouseMode MouseMode = 1 << iota
 385	// DragMouseMode enables basic mouse buttons [ButtonMouseMode] as well as
 386	// click-and-drag mouse motion events.
 387	DragMouseMode
 388	// AllMouseMode enables all mouse events including button clicks, releases,
 389	// and all motion events. This inclodes the [ButtonMouseMode] and
 390	// [DragMouseMode] modes.
 391	AllMouseMode
 392)
 393
 394// SetForegroundColor sets the terminal default foreground color.
 395//
 396// Note that this won't take any effect until the next [Terminal.Display] or
 397// [Terminal.Flush] call.
 398func (t *Terminal) SetForegroundColor(c color.Color) {
 399	t.setFg = c
 400	col, ok := colorful.MakeColor(c)
 401	if ok {
 402		t.scr.WriteString(ansi.SetForegroundColor(col.Hex())) //nolint:errcheck
 403	}
 404}
 405
 406// RequestForegroundColor requests the current foreground color of the terminal.
 407//
 408// Note that this won't take any effect until the next [Terminal.Display] or
 409// [Terminal.Flush] call.
 410func (t *Terminal) RequestForegroundColor() {
 411	t.scr.WriteString(ansi.RequestForegroundColor) //nolint:errcheck
 412}
 413
 414// ResetForegroundColor resets the terminal foreground color to the
 415// default color.
 416//
 417// Note that this won't take any effect until the next [Terminal.Display] or
 418// [Terminal.Flush] call.
 419func (t *Terminal) ResetForegroundColor() {
 420	t.setFg = nil
 421	t.scr.WriteString(ansi.ResetForegroundColor) //nolint:errcheck
 422}
 423
 424// SetBackgroundColor sets the terminal default background color.
 425//
 426// Note that this won't take any effect until the next [Terminal.Display] or
 427// [Terminal.Flush] call.
 428func (t *Terminal) SetBackgroundColor(c color.Color) {
 429	t.setBg = c
 430	col, ok := colorful.MakeColor(c)
 431	if ok {
 432		t.scr.WriteString(ansi.SetBackgroundColor(col.Hex())) //nolint:errcheck
 433	}
 434}
 435
 436// RequestBackgroundColor requests the current background color of the terminal.
 437//
 438// Note that this won't take any effect until the next [Terminal.Display] or
 439// [Terminal.Flush] call.
 440func (t *Terminal) RequestBackgroundColor() {
 441	t.scr.WriteString(ansi.RequestBackgroundColor) //nolint:errcheck
 442}
 443
 444// ResetBackgroundColor resets the terminal background color to the
 445// default color.
 446//
 447// Note that this won't take any effect until the next [Terminal.Display] or
 448// [Terminal.Flush] call.
 449func (t *Terminal) ResetBackgroundColor() {
 450	t.setBg = nil
 451	t.scr.WriteString(ansi.ResetBackgroundColor) //nolint:errcheck
 452}
 453
 454// SetCursorColor sets the terminal cursor color.
 455//
 456// Note that this won't take any effect until the next [Terminal.Display] or
 457// [Terminal.Flush] call.
 458func (t *Terminal) SetCursorColor(c color.Color) {
 459	t.setCc = c
 460	col, ok := colorful.MakeColor(c)
 461	if ok {
 462		t.scr.WriteString(ansi.SetCursorColor(col.Hex())) //nolint:errcheck
 463	}
 464}
 465
 466// RequestCursorColor requests the current cursor color of the terminal.
 467//
 468// Note that this won't take any effect until the next [Terminal.Display] or
 469// [Terminal.Flush] call.
 470func (t *Terminal) RequestCursorColor() {
 471	t.scr.WriteString(ansi.RequestCursorColor) //nolint:errcheck
 472}
 473
 474// ResetCursorColor resets the terminal cursor color to the
 475// default color.
 476//
 477// Note that this won't take any effect until the next [Terminal.Display] or
 478// [Terminal.Flush] call.
 479func (t *Terminal) ResetCursorColor() {
 480	t.setCc = nil
 481	t.scr.WriteString(ansi.ResetCursorColor) //nolint:errcheck
 482}
 483
 484// SetCursorShape sets the terminal cursor shape and blinking style.
 485//
 486// Note that this won't take any effect until the next [Terminal.Display] or
 487// [Terminal.Flush] call.
 488func (t *Terminal) SetCursorShape(shape CursorShape, blink bool) {
 489	style := shape.Encode(blink)
 490	t.curStyle = style
 491	t.scr.WriteString(ansi.SetCursorStyle(style)) //nolint:errcheck
 492}
 493
 494// EnableMouse enables mouse support on the terminal.
 495// Calling this without any modes will enable all mouse modes by default.
 496// The available modes are:
 497//   - [ButtonMouseMode] Enables basic mouse button clicks and releases.
 498//   - [DragMouseMode] Enables basic mouse buttons [ButtonMouseMode] as well as
 499//     click-and-drag mouse motion events.
 500//   - [AllMouseMode] Enables all mouse events including button clicks, releases,
 501//     and all motion events. This inclodes the [ButtonMouseMode] and
 502//     [DragMouseMode] modes.
 503//
 504// Note that on Unix, this won't take any effect until the next
 505// [Terminal.Display] or [Terminal.Flush] call.
 506func (t *Terminal) EnableMouse(modes ...MouseMode) {
 507	var mode MouseMode
 508	for _, m := range modes {
 509		mode |= m
 510	}
 511	if len(modes) == 1 {
 512		if mode&AllMouseMode != 0 {
 513			mode |= ButtonMouseMode | DragMouseMode
 514		}
 515		if mode&DragMouseMode != 0 {
 516			mode |= ButtonMouseMode
 517		}
 518	}
 519	if mode == 0 {
 520		mode = ButtonMouseMode | DragMouseMode | AllMouseMode
 521	}
 522	t.mouseMode = mode
 523	if runtime.GOOS != "windows" {
 524		modes := []ansi.Mode{}
 525		if t.mouseMode&AllMouseMode != 0 {
 526			modes = append(modes, ansi.AnyEventMouseMode)
 527		} else if t.mouseMode&DragMouseMode != 0 {
 528			modes = append(modes, ansi.ButtonEventMouseMode)
 529		} else if t.mouseMode&ButtonMouseMode != 0 {
 530			modes = append(modes, ansi.NormalMouseMode)
 531		}
 532		modes = append(modes, ansi.SgrExtMouseMode)
 533		t.EnableMode(modes...)
 534	}
 535	t.enableWindowsMouse() //nolint:errcheck
 536}
 537
 538// DisableMouse disables mouse support on the terminal. This will disable mouse
 539// button and button motion events.
 540//
 541// Note that on Unix, this won't take any effect until the next
 542// [Terminal.Display] or [Terminal.Flush] call.
 543func (t *Terminal) DisableMouse() {
 544	t.mouseMode = 0
 545	if runtime.GOOS != "windows" {
 546		var modes []ansi.Mode
 547		if t.modes.Get(ansi.AnyEventMouseMode).IsSet() {
 548			modes = append(modes, ansi.AnyEventMouseMode)
 549		}
 550		if t.modes.Get(ansi.ButtonEventMouseMode).IsSet() {
 551			modes = append(modes, ansi.ButtonEventMouseMode)
 552		}
 553		if t.modes.Get(ansi.NormalMouseMode).IsSet() {
 554			modes = append(modes, ansi.NormalMouseMode)
 555		}
 556		if t.modes.Get(ansi.SgrExtMouseMode).IsSet() {
 557			modes = append(modes, ansi.SgrExtMouseMode)
 558		}
 559		t.DisableMode(modes...)
 560	}
 561	t.disableWindowsMouse() //nolint:errcheck
 562}
 563
 564// EnableBracketedPaste enables bracketed paste mode on the terminal. This is
 565// typically used to enable support for pasting text into the terminal without
 566// interfering with the terminal's input handling.
 567func (t *Terminal) EnableBracketedPaste() {
 568	t.EnableMode(ansi.BracketedPasteMode)
 569}
 570
 571// DisableBracketedPaste disables bracketed paste mode on the terminal. This is
 572// typically used to disable support for pasting text into the terminal.
 573func (t *Terminal) DisableBracketedPaste() {
 574	t.DisableMode(ansi.BracketedPasteMode)
 575}
 576
 577// EnableFocusEvents enables focus/blur receiving notification events on the
 578// terminal.
 579func (t *Terminal) EnableFocusEvents() {
 580	t.EnableMode(ansi.FocusEventMode)
 581}
 582
 583// DisableFocusEvents disables focus/blur receiving notification events on the
 584// terminal.
 585func (t *Terminal) DisableFocusEvents() {
 586	t.DisableMode(ansi.FocusEventMode)
 587}
 588
 589// EnterAltScreen enters the alternate screen buffer. This is typically used
 590// for applications that want to take over the entire terminal screen.
 591//
 592// The [Terminal] manages the alternate screen buffer for you based on the
 593// [Viewport] used during [Terminal.Display]. This means that you don't need to
 594// call this unless you know what you're doing.
 595//
 596// Note that this won't take any effect until the next [Terminal.Display] or
 597// [Terminal.Flush] call.
 598func (t *Terminal) EnterAltScreen() {
 599	t.enterAltScreen(true)
 600}
 601
 602// cursor indicates whether we want to set the cursor visibility state after
 603// entering the alt screen.
 604// We do this because some terminals maintain a separate cursor visibility
 605// state for the alt screen and the normal screen.
 606func (t *Terminal) enterAltScreen(cursor bool) {
 607	altscreen := t.scr.AltScreen()
 608	t.scr.EnterAltScreen()
 609	if cursor && !altscreen {
 610		if t.scr.CursorHidden() {
 611			t.scr.WriteString(ansi.HideCursor) //nolint:errcheck
 612		} else {
 613			t.scr.WriteString(ansi.ShowCursor) //nolint:errcheck
 614		}
 615	}
 616	t.scr.SetRelativeCursor(false)
 617	t.modes[ansi.AltScreenSaveCursorMode] = ansi.ModeSet
 618}
 619
 620// ExitAltScreen exits the alternate screen buffer and returns to the normal
 621// screen buffer.
 622//
 623// The [Terminal] manages the alternate screen buffer for you based on the
 624// [Viewport] used during [Terminal.Display]. This means that you don't need to
 625// call this unless you know what you're doing.
 626//
 627// Note that this won't take any effect until the next [Terminal.Display] or
 628// [Terminal.Flush] call.
 629func (t *Terminal) ExitAltScreen() {
 630	t.exitAltScreen(true)
 631}
 632
 633// cursor indicates whether we want to set the cursor visibility state after
 634// exiting the alt screen.
 635// We do this because some terminals maintain a separate cursor visibility
 636// state for the alt screen and the normal screen.
 637func (t *Terminal) exitAltScreen(cursor bool) {
 638	altscreen := t.scr.AltScreen()
 639	t.scr.ExitAltScreen()
 640	if cursor && altscreen {
 641		if t.scr.CursorHidden() {
 642			t.scr.WriteString(ansi.HideCursor) //nolint:errcheck
 643		} else {
 644			t.scr.WriteString(ansi.ShowCursor) //nolint:errcheck
 645		}
 646	}
 647	t.scr.SetRelativeCursor(true)
 648	t.modes[ansi.AltScreenSaveCursorMode] = ansi.ModeReset
 649}
 650
 651// ShowCursor shows the terminal cursor.
 652//
 653// The [Terminal] manages the visibility of the cursor for you based on the
 654// [Viewport] used during [Terminal.Display]. This means that you don't need to
 655// call this unless you know what you're doing.
 656//
 657// Note that this won't take any effect until the next [Terminal.Display] or
 658// [Terminal.Flush] call.
 659func (t *Terminal) ShowCursor() {
 660	t.showCursor()
 661}
 662
 663func (t *Terminal) showCursor() {
 664	t.cursorHidden = false
 665	t.scr.ShowCursor()
 666	t.modes[ansi.TextCursorEnableMode] = ansi.ModeSet
 667}
 668
 669// HideCursor hides the terminal cursor.
 670//
 671// The [Terminal] manages the visibility of the cursor for you based on the
 672// [Viewport] used during [Terminal.Display]. This means that you don't need to
 673// call this unless you know what you're doing.
 674//
 675// Note that this won't take any effect until the next [Terminal.Display] or
 676// [Terminal.Flush] call.
 677func (t *Terminal) HideCursor() {
 678	t.hideCursor()
 679}
 680
 681func (t *Terminal) hideCursor() {
 682	t.cursorHidden = true
 683	t.scr.HideCursor()
 684	t.modes[ansi.TextCursorEnableMode] = ansi.ModeReset
 685}
 686
 687// SetTitle sets the title of the terminal window. This is typically used to
 688// set the title of the terminal window to the name of the application.
 689//
 690// Note that this won't take any effect until the next [Terminal.Display] or
 691// [Terminal.Flush] call.
 692func (t *Terminal) SetTitle(title string) {
 693	_, _ = t.scr.WriteString(ansi.SetWindowTitle(title))
 694}
 695
 696// Resize resizes the terminal screen buffer to the given width and height.
 697// This won't affect [Terminal.Size] or the terminal size, but it will resize
 698// the screen buffer used by the terminal.
 699func (t *Terminal) Resize(width, height int) error {
 700	t.buf.Resize(width, height)
 701	t.scr.Resize(width, height)
 702	return nil
 703}
 704
 705// MakeRaw puts the terminal in raw mode, which disables line buffering and
 706// echoing. The terminal will automatically be restored to its original state
 707// on [Terminal.Close] or [Terminal.Shutdown], or by manually calling
 708// [Terminal.Restore].
 709func (t *Terminal) MakeRaw() error {
 710	if err := t.makeRaw(); err != nil {
 711		return fmt.Errorf("error entering raw mode: %w", err)
 712	}
 713	return nil
 714}
 715
 716// Start prepares the terminal for use. It starts the input reader and
 717// initializes the terminal state. This should be called before using the
 718// terminal.
 719func (t *Terminal) Start() error {
 720	if t.started {
 721		return ErrStarted
 722	}
 723
 724	if t.inTty == nil && t.outTty == nil {
 725		return ErrNotTerminal
 726	}
 727
 728	t.winchTty = t.inTty
 729	if t.winchTty == nil {
 730		t.winchTty = t.outTty
 731	}
 732
 733	// Get the initial terminal size.
 734	var err error
 735	t.size.Width, t.size.Height, err = t.GetSize()
 736	if err != nil {
 737		return err
 738	}
 739
 740	if t.buf.Width() == 0 && t.buf.Height() == 0 {
 741		// If the buffer is not initialized, set it to the terminal size.
 742		t.buf.Resize(t.size.Width, t.size.Height)
 743		t.scr.Erase()
 744	}
 745
 746	// We need to call [Terminal.optimizeMovements] before creating the screen
 747	// to populate [Terminal.useBspace] and [Terminal.useTabs].
 748	t.optimizeMovements()
 749	t.configureRenderer()
 750
 751	if t.modes.Get(ansi.AltScreenSaveCursorMode).IsSet() {
 752		t.enterAltScreen(false) //nolint:errcheck
 753	}
 754	if t.cursorHidden == t.modes.Get(ansi.TextCursorEnableMode).IsReset() {
 755		// We always hide the cursor when we start.
 756		t.hideCursor() //nolint:errcheck
 757	}
 758	// Restore terminal modes.
 759	for m, s := range t.modes {
 760		switch m {
 761		case ansi.TextCursorEnableMode, ansi.AltScreenSaveCursorMode:
 762			// These modes are handled by the renderer above.
 763			continue
 764		default:
 765			if s.IsSet() {
 766				t.scr.WriteString(ansi.SetMode(m)) //nolint:errcheck
 767			}
 768		}
 769	}
 770	// Restore fg, bg, cursor colors, and cursor shape.
 771	for _, c := range []struct {
 772		setter func(string) string
 773		colorp *color.Color
 774	}{
 775		{ansi.SetForegroundColor, &t.setFg},
 776		{ansi.SetBackgroundColor, &t.setBg},
 777		{ansi.SetCursorColor, &t.setCc},
 778	} {
 779		if c.colorp != nil && *c.colorp != nil {
 780			col, ok := colorful.MakeColor(*c.colorp)
 781			if ok {
 782				t.scr.WriteString(c.setter(col.Hex())) //nolint:errcheck
 783			}
 784		}
 785	}
 786	if t.curStyle > 1 {
 787		t.scr.WriteString(ansi.SetCursorStyle(t.curStyle)) //nolint:errcheck
 788	}
 789
 790	if err := t.rd.Start(); err != nil {
 791		return fmt.Errorf("error starting terminal: %w", err)
 792	}
 793
 794	recvs := []InputReceiver{t.rd}
 795	if runtime.GOOS != "windows" {
 796		t.wrdr = &WinChReceiver{t.winchTty}
 797		if err := t.wrdr.Start(); err != nil {
 798			return fmt.Errorf("error starting window size receiver: %w", err)
 799		}
 800		recvs = append(recvs, t.wrdr)
 801	}
 802
 803	t.im = NewInputManager(recvs...)
 804	t.started = true
 805
 806	return nil
 807}
 808
 809// Restore restores the terminal to its original state. This can be called
 810// after [Terminal.MakeRaw] to restore the terminal to its original state.
 811// It will also disable any modes that were enabled by the terminal, such as
 812// exiting the alternate screen buffer, showing the cursor, and resetting
 813// terminal modes.
 814//
 815// Most of the time, you don't need to call this manually, as it is called
 816// automatically when the terminal is shutdown or closed using [Terminal.Close]
 817// or [Terminal.Shutdown].
 818func (t *Terminal) Restore() error {
 819	if t.inTtyState != nil {
 820		if err := term.Restore(t.inTty.Fd(), t.inTtyState); err != nil {
 821			return err
 822		}
 823		t.inTtyState = nil
 824	}
 825	if t.outTtyState != nil {
 826		if err := term.Restore(t.outTty.Fd(), t.outTtyState); err != nil {
 827			return err
 828		}
 829		t.outTtyState = nil
 830	}
 831	altscreen := t.modes.Get(ansi.AltScreenSaveCursorMode).IsSet()
 832	cursorHidden := t.modes.Get(ansi.TextCursorEnableMode).IsReset()
 833	if !altscreen {
 834		// Go to the bottom of the screen.
 835		t.scr.MoveTo(0, t.buf.Height()-1)
 836	}
 837	if altscreen {
 838		t.exitAltScreen(false) //nolint:errcheck
 839	}
 840	if cursorHidden {
 841		t.showCursor() //nolint:errcheck
 842		// Override the cursor hidden state so that we can auto hide it again
 843		// when the terminal resumes using [Terminal.Start].
 844		t.cursorHidden = false
 845	}
 846	var buf bytes.Buffer
 847	for m, s := range t.modes {
 848		switch m {
 849		case ansi.TextCursorEnableMode, ansi.AltScreenSaveCursorMode:
 850			// These modes are handled by the renderer.
 851			continue
 852		}
 853		var reset bool
 854		ds, ok := defaultModes[m]
 855		if ok && s != ds {
 856			reset = s.IsSet() != ds.IsSet()
 857		} else {
 858			reset = s.IsSet()
 859		}
 860		if reset {
 861			buf.WriteString(ansi.ResetMode(m))
 862		}
 863	}
 864	if t.setFg != nil {
 865		buf.WriteString(ansi.ResetForegroundColor)
 866	}
 867	if t.setBg != nil {
 868		buf.WriteString(ansi.ResetBackgroundColor)
 869	}
 870	if t.setCc != nil {
 871		buf.WriteString(ansi.ResetCursorColor)
 872	}
 873	if t.curStyle > 1 {
 874		buf.WriteString(ansi.SetCursorStyle(0))
 875	}
 876	if _, err := t.scr.WriteString(buf.String()); err != nil {
 877		return fmt.Errorf("error resetting terminal modes: %w", err)
 878	}
 879	return t.scr.Flush()
 880}
 881
 882// Shutdown restores the terminal to its original state and stops the event
 883// channel in a graceful manner.
 884// This waits for any pending events to be processed or the context to be
 885// done before closing the event channel.
 886func (t *Terminal) Shutdown(ctx context.Context) (rErr error) {
 887	defer func() {
 888		err := t.close(false)
 889		if rErr == nil {
 890			rErr = err
 891		}
 892	}()
 893
 894	// Cancel the input reader.
 895	t.rd.Cancel()
 896
 897	// Consume any pending events or listen for the context to be done.
 898	for {
 899		if len(t.evch) == 0 {
 900			return nil
 901		}
 902		select {
 903		case <-ctx.Done():
 904			return nil
 905		case <-t.evch:
 906		}
 907	}
 908}
 909
 910// close closes any resources used by the terminal. This is typically used to
 911// close the terminal when it is no longer needed. When reset is true, it will
 912// also reset the terminal screen.
 913func (t *Terminal) close(reset bool) (rErr error) {
 914	defer t.closeChannels()
 915
 916	defer func() {
 917		err := t.Restore()
 918		if rErr == nil && err != nil {
 919			rErr = fmt.Errorf("error restoring terminal state: %w", err)
 920		}
 921		if reset {
 922			// Reset screen.
 923			t.scr = NewTerminalRenderer(t.out, t.environ)
 924			t.configureRenderer()
 925		}
 926	}()
 927
 928	defer func() {
 929		err := t.rd.Close()
 930		if rErr == nil && err != nil {
 931			rErr = fmt.Errorf("error closing terminal reader: %w", err)
 932		}
 933	}()
 934
 935	return
 936}
 937
 938// Close close any resources used by the terminal and restore the terminal to
 939// its original state.
 940func (t *Terminal) Close() error {
 941	return t.close(true)
 942}
 943
 944// closeChannels closes the event and error channels.
 945func (t *Terminal) closeChannels() {
 946	t.once.Do(func() {
 947		close(t.evch)
 948	})
 949}
 950
 951// SendEvent is a helper function to send an event to the event channel. It
 952// blocks until the event is sent or the context is done. If the context is
 953// done, it will not send the event and will return immediately.
 954// This is useful to control the terminal from outside the event loop.
 955func (t *Terminal) SendEvent(ctx context.Context, ev Event) {
 956	select {
 957	case <-ctx.Done():
 958	case t.evch <- ev:
 959	}
 960}
 961
 962// Events returns an event channel that will receive events from the terminal.
 963// Use [Terminal.Err] to check for errors that occurred while receiving events.
 964// The event channel is closed when the terminal is closed or when the context
 965// is done.
 966func (t *Terminal) Events(ctx context.Context) <-chan Event {
 967	// Start receiving events from the terminal if it hasn't been started yet.
 968	t.evOnce.Do(func() {
 969		evch := make(chan Event)
 970		// Intercept useful events like window size changes.
 971		go func() {
 972			for ev := range evch {
 973				switch ev := ev.(type) {
 974				case WindowSizeEvent:
 975					t.size.Width = ev.Width
 976					t.size.Height = ev.Height
 977				}
 978				select {
 979				case <-ctx.Done():
 980					return
 981				case t.evch <- ev:
 982				}
 983			}
 984		}()
 985		go func() {
 986			defer t.closeChannels()
 987
 988			t.err = t.im.ReceiveEvents(ctx, evch)
 989			if errors.Is(t.err, io.EOF) || errors.Is(t.err, cancelreader.ErrCanceled) {
 990				t.err = nil
 991			}
 992			close(evch) // Close the event channel when done.
 993		}()
 994	})
 995	return t.evch
 996}
 997
 998// Err returns the error that occurred while receiving events from the
 999// terminal.
1000// This is typically used to check for errors that occurred while
1001// receiving events.
1002func (t *Terminal) Err() error {
1003	return t.err
1004}
1005
1006// PrependString adds the given string to the top of the terminal screen. The
1007// string is split into lines and each line is added as a new line at the top
1008// of the screen. The added lines are not managed by the terminal and will not
1009// be cleared or updated by the [Terminal].
1010//
1011// This will truncate each line to the terminal width, so if the string is
1012// longer than the terminal width, it will be truncated to fit.
1013//
1014// Using this when the terminal is using the alternate screen or when occupying
1015// the whole screen may not produce any visible effects. This is because once
1016// the terminal writes the prepended lines, they will get overwritten by the
1017// next frame.
1018//
1019// Note that this won't take any effect until the next [Terminal.Display] or
1020// [Terminal.Flush] call.
1021func (t *Terminal) PrependString(str string) error {
1022	// We truncate the string to the terminal width.
1023	var sb strings.Builder
1024	lines := strings.Split(str, "\n")
1025	for i, line := range lines {
1026		if ansi.StringWidth(line) > t.size.Width {
1027			sb.WriteString(ansi.Truncate(line, t.size.Width, ""))
1028		} else {
1029			sb.WriteString(line)
1030		}
1031		if i < len(lines)-1 {
1032			sb.WriteByte('\n')
1033		}
1034	}
1035
1036	t.scr.PrependString(sb.String())
1037	return nil
1038}
1039
1040// PrependLines adds lines of cells to the top of the terminal screen. The
1041// added line is unmanaged and will not be cleared or updated by the
1042// [Terminal].
1043//
1044// This will truncate each line to the terminal width, so if the string is
1045// longer than the terminal width, it will be truncated to fit.
1046//
1047// Using this when the terminal is using the alternate screen or when occupying
1048// the whole screen may not produce any visible effects. This is because once
1049// the terminal writes the prepended lines, they will get overwritten by the
1050// next frame.
1051func (t *Terminal) PrependLines(lines ...Line) error {
1052	truncatedLines := make([]Line, 0, len(lines))
1053	for _, l := range lines {
1054		// We truncate the line to the terminal width.
1055		if len(l) > t.size.Width {
1056			truncatedLines = append(truncatedLines, l[:t.size.Width])
1057		} else {
1058			truncatedLines = append(truncatedLines, l)
1059		}
1060	}
1061
1062	t.scr.PrependLines(truncatedLines...)
1063	return nil
1064}
1065
1066// Write writes the given bytes to the underlying terminal renderer.
1067// This is typically used to write arbitrary data to the terminal, usually
1068// escape sequences or control characters.
1069//
1070// This can affect the renderer state and the terminal screen, so it should be
1071// used with caution.
1072//
1073// Note that this won't take any effect until the next [Terminal.Display] or
1074// [Terminal.Flush] call.
1075func (t *Terminal) Write(p []byte) (n int, err error) {
1076	return t.scr.Write(p)
1077}
1078
1079// WriteString writes the given string to the underlying terminal renderer.
1080// This is typically used to write arbitrary data to the terminal, usually
1081// escape sequences or control characters.
1082//
1083// This can affect the renderer state and the terminal screen, so it should be
1084// used with caution.
1085//
1086// Note that this won't take any effect until the next [Terminal.Display] or
1087// [Terminal.Flush] call.
1088func (t *Terminal) WriteString(s string) (n int, err error) {
1089	return t.scr.WriteString(s)
1090}