1package tea
2
3import (
4 "fmt"
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 "github.com/lucasb-eyer/go-colorful"
14)
15
16type cursedRenderer struct {
17 w io.Writer
18 scr *uv.TerminalRenderer
19 buf uv.ScreenBuffer
20 lastFrame *string
21 lastCur *Cursor
22 env []string
23 term string // the terminal type $TERM
24 width, height int
25 lastFrameHeight int // the height of the last rendered frame, used to determine if we need to resize the screen buffer
26 mu sync.Mutex
27 profile colorprofile.Profile
28 cursor Cursor
29 method ansi.Method
30 logger uv.Logger
31 layer Layer // the last rendered layer
32 setCc, setFg, setBg color.Color
33 windowTitleSet string // the last set window title
34 windowTitle string // the desired title of the terminal window
35 altScreen bool
36 cursorHidden bool
37 hardTabs bool // whether to use hard tabs to optimize cursor movements
38 backspace bool // whether to use backspace to optimize cursor movements
39 mapnl bool
40}
41
42var _ renderer = &cursedRenderer{}
43
44func newCursedRenderer(w io.Writer, env []string, width, height int, hardTabs, backspace, mapnl bool, logger uv.Logger) (s *cursedRenderer) {
45 s = new(cursedRenderer)
46 s.w = w
47 s.env = env
48 s.term = uv.Environ(env).Getenv("TERM")
49 s.logger = logger
50 s.hardTabs = hardTabs
51 s.backspace = backspace
52 s.mapnl = mapnl
53 s.width, s.height = width, height // This needs to happen before [cursedRenderer.reset].
54 s.buf = uv.NewScreenBuffer(s.width, s.height)
55 reset(s)
56 return
57}
58
59// close implements renderer.
60func (s *cursedRenderer) close() (err error) {
61 s.mu.Lock()
62 defer s.mu.Unlock()
63
64 // Go to the bottom of the screen.
65 s.scr.MoveTo(0, s.buf.Height()-1)
66
67 // Exit the altScreen and show cursor before closing. It's important that
68 // we don't change the [cursedRenderer] altScreen and cursorHidden states
69 // so that we can restore them when we start the renderer again. This is
70 // used when the user suspends the program and then resumes it.
71 if s.altScreen {
72 s.scr.ExitAltScreen()
73 }
74 if s.cursorHidden {
75 s.scr.ShowCursor()
76 s.cursorHidden = false
77 }
78
79 if err := s.scr.Flush(); err != nil {
80 return fmt.Errorf("bubbletea: error closing screen writer: %w", err)
81 }
82
83 x, y := s.scr.Position()
84
85 // We want to clear the renderer state but not the cursor position. This is
86 // because we might be putting the tea process in the background, run some
87 // other process, and then return to the tea process. We want to keep the
88 // cursor position so that we can continue where we left off.
89 reset(s)
90 s.scr.SetPosition(x, y)
91
92 // Reset cursor style state so that we can restore it again when we start
93 // the renderer again.
94 s.cursor = Cursor{}
95
96 return nil
97}
98
99// writeString implements renderer.
100func (s *cursedRenderer) writeString(str string) (int, error) {
101 s.mu.Lock()
102 defer s.mu.Unlock()
103
104 return s.scr.WriteString(str)
105}
106
107// resetLinesRendered implements renderer.
108func (s *cursedRenderer) resetLinesRendered() {
109 s.mu.Lock()
110 defer s.mu.Unlock()
111
112 if !s.altScreen {
113 var frameHeight int
114 if s.lastFrame != nil {
115 frameHeight = strings.Count(*s.lastFrame, "\n") + 1
116 }
117
118 io.WriteString(s.w, strings.Repeat("\n", max(0, frameHeight-1))) //nolint:errcheck,gosec
119 }
120}
121
122// flush implements renderer.
123func (s *cursedRenderer) flush(p *Program) error {
124 s.mu.Lock()
125 defer s.mu.Unlock()
126
127 // Set window title.
128 if s.windowTitle != s.windowTitleSet {
129 _, _ = s.scr.WriteString(ansi.SetWindowTitle(s.windowTitle))
130 s.windowTitleSet = s.windowTitle
131 }
132 // Set terminal colors.
133 for _, c := range []struct {
134 rendererColor *color.Color
135 programColor *color.Color
136 reset string
137 setter func(string) string
138 }{
139 {rendererColor: &s.setCc, programColor: &p.setCc, reset: ansi.ResetCursorColor, setter: ansi.SetCursorColor},
140 {rendererColor: &s.setFg, programColor: &p.setFg, reset: ansi.ResetForegroundColor, setter: ansi.SetForegroundColor},
141 {rendererColor: &s.setBg, programColor: &p.setBg, reset: ansi.ResetBackgroundColor, setter: ansi.SetBackgroundColor},
142 } {
143 if *c.rendererColor != *c.programColor {
144 if *c.rendererColor == nil {
145 // Reset the color if it was set to nil.
146 _, _ = s.scr.WriteString(c.reset)
147 } else {
148 // Set the color.
149 col, ok := colorful.MakeColor(*c.rendererColor)
150 if ok {
151 _, _ = s.scr.WriteString(c.setter(col.Hex()))
152 }
153 }
154 *c.programColor = *c.rendererColor
155 }
156 }
157
158 if s.lastCur != nil {
159 if s.lastCur.Shape != s.cursor.Shape || s.lastCur.Blink != s.cursor.Blink {
160 cursorStyle := encodeCursorStyle(s.lastCur.Shape, s.lastCur.Blink)
161 _, _ = s.scr.WriteString(ansi.SetCursorStyle(cursorStyle))
162 s.cursor.Shape = s.lastCur.Shape
163 s.cursor.Blink = s.lastCur.Blink
164 }
165 if s.lastCur.Color != s.cursor.Color {
166 seq := ansi.ResetCursorColor
167 if s.lastCur.Color != nil {
168 c, ok := colorful.MakeColor(s.lastCur.Color)
169 if ok {
170 seq = ansi.SetCursorColor(c.Hex())
171 }
172 }
173 _, _ = s.scr.WriteString(seq)
174 s.cursor.Color = s.lastCur.Color
175 }
176 }
177
178 // Render and queue changes to the screen buffer.
179 s.scr.Render(s.buf.Buffer)
180 if s.lastCur != nil {
181 // MoveTo must come after [uv.TerminalRenderer.Render] because the
182 // cursor position might get updated during rendering.
183 s.scr.MoveTo(s.lastCur.X, s.lastCur.Y)
184 s.cursor.Position = s.lastCur.Position
185 }
186
187 if err := s.scr.Flush(); err != nil {
188 return fmt.Errorf("bubbletea: error flushing screen writer: %w", err)
189 }
190 return nil
191}
192
193// render implements renderer.
194func (s *cursedRenderer) render(v View) {
195 s.mu.Lock()
196 defer s.mu.Unlock()
197
198 frameArea := uv.Rect(0, 0, s.width, s.height)
199 if v.Layer == nil {
200 // If the component is nil, we should clear the screen buffer.
201 frameArea.Max.Y = 0
202 }
203
204 if !s.altScreen {
205 // Inline mode resizes the screen based on the frame height and
206 // terminal width. This is because the frame height can change based on
207 // the content of the frame. For example, if the frame contains a list
208 // of items, the height of the frame will be the number of items in the
209 // list. This is different from the alt screen buffer, which has a
210 // fixed height and width.
211 switch l := v.Layer.(type) {
212 case *uv.StyledString:
213 frameArea.Max.Y = l.Height()
214 case interface{ Bounds() uv.Rectangle }:
215 frameArea.Max.Y = l.Bounds().Dy()
216 }
217
218 // Resize the screen buffer to match the frame area. This is necessary
219 // to ensure that the screen buffer is the same size as the frame area
220 // and to avoid rendering issues when the frame area is smaller than
221 // the screen buffer.
222 s.buf.Resize(frameArea.Dx(), frameArea.Dy())
223 }
224 // Clear our screen buffer before copying the new frame into it to ensure
225 // we erase any old content.
226 s.buf.Clear()
227 if v.Layer != nil {
228 v.Layer.Draw(s.buf, frameArea)
229 }
230
231 frame := s.buf.Render()
232
233 // If an empty string was passed we should clear existing output and
234 // rendering nothing. Rather than introduce additional state to manage
235 // this, we render a single space as a simple (albeit less correct)
236 // solution.
237 if frame == "" {
238 frame = " "
239 }
240
241 cur := v.Cursor
242
243 s.windowTitle = v.WindowTitle
244
245 // Ensure we have any desired terminal colors set.
246 s.setBg = v.BackgroundColor
247 s.setFg = v.ForegroundColor
248 if cur != nil {
249 s.setCc = cur.Color
250 }
251 if s.lastFrame != nil && frame == *s.lastFrame &&
252 (s.lastCur == nil && cur == nil || s.lastCur != nil && cur != nil && *s.lastCur == *cur) {
253 return
254 }
255
256 s.layer = v.Layer
257 s.lastCur = cur
258 s.lastFrameHeight = frameArea.Dy()
259
260 // Cache the last rendered frame so we can avoid re-rendering it if
261 // the frame hasn't changed.
262 lastFrame := frame
263 s.lastFrame = &lastFrame
264
265 if cur == nil {
266 enableTextCursor(s, false)
267 } else {
268 enableTextCursor(s, true)
269 }
270}
271
272// hit implements renderer.
273func (s *cursedRenderer) hit(mouse MouseMsg) []Msg {
274 s.mu.Lock()
275 defer s.mu.Unlock()
276
277 if s.layer != nil {
278 if h, ok := s.layer.(Hittable); ok {
279 m := mouse.Mouse()
280 if id := h.Hit(m.X, m.Y); id != "" {
281 return []Msg{LayerHitMsg{
282 ID: id,
283 Mouse: mouse,
284 }}
285 }
286 }
287 }
288
289 return []Msg{}
290}
291
292// setCursorColor implements renderer.
293func (s *cursedRenderer) setCursorColor(c color.Color) {
294 s.mu.Lock()
295 s.setCc = c
296 s.mu.Unlock()
297}
298
299// setForegroundColor implements renderer.
300func (s *cursedRenderer) setForegroundColor(c color.Color) {
301 s.mu.Lock()
302 s.setFg = c
303 s.mu.Unlock()
304}
305
306// setBackgroundColor implements renderer.
307func (s *cursedRenderer) setBackgroundColor(c color.Color) {
308 s.mu.Lock()
309 s.setBg = c
310 s.mu.Unlock()
311}
312
313// setWindowTitle implements renderer.
314func (s *cursedRenderer) setWindowTitle(title string) {
315 s.mu.Lock()
316 s.windowTitle = title
317 s.mu.Unlock()
318}
319
320// reset implements renderer.
321func (s *cursedRenderer) reset() {
322 s.mu.Lock()
323 reset(s)
324 s.mu.Unlock()
325}
326
327func reset(s *cursedRenderer) {
328 scr := uv.NewTerminalRenderer(s.w, s.env)
329 scr.SetColorProfile(s.profile)
330 scr.SetRelativeCursor(!s.altScreen)
331 scr.SetTabStops(s.width)
332 scr.SetBackspace(s.backspace)
333 scr.SetMapNewline(s.mapnl)
334 scr.SetLogger(s.logger)
335 if s.altScreen {
336 scr.EnterAltScreen()
337 } else {
338 scr.ExitAltScreen()
339 }
340 if !s.cursorHidden {
341 scr.ShowCursor()
342 } else {
343 scr.HideCursor()
344 }
345 s.scr = scr
346}
347
348// setColorProfile implements renderer.
349func (s *cursedRenderer) setColorProfile(p colorprofile.Profile) {
350 s.mu.Lock()
351 s.profile = p
352 s.scr.SetColorProfile(p)
353 s.mu.Unlock()
354}
355
356// resize implements renderer.
357func (s *cursedRenderer) resize(w, h int) {
358 s.mu.Lock()
359 if s.altScreen || w != s.width {
360 // We need to mark the screen for clear to force a redraw. However, we
361 // only do so if we're using alt screen or the width has changed.
362 // That's because redrawing is expensive and we can avoid it if the
363 // width hasn't changed in inline mode. On the other hand, when using
364 // alt screen mode, we always want to redraw because some terminals
365 // would scroll the screen and our content would be lost.
366 s.scr.Erase()
367 }
368 if s.altScreen {
369 s.buf.Resize(w, h)
370 }
371
372 // We need to reset the touched lines buffer to match the new height.
373 s.buf.Touched = nil
374
375 s.scr.Resize(s.width, s.height)
376 s.width, s.height = w, h
377 repaint(s)
378 s.mu.Unlock()
379}
380
381// clearScreen implements renderer.
382func (s *cursedRenderer) clearScreen() {
383 s.mu.Lock()
384 // Move the cursor to the top left corner of the screen and trigger a full
385 // screen redraw.
386 _, _ = s.scr.WriteString(ansi.CursorHomePosition)
387 s.scr.Redraw(s.buf.Buffer) // force redraw
388 repaint(s)
389 s.mu.Unlock()
390}
391
392// enableAltScreen sets the alt screen mode.
393func enableAltScreen(s *cursedRenderer, enable bool) {
394 s.altScreen = enable
395 if enable {
396 s.scr.EnterAltScreen()
397 } else {
398 s.scr.ExitAltScreen()
399 }
400 s.scr.SetRelativeCursor(!s.altScreen)
401 repaint(s)
402}
403
404// enterAltScreen implements renderer.
405func (s *cursedRenderer) enterAltScreen() {
406 s.mu.Lock()
407 enableAltScreen(s, true)
408 s.mu.Unlock()
409}
410
411// exitAltScreen implements renderer.
412func (s *cursedRenderer) exitAltScreen() {
413 s.mu.Lock()
414 enableAltScreen(s, false)
415 s.mu.Unlock()
416}
417
418// enableTextCursor sets the text cursor mode.
419func enableTextCursor(s *cursedRenderer, enable bool) {
420 s.cursorHidden = !enable
421 if enable {
422 s.scr.ShowCursor()
423 } else {
424 s.scr.HideCursor()
425 }
426}
427
428// showCursor implements renderer.
429func (s *cursedRenderer) showCursor() {
430 s.mu.Lock()
431 enableTextCursor(s, true)
432 s.mu.Unlock()
433}
434
435// hideCursor implements renderer.
436func (s *cursedRenderer) hideCursor() {
437 s.mu.Lock()
438 enableTextCursor(s, false)
439 s.mu.Unlock()
440}
441
442// insertAbove implements renderer.
443func (s *cursedRenderer) insertAbove(lines string) {
444 s.mu.Lock()
445 strLines := strings.Split(lines, "\n")
446 for i, line := range strLines {
447 if ansi.StringWidth(line) > s.width {
448 // If the line is wider than the screen, truncate it.
449 line = ansi.Truncate(line, s.width, "")
450 }
451 strLines[i] = line
452 }
453 s.scr.PrependString(strings.Join(strLines, "\n"))
454 s.mu.Unlock()
455}
456
457func (s *cursedRenderer) repaint() {
458 s.mu.Lock()
459 repaint(s)
460 s.mu.Unlock()
461}
462
463func repaint(s *cursedRenderer) {
464 s.lastFrame = nil
465}