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
nextRenderBufferwork 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
currentRenderBufferalways 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(), andgetSpanLines()throw because a hook cannot read the frame.setRespectAlpha()throws.drawSuperSampleBuffer()anddrawPackedBuffer()accept aUint8Array, not a native address.drawImage()returnstruebecause 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, andbackgroundColor - Optional
borderStyle,customBorderChars, andshouldFill, which defaults tofalse - Optional
title,titleColor, andtitleAlignment, which defaults to"left" - Optional
bottomTitleandbottomTitleAlignment, 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)clampsvalueto0..1and multiplies it by the current opacity.popOpacity()removes one level.getCurrentOpacity()returns1for 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#
- FrameBuffer owns a buffer in the render tree.
- Text and terminal cells defines graphemes, links, and display width.
- Rendering pipeline explains current and next buffers.
FrameBuffercolor matrices defines native color transforms.- Post-processing effects lists the experimental helpers.