Buffer

The buffer classes represent rendered terminal content in memory before it is written to the screen.

ReadableBuffer provides a read-only inspection API, WritableBuffer extends this with mutation and drawing operations, and Buffer is the concrete 2D storage type used in most applications.

For more specialized use cases, RemappedBuffer adds efficient row- and column-based reordering. This is ideal for editors, scrollback views, or any workload that frequently inserts, deletes, or moves whole lines.

Building on top of that, CursorBuffer provides a VT-style cursor-writing interface via CursorWriter.

Use these types whenever you want to build frames off-screen, compare frames, copy content between buffers, or derive masks from rendered characters.

Usage

Reading and Writing Through Buffer Interfaces

ReadableBuffer and WritableBuffer allow helper functions to operate on terminal content without depending on a specific implementation.

auto renderStatusPanel(WritableBuffer &target, Rectangle panel) -> void {
    target.fill(panel, Block{" ", Color{fg::Inherited, bg::Blue}});
    target.drawBlockText(
        "Status",
        panel.insetBy(Margins{1}),
        Alignment::TopLeft,
        Color{fg::BrightWhite, bg::Blue});
}

auto screen = Buffer{Size{80, 24}};
renderStatusPanel(screen, Rectangle{2, 2, 24, 8});

Use ReadableBuffer when your function only needs to inspect content, count differences, or derive masks.

Use WritableBuffer when your function should modify the target buffer without caring whether it operates on a standalone Buffer or another writable implementation.

Write-Clipped Paint Operations

WriteClippedBufferRef is a thin writable wrapper for temporary subsurface painting. It exposes source coordinates to the drawing code and clips write operations to a target rectangle in the wrapped buffer. Read operations are translated into the wrapped buffer without applying the write clip, which lets drawing helpers sample existing cells around the clipped area.

auto screen = Buffer{Size{80, 24}};
auto panel = WriteClippedBufferRef{
    screen,
    Position{0, 0},
    Rectangle{10, 4, 32, 8}};

panel.fill(panel.sourceRect(), Block{" ", Color{fg::Inherited, bg::Blue}});
panel.drawBlockText("Panel title", Rectangle{0, 0, 32, 1}, Alignment::Center);

Use WriteClippedBuffer when the wrapper must store a shared pointer to the wrapped buffer. Use WriteClippedBufferRef for short-lived paint passes where the wrapped buffer already outlives the wrapper.

Cloning, Copying, and Resizing

Buffer supports the typical frame-management tasks required by interactive terminal applications: creating new frames, cloning the current state, and resizing buffers when the terminal size changes.

auto current = Buffer{Size{80, 24}};
current.fill(Block{" ", Color{fg::Inherited, bg::Black}});

const auto previous = current.clone();
current.resize(Size{100, 30}, BufferResizeMode::PreserveContent, Block::space());

clone() returns a writable copy through the abstract interface. This makes it easy to store previous frames for diffing, animation steps, or rollback logic.

Use BufferResizeMode to make the resize intent explicit:

  • BufferResizeMode::Fast uses the fastest resize path and leaves existing content undefined.

  • BufferResizeMode::PreserveContent keeps the visible rectangle stable and fills newly exposed cells with the provided fill character.

Working with Remapped Buffers

RemappedBuffer is designed for workloads where the content remains logically grid-based, but rows or columns are frequently reshuffled.

Instead of rewriting every affected cell, the buffer maintains remapping tables and only updates rows or columns that become newly visible. This keeps operations like scrolling or line insertion efficient, even for large buffers.

auto history = RemappedBuffer{Size{80, 2'000}, Orientation::Vertical};
history.fill(Block::space());

history.eraseRows(0, Block::space(), 1);      // Scroll everything up by one row.
history.set(Position{0, 1'999}, BlockString{"new log line"});

history.resize(Size{100, 2'000}, BufferResizeMode::PreserveContent, Block::space());

Use the plain RemappedBuffer::resize() overload when you want maximum performance and plan to redraw the content anyway.

Use BufferResizeMode::PreserveContent when the visible order must remain stable while expanding or cropping the buffer.

For RemappedBuffer, this preserve-content mode is fast when only the primary orientation axis changes. If the secondary axis changes, preserving content requires rebuilding the logical view and is therefore significantly more expensive.

Streaming Scrollback with CursorBuffer

CursorBuffer is the right choice when text is appended over time, as if it were written directly to a terminal.

It tracks a cursor position, maintains an active color, and supports streaming writes via CursorWriter. When the cursor reaches the bottom edge, it can wrap, scroll, or grow vertically depending on the configured overflow mode.

Newly created cells are initialized using fillChar(), allowing you to keep a consistent background color or placeholder glyph as the buffer grows.

auto logHistory = CursorBuffer{
    Size{120, 10},
    CursorBuffer::OverflowMode::ExpandThenShift,
    Size{120, 500},
    Block{" ", Color{fg::Default, bg::Black}}};

logHistory.setColor(Color{fg::BrightBlue, bg::Black});
logHistory.printParagraph("2026-03-26 09:02:23 INF Request completed in 43 ms");

logHistory.setColor(Color{fg::BrightYellow, bg::Black});
logHistory.printParagraph("2026-03-26 09:03:04 WRN Cache refresh is still pending");

const auto visibleTop = std::max(0, logHistory.size().height() - 20);
auto view = BufferConstRefView{logHistory, Rectangle{0, visibleTop, 120, 20}};
terminal.updateScreen(view);

This pattern works especially well for log viewers, REPL-style tools, dashboards, or any application that needs a growing history buffer with a live viewport onto the most recent content.

If your fill strategy changes later, update it with CursorBuffer::setFillChar.

For details about the streaming API itself—such as print(), printLine(), and cursor movement—see Cursor Output.

Important

For an efficient render loop, keep a persistent instance of Buffer and simply resize it when the terminal size changes.

Reusing the same buffer avoids unnecessary memory allocations and helps keep rendering predictable and fast.

A typical render loop might look like this:

struct MyApp {
    void renderLoop() {
        for (;;) {
            _terminal.testScreenSize();
            _buffer.resize(_terminal.size());

            // Render the current frame into the buffer.
            _terminal.updateScreen(_buffer);

            // Handle key presses or other input.
        }
    }

    Terminal _terminal;
    Buffer _buffer;
};

Building Buffers from BlockText Lines

For status panels, generated reports, or static UI elements, Buffer can be constructed directly from line-oriented text.

const auto help = Buffer::fromLinesInString(BlockString{
    "Q  Quit\n"
    "R  Refresh\n"
    "H  Toggle help"});

auto screen = Buffer{Size{40, 12}};
screen.setFrom(help, Block::space());

This is often the fastest way to turn preformatted terminal text into a buffer that can later be positioned within a larger layout.

Copying and Aligning Sub-Regions

BufferDrawOptions makes buffer-to-buffer composition explicit. It bundles the target region, the optional source crop, and the color-combination strategy for WritableBuffer::drawBuffer().

auto frame = Buffer{Size{52, 16}};
auto sprite = Buffer{Size{12, 5}};
sprite.drawFrame(sprite.rect(), FrameStyle::Double, Color{fg::BrightCyan, bg::Inherited});
sprite.drawBlockText("CPU", sprite.rect(), Alignment::Center, Color{fg::BrightWhite, bg::Inherited});

auto options = BufferDrawOptions{
    Rectangle{30, 3, 18, 7},
    Rectangle{0, 0, 12, 5}};
options.setOverwriteColors(false);

frame.drawBuffer(sprite, options);

Use a zero-sized targetRect when you only need an exact target position. Use a zero-sized sourceRect when the whole source buffer should be copied.

If you need to preserve the target glyphs while only adopting parts of the source style, attach a BlockCombinationStyle through setCombinationStyle().

Comparing Frames and Deriving Masks

ReadableBuffer also provides analysis helpers that are useful for tests, animation pipelines, and bitmap-based effects.

const auto changedCells = previous->countDifferencesTo(current);
const auto frameMask = current.toMask({U'|', U'-', U'+', U'┌', U'┐', U'└', U'┘'});

if (changedCells > 0 && frameMask.size().contains(Position{0, 0})) {
    // React to the changed frame content.
}

Use toMask() when you want to reason about the structure of rendered content (for example, line art or borders) instead of raw character or color data.