These docs describe the main branch, including unreleased changes. Read the 0.6.0 docs.

Buffer API

This advanced reference is for renderable and component authors who use OptimizedBuffer directly.

An OptimizedBuffer is a two-dimensional grid of terminal cells. It is not a pixel buffer or a JavaScript character array. Each cell stores a character value, foreground and background RGBA data, and attributes.

Use FrameBuffer when a component should own and display the buffer. Create an OptimizedBuffer directly when your code owns an independent drawing surface.

Create and destroy a buffer#

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

const owner = new ResourceContext({ objectCapacity: 4, renderCellsMax: 400 })
try {
  const buffer = OptimizedBuffer.create(40, 10, "unicode", {
    owner,
    id: "preview",
    respectAlpha: true,
  })
  buffer.clear(RGBA.fromInts(0, 0, 0, 0))
  buffer.drawText("status", 1, 1, RGBA.fromInts(255, 255, 255))
  buffer.destroy()
} finally {
  owner.destroy()
}

create(width, height, widthMethod, options) requires positive cell dimensions and an explicit owner. For renderer-owned resources, pass renderer.nativeScene. For standalone work, create a ResourceContext.

ResourceContext creates a native Context without a renderer or terminal Session. objectCapacity sets the initial number of native object slots, including the cell leases that withBuffers() holds; the Context grows it as needed. renderCellsMax limits the cell count (width * height) of each drawing buffer. Destroy the Context only after every withBuffers() callback returns. Destroying the Context releases its remaining resources.

Read How Core uses native for Core resource factories and their native owners.

widthMethod is "unicode" or "wcwidth". The optional respectAlpha field defaults to false. The optional id field defaults to an internal ID.

Call destroy() to release an independent buffer before its Context ends. Destruction is idempotent. A raw cell view is valid only inside its withBuffers() callback. Most methods throw after buffer or Context destruction.

The public lifecycle method is named destroy(), not dispose().

Renderer buffers#

CliRenderer exposes nextRenderBuffer and currentRenderBuffer. The renderer owns both.

  • Native code paints each frame into nextRenderBuffer. Then post-process functions draw into it.
  • Drawing calls on nextRenderBuffer work only inside a post-process function. At other times, they throw.
  • Native rendering compares the next buffer with currentRenderBuffer.
  • The native encoder can change the current buffer before output admission or presentation. It is not a last-presented snapshot.
  • The native encoder clears the next buffer after encoding.
  • Drawing calls on currentRenderBuffer always throw.
  • withBuffers() reads either buffer between frames and inside post-process functions. It throws while a frame is in progress, while its output is pending, and while the renderer is suspended.

Do not destroy either renderer buffer. Do not write to the cell arrays of currentRenderBuffer. Such a write can hide a real change from the terminal diff.

The wrappers keep their identity during renderer resize, but their native storage changes. Use a new cell scope after resize. Do not reuse raw views from an earlier scope.

Paint hooks receive a separate recording buffer. Its drawing calls record commands. Native code plays the commands when it paints nextRenderBuffer. A recording buffer has these limits:

  • buffers, withBuffers(), getRealCharBytes(), and getSpanLines() throw because a hook cannot read the frame.
  • setRespectAlpha() throws.
  • drawSuperSampleBuffer() and drawPackedBuffer() accept a Uint8Array, not a native address.
  • drawImage() returns true because native code places the image later.
  • The recording for one frame can use at most 64 MiB. A call that goes past this limit throws RangeError.
  • The buffer records only while its frame runs hooks. A saved hook buffer throws if it draws later.

A renderable with buffered: true gives its paint hooks its own buffer instead. Read Custom renderables for hook timing and errors.

Cell model#

Use withBuffers() to borrow the cell arrays for a synchronous callback. The callback receives these arrays, plus the dimensions and storage generation:

{
  char: Uint32Array
  fg: Uint16Array
  bg: Uint16Array
  attributes: Uint32Array
}

The callback must be synchronous. A callback that returns a promise throws TypeError, and its arrays stop working.

The buffers getter returns the same arrays without a callback. It works only on the buffer that a post-process function receives, during the synchronous part of that function. On every other buffer, it throws.

char has one entry per cell. attributes also has one entry per cell. fg and bg have four Uint16 entries per cell. The low byte stores each RGBA8 channel. Higher bits store color intent metadata.

Character entries can be tagged values for grapheme starts, continuation cells, and image placements. Attribute bits above the base text flags can store hyperlink IDs. Treat all four raw arrays as internal state.

Draw one cell or grapheme#

buffer.setCell(x, y, char, fg, bg, attributes)
buffer.setCellWithAlphaBlending(x, y, char, fg, bg, attributes)

Both string methods use only char.codePointAt(0). They do not encode a joined grapheme. They also do not reserve continuation cells for a wide scalar. Restrict char to one scalar that occupies one terminal cell.

Use drawText() for normal Unicode strings. It segments graphemes and writes continuation cells.

For repeated encoded drawing, use encodeUnicode() and drawChar():

const encoded = buffer.encodeUnicode("A\u{1F44B}B")
if (encoded) {
  try {
    let x = 0
    for (const glyph of encoded.data) {
      buffer.drawChar(glyph.char, x, 0, fg, bg)
      x += glyph.width
    }
  } finally {
    buffer.freeUnicode(encoded)
  }
}

encodeUnicode() allocates native grapheme data. Always pass its complete result to freeUnicode(). The char values in data are opaque tokens owned by that Context, not pointers or raw cell values. Pass them to drawChar() on a buffer with the same owner. Do not inspect their bits or retain them after freeUnicode().

Drawing inventory#

Basic drawing#

Method Signature and behavior
clear clear(bg = opaque black) resets every cell, link, grapheme, attribute, and image placement
setCell setCell(x, y, char, fg, bg, attributes = 0) replaces one cell without alpha blending
setCellWithAlphaBlending Same arguments, with foreground and background alpha composition
drawChar drawChar(encodedChar, x, y, fg, bg, attributes = 0) accepts a scalar or same-Context token from encodeUnicode()
drawText drawText(text, x, y, fg, bg?, attributes = 0, selection?) segments and clips Unicode text
fillRect fillRect(x, y, width, height, bg) paints the rectangle through tracker-aware cell paths

Positions are integer cells from -2^31 to 2^31 - 1, so they can be negative. These methods draw only the cells inside the buffer. drawText() skips a wide grapheme that starts left of column 0. fillRect() draws nothing when width or height is 0 or less.

clear() ignores the scissor and opacity stacks. In a paint hook, it clears the whole frame.

Control characters (C0 except tab, DEL, and C1) take no cells in drawText(), box titles, and encodeUnicode(). A tab draws as two spaces. A grapheme longer than the 128 UTF-8 bytes that a cell can hold draws as spaces of its width. setCell(), setCellWithAlphaBlending(), and drawChar() write a space for a control character. None of these inputs throws.

drawText() text and each drawBox() title can use at most NATIVE_BUFFER_TEXT_BYTES_MAX (65,536) UTF-8 bytes. Longer text throws RangeError.

The attributes argument accepts only the eight base attribute bits. A higher bit makes a direct draw call throw NativeError. In a paint hook, the frame fails when native code plays the recording.

The optional drawText() selection object has { start, end, bgColor?, fgColor? }. Its implementation slices the JavaScript string with UTF-16 indexes and adds the same values to the cell x-coordinate. It is reliable only for one-cell Basic Multilingual Plane text. Use TextBufferView or EditorView selection for general Unicode text.

An opaque fillRect() replaces covered cells with spaces, default foreground, the supplied background, and no text attributes. It clears text and links in that rectangle. Translucent fills can blend with ordinary one-cell content.

Boxes and grids#

drawBox(options) accepts these fields:

  • Required x, y, width, height, border, borderColor, and backgroundColor
  • Optional borderStyle, customBorderChars, and shouldFill, which defaults to false
  • Optional title, titleColor, and titleAlignment, which defaults to "left"
  • Optional bottomTitle and bottomTitleAlignment, which defaults to "left"

The default border style is "single". titleColor defaults to borderColor.

drawGrid(options) takes border characters and colors, cell-column and row offset arrays, and drawInner and drawOuter flags. Offset arrays describe boundaries, so an array with n + 1 entries describes n columns or rows.

Compose buffers and views#

Method Purpose
drawFrameBuffer(destX, destY, source, sourceX?, sourceY?, sourceWidth?, sourceHeight?) Clips and composites a source cell rectangle
drawTextBuffer(view, x, y) Draws a TextBufferView
drawEditorView(view, x, y) Draws an EditorView

drawFrameBuffer() defaults to the complete source buffer. The source must be an owned buffer in the same Context, or the call throws. Composition clips to the scissor rectangle and keeps grapheme, continuation, link, image, and color-intent state.

Composition copies cells directly when the current opacity is 1, the source’s respectAlpha is false, and neither buffer holds graphemes, links, or images. Otherwise, it alpha-blends each source cell.

Images and numeric buffers#

Method Purpose
drawImage(image, x, y, width, height, pixelWidth = 0, pixelHeight = 0, sourceX = 0, sourceY = 0, sourceWidth = image.width, sourceHeight = image.height, protocol = "auto") Records a native image placement and returns whether it is visible and valid
drawSuperSampleBuffer(x, y, data, length, format, alignedBytesPerRow) Converts "bgra8unorm" or "rgba8unorm" pixel rows to terminal cells
drawGrayscaleBuffer(x, y, intensities, sourceWidth, sourceHeight, fg = null, bg = null) Draws one intensity value per source cell
drawGrayscaleBufferSupersampled(...) Draws grayscale source samples with supersampling
drawPackedBuffer(data, length, x, y, terminalWidthCells, terminalHeightCells) Reads the native packed-buffer format

Image destination positions and dimensions use cells. pixelWidth and pixelHeight use terminal pixels. Source coordinates use image pixels.

A successful drawImage() retains a Context-owned image copy until image placements are cleared, materialized as fallbacks, or the buffer is resized or destroyed. Drawing cells over the image marker does not release that retained placement. The caller still owns and can dispose its source image handle. Read NativeImage for decode and pixel ownership.

drawPackedBuffer() exposes no public TypeScript description of the packed byte format. Treat it as an integration surface for a producer that already implements the native format. Do not pass arbitrary bytes.

Clipping and opacity#

The scissor stack uses cell rectangles:

Method Behavior
pushScissorRect(x, y, width, height) Intersects the new rectangle with the active rectangle
popScissorRect() Removes one rectangle and does nothing on an empty stack
clearScissorRects() Removes every rectangle

Normal drawing operations clip against the active rectangle. Raw buffer writes and color-matrix methods do not apply the scissor stack for you.

The opacity stack stores a cumulative value:

  • pushOpacity(value) clamps value to 0..1 and multiplies it by the current opacity.
  • popOpacity() removes one level.
  • getCurrentOpacity() returns 1 for an empty stack.
  • clearOpacity() removes all levels.

Normal alpha-aware drawing reads the current opacity. Raw writes and matrix methods do not.

Every buffer checks stack operations. Each stack allows at most 256 custom entries. Scissor coordinates and dimensions must be integers, and each rectangle edge must fit in a signed 32-bit integer. A negative width or height pushes an empty rectangle. Opacity must be finite. Finite values clamp to 0..1. A rejected operation leaves the stack unchanged.

A paint hook’s buffer starts each hook with the node’s clipping and opacity. Its pop and clear calls remove only the entries that the hook pushed, not that inherited state. Custom entries reset between hooks.

Owned buffers keep their stacks until you pop, clear, or destroy them. Use try/finally to balance scopes when your drawing code can throw. Use setCellWithAlphaBlending() for opacity-aware cell drawing. drawText() with an opaque background keeps its direct-write behavior.

setRespectAlpha(true) makes drawFrameBuffer() always blend this buffer’s cells when this buffer is the source. It does not convert existing cells. It throws on renderer buffers and paint hook buffers.

Color transforms#

colorMatrix() applies a 4x4 matrix to masked cells. colorMatrixUniform() applies it to all cells. Both can target foreground, background, or both.

Read FrameBuffer color matrices for the exact matrix, mask, clamp, and color-intent rules. Read Post-processing effects before a helper mutates raw arrays.

Capture operations#

getRealCharBytes(addLineBreaks = false) resolves tagged graphemes and image fallback glyphs to UTF-8. The optional line-break mode is a capture helper, not a lossless serialization.

getSpanLines() returns one CapturedLine per row. Adjacent cells with equal foreground, background, and base attributes share a CapturedSpan. Each span has text, fg, bg, attributes, and a cell width.

Captured spans have limits:

  • They omit hyperlink URLs and IDs because attributes are reduced to the low eight base bits.
  • They expose image fallback glyphs, not image pixels or protocol metadata.
  • Their text reconstruction uses native per-cell byte lengths, including multi-code-point graphemes and zero-length continuation cells.
  • Span width counts terminal cells and can be larger than JavaScript string length.

Use capture spans for tests that compare ordinary text and style. Do not use them as a lossless buffer serialization.

Resize and raw views#

When the dimensions change, resize(width, height) reallocates the arrays and clears all cells. It keeps the native buffer object. A call with the current dimensions does nothing. Dimensions must stay positive, and width * height must not exceed the Context’s renderCellsMax. Only owned buffers support resize(). Renderer buffers and paint hook buffers throw. After a dimension-changing resize, get a new cell scope.

Never mutate the scoped char or attributes arrays for arbitrary text. Direct writes bypass grapheme, continuation, link, and image-placement bookkeeping. Direct color writes can discard palette and default-color intent in the high bits.

The raw views remain public for specialized controlled effects. Restrict those effects to known printable ASCII text and explicit RGB colors. Text drawing stores every other character as a tagged grapheme entry. Prefer drawing methods for all general content.

Next#

Changes0.6.0, 0.5.15, 0.5.13
0.6.0
core: Drawing methods throw RangeError for fractional, NaN, or infinite coordinates and sizes. drawText() and box titles throw RangeError for text larger than 64 KiB of UTF-8. setCell(), setCellWithAlphaBlending(), drawChar(), and drawText() throw NativeError for attribute bits 8 to 31. In a paint hook, the frame fails instead. (#1479)
native: Buffers write cells that are not grapheme clusters on a faster path, and drawText() writes printable ASCII without copying it. See Buffer API. (#1591)
Added OptimizedBuffer.fromSession, OptimizedBuffer.withBuffers.
Changed OptimizedBuffer.create, OptimizedBuffer.drawGrid, OptimizedBuffer.encodeUnicode, OptimizedBuffer.freeUnicode.
Removed OptimizedBuffer.
0.5.15
core: drawBox and drawPackedBuffer draw nothing when the width or height is zero or negative. Before, a negative extent reached native code as a very large value and could crash it, and a zero packed-buffer width divided by zero. pushScissorRect clips to an empty rectangle for such extents, so the matching pop stays balanced. (#1559)
native: Blended cell writes reject negative coordinates. Before, an off-screen write could overflow or address an invalid cell. (#1556)
0.5.13
core: Drawing text and box titles, resizing buffers, blending colors, and updating TextTable selections now use less CPU. (#1538, #1539)
native: Color rounding no longer calls roundf, and buffer resizes reuse the existing cell arrays while the new size fits. A failed resize leaves the buffer unchanged. (#1536, #1537)