charmbracelet/bubbletea
Cursed renderer
Active contributors: aymanbagabas, meowgorithm
Purpose
The cursed renderer is Bubble Tea's default frame-to-terminal pipeline. It maintains an off-screen uv.ScreenBuffer, diffs it against the previous frame, and emits the minimal sequence of ANSI escapes required to advance the terminal to the new state. The name is a nod to ncurses-style cell diffing — and to Charm's house humor (see the renaming commit).
Directory layout
renderer.go ← renderer interface, Println/Printf helpers, cursor encoding
cursed_renderer.go ← cursedRenderer (the default)
nil_renderer.go ← nilRenderer (no-op for headless mode)Key abstractions
| Type / func | File | Description |
|---|---|---|
renderer (interface) |
renderer.go |
Contract every renderer must satisfy: start, render, flush, reset, resize, clearScreen, setColorProfile, setSyncdUpdates, setWidthMethod, insertAbove, writeString, onMouse, close. |
cursedRenderer (struct) |
cursed_renderer.go |
Holds output writer, internal uv.TerminalRenderer, double-buffered uv.ScreenBuffer, last view, color profile, optimization flags. |
nilRenderer (struct) |
nil_renderer.go |
Used when WithoutRenderer() is set; methods all return zero values. |
defaultFPS (60), maxFPS (120) |
renderer.go |
Frame rate bounds. |
Println / Printf |
renderer.go |
Public commands that emit printLineMessage to insertAbove. |
encodeCursorStyle |
renderer.go |
Maps (CursorShape, Blink) to the integer used by CSI Ps SP q. |
How it works
graph TD
Tick["60 Hz ticker<br/>Program.startRenderer"] -->|p.ticker.C| Flush["cursedRenderer.flush"]
Update["Program.eventLoop"] -->|render(View)| Render["cursedRenderer.render"]
Render --> Diff["uv.ScreenBuffer.Draw + diff"]
Flush --> Diff
Diff --> Out["uv.TerminalRenderer.Flush<br/>(ANSI bytes)"]
Out --> Stdout["os.Stdout"]State machine
The renderer has four observable states:
| State | Triggered by | Behavior |
|---|---|---|
| Idle | After close() |
Buffer cleared, cursor restored. |
| Starting | start() |
Replays last View mode flags (alt screen, mouse mode, bracketed paste, focus, cursor, colors, progress bar, keyboard enhancements). Sets s.starting = true. |
| Running | After first successful flush |
setSyncdUpdates and setWidthMethod are honored. |
| Closing | close() |
Resets all DEC modes, optionally exits alt screen, returns the cursor to the bottom of the screen. |
On reentry (suspend → resume), start() re-emits all enabled-mode escape sequences so the terminal returns to the same state without the user toggling anything.
The render pipeline
Program.render(model)callss.render(view). The renderer stores the latest view ins.view.- The 60 Hz ticker fires
s.flush(false). Flush:- Computes the frame area. In altscreen mode it is full size; in inline mode it is
width × content.Height(). - Skips the work if the new view equals the previous view and the frame area is unchanged (
viewEqualsshort-circuit). - On size change, calls
s.scr.Erase()for a forced full redraw. - Clears the cell buffer and re-draws the styled string into it (
content.Draw). - When the frame is taller than the terminal, slices off the top of the buffer.
- Toggles alt screen / mouse mode / cursor / colors / window title / progress bar / keyboard protocol if anything in the view changed.
- Calls
s.scr.Flush()to write the diff to the output writer.
- Computes the frame area. In altscreen mode it is full size; in inline mode it is
- On
close(false)(graceful), the renderer flushes one final frame, exits alt screen if needed, restores the cursor, resets keyboard enhancements, and clears modes. - On
close(true)(kill), the buffer is dropped without flushing.
Cursor movement optimizations
setOptimizations(hardTabs, backspace, mapNl) configures three heuristics that pick the shortest path between cursor positions:
- Hard tabs: when the destination column aligns with a tab stop, send
\tinstead of multipleCSI Ccursor-right sequences. Only enabled if the terminal honors tab stops; the renderer issuesCSI ? 5 Wto set tab stops every 8 columns when starting. - Backspace: when moving the cursor left a small amount, send literal
\bcharacters instead ofCSI D. - Mapnl: on Unix when there is no input TTY, map
\nto\r\n. On Windows the OS already handles this, so the option stays off.
Synchronized output
If the terminal supports DEC mode 2026 (synchronized output / "BSU/ESU"), the renderer wraps each frame in begin/end markers so the user never sees a partially drawn screen. The runtime queries support during Run (shouldQuerySynchronizedOutput and RequestModeSynchronizedOutput) and the renderer flips on setSyncdUpdates(true) when a positive ModeReportMsg arrives.
Mouse handling at render time
onMouse(msg) returns an optional Cmd and is called by the event loop before the model sees the mouse message. It exists so that view-level mouse hit testing (View.OnMouse) can intercept events that target a specific portion of the rendered content.
insertAbove
insertAbove(s) is what powers tea.Println/tea.Printf. In inline mode it writes lines above the program's render area without disturbing it. In alt screen mode it is a no-op (the alternate buffer has no scrollback to insert above).
Color profile downsampling
The runtime sends a ColorProfileMsg after detection, and the renderer applies it via setColorProfile. From that moment on, every styled string the renderer draws goes through colorprofile.Profile's downsampler before the cell buffer ever sees it. A 24-bit color in user code becomes the closest 256-color (or 16-color, or grayscale) the terminal supports.
Integration points
- Runtime:
Program.startRendererandstopRendererdrive the ticker.Program.renderis the only caller ofrenderer.render. - Output: writes go to
Program.outputBufvia the renderer's internal buffer;Program.flushdrains the buffer toos.Stdout. - Color profile:
colorprofile.Profileflows fromProgram.profileintorenderer.setColorProfile. - Width method: when the terminal confirms unicode core mode (DEC 2027),
setWidthMethod(GraphemeWidth)switches the cell-width algorithm.
Entry points for modification
- A new
Viewfield that toggles a terminal mode → handle it both in thestart()replay block and in theflush()diff section. See howView.AltScreen,View.MouseMode,View.WindowTitle, andView.ProgressBarare paired. - A new ANSI optimization → extend
setOptimizationsand the corresponding code inuv.TerminalRenderer(upstream inultraviolet). - A second concrete renderer (e.g. for the web) → implement the
rendererinterface in a new file.nilRendereris the simplest reference.
Key source files
| File | Purpose |
|---|---|
cursed_renderer.go |
The 854-line renderer. |
renderer.go |
renderer interface, FPS constants, Println/Printf. |
nil_renderer.go |
Headless renderer. |
screen.go |
WindowSizeMsg, ClearScreen, ModeReportMsg. |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.