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