Skip to main content

Overview

The FrameBuffer (also called OptimizedBuffer) is OpenTUI’s low-level rendering surface. It provides direct pixel-level access to the terminal buffer, allowing you to:
  • Draw custom graphics
  • Create complex visual effects
  • Implement custom renderables
  • Optimize rendering performance
  • Work with transparency and alpha blending
Every renderable ultimately draws to a frame buffer, which is then output to the terminal.
The FrameBuffer is implemented in native code (Zig) for maximum performance, but exposed to TypeScript with a clean API.

Creating a FrameBuffer

Standalone Buffer

Create a buffer directly:
import { OptimizedBuffer } from "@opentui/core"

const buffer = OptimizedBuffer.create(
  80,          // width in cells
  24,          // height in cells
  "unicode",   // width method: "unicode" | "wcwidth"
  {
    respectAlpha: true,   // Enable alpha blending
    id: "my-buffer",      // Optional debug ID
  }
)

FrameBuffer Renderable

Use a renderable that wraps a buffer:
import { FrameBufferRenderable, RGBA } from "@opentui/core"

const canvas = new FrameBufferRenderable(renderer, {
  id: "canvas",
  width: 50,
  height: 20,
  position: "absolute",
  left: 5,
  top: 5,
})

// Access the underlying buffer
canvas.frameBuffer.fillRect(0, 0, 10, 10, RGBA.fromHex("#FF0000"))

renderer.root.add(canvas)

Buffered Renderables

Enable double-buffering for any renderable:
import { BoxRenderable } from "@opentui/core"

const buffered = new BoxRenderable(renderer, {
  buffered: true,  // Enable internal frame buffer
  width: 30,
  height: 15,
})

// Access the buffer in render hooks
const box = new BoxRenderable(renderer, {
  buffered: true,
  renderBefore(buffer) {
    // `buffer` is the renderable's frame buffer
    buffer.drawText(
      "Custom rendering",
      5, 5,
      RGBA.fromHex("#FFFFFF")
    )
  },
})

Drawing Operations

Set Individual Cells

import { RGBA, TextAttributes } from "@opentui/core"

buffer.setCell(
  10,                           // x position
  5,                            // y position
  "█",                          // character
  RGBA.fromHex("#FFFFFF"),      // foreground color
  RGBA.fromHex("#000000"),      // background color
  TextAttributes.BOLD           // text attributes
)

Alpha Blending

Set cells with alpha blending:
buffer.setCellWithAlphaBlending(
  10, 5,
  "█",
  RGBA.fromValues(1, 0, 0, 0.5),  // 50% transparent red
  RGBA.fromValues(0, 0, 0, 0),    // Fully transparent background
  0
)
Alpha blending only works if the buffer was created with respectAlpha: true.

Draw Text

buffer.drawText(
  "Hello, World!",              // text content
  10,                           // x position
  5,                            // y position  
  RGBA.fromHex("#00FF00"),      // foreground
  RGBA.fromHex("#000000"),      // background (optional)
  TextAttributes.UNDERLINE      // attributes (optional)
)

Text with Selection

buffer.drawText(
  "Selectable text",
  0, 0,
  RGBA.fromHex("#FFFFFF"),
  RGBA.fromHex("#000000"),
  0,
  {
    start: 5,                       // Selection start index
    end: 10,                        // Selection end index
    bgColor: RGBA.fromHex("#0000FF"), // Selection background
    fgColor: RGBA.fromHex("#FFFFFF"), // Selection foreground
  }
)

Fill Rectangle

buffer.fillRect(
  10,                          // x position
  5,                           // y position
  20,                          // width
  10,                          // height
  RGBA.fromHex("#FF0000")      // background color
)

Draw Box

Draw boxes with borders and titles:
import { RGBA } from "@opentui/core"

buffer.drawBox({
  x: 5,
  y: 3,
  width: 40,
  height: 15,
  borderStyle: "rounded",        // "single" | "double" | "rounded" | "heavy" | "dashed"
  border: true,                   // or ["top", "right", "bottom", "left"]
  borderColor: RGBA.fromHex("#00FFFF"),
  backgroundColor: RGBA.fromHex("#1a1a1a"),
  shouldFill: true,               // Fill background
  title: "My Box",
  titleAlignment: "center",       // "left" | "center" | "right"
})

Custom Border Characters

const customBorder = new Uint32Array([
  0x256D, 0x2500, 0x256E,  // top-left, horizontal, top-right
  0x2502, 0x2502,          // vertical-left, vertical-right
  0x2570, 0x2500, 0x256F,  // bottom-left, horizontal, bottom-right
  0x253C,                  // cross
])

buffer.drawBox({
  x: 0,
  y: 0,
  width: 20,
  height: 10,
  customBorderChars: customBorder,
  border: true,
  borderColor: RGBA.fromHex("#FFFFFF"),
  backgroundColor: RGBA.fromHex("#000000"),
})

Clear Buffer

// Clear with black background
buffer.clear(RGBA.fromHex("#000000"))

// Clear with transparent background
buffer.clear(RGBA.fromValues(0, 0, 0, 0))

Compositing

Draw Another FrameBuffer

Composite one buffer onto another:
const source = OptimizedBuffer.create(20, 10, "unicode")
source.drawText("Hello", 0, 0, RGBA.fromHex("#FFFFFF"))

const dest = OptimizedBuffer.create(80, 24, "unicode")

// Draw entire source buffer at position (10, 5)
dest.drawFrameBuffer(10, 5, source)

// Draw a region of the source buffer
dest.drawFrameBuffer(
  10, 5,     // destination position
  source,    // source buffer
  2, 2,      // source x, y
  15, 8      // source width, height
)

Draw TextBufferView

Render a text buffer (from the editor):
import { TextBufferView } from "@opentui/core"

const view: TextBufferView = /* ... */
buffer.drawTextBuffer(view, 0, 0)

Draw EditorView

import { EditorView } from "@opentui/core"

const editorView: EditorView = /* ... */
buffer.drawEditorView(editorView, 0, 0)

Advanced Features

Scissor Rectangles

Clip drawing to a region:
// Push a clipping region
buffer.pushScissorRect(10, 5, 30, 15)

// All drawing operations are now clipped to this rectangle
buffer.drawText("Clipped text", 0, 0, RGBA.fromHex("#FFFFFF"))

// Pop the clipping region
buffer.popScissorRect()

// Drawing is no longer clipped
buffer.drawText("Not clipped", 0, 0, RGBA.fromHex("#FFFFFF"))

// Clear all scissor rectangles
buffer.clearScissorRects()
Scissor rects stack - you can push multiple regions:
buffer.pushScissorRect(0, 0, 80, 24)    // Outer bounds
buffer.pushScissorRect(10, 10, 60, 14)  // Inner bounds (intersection)
// Drawing is clipped to the intersection
buffer.popScissorRect()  // Back to outer bounds
buffer.popScissorRect()  // No clipping

Opacity Stack

Apply opacity to drawing operations:
// Push opacity (0.0 - 1.0)
buffer.pushOpacity(0.5)  // 50% transparent

// All drawing operations are now semi-transparent
buffer.drawText("Faded text", 0, 0, RGBA.fromHex("#FFFFFF"))

// Pop opacity
buffer.popOpacity()

// Get current opacity
const currentOpacity = buffer.getCurrentOpacity()

// Clear all opacity
buffer.clearOpacity()
Opacity also stacks:
buffer.pushOpacity(0.5)  // 50%
buffer.pushOpacity(0.8)  // Effective: 0.5 * 0.8 = 40%
buffer.popOpacity()      // Back to 50%
buffer.popOpacity()      // Back to 100%

Grayscale Rendering

Draw grayscale intensity data:
const intensities = new Float32Array(100 * 50)  // 100x50 pixels
for (let i = 0; i < intensities.length; i++) {
  intensities[i] = Math.random()  // Random intensities 0.0-1.0
}

buffer.drawGrayscaleBuffer(
  0, 0,                          // position
  intensities,
  100, 50,                       // source width, height
  RGBA.fromHex("#FFFFFF"),       // foreground (bright areas)
  RGBA.fromHex("#000000")        // background (dark areas)
)

Supersampled Grayscale

For higher quality:
buffer.drawGrayscaleBufferSupersampled(
  0, 0,
  intensities,
  100, 50,
  RGBA.fromHex("#FFFFFF"),
  RGBA.fromHex("#000000")
)

Grid Drawing

Draw table grids efficiently:
import { BorderCharArrays } from "@opentui/core"

const columnOffsets = new Int32Array([0, 20, 40, 60, 80])
const rowOffsets = new Int32Array([0, 5, 10, 15, 20])

buffer.drawGrid({
  borderChars: BorderCharArrays.single,
  borderFg: RGBA.fromHex("#888888"),
  borderBg: RGBA.fromHex("#000000"),
  columnOffsets,
  rowOffsets,
  drawInner: true,   // Draw inner grid lines
  drawOuter: true,   // Draw outer border
})

Buffer Inspection

Access Raw Data

const { char, fg, bg, attributes } = buffer.buffers

// char: Uint32Array - Unicode code points
// fg: Float32Array - Foreground colors (RGBA, 4 values per cell)
// bg: Float32Array - Background colors (RGBA, 4 values per cell)
// attributes: Uint32Array - Text attributes (bold, italic, etc.)

// Access cell at (x, y)
const index = y * buffer.width + x
const codePoint = char[index]
const fgRed = fg[index * 4]
const fgGreen = fg[index * 4 + 1]
const fgBlue = fg[index * 4 + 2]
const fgAlpha = fg[index * 4 + 3]

Get Resolved Characters

// Get buffer content as UTF-8 bytes
const bytes = buffer.getRealCharBytes(false)  // false = no line breaks
const text = new TextDecoder().decode(bytes)
console.log(text)

// With line breaks
const bytesWithBreaks = buffer.getRealCharBytes(true)
const lines = new TextDecoder().decode(bytesWithBreaks).split("\n")

Capture as Spans

Get buffer content as styled spans (useful for copying/testing):
const lines = buffer.getSpanLines()

for (const line of lines) {
  for (const span of line.spans) {
    console.log({
      text: span.text,
      fg: span.fg.toHex(),
      bg: span.bg.toHex(),
      attributes: span.attributes,
      width: span.width,
    })
  }
}

Resizing

buffer.resize(100, 30)  // Resize to 100x30

// Check new dimensions
console.log(buffer.width, buffer.height)
Resizing clears the buffer and reallocates memory.

Memory Management

Cleanup

Always destroy buffers when done:
buffer.destroy()
Using a destroyed buffer will throw an error. Always check buffer._destroyed if you’re unsure.

Alpha Blending

Toggle alpha blending dynamically:
buffer.setRespectAlpha(true)   // Enable alpha blending
buffer.setRespectAlpha(false)  // Disable alpha blending

Unicode Encoding

Encode text with width information:
const encoded = buffer.encodeUnicode("Hello 👋 World")

if (encoded) {
  console.log(encoded.data)  // Array of { char: number, width: number }

  // Free when done
  buffer.freeUnicode(encoded)
}

Practical Examples

Custom Progress Bar

import { RGBA, OptimizedBuffer } from "@opentui/core"

function drawProgressBar(
  buffer: OptimizedBuffer,
  x: number,
  y: number,
  width: number,
  progress: number  // 0.0 - 1.0
) {
  const filledWidth = Math.floor(width * progress)
  const emptyWidth = width - filledWidth

  const filledColor = RGBA.fromHex("#00FF00")
  const emptyColor = RGBA.fromHex("#333333")

  // Draw filled portion
  for (let i = 0; i < filledWidth; i++) {
    buffer.setCell(x + i, y, "█", filledColor, filledColor, 0)
  }

  // Draw empty portion
  for (let i = 0; i < emptyWidth; i++) {
    buffer.setCell(x + filledWidth + i, y, "░", emptyColor, emptyColor, 0)
  }
}

// Use it
const buffer = renderer.nextRenderBuffer
drawProgressBar(buffer, 10, 5, 50, 0.65)  // 65% complete

Gradient Background

function drawGradient(
  buffer: OptimizedBuffer,
  x: number,
  y: number,
  width: number,
  height: number
) {
  for (let row = 0; row < height; row++) {
    const ratio = row / height
    const red = ratio
    const blue = 1 - ratio

    const color = RGBA.fromValues(red, 0, blue, 1)

    for (let col = 0; col < width; col++) {
      buffer.setCell(x + col, y + row, " ", color, color, 0)
    }
  }
}

Custom Renderable with FrameBuffer

import { Renderable, OptimizedBuffer, RGBA } from "@opentui/core"

class WaveRenderable extends Renderable {
  private phase = 0

  protected onUpdate(deltaTime: number): void {
    this.phase += deltaTime * 0.001  // Animate over time
  }

  protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {
    const amplitude = this.height / 2
    const frequency = 0.2

    for (let x = 0; x < this.width; x++) {
      const y = Math.floor(
        amplitude + amplitude * Math.sin(x * frequency + this.phase)
      )

      if (y >= 0 && y < this.height) {
        buffer.setCell(
          this.x + x,
          this.y + y,
          "█",
          RGBA.fromHex("#00FFFF"),
          RGBA.fromHex("#000000"),
          0
        )
      }
    }
  }
}

API Reference

Properties

buffer.width: number              // Buffer width in cells
buffer.height: number             // Buffer height in cells
buffer.widthMethod: WidthMethod   // "unicode" | "wcwidth"
buffer.respectAlpha: boolean      // Alpha blending enabled
buffer.id: string                 // Debug identifier

Core Methods

// Drawing
setCell(x, y, char, fg, bg, attributes?): void
setCellWithAlphaBlending(x, y, char, fg, bg, attributes?): void
drawText(text, x, y, fg, bg?, attributes?, selection?): void
drawChar(char, x, y, fg, bg, attributes?): void
fillRect(x, y, width, height, bg): void
drawBox(options): void
clear(bg): void

// Compositing
drawFrameBuffer(destX, destY, source, srcX?, srcY?, srcW?, srcH?): void

// Clipping & opacity
pushScissorRect(x, y, width, height): void
popScissorRect(): void
clearScissorRects(): void
pushOpacity(opacity): void
popOpacity(): void
getCurrentOpacity(): number
clearOpacity(): void

// Inspection
getRealCharBytes(addLineBreaks): Uint8Array
getSpanLines(): CapturedLine[]

// Management
resize(width, height): void
destroy(): void
setRespectAlpha(respectAlpha): void

Build docs developers (and LLMs) love