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}