Open-Source Wikis

/

Bubble Tea

/

Systems

/

Cursed renderer

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

  1. Program.render(model) calls s.render(view). The renderer stores the latest view in s.view.
  2. 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 (viewEquals short-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.
  3. On close(false) (graceful), the renderer flushes one final frame, exits alt screen if needed, restores the cursor, resets keyboard enhancements, and clears modes.
  4. 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 \t instead of multiple CSI C cursor-right sequences. Only enabled if the terminal honors tab stops; the renderer issues CSI ? 5 W to set tab stops every 8 columns when starting.
  • Backspace: when moving the cursor left a small amount, send literal \b characters instead of CSI D.
  • Mapnl: on Unix when there is no input TTY, map \n to \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.startRenderer and stopRenderer drive the ticker. Program.render is the only caller of renderer.render.
  • Output: writes go to Program.outputBuf via the renderer's internal buffer; Program.flush drains the buffer to os.Stdout.
  • Color profile: colorprofile.Profile flows from Program.profile into renderer.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 View field that toggles a terminal mode → handle it both in the start() replay block and in the flush() diff section. See how View.AltScreen, View.MouseMode, View.WindowTitle, and View.ProgressBar are paired.
  • A new ANSI optimization → extend setOptimizations and the corresponding code in uv.TerminalRenderer (upstream in ultraviolet).
  • A second concrete renderer (e.g. for the web) → implement the renderer interface in a new file. nilRenderer is 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.

Cursed renderer – Bubble Tea wiki | Factory