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}