1package tea
2
3import (
4 "bytes"
5 "image/color"
6 "io"
7 "strings"
8 "sync"
9
10 "github.com/charmbracelet/colorprofile"
11 uv "github.com/charmbracelet/ultraviolet"
12 "github.com/charmbracelet/x/ansi"
13)
14
15// standardRenderer is a framerate-based terminal renderer, updating the view
16// at a given framerate to avoid overloading the terminal emulator.
17//
18// In cases where very high performance is needed the renderer can be told
19// to exclude ranges of lines, allowing them to be written to directly.
20type standardRenderer struct {
21 mtx *sync.Mutex
22 out *colorprofile.Writer
23
24 buf bytes.Buffer
25 queuedMessageLines []string
26 done chan struct{}
27 lastRender string
28 lastRenderedLines []string
29 linesRendered int
30 altLinesRendered int
31
32 // cursor visibility state
33 cursorHidden bool
34
35 // essentially whether or not we're using the full size of the terminal
36 altScreenActive bool
37
38 // renderer dimensions; usually the size of the window
39 width int
40 height int
41
42 // lines explicitly set not to render
43 ignoreLines map[int]struct{}
44}
45
46// newRenderer creates a new renderer. Normally you'll want to initialize it
47// with os.Stdout as the first argument.
48func newRenderer(out io.Writer) renderer {
49 r := &standardRenderer{
50 out: &colorprofile.Writer{
51 Forward: out,
52 },
53 mtx: &sync.Mutex{},
54 done: make(chan struct{}),
55 queuedMessageLines: []string{},
56 }
57 return r
58}
59
60// setColorProfile sets the color profile.
61func (r *standardRenderer) setColorProfile(p colorprofile.Profile) {
62 r.mtx.Lock()
63 r.out.Profile = p
64 r.mtx.Unlock()
65}
66
67// reset resets the renderer to its initial state.
68func (r *standardRenderer) reset() {
69 // no-op
70}
71
72// close closes the renderer and flushes any remaining data.
73func (r *standardRenderer) close() error {
74 // flush locks the mutex
75 _ = r.flush(nil)
76
77 r.mtx.Lock()
78 defer r.mtx.Unlock()
79
80 r.execute(ansi.EraseEntireLine)
81 // Move the cursor back to the beginning of the line
82 r.execute("\r")
83
84 return nil
85}
86
87// execute writes a sequence to the terminal.
88func (r *standardRenderer) execute(seq string) {
89 _, _ = io.WriteString(r.out, seq)
90}
91
92// writeString writes a string to the internal buffer.
93func (r *standardRenderer) writeString(s string) (int, error) {
94 r.mtx.Lock()
95 defer r.mtx.Unlock()
96
97 return r.buf.WriteString(s)
98}
99
100// flush renders the buffer.
101func (r *standardRenderer) flush(*Program) error {
102 r.mtx.Lock()
103 defer r.mtx.Unlock()
104
105 if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
106 // Nothing to do.
107 return nil
108 }
109
110 // Output buffer.
111 buf := &bytes.Buffer{}
112
113 // Moving to the beginning of the section, that we rendered.
114 if r.altScreenActive {
115 buf.WriteString(ansi.CursorHomePosition)
116 } else if r.linesRendered > 1 {
117 buf.WriteString(ansi.CursorUp(r.linesRendered - 1))
118 }
119
120 newLines := strings.Split(r.buf.String(), "\n")
121
122 // If we know the output's height, we can use it to determine how many
123 // lines we can render. We drop lines from the top of the render buffer if
124 // necessary, as we can't navigate the cursor into the terminal's scrollback
125 // buffer.
126 if r.height > 0 && len(newLines) > r.height {
127 newLines = newLines[len(newLines)-r.height:]
128 }
129
130 flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive
131
132 if flushQueuedMessages {
133 // Dump the lines we've queued up for printing.
134 for _, line := range r.queuedMessageLines {
135 if ansi.StringWidth(line) < r.width {
136 // We only erase the rest of the line when the line is shorter than
137 // the width of the terminal. When the cursor reaches the end of
138 // the line, any escape sequences that follow will only affect the
139 // last cell of the line.
140
141 // Removing previously rendered content at the end of line.
142 line = line + ansi.EraseLineRight
143 }
144
145 _, _ = buf.WriteString(line)
146 _, _ = buf.WriteString("\r\n")
147 }
148 // Clear the queued message lines.
149 r.queuedMessageLines = []string{}
150 }
151
152 // Paint new lines.
153 for i := range newLines {
154 canSkip := !flushQueuedMessages && // Queuing messages triggers repaint -> we don't have access to previous frame content.
155 len(r.lastRenderedLines) > i && r.lastRenderedLines[i] == newLines[i] // Previously rendered line is the same.
156
157 if _, ignore := r.ignoreLines[i]; ignore || canSkip {
158 // Unless this is the last line, move the cursor down.
159 if i < len(newLines)-1 {
160 buf.WriteByte('\n')
161 }
162 continue
163 }
164
165 if i == 0 && r.lastRender == "" {
166 // On first render, reset the cursor to the start of the line
167 // before writing anything.
168 buf.WriteByte('\r')
169 }
170
171 line := newLines[i]
172
173 // Truncate lines wider than the width of the window to avoid
174 // wrapping, which will mess up rendering. If we don't have the
175 // width of the window this will be ignored.
176 //
177 // Note that on Windows we only get the width of the window on
178 // program initialization, so after a resize this won't perform
179 // correctly (signal SIGWINCH is not supported on Windows).
180 if r.width > 0 {
181 line = ansi.Truncate(line, r.width, "")
182 }
183
184 if ansi.StringWidth(line) < r.width {
185 // We only erase the rest of the line when the line is shorter than
186 // the width of the terminal. When the cursor reaches the end of
187 // the line, any escape sequences that follow will only affect the
188 // last cell of the line.
189
190 // Removing previously rendered content at the end of line.
191 line = line + ansi.EraseLineRight
192 }
193
194 _, _ = buf.WriteString(line)
195
196 if i < len(newLines)-1 {
197 _, _ = buf.WriteString("\r\n")
198 }
199 }
200
201 // Clearing left over content from last render.
202 if r.lastLinesRendered() > len(newLines) {
203 buf.WriteString(ansi.EraseScreenBelow)
204 }
205
206 if r.altScreenActive {
207 r.altLinesRendered = len(newLines)
208 } else {
209 r.linesRendered = len(newLines)
210 }
211
212 // Make sure the cursor is at the start of the last line to keep rendering
213 // behavior consistent.
214 if r.altScreenActive {
215 // This case fixes a bug in macOS terminal. In other terminals the
216 // other case seems to do the job regardless of whether or not we're
217 // using the full terminal window.
218 buf.WriteString(ansi.CursorPosition(0, len(newLines)))
219 } else {
220 buf.WriteString(ansi.CursorBackward(r.width))
221 }
222
223 _, _ = r.out.Write(buf.Bytes())
224 r.lastRender = r.buf.String()
225
226 // Save previously rendered lines for comparison in the next render. If we
227 // don't do this, we can't skip rendering lines that haven't changed.
228 // See https://github.com/charmbracelet/bubbletea/pull/1233
229 r.lastRenderedLines = newLines
230 r.buf.Reset()
231
232 return nil
233}
234
235// lastLinesRendered returns the number of lines rendered lastly.
236func (r *standardRenderer) lastLinesRendered() int {
237 if r.altScreenActive {
238 return r.altLinesRendered
239 }
240 return r.linesRendered
241}
242
243// write writes to the internal buffer. The buffer will be outputted via the
244// ticker which calls flush().
245func (r *standardRenderer) render(v View) {
246 r.mtx.Lock()
247 defer r.mtx.Unlock()
248 r.buf.Reset()
249
250 area := uv.Rect(0, 0, r.width, r.height)
251 if b, ok := v.Layer.(interface{ Bounds() uv.Rectangle }); ok {
252 if !r.altScreenActive {
253 area.Max.Y = b.Bounds().Max.Y
254 }
255 }
256
257 buf := uv.NewScreenBuffer(area.Dx(), area.Dy())
258 v.Layer.Draw(buf, area)
259 s := buf.Render()
260
261 // If an empty string was passed we should clear existing output and
262 // rendering nothing. Rather than introduce additional state to manage
263 // this, we render a single space as a simple (albeit less correct)
264 // solution.
265 if s == "" {
266 s = " "
267 }
268
269 _, _ = r.buf.WriteString(s)
270}
271
272func (r *standardRenderer) repaint() {
273 r.lastRender = ""
274 r.lastRenderedLines = nil
275}
276
277func (r *standardRenderer) clearScreen() {
278 r.mtx.Lock()
279 defer r.mtx.Unlock()
280
281 r.execute(ansi.EraseEntireScreen)
282 r.execute(ansi.CursorHomePosition)
283
284 r.repaint()
285}
286
287func (r *standardRenderer) enterAltScreen() {
288 r.mtx.Lock()
289 defer r.mtx.Unlock()
290
291 if r.altScreenActive {
292 return
293 }
294
295 r.altScreenActive = true
296 r.execute(ansi.SetAltScreenSaveCursorMode)
297
298 // Ensure that the terminal is cleared, even when it doesn't support
299 // alt screen (or alt screen support is disabled, like GNU screen by
300 // default).
301 //
302 // Note: we can't use r.clearScreen() here because the mutex is already
303 // locked.
304 r.execute(ansi.EraseEntireScreen)
305 r.execute(ansi.CursorHomePosition)
306
307 // cmd.exe and other terminals keep separate cursor states for the AltScreen
308 // and the main buffer. We have to explicitly reset the cursor visibility
309 // whenever we enter AltScreen.
310 if r.cursorHidden {
311 r.execute(ansi.HideCursor)
312 } else {
313 r.execute(ansi.ShowCursor)
314 }
315
316 // Entering the alt screen resets the lines rendered count.
317 r.altLinesRendered = 0
318
319 r.repaint()
320}
321
322func (r *standardRenderer) exitAltScreen() {
323 r.mtx.Lock()
324 defer r.mtx.Unlock()
325
326 if !r.altScreenActive {
327 return
328 }
329
330 r.altScreenActive = false
331 r.execute(ansi.ResetAltScreenSaveCursorMode)
332
333 // cmd.exe and other terminals keep separate cursor states for the AltScreen
334 // and the main buffer. We have to explicitly reset the cursor visibility
335 // whenever we exit AltScreen.
336 if r.cursorHidden {
337 r.execute(ansi.HideCursor)
338 } else {
339 r.execute(ansi.ShowCursor)
340 }
341
342 r.repaint()
343}
344
345func (r *standardRenderer) showCursor() {
346 r.mtx.Lock()
347 defer r.mtx.Unlock()
348
349 r.cursorHidden = false
350 r.execute(ansi.ShowCursor)
351}
352
353func (r *standardRenderer) hideCursor() {
354 r.mtx.Lock()
355 defer r.mtx.Unlock()
356
357 r.cursorHidden = true
358 r.execute(ansi.HideCursor)
359}
360
361func (r *standardRenderer) resize(width, height int) {
362 r.mtx.Lock()
363 defer r.mtx.Unlock()
364
365 r.width = width
366 r.height = height
367 r.repaint()
368}
369
370func (r *standardRenderer) insertAbove(s string) {
371 if !r.altScreenActive {
372 lines := strings.Split(s, "\n")
373 r.mtx.Lock()
374 r.queuedMessageLines = append(r.queuedMessageLines, lines...)
375 r.repaint()
376 r.mtx.Unlock()
377 }
378}
379
380func (r *standardRenderer) setCursorColor(c color.Color) {}
381func (r *standardRenderer) setForegroundColor(c color.Color) {}
382func (r *standardRenderer) setBackgroundColor(c color.Color) {}
383func (r *standardRenderer) setWindowTitle(s string) {}
384func (r *standardRenderer) hit(MouseMsg) []Msg { return nil }
385func (r *standardRenderer) resetLinesRendered() {
386 r.linesRendered = 0
387}