gitstack

grindlemire/go-tui code browser

11.9 KB markdown 448 lines 2026-03-23 · a47141f raw

Buffer and Rendering Reference

Overview

go-tui uses a double-buffered character grid to manage terminal output. Every frame, your component tree renders into the back buffer. The framework then diffs the back buffer against the front buffer, sends only the changed cells to the terminal, and swaps the buffers. This keeps screen updates fast and flicker-free.

Most applications never interact with the buffer directly. The App handles layout, rendering, and flushing automatically. These types matter when you're writing tests (reading buffer contents), building custom rendering logic, or working with the lower-level drawing functions.

Buffer

type Buffer struct {
    // unexported fields
}

A double-buffered 2D grid of Cell values. Writes go to the back buffer. Diff() computes what changed, and Swap() promotes the back buffer to front.

NewBuffer

func NewBuffer(width, height int) *Buffer

Creates a new buffer with the given dimensions. Both buffers are initialized with spaces and default styling. Negative dimensions are clamped to 0.

buf := tui.NewBuffer(80, 24)

Query methods

Width

func (b *Buffer) Width() int

Returns the buffer width in columns.

Height

func (b *Buffer) Height() int

Returns the buffer height in rows.

Size

func (b *Buffer) Size() (width, height int)

Returns both dimensions at once.

Rect

func (b *Buffer) Rect() Rect

Returns the buffer bounds as a Rect starting at (0, 0).

r := buf.Rect() // Rect{X: 0, Y: 0, Width: 80, Height: 24}

Reading cells

Cell

func (b *Buffer) Cell(x, y int) Cell

Returns the cell at (x, y) from the back buffer. Returns an empty Cell{} if the position is out of bounds.

c := buf.Cell(5, 3)
if c.Rune == '>' {
    // ...
}

Writing cells

SetCell

func (b *Buffer) SetCell(x, y int, c Cell)

Sets the cell at (x, y) in the back buffer. Does nothing if the position is out of bounds.

buf.SetCell(0, 0, tui.NewCell('X', tui.NewStyle().Bold()))

SetRune

func (b *Buffer) SetRune(x, y int, r rune, style Style)

Sets a rune at (x, y) with the given style. Handles wide characters (CJK, emoji) by automatically placing continuation cells. Also cleans up any overlapped wide characters at the target position.

If a wide character (width 2) would land in the last column, a space is placed instead since the character can't fit.

buf.SetRune(10, 5, 'A', tui.NewStyle().Foreground(tui.ANSIColor(tui.Cyan)))

SetString

func (b *Buffer) SetString(x, y int, s string, style Style) int

Writes a string starting at (x, y). Returns the total display width consumed. Stops at the buffer edge without wrapping. Handles wide characters correctly.

w := buf.SetString(2, 0, "Hello, world!", tui.NewStyle())
// w == 13

SetStringClipped

func (b *Buffer) SetStringClipped(x, y int, s string, style Style, clipRect Rect) int

Writes a string clipped to a rectangle. Characters outside clipRect are not rendered. Returns the display width of rendered characters.

clip := tui.NewRect(5, 0, 10, 1)
buf.SetStringClipped(0, 0, "This is a long string", tui.NewStyle(), clip)
// Only characters within columns 5-14 are drawn

Gradient writing

SetStringGradient

func (b *Buffer) SetStringGradient(x, y int, s string, g Gradient, baseStyle Style) int

Writes a string with a gradient applied per-character along the horizontal axis. The gradient color is applied to the foreground. Returns the total display width consumed.

g := tui.NewGradient(tui.ANSIColor(tui.Cyan), tui.ANSIColor(tui.Magenta))
buf.SetStringGradient(0, 0, "Rainbow Text", g, tui.NewStyle())

FillGradient

func (b *Buffer) FillGradient(rect Rect, r rune, g Gradient, baseStyle Style)

Fills a rectangle with a gradient background. The gradient direction determines how the color interpolates:

Direction Interpolation
GradientHorizontal Left to right
GradientVertical Top to bottom
GradientDiagonalDown Top-left to bottom-right
GradientDiagonalUp Bottom-left to top-right

The gradient color is applied to the background of each cell.

g := tui.NewGradient(tui.ANSIColor(tui.Blue), tui.ANSIColor(tui.Green))
g = g.WithDirection(tui.GradientVertical)
buf.FillGradient(buf.Rect(), ' ', g, tui.NewStyle())

Fill and clear

Fill

func (b *Buffer) Fill(rect Rect, r rune, style Style)

Fills a rectangle with the given rune and style. The rect is intersected with the buffer bounds, so out-of-bounds regions are ignored.

buf.Fill(tui.NewRect(0, 0, 20, 5), '.', tui.NewStyle().Foreground(tui.ANSIColor(tui.BrightBlack)))

Clear

func (b *Buffer) Clear()

Clears the entire back buffer to spaces with default styling. Equivalent to ClearRect(buf.Rect()).

ClearRect

func (b *Buffer) ClearRect(rect Rect)

Clears a rectangular region to spaces with default styling. Handles wide characters at the edges of the cleared region (clears originating or continuation cells as needed).

Diffing and swapping

Diff

func (b *Buffer) Diff() []CellChange

Returns all cells that changed between the front and back buffers. Changes are returned in row-major order (top-to-bottom, left-to-right), which minimizes cursor movement when writing to the terminal.

Swap

func (b *Buffer) Swap()

Copies the back buffer to the front buffer. Call this after flushing changes to the terminal so the next diff starts from the current state.

The typical rendering cycle is:

// 1. Write to the back buffer
buf.SetString(0, 0, "Hello", style)

// 2. Compute what changed
changes := buf.Diff()

// 3. Send changes to the terminal
term.Flush(changes)

// 4. Promote back to front
buf.Swap()

Output

String

func (b *Buffer) String() string

Renders the back buffer to a string for debugging or testing. Each row is separated by a newline. Continuation cells (from wide characters) are skipped.

fmt.Println(buf.String())

StringTrimmed

func (b *Buffer) StringTrimmed() string

Like String(), but trailing spaces are removed from each line. Useful in tests where you want to assert on content without worrying about trailing whitespace.

got := buf.StringTrimmed()
if got != "Hello\nWorld" {
    t.Errorf("unexpected: %q", got)
}

Resizing

Resize

func (b *Buffer) Resize(width, height int)

Changes the buffer dimensions. Content in the overlapping region is preserved; new areas are filled with spaces. Does nothing if the dimensions haven't changed. Negative values are clamped to 0.

Cell

type Cell struct {
    Rune  rune   // The character (0 for continuation cells)
    Style Style  // Visual styling
    Width uint8  // Display width: 1 for normal, 2 for wide, 0 for continuation
}

A single character cell in the terminal buffer. Wide characters (CJK, emoji) occupy two cells: the first cell holds the rune with Width == 2, and the next cell is a continuation with Rune == 0 and Width == 0.

NewCell

func NewCell(r rune, style Style) Cell

Creates a Cell with automatic width detection via RuneWidth().

c := tui.NewCell('A', tui.NewStyle().Bold())
// c.Width == 1

c = tui.NewCell('漢', tui.NewStyle())
// c.Width == 2

NewCellWithWidth

func NewCellWithWidth(r rune, style Style, width uint8) Cell

Creates a Cell with an explicit width. Use this for continuation cells (width == 0) or when the width is already known.

// Continuation cell for the second half of a wide character
cont := tui.NewCellWithWidth(0, tui.NewStyle(), 0)

IsContinuation

func (c Cell) IsContinuation() bool

Returns true if this cell is the second half of a wide character (Width == 0). Continuation cells should not be drawn directly.

Equal

func (c Cell) Equal(other Cell) bool

Returns true if both cells have the same rune, style, and width. Used internally by Diff() to detect changes.

IsEmpty

func (c Cell) IsEmpty() bool

Returns true if the cell is blank. A cell is empty when:

  • Its rune is 0 (regardless of style), or
  • Its rune is ' ' (space) with default styling

RuneWidth

func RuneWidth(r rune) int

Package-level function that returns the display width of a rune in terminal cells. Returns 1 for most characters and 2 for wide characters (CJK ideographs, fullwidth forms, emoji).

Zero-width Unicode categories (combining marks, variation selectors, format controls) are treated as width 1 in this model because Width == 0 is reserved for continuation cells.

tui.RuneWidth('A')  // 1
tui.RuneWidth('漢') // 2
tui.RuneWidth('🎉') // 2

CellChange

type CellChange struct {
    X, Y int
    Cell Cell
}

Represents a single cell that differs between the front and back buffers. Returned by Buffer.Diff() and consumed by Terminal.Flush().

Render functions

These package-level functions handle the full render cycle.

Render

func Render(term Terminal, buf *Buffer)

The primary rendering function for normal frame updates. Computes the diff between front and back buffers, flushes only the changed cells to the terminal via term.Flush(), then swaps the buffers. If nothing changed, no terminal I/O occurs.

RenderFull

func RenderFull(term Terminal, buf *Buffer)

Forces a complete redraw. Sends every cell to the terminal regardless of whether it changed. Calls term.Clear() first, then flushes all cells, then swaps. Use this after:

  • Initial application startup
  • Terminal resize
  • Recovering from external terminal corruption
  • Switching back from alternate screen

RenderTree

func RenderTree(buf *Buffer, root *Element)

Traverses an element tree and draws each element to the buffer. Handles background fills, borders, text content, gradients, scroll clipping, and overflow clipping. This is the bridge between the layout system and the buffer: after layout calculates positions, RenderTree writes the visual output.

root := tui.New(
    tui.WithText("Hello"),
    tui.WithBorder(tui.BorderRounded),
)
root.Calculate(80, 24)

buf := tui.NewBuffer(80, 24)
tui.RenderTree(buf, root)
fmt.Println(buf.StringTrimmed())

Drawing functions

These functions draw borders and filled boxes directly into a buffer. They live in the tui package alongside the buffer types. For full details, see Styling Reference.

func DrawBox(buf *Buffer, rect Rect, border BorderStyle, style Style)
func DrawBoxGradient(buf *Buffer, rect Rect, border BorderStyle, g Gradient, baseStyle Style)
func DrawBoxClipped(buf *Buffer, rect Rect, border BorderStyle, style Style, clipRect Rect)
func DrawBoxGradientClipped(buf *Buffer, rect Rect, border BorderStyle, g Gradient, baseStyle Style, clipRect Rect)
func DrawBoxWithTitle(buf *Buffer, rect Rect, border BorderStyle, title string, style Style)
func FillBox(buf *Buffer, rect Rect, r rune, style Style)
Function Description
DrawBox Draws a border around a rectangle.
DrawBoxGradient Draws a border with a gradient applied to the border characters.
DrawBoxClipped Draws a border clipped to a visible region.
DrawBoxGradientClipped Draws a gradient border clipped to a visible region.
DrawBoxWithTitle Draws a border with a title string inset in the top edge.
FillBox Fills the interior of a bordered rectangle (inside the border, not including it).

See also