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

Native rendering

OpenTUI’s native library turns a tree of UI nodes into terminal output. You can call it from C or Zig without JavaScript.

This walkthrough follows one small interface: a bordered box containing the label Hello. Its drawing area is 12 columns wide and three rows high:

Your program describes the box and label. The native library calculates their positions, draws their cells, and produces terminal output. The program then delivers that output to its destination.

After the first frame, you will change the label to Ready and render again without rebuilding the tree.

Describe the interface as a tree#

The box and label need three native nodes. Each node stores one part of the interface:

The root is the top of the tree. The box contains the label. Adding the label to the box establishes that relationship. It does not specify where to draw the first character.

Nodes store layout properties, such as width and padding, and drawing properties, such as background color and borders. A text node also stores its content. Node creation, property changes, and parent-child relationships are native API operations.

The native library keeps this tree, or scene, between frames. This is the retained scene model: change the nodes you already have, then render them again. You do not submit a fresh list of drawing commands for each frame.

Calculate positions and sizes#

Layout turns the tree’s relationships and size rules into positions and dimensions. The scene uses Yoga, a flexbox layout engine, with one Yoga node for each scene node. Dimensions count terminal cells, not pixels.

The root fills the drawing area, and the box stretches across it. The border takes one cell on each side, leaving ten cells for the label. That places Hello at column 1, row 1, just inside the border. Coordinates start at zero.

You did not calculate the text position yourself. The border and parent-child relationship determine it. The same layout rules also handle padding, alignment, flexible sizes, and nested containers.

When you change a layout property, the next layout pass calculates the result. Layout queries read stored geometry. They do not start a new pass.

Draw a grid of cells#

After layout, the scene paints into an in-memory grid called the framebuffer. The box draws its border, then the label draws Hello inside it. Each cell stores character information, foreground and background colors, and attributes such as bold or underline.

The H in Hello occupies one cell. Other text can occupy several cells or combine several code points into one displayed character. Text drawing APIs handle that conversion. A UTF-8 byte count is not a terminal-cell width.

Painting changes the framebuffer, not the terminal. No terminal connection is necessary at this stage.

Send the result to an output destination#

A terminal accepts bytes, not a node tree or a framebuffer. The native renderer encodes the cells as text, cursor movement, colors, and other control sequences. It compares cells with its existing encoding state, so an update need not send the entire screen again.

The bytes wait in an output queue until your program delivers them. Your program can copy them out and write them itself. It can also let the native library write them to standard output or pass them to a writer function that your program supplies. The delivery path is the transport. You can write to a file, a local terminal, or a connection. The diagram shows the copy path:

A slow connection can still be sending a frame after painting finishes. The program reports delivery as bytes reach the transport. Once delivery covers the frame’s output, the native library marks that frame as presented. This does not prove that a remote terminal visibly displayed it.

Change the label, not the tree#

After the first frame completes, set the label’s text to Ready and repeat painting, encoding, and delivery:

The root, box, and label are the same nodes. Their relationships and border settings did not change. Only the label’s content changed. The retained scene produces the new frame.

Layout, painting, and encoding still have a cost. Retaining the scene means your program describes changes instead of directing every node’s drawing on every frame.

Build the program around the scene#

Your program is the host. It owns application state and decides when to render. The native library does not start an application event loop or a rendering worker.

An interactive host reads input, changes nodes, and requests frames. The programs in the language guides create this box and label, render Hello, then change it to Ready. They write both frames to files, using the same operations as a terminal application.

Continue with Resources and ownership for object lifetimes, then Frames and output for drawing and delivery.

The C application binary interface (ABI) is experimental. Use the native library, headers, and bindings from the same revision.

Core provides a TypeScript application API over this library through Renderable and CliRenderer, not the native frame protocol. How Core uses native explains that integration separately.

Read Host I/O and time before connecting a terminal transport. Then choose the C or Zig guide to build and run the box-and-label program.

Changes0.6.0, 0.5.12, 0.5.11
0.6.0
native: opentui.h declares an experimental C ABI, and the opentui Zig module gains the same Context, scene, and Session model. A Context’s object table starts at object_capacity slots and doubles when it is full, up to 4,194,304 slots or object_capacity, whichever is larger. Past that limit, object creation fails with OT_OBJECT_LIMIT. See Native rendering. (#1479, #1622)
Added FFIRenderLib, NATIVE_BUFFER_TEXT_BYTES_MAX, NATIVE_EDGE_NONE, NATIVE_SESSION_CONTROL_PACKET_BYTES, NativeBorder, NativeEditCommand and 76 more.
Changed NativeClipboardCancelStatus.AlreadyTerminal, NativeClipboardCancelStatus.InvalidHandle, NativeClipboardCancelStatus.Requested, NativeClipboardCopyStatus.BufferTooSmall, NativeClipboardCopyStatus.InvalidArgument, NativeClipboardCopyStatus.InvalidHandle and 28 more.
Removed RenderLib, AudioEngineLib.audioClearCaptureDeviceSelection, AudioEngineLib.audioClearPlaybackDeviceSelection, AudioEngineLib.audioCloseStream, AudioEngineLib.audioCreateGroup, AudioEngineLib.audioCreateStream and 38 more.
0.5.12
Added RenderLib.embeddedTerminalSetTransparentBackground, RenderLib.textBufferViewSetTextAlign.
0.5.11
Added RenderLib.linkGetUrl, RenderLib.textBufferViewGetLineSources.
Changed NativeAudioStreamFormat, AudioStreamCreateOptions.