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}