tuil
Guides

Renderer selection

Choose Ink or cell at the correct application boundary.

View rawEdit

TUIL keeps layout projection, hit testing, frame scheduling, and output sessions behind RendererBackend. Use ink for React Ink compatibility and cell for deterministic cell composition, minimal ANSI output, and high-volume screens. Backend metadata distinguishes renderer-application support from react-ink-components; cell advertises only the former.

import { CellRendererBackend } from "@mwillbanks/tuil-cell";
import { createApp } from "@mwillbanks/tuil";
import { render } from "@mwillbanks/tuil-ink";
import { createRendererComponentRuntime } from "@mwillbanks/tuil-renderer";

const cell = new CellRendererBackend();
const component = createRendererComponentRuntime({
  initialState: { count: 0, width: 0, height: 0 },
  component: ({ state }) => ({
    lines: [`Count ${state.count}`, `${state.width}x${state.height}`],
    semantics: [
      { id: "counter", role: "button", label: `Count ${state.count}` },
    ],
  }),
  input: (state, input) =>
    input === "\r" ? { ...state, count: state.count + 1 } : undefined,
  resize: (state, width, height) => ({ ...state, width, height }),
});
const app = createApp({
  component,
  renderer: cell.id,
  renderers: [cell],
});

const instance = await render(app);
await instance.waitUntilExit();

Both backends consume this single renderer-neutral component runtime and share state, lifecycle, resize, keyboard, pointer, semantics, layout, invalidation, and cleanup behavior. React components built on Ink primitives require the Ink renderer; selecting the cell backend for that tree fails explicitly instead of mounting Ink and reparsing ANSI. Migrate state and projection to RendererApplication before selecting cell.

The scheduler paces frames at targetFps; maximumFps remains a hard upper bound and cannot accidentally make the target run faster. Output ownership is behavioral: alternate, main, inline, split-footer, and embedded sessions use different cursor, scroll-region, capture, and cleanup protocols. Main and inline modes never clear the full display to commit scrollback. Inline and split-footer sessions expose only their configured owned rows as the renderer viewport. Embedded output is explicit: capture writes only to memory, passthrough writes only to the host target, and tee does both. passthrough is the safe default. Capture and tee retain at most the configured embeddedCaptureLimitBytes; captureSnapshot() reports the retained bytes, limit, dropped-byte count, and truncation state so long-lived hosts cannot silently accumulate unbounded frame history.

Renderer scenes use a closed color union (the named ANSI palette, indexed 0–255 colors, RGB channels, or the terminal default), reject control-bearing hyperlinks, and require styled runs to reproduce their corresponding plain line. Both backends validate and clip that same scene before projection. The shared core and renderer public contracts use Web/Bun primitives rather than Node stream or Buffer types.