tty.go

  1package tea
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7	"time"
  8
  9	uv "github.com/charmbracelet/ultraviolet"
 10	"github.com/charmbracelet/x/ansi"
 11	"github.com/charmbracelet/x/term"
 12	"github.com/muesli/cancelreader"
 13)
 14
 15func (p *Program) suspend() {
 16	if err := p.releaseTerminal(true); err != nil {
 17		// If we can't release input, abort.
 18		return
 19	}
 20
 21	suspendProcess()
 22
 23	_ = p.RestoreTerminal()
 24	go p.Send(ResumeMsg{})
 25}
 26
 27func (p *Program) initTerminal() error {
 28	if !hasView(p.initialModel) {
 29		// No need to initialize the terminal if we're not rendering
 30		return nil
 31	}
 32
 33	return p.initInput()
 34}
 35
 36// restoreTerminalState restores the terminal to the state prior to running the
 37// Bubble Tea program.
 38func (p *Program) restoreTerminalState() error {
 39	// We don't need to reset [ansi.AltScreenSaveCursorMode] and
 40	// [ansi.TextCursorEnableMode] because they are automatically reset when we
 41	// close the renderer. See [screenRenderer.close] and
 42	// [cellbuf.Screen.Close].
 43
 44	if p.modes.IsSet(ansi.BracketedPasteMode) {
 45		p.execute(ansi.ResetBracketedPasteMode)
 46	}
 47
 48	btnEvents := p.modes.IsSet(ansi.ButtonEventMouseMode)
 49	allEvents := p.modes.IsSet(ansi.AnyEventMouseMode)
 50	if btnEvents || allEvents {
 51		if btnEvents {
 52			p.execute(ansi.ResetButtonEventMouseMode)
 53		}
 54		if allEvents {
 55			p.execute(ansi.ResetAnyEventMouseMode)
 56		}
 57		p.execute(ansi.ResetSgrExtMouseMode)
 58	}
 59	if p.activeEnhancements.modifyOtherKeys != 0 {
 60		p.execute(ansi.ResetModifyOtherKeys)
 61	}
 62	if p.activeEnhancements.kittyFlags != 0 {
 63		p.execute(ansi.DisableKittyKeyboard)
 64	}
 65	if p.modes.IsSet(ansi.FocusEventMode) {
 66		p.execute(ansi.ResetFocusEventMode)
 67	}
 68	if p.modes.IsSet(ansi.GraphemeClusteringMode) {
 69		p.execute(ansi.ResetGraphemeClusteringMode)
 70	}
 71
 72	// Restore terminal colors.
 73	if p.setBg != nil {
 74		p.execute(ansi.ResetBackgroundColor)
 75	}
 76	if p.setFg != nil {
 77		p.execute(ansi.ResetForegroundColor)
 78	}
 79	if p.setCc != nil {
 80		p.execute(ansi.ResetCursorColor)
 81	}
 82
 83	// Flush queued commands.
 84	_ = p.flush()
 85
 86	return p.restoreInput()
 87}
 88
 89// restoreInput restores the tty input to its original state.
 90func (p *Program) restoreInput() error {
 91	if p.ttyInput != nil && p.previousTtyInputState != nil {
 92		if err := term.Restore(p.ttyInput.Fd(), p.previousTtyInputState); err != nil {
 93			return fmt.Errorf("bubbletea: error restoring console: %w", err)
 94		}
 95	}
 96	if p.ttyOutput != nil && p.previousOutputState != nil {
 97		if err := term.Restore(p.ttyOutput.Fd(), p.previousOutputState); err != nil {
 98			return fmt.Errorf("bubbletea: error restoring console: %w", err)
 99		}
100	}
101	return nil
102}
103
104// initInputReader (re)commences reading inputs.
105func (p *Program) initInputReader(cancel bool) error {
106	if cancel && p.inputReader != nil {
107		p.inputReader.Cancel()
108		p.waitForReadLoop()
109	}
110
111	term := p.environ.Getenv("TERM")
112
113	// Initialize the input reader.
114	// This need to be done after the terminal has been initialized and set to
115	// raw mode.
116
117	drv := uv.NewTerminalReader(p.input, term)
118	drv.SetLogger(p.logger)
119	if p.mouseMode {
120		mouseMode := uv.ButtonMouseMode | uv.DragMouseMode | uv.AllMouseMode
121		drv.MouseMode = &mouseMode
122	}
123	p.inputReader = drv
124	p.readLoopDone = make(chan struct{})
125	if err := p.inputReader.Start(); err != nil {
126		return fmt.Errorf("bubbletea: error starting input reader: %w", err)
127	}
128
129	go p.readLoop()
130
131	return nil
132}
133
134func (p *Program) readLoop() {
135	defer close(p.readLoopDone)
136
137	err := p.inputReader.ReceiveEvents(p.ctx, p.msgs)
138	if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) {
139		select {
140		case <-p.ctx.Done():
141		case p.errs <- err:
142		}
143	}
144}
145
146// waitForReadLoop waits for the cancelReader to finish its read loop.
147func (p *Program) waitForReadLoop() {
148	select {
149	case <-p.readLoopDone:
150	case <-time.After(500 * time.Millisecond): //nolint:mnd
151		// The read loop hangs, which means the input
152		// cancelReader's cancel function has returned true even
153		// though it was not able to cancel the read.
154	}
155}
156
157// checkResize detects the current size of the output and informs the program
158// via a WindowSizeMsg.
159func (p *Program) checkResize() {
160	if p.ttyOutput == nil {
161		// can't query window size
162		return
163	}
164
165	w, h, err := term.GetSize(p.ttyOutput.Fd())
166	if err != nil {
167		select {
168		case <-p.ctx.Done():
169		case p.errs <- err:
170		}
171
172		return
173	}
174
175	var resizeMsg WindowSizeMsg
176	p.width, p.height = w, h
177	resizeMsg.Width, resizeMsg.Height = w, h
178	p.Send(resizeMsg)
179}