From 2a9fb918cae2a7d2fbbfe59fdf51c4f2a89d174e Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 26 Jan 2026 11:47:40 -0500 Subject: [PATCH 1/3] feat(render): add option to show render time in the view This change makes it possible to display the time taken to render each frame on the terminal screen itself. This is controlled by the TEA_RENDER_DEBUG environment variable. When set to true, the renderer will overlay the render time on the view. --- cursed_renderer.go | 54 ++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/cursed_renderer.go b/cursed_renderer.go index 4c9f892e02..4e13a9a73a 100644 --- a/cursed_renderer.go +++ b/cursed_renderer.go @@ -6,8 +6,10 @@ import ( "image/color" "io" "runtime" + "strconv" "strings" "sync" + "time" "github.com/charmbracelet/colorprofile" uv "github.com/charmbracelet/ultraviolet" @@ -16,24 +18,26 @@ import ( ) type cursedRenderer struct { - w io.Writer - buf bytes.Buffer // updates buffer to be flushed to [w] - scr *uv.TerminalRenderer - cellbuf uv.ScreenBuffer - lastView *View - env []string - term string // the terminal type $TERM - width, height int - mu sync.Mutex - profile colorprofile.Profile - logger uv.Logger - view View - hardTabs bool // whether to use hard tabs to optimize cursor movements - backspace bool // whether to use backspace to optimize cursor movements - mapnl bool - syncdUpdates bool // whether to use synchronized output mode for updates - prependLines []string - starting bool // indicates whether the renderer is starting after being stopped + w io.Writer + buf bytes.Buffer // updates buffer to be flushed to [w] + scr *uv.TerminalRenderer + cellbuf uv.ScreenBuffer + lastView *View + env []string + term string // the terminal type $TERM + width, height int + mu sync.Mutex + profile colorprofile.Profile + logger uv.Logger + view View + hardTabs bool // whether to use hard tabs to optimize cursor movements + backspace bool // whether to use backspace to optimize cursor movements + mapnl bool + syncdUpdates bool // whether to use synchronized output mode for updates + prependLines []string + starting bool // indicates whether the renderer is starting after being stopped + showRenderDebug bool + lastRenderTime time.Duration } var _ renderer = &cursedRenderer{} @@ -45,6 +49,7 @@ func newCursedRenderer(w io.Writer, env []string, width, height int) (s *cursedR s.term = uv.Environ(env).Getenv("TERM") s.width, s.height = width, height // This needs to happen before [cursedRenderer.reset]. s.cellbuf = uv.NewScreenBuffer(s.width, s.height) + s.showRenderDebug, _ = strconv.ParseBool(uv.Environ(env).Getenv("TEA_RENDER_DEBUG")) reset(s) return } @@ -248,6 +253,8 @@ func (s *cursedRenderer) flush(closing bool) error { s.mu.Lock() defer s.mu.Unlock() + startTime := time.Now() + view := s.view frameArea := uv.Rect(0, 0, s.width, s.height) if len(view.Content) == 0 { @@ -452,6 +459,14 @@ func (s *cursedRenderer) flush(closing bool) error { // Render and queue changes to the screen buffer. s.scr.Render(s.cellbuf.Buffer) + if s.showRenderDebug { + renderTime := uv.NewStyledString(fmt.Sprintf("render time: %s", s.lastRenderTime)) + if len(content.Text) > 0 && !frameArea.Empty() { + renderTime.Draw(s.cellbuf, renderTime.Bounds()) + s.scr.Render(s.cellbuf.Buffer) + } + } + if cur := view.Cursor; cur != nil { // MoveTo must come after [uv.TerminalRenderer.Render] because the // cursor position might get updated during rendering. @@ -563,6 +578,9 @@ func (s *cursedRenderer) flush(closing bool) error { s.lastView = &view + endTime := time.Now() + s.lastRenderTime = endTime.Sub(startTime) + return nil } From d94fddf4501930b5cd0a730c5fea933f175cdd16 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 26 Jan 2026 12:04:44 -0500 Subject: [PATCH 2/3] fix(render): apply render time before calculating the diffs --- cursed_renderer.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cursed_renderer.go b/cursed_renderer.go index 4e13a9a73a..6c07400122 100644 --- a/cursed_renderer.go +++ b/cursed_renderer.go @@ -456,17 +456,16 @@ func (s *cursedRenderer) flush(closing bool) error { setProgressBar(s, view.ProgressBar) } - // Render and queue changes to the screen buffer. - s.scr.Render(s.cellbuf.Buffer) - if s.showRenderDebug { renderTime := uv.NewStyledString(fmt.Sprintf("render time: %s", s.lastRenderTime)) if len(content.Text) > 0 && !frameArea.Empty() { renderTime.Draw(s.cellbuf, renderTime.Bounds()) - s.scr.Render(s.cellbuf.Buffer) } } + // Render and queue changes to the screen buffer. + s.scr.Render(s.cellbuf.Buffer) + if cur := view.Cursor; cur != nil { // MoveTo must come after [uv.TerminalRenderer.Render] because the // cursor position might get updated during rendering. From 8fe536807f3fd9c7105766565085d8d594fdb446 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 26 Jan 2026 12:11:02 -0500 Subject: [PATCH 3/3] fix(render): remove render debug prefix Co-authored-by: Andrey Nering --- cursed_renderer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cursed_renderer.go b/cursed_renderer.go index 6c07400122..7997bfe58e 100644 --- a/cursed_renderer.go +++ b/cursed_renderer.go @@ -457,7 +457,7 @@ func (s *cursedRenderer) flush(closing bool) error { } if s.showRenderDebug { - renderTime := uv.NewStyledString(fmt.Sprintf("render time: %s", s.lastRenderTime)) + renderTime := uv.NewStyledString(fmt.Sprintf("%s", s.lastRenderTime)) if len(content.Text) > 0 && !frameArea.Empty() { renderTime.Draw(s.cellbuf, renderTime.Bounds()) }