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// flush implements renderer.
108func (s *cursedRenderer) flush(p *Program) error {
109 s.mu.Lock()
110 defer s.mu.Unlock()
111
112 // Set window title.
113 if s.windowTitle != s.windowTitleSet {
114 _, _ = s.scr.WriteString(ansi.SetWindowTitle(s.windowTitle))
115 s.windowTitleSet = s.windowTitle
116 }
117 // Set terminal colors.
118 for _, c := range []struct {
119 rendererColor *color.Color
120 programColor *color.Color
121 reset string
122 setter func(string) string
123 }{
124 {rendererColor: &s.setCc, programColor: &p.setCc, reset: ansi.ResetCursorColor, setter: ansi.SetCursorColor},
125 {rendererColor: &s.setFg, programColor: &p.setFg, reset: ansi.ResetForegroundColor, setter: ansi.SetForegroundColor},
126 {rendererColor: &s.setBg, programColor: &p.setBg, reset: ansi.ResetBackgroundColor, setter: ansi.SetBackgroundColor},
127 } {
128 if *c.rendererColor != *c.programColor {
129 if *c.rendererColor == nil {
130 // Reset the color if it was set to nil.
131 _, _ = s.scr.WriteString(c.reset)
132 } else {
133 // Set the color.
134 col, ok := colorful.MakeColor(*c.rendererColor)
135 if ok {
136 _, _ = s.scr.WriteString(c.setter(col.Hex()))
137 }
138 }
139 *c.programColor = *c.rendererColor
140 }
141 }
142
143 if s.lastCur != nil {
144 if s.lastCur.Shape != s.cursor.Shape || s.lastCur.Blink != s.cursor.Blink {
145 cursorStyle := encodeCursorStyle(s.lastCur.Shape, s.lastCur.Blink)
146 _, _ = s.scr.WriteString(ansi.SetCursorStyle(cursorStyle))
147 s.cursor.Shape = s.lastCur.Shape
148 s.cursor.Blink = s.lastCur.Blink
149 }
150 if s.lastCur.Color != s.cursor.Color {
151 seq := ansi.ResetCursorColor
152 if s.lastCur.Color != nil {
153 c, ok := colorful.MakeColor(s.lastCur.Color)
154 if ok {
155 seq = ansi.SetCursorColor(c.Hex())
156 }
157 }
158 _, _ = s.scr.WriteString(seq)
159 s.cursor.Color = s.lastCur.Color
160 }
161 }
162
163 // Render and queue changes to the screen buffer.
164 s.scr.Render(s.buf.Buffer)
165 if s.lastCur != nil {
166 // MoveTo must come after [uv.TerminalRenderer.Render] because the
167 // cursor position might get updated during rendering.
168 s.scr.MoveTo(s.lastCur.X, s.lastCur.Y)
169 s.cursor.Position = s.lastCur.Position
170 }
171
172 if err := s.scr.Flush(); err != nil {
173 return fmt.Errorf("bubbletea: error flushing screen writer: %w", err)
174 }
175 return nil
176}
177
178// render implements renderer.
179func (s *cursedRenderer) render(v View) {
180 s.mu.Lock()
181 defer s.mu.Unlock()
182
183 frameArea := uv.Rect(0, 0, s.width, s.height)
184 if v.Layer == nil {
185 // If the component is nil, we should clear the screen buffer.
186 frameArea.Max.Y = 0
187 }
188
189 if !s.altScreen {
190 // Inline mode resizes the screen based on the frame height and
191 // terminal width. This is because the frame height can change based on
192 // the content of the frame. For example, if the frame contains a list
193 // of items, the height of the frame will be the number of items in the
194 // list. This is different from the alt screen buffer, which has a
195 // fixed height and width.
196 switch l := v.Layer.(type) {
197 case *uv.StyledString:
198 frameArea.Max.Y = l.Height()
199 case interface{ Bounds() uv.Rectangle }:
200 frameArea.Max.Y = l.Bounds().Dy()
201 }
202
203 // Resize the screen buffer to match the frame area. This is necessary
204 // to ensure that the screen buffer is the same size as the frame area
205 // and to avoid rendering issues when the frame area is smaller than
206 // the screen buffer.
207 s.buf.Resize(frameArea.Dx(), frameArea.Dy())
208 }
209 // Clear our screen buffer before copying the new frame into it to ensure
210 // we erase any old content.
211 s.buf.Clear()
212 if v.Layer != nil {
213 v.Layer.Draw(s.buf, frameArea)
214 }
215
216 frame := s.buf.Render()
217
218 // If an empty string was passed we should clear existing output and
219 // rendering nothing. Rather than introduce additional state to manage
220 // this, we render a single space as a simple (albeit less correct)
221 // solution.
222 if frame == "" {
223 frame = " "
224 }
225
226 cur := v.Cursor
227
228 s.windowTitle = v.WindowTitle
229
230 // Ensure we have any desired terminal colors set.
231 s.setBg = v.BackgroundColor
232 s.setFg = v.ForegroundColor
233 if cur != nil {
234 s.setCc = cur.Color
235 }
236 if s.lastFrame != nil && frame == *s.lastFrame &&
237 (s.lastCur == nil && cur == nil || s.lastCur != nil && cur != nil && *s.lastCur == *cur) {
238 return
239 }
240
241 s.layer = v.Layer
242 s.lastCur = cur
243 s.lastFrameHeight = frameArea.Dy()
244
245 // Cache the last rendered frame so we can avoid re-rendering it if
246 // the frame hasn't changed.
247 lastFrame := frame
248 s.lastFrame = &lastFrame
249
250 if cur == nil {
251 enableTextCursor(s, false)
252 } else {
253 enableTextCursor(s, true)
254 }
255}
256
257// hit implements renderer.
258func (s *cursedRenderer) hit(mouse MouseMsg) []Msg {
259 s.mu.Lock()
260 defer s.mu.Unlock()
261
262 if s.layer != nil {
263 if h, ok := s.layer.(Hittable); ok {
264 m := mouse.Mouse()
265 if id := h.Hit(m.X, m.Y); id != "" {
266 return []Msg{LayerHitMsg{
267 ID: id,
268 Mouse: mouse,
269 }}
270 }
271 }
272 }
273
274 return []Msg{}
275}
276
277// setCursorColor implements renderer.
278func (s *cursedRenderer) setCursorColor(c color.Color) {
279 s.mu.Lock()
280 s.setCc = c
281 s.mu.Unlock()
282}
283
284// setForegroundColor implements renderer.
285func (s *cursedRenderer) setForegroundColor(c color.Color) {
286 s.mu.Lock()
287 s.setFg = c
288 s.mu.Unlock()
289}
290
291// setBackgroundColor implements renderer.
292func (s *cursedRenderer) setBackgroundColor(c color.Color) {
293 s.mu.Lock()
294 s.setBg = c
295 s.mu.Unlock()
296}
297
298// setWindowTitle implements renderer.
299func (s *cursedRenderer) setWindowTitle(title string) {
300 s.mu.Lock()
301 s.windowTitle = title
302 s.mu.Unlock()
303}
304
305// reset implements renderer.
306func (s *cursedRenderer) reset() {
307 s.mu.Lock()
308 reset(s)
309 s.mu.Unlock()
310}
311
312func reset(s *cursedRenderer) {
313 scr := uv.NewTerminalRenderer(s.w, s.env)
314 scr.SetColorProfile(s.profile)
315 scr.SetRelativeCursor(!s.altScreen)
316 scr.SetTabStops(s.width)
317 scr.SetBackspace(s.backspace)
318 scr.SetMapNewline(s.mapnl)
319 scr.SetLogger(s.logger)
320 if s.altScreen {
321 scr.EnterAltScreen()
322 } else {
323 scr.ExitAltScreen()
324 }
325 if !s.cursorHidden {
326 scr.ShowCursor()
327 } else {
328 scr.HideCursor()
329 }
330 s.scr = scr
331}
332
333// setColorProfile implements renderer.
334func (s *cursedRenderer) setColorProfile(p colorprofile.Profile) {
335 s.mu.Lock()
336 s.profile = p
337 s.scr.SetColorProfile(p)
338 s.mu.Unlock()
339}
340
341// resize implements renderer.
342func (s *cursedRenderer) resize(w, h int) {
343 s.mu.Lock()
344 if s.altScreen || w != s.width {
345 // We need to mark the screen for clear to force a redraw. However, we
346 // only do so if we're using alt screen or the width has changed.
347 // That's because redrawing is expensive and we can avoid it if the
348 // width hasn't changed in inline mode. On the other hand, when using
349 // alt screen mode, we always want to redraw because some terminals
350 // would scroll the screen and our content would be lost.
351 s.scr.Erase()
352 }
353 if s.altScreen {
354 s.buf.Resize(w, h)
355 }
356
357 // We need to reset the touched lines buffer to match the new height.
358 s.buf.Touched = nil
359
360 s.scr.Resize(s.width, s.height)
361 s.width, s.height = w, h
362 repaint(s)
363 s.mu.Unlock()
364}
365
366// clearScreen implements renderer.
367func (s *cursedRenderer) clearScreen() {
368 s.mu.Lock()
369 // Move the cursor to the top left corner of the screen and trigger a full
370 // screen redraw.
371 _, _ = s.scr.WriteString(ansi.CursorHomePosition)
372 s.scr.Redraw(s.buf.Buffer) // force redraw
373 repaint(s)
374 s.mu.Unlock()
375}
376
377// enableAltScreen sets the alt screen mode.
378func enableAltScreen(s *cursedRenderer, enable bool) {
379 s.altScreen = enable
380 if enable {
381 s.scr.EnterAltScreen()
382 } else {
383 s.scr.ExitAltScreen()
384 }
385 s.scr.SetRelativeCursor(!s.altScreen)
386 repaint(s)
387}
388
389// enterAltScreen implements renderer.
390func (s *cursedRenderer) enterAltScreen() {
391 s.mu.Lock()
392 enableAltScreen(s, true)
393 s.mu.Unlock()
394}
395
396// exitAltScreen implements renderer.
397func (s *cursedRenderer) exitAltScreen() {
398 s.mu.Lock()
399 enableAltScreen(s, false)
400 s.mu.Unlock()
401}
402
403// enableTextCursor sets the text cursor mode.
404func enableTextCursor(s *cursedRenderer, enable bool) {
405 s.cursorHidden = !enable
406 if enable {
407 s.scr.ShowCursor()
408 } else {
409 s.scr.HideCursor()
410 }
411}
412
413// showCursor implements renderer.
414func (s *cursedRenderer) showCursor() {
415 s.mu.Lock()
416 enableTextCursor(s, true)
417 s.mu.Unlock()
418}
419
420// hideCursor implements renderer.
421func (s *cursedRenderer) hideCursor() {
422 s.mu.Lock()
423 enableTextCursor(s, false)
424 s.mu.Unlock()
425}
426
427// insertAbove implements renderer.
428func (s *cursedRenderer) insertAbove(lines string) {
429 s.mu.Lock()
430 strLines := strings.Split(lines, "\n")
431 for i, line := range strLines {
432 if ansi.StringWidth(line) > s.width {
433 // If the line is wider than the screen, truncate it.
434 line = ansi.Truncate(line, s.width, "")
435 }
436 strLines[i] = line
437 }
438 s.scr.PrependString(strings.Join(strLines, "\n"))
439 s.mu.Unlock()
440}
441
442func (s *cursedRenderer) repaint() {
443 s.mu.Lock()
444 repaint(s)
445 s.mu.Unlock()
446}
447
448func repaint(s *cursedRenderer) {
449 s.lastFrame = nil
450}