TypeScript Buffer Guide

TypeScript patterns for indexing, packing attributes, and writing helpers for raw-buffer rendering in @visulima/tui/core

TypeScript Buffer Guide

This guide focuses on the TypeScript side of raw-buffer rendering: indexing, packing attributes, and writing helpers you can reuse in demos or games.

1. Mental Model: Grid → Flat Array

You draw to a terminal grid (cols × rows), but the renderer consumes a flat Uint32Array.

Each cell uses 2 slots:

  • slot 0: char codepoint
  • slot 1: attr code

Example for cols = 4, rows = 3:

Grid cell index (y * cols + x)

y=0   0   1   2   3
y=1   4   5   6   7
y=2   8   9  10  11
Flat Uint32Array slots (2 per cell)

cell 0  -> slots 0,1
cell 1  -> slots 2,3
cell 2  -> slots 4,5
...
cell 11 -> slots 22,23

Formulas:

const cell = y * cols + x;
const charSlot = cell * 2;
const attrSlot = charSlot + 1;

Equivalent one-liner:

const base = (y * cols + x) * 2;

2. Attr Code Bit Layout

attr is packed as:

attr = (styles << 16) | (bg << 8) | fg

Bit view:

31                     24 23          16 15           8 7            0
+------------------------+--------------+--------------+--------------+
|        unused          |    styles    |      bg      |      fg      |
+------------------------+--------------+--------------+--------------+
  • fg, bg: ANSI 256 color indices (0-255)
  • styles: bitmask

Style bits:

BitStyle
1Bold
2Dim
4Italic
8Underline
16Blink
32Invert
64Hidden
128Strikethrough

3. Practical Helper Functions

Use helpers so you never repeat indexing math.

function inBounds(x: number, y: number, cols: number, rows: number) {
    return x >= 0 && x < cols && y >= 0 && y < rows;
}

function packAttr(fg = 255, bg = 255, styles = 0) {
    return ((styles & 0xff) << 16) | ((bg & 0xff) << 8) | (fg & 0xff);
}

function setCell(buf: Uint32Array, cols: number, rows: number, x: number, y: number, ch: string, fg = 255, bg = 255, styles = 0) {
    if (!inBounds(x, y, cols, rows)) return;
    const base = (y * cols + x) * 2;
    buf[base] = ch.codePointAt(0) ?? 32;
    buf[base + 1] = packAttr(fg, bg, styles);
}

function drawText(buf: Uint32Array, cols: number, rows: number, x: number, y: number, text: string, fg = 255, bg = 255, styles = 0) {
    let cx = x;
    for (const ch of text) {
        setCell(buf, cols, rows, cx, y, ch, fg, bg, styles);
        cx++;
    }
}

4. Frame Loop Template

import { Renderer, TerminalGuard, terminalSize } from "@visulima/tui/core";

const guard = new TerminalGuard();
let { cols, rows } = terminalSize();
let renderer = new Renderer(cols, rows);
let buf = new Uint32Array(cols * rows * 2);
let frame = 0;

const timer = setInterval(() => {
    buf.fill(0);

    drawText(buf, cols, rows, 2, 1, `frame ${frame++}`, 46);
    drawText(buf, cols, rows, 2, 3, "no react / pure ts", 51);

    renderer.render(buf);
}, 16);

process.on("SIGWINCH", () => {
    ({ cols, rows } = terminalSize());
    renderer.resize(cols, rows);
    buf = new Uint32Array(cols * rows * 2);
});

process.on("SIGINT", () => {
    clearInterval(timer);
    guard.leave();
    process.exit(0);
});

5. ASCII Drawing Patterns

Horizontal Line

for (let x = 0; x < cols; x++) {
    setCell(buf, cols, rows, x, 5, "─", 244);
}

Filled Rectangle

function fillRect(buf: Uint32Array, cols: number, rows: number, x: number, y: number, w: number, h: number, ch: string, fg = 255, bg = 255) {
    for (let yy = y; yy < y + h; yy++) {
        for (let xx = x; xx < x + w; xx++) {
            setCell(buf, cols, rows, xx, yy, ch, fg, bg);
        }
    }
}

Box Border (ASCII-friendly)

function drawBox(buf: Uint32Array, cols: number, rows: number, x: number, y: number, w: number, h: number, fg = 255) {
    if (w < 2 || h < 2) return;

    setCell(buf, cols, rows, x, y, "+", fg);
    setCell(buf, cols, rows, x + w - 1, y, "+", fg);
    setCell(buf, cols, rows, x, y + h - 1, "+", fg);
    setCell(buf, cols, rows, x + w - 1, y + h - 1, "+", fg);

    for (let xx = x + 1; xx < x + w - 1; xx++) {
        setCell(buf, cols, rows, xx, y, "-", fg);
        setCell(buf, cols, rows, xx, y + h - 1, "-", fg);
    }

    for (let yy = y + 1; yy < y + h - 1; yy++) {
        setCell(buf, cols, rows, x, yy, "|", fg);
        setCell(buf, cols, rows, x + w - 1, yy, "|", fg);
    }
}

6. Common Mistakes

MistakeSymptomFix
Forgetting * 2 slot strideCorrupt output or random stylesAlways compute base = (y * cols + x) * 2
Writing out of boundsMissing chars or runtime weirdnessAdd inBounds() checks
Using charCodeAt on complex Unicode in loops by indexBroken emoji/surrogate renderingIterate with for (const ch of text) and codePointAt(0)
Not reallocating buffer on resizeStretched/truncated framesOn SIGWINCH, call renderer.resize() and recreate buf
Forgetting guard.leave() on exitTerminal remains in raw modeAlways cleanup on SIGINT/shutdown

7. Harness vs Direct Renderer

If you're inside this repo, use the helper harness at examples-raw/harness.ts:

  • createLoop(...)
  • setCell(...)
  • fillRect(...)

If you're writing package-consumer code, use the public API directly (Renderer, TerminalGuard, terminalSize).

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues