Ink Compatibility Layer
@rezi-ui/ink-compat lets Ink apps run on Rezi's renderer.
@rezi-ui/ink-compat lets Ink apps run on Rezi's renderer.
It is designed for practical compatibility: keep React + Ink component/hook semantics, but replace Ink's renderer backend with Rezi's deterministic layout and draw pipeline.
If you are actively migrating an app, start with Ink to Ink-Compat Migration and use this page as the runtime/internals reference.
What this gives you
- Reuse existing Ink app code with minimal migration.
- Keep common Ink APIs (
render,Box,Text,useInput,useFocus, etc.). - Route rendering through Rezi's engine for better performance and deterministic frame behavior.
- Debug parity issues with structured traces instead of ad-hoc logging.
Scope and expectations
Goals:
- High-fidelity behavior for real-world Ink apps.
- Stable, deterministic diagnostics for parity work.
- Clear compatibility boundaries.
Non-goals:
- Re-implement all Ink internals byte-for-byte.
- Guarantee every undocumented edge-case behavior from every Ink version/fork.
Install and use
Option A: explicit import swap (recommended)
Use this when you can edit source imports.
// Before
import { render, Box, Text } from "ink";
// After
import { render, Box, Text } from "@rezi-ui/ink-compat";Option B: package aliasing (no app source changes)
Use this when you want existing import "ink" calls to keep working.
Install alias packages under Ink names:
npm install \
ink@npm:@rezi-ui/ink-compat@latest \
ink-gradient@npm:ink-gradient-shim@latest \
ink-spinner@npm:ink-spinner-shim@latestEquivalent with pnpm:
pnpm add \
ink@npm:@rezi-ui/ink-compat@latest \
ink-gradient@npm:ink-gradient-shim@latest \
ink-spinner@npm:ink-spinner-shim@latestEquivalent with Yarn:
yarn add \
ink@npm:@rezi-ui/ink-compat@latest \
ink-gradient@npm:ink-gradient-shim@latest \
ink-spinner@npm:ink-spinner-shim@latestShims and ecosystem packages
Compat includes dedicated shims for commonly-used Ink ecosystem packages:
ink-gradient->ink-gradient-shimink-spinner->ink-spinner-shim
You can also import shim implementations from @rezi-ui/ink-compat directly:
@rezi-ui/ink-compat/shims/ink-gradient@rezi-ui/ink-compat/shims/ink-spinner
Wiring verification (recommended in CI)
To ensure you are not silently running real Ink:
- Verify resolved package identity:
node -e "const p=require('ink/package.json'); if(p.name!=='@rezi-ui/ink-compat') throw new Error('ink resolves to '+p.name); console.log('ink-compat active:', p.version);"- Verify resolved module path:
node -e "const fs=require('node:fs'); const path=require('node:path'); const pkg=require.resolve('ink/package.json'); console.log(fs.realpathSync(path.dirname(pkg)));"-
For bundled CLIs, rebuild the bundle after aliasing and validate expected compat-only markers in generated output.
-
For rendering/layout/theme parity checks, run a live PTY with
REZI_FRAME_AUDIT=1and generate evidence withnode scripts/frame-audit-report.mjs.
Ink-Compat Bench (Ink vs Ink-Compat)
This repo includes a fairness-focused benchmark + profiling suite that runs the same TUI app code against:
real-ink:@jrichman/inkink-compat:@rezi-ui/ink-compat
Key commands:
# build bench packages
npm run prebench
# (optional) set up module resolution for bench-app explicitly
npm run prepare:real-ink
npm run prepare:ink-compat
# run a scenario (3 replicates)
npm run -s bench -- --scenario streaming-chat --renderer real-ink --runs 3 --out results/
npm run -s bench -- --scenario streaming-chat --renderer ink-compat --runs 3 --out results/
# CPU profiling (writes .cpuprofile under results/.../run_XX/cpu-prof/)
npm run -s bench -- --scenario dashboard-grid --renderer ink-compat --runs 1 --cpu-prof --out results/
# final-screen equivalence gate
npm run -s verify -- --scenario streaming-chat --compare real-ink,ink-compat --out results/Docs + reports:
- Methodology + metric definitions:
BENCHMARK_VALIDITY.md - Latest report:
results/report_2026-02-27.md - Bottlenecks + fixes:
results/bottlenecks.md - Porting and architecture docs:
../migration/ink-to-ink-compat.md../dev/ink-compat-debugging.md
Public compatibility surface
Components
| Export | Notes |
|---|---|
Box | Ink-compatible layout/container props, including overflow/scroll props used by modern Ink forks |
Text | Ink text styling props + wrapping/truncation behavior |
Newline | Line break helper |
Spacer | Flexible spacer helper |
Static | Static channel output compatible with Ink-style scrollback behavior |
Transform | Line transform wrapper (e.g. post-process text lines) |
Hooks
| Export | Notes |
|---|---|
useApp | { exit, rerender } interface |
useInput | Input subscription + raw mode management |
useFocus | Focus registration and focus state |
useFocusManager | Focus traversal/control helpers |
useStdin / useStdout / useStderr | Stream access helpers |
useIsScreenReaderEnabled | Reads compat screen-reader flag |
useCursor | Cursor visibility/position integration |
Runtime APIs
| Export | Notes |
|---|---|
render | Primary runtime entrypoint |
renderToString | Non-interactive rendering for tests/snapshots |
measureElement | Layout measurement by host node ref |
ResizeObserver | Compat resize observer export |
getBoundingBox | Host node geometry helper |
getInnerHeight / getScrollHeight | DOM-like helpers |
Testing entrypoint
@rezi-ui/ink-compat/testing
Provides a compact Ink-testing-library-like renderer for frame assertions and input simulation.
Keyboard helpers
kittyFlagskittyModifiers
render() options
render(element, options) supports:
| Option | Default | Notes |
|---|---|---|
stdout | process.stdout | Render target stream |
stdin | process.stdin | Input source stream |
stderr | process.stderr | Diagnostics/error output |
exitOnCtrlC | true | Ctrl+C triggers exit() unless disabled |
patchConsole | true | Patches console writes so logs do not destroy UI frame |
debug | false | Enables verbose internal diagnostics |
maxFps | 30 | Frame throttling; \<=0 disables throttling |
concurrent | false | Kept for API compatibility; not a React scheduling mode toggle |
kittyKeyboard | { mode: "disabled" } | Kitty keyboard protocol support |
isScreenReaderEnabled | process.env.INK_SCREEN_READER === "true" | Accessibility mode hint |
onRender | undefined | Per-frame callback with renderTime, output, staticOutput? |
alternateBuffer | false | Use terminal alternate screen (?1049h) |
incrementalRendering | false | Incremental write mode instead of full-screen rewrite |
How it works
High-level pipeline
1. React reconciler host tree
Ink-compat provides a custom React reconciler host config that stores an InkHostNode tree.
- Host nodes keep type/props/children/text data.
- Focus registration and key routing are handled in bridge/context state.
- React semantics are preserved (state/effects/context/suspense in app code).
2. Translation layer
translation/propsToVNode.ts converts host nodes into Rezi VNodes.
Key mappings:
- Ink layout props -> Rezi layout props (
flex*, spacing, min/max sizes, positioning). - Ink border styles/colors -> Rezi border style maps.
- Ink text styling -> Rezi text style maps.
- Overflow/scroll props -> Rezi overflow and scroll props.
- Virtual nodes (
Spacer,Newline,Transform) -> dedicated Rezi equivalents.
The translator also supports mode-based extraction:
- full tree (
translateTree) - dynamic subtree only (
translateDynamicTree) - static subtree only (
translateStaticTree)
This is used for static channel behavior described below.
3. Dynamic + static channels
<Static> output is treated as a scrollback-oriented channel:
- Static subtree renders separately.
- Static output accumulates above dynamic frame output.
- Dynamic viewport is reduced by static row count so footers/prompts remain anchored.
This is critical for parity with Ink apps that stream logs while keeping an interactive prompt anchored.
4. Viewport, layout, and percent resolution
Render pass behavior:
- Read viewport from
stdout/fallback stream/env. - Translate dynamic subtree.
- Resolve percent markers against current layout viewport.
- Render once; if percent markers were present, render a second pass with resolved values.
- Compute content bounds (
maxRectBottom) from layout nodes. - In non-alternate-buffer mode, size ANSI grid to content height (not full terminal rows).
Additional parity behavior:
- Root viewport coercion for overflow-clipped roots.
- Resize-event timeline handling.
- Stable-output preservation on transient empty frames after resize.
5. ANSI output + color strategy
Color support resolution order:
NO_COLOR(non-empty) disables color.FORCE_COLORoverrides level (0..3).stdout.getColorDepth()if available.- Fallback defaults to truecolor.
When host text already contains ANSI SGR sequences, compat forces truecolor handling for that frame/path to avoid degrading pre-styled output.
6. Input, focus, cursor
Input flow is bridge-driven:
- Parses standard ANSI/CSI sequences.
- Optional Kitty keyboard protocol parsing.
- Emits normalized
keyobject touseInputhandlers. - Handles Tab/Shift+Tab focus traversal.
- Handles Ctrl+C exit (unless
exitOnCtrlC: false).
Focus flow:
useFocusregisters focusable IDs in bridge context.useFocusManagercontrols traversal and direct focus.- Focus changes trigger rerender where needed.
Cursor flow:
useCursorsets cursor position/visibility in context.- Runtime updates terminal cursor state around frame writes.
7. Instance lifecycle model
render() behavior by stdout:
- One active compat instance per
stdoutstream. - Calling
render()again on the samestdoutrerenders existing instance. unmount()andcleanup()release stream listeners, timers, raw mode, and terminal protocol state.
Recommended integration patterns
Pattern A: migrate imports directly
Best when you control app source and want explicitness.
- Replace
inkimports with@rezi-ui/ink-compat. - Keep app code structure unchanged first.
- Validate UI parity before broader refactors.
Pattern B: alias package names
Best when you want a no-source-change adoption path.
- Alias
inkto@rezi-ui/ink-compat. - Alias
ink-gradient/ink-spinnerto shim packages. - Run parity checks before and after dependency lockfile updates.
Pattern C: test-first rollout
- Use
@rezi-ui/ink-compat/testingto snapshot important frame states. - Add keyboard/focus regression tests around core interaction loops.
- Run compatibility traces in CI for known-problem screens.
Diagnostics and tracing
Compat diagnostics are env-gated and deterministic.
| Env var | Purpose |
|---|---|
INK_COMPAT_TRACE=1 | Enables compat trace stream |
INK_COMPAT_TRACE_FILE=/path/log | Writes trace lines to file |
INK_COMPAT_TRACE_STDERR=1 | Mirrors trace lines to stderr |
INK_COMPAT_TRACE_DETAIL=1 | Adds node/op snapshots |
INK_COMPAT_TRACE_DETAIL_FULL=1 | Adds full VNode/grid snapshots + translation traces |
INK_COMPAT_TRACE_ALL_FRAMES=1 | Disables frame sampling |
INK_COMPAT_TRACE_IO=1 | Includes output/write queue diagnostics |
INK_COMPAT_TRACE_RESIZE_VERBOSE=1 | Includes resize timeline detail |
INK_COMPAT_TRACE_POLL_EVERY=<n> | Sampling cadence |
INK_COMPAT_TRACE_JSON_MAX_DEPTH=<n> | JSON trace depth limit |
INK_COMPAT_TRACE_JSON_ARRAY_LIMIT=<n> | JSON array truncation limit |
INK_COMPAT_TRACE_JSON_OBJECT_LIMIT=<n> | JSON object-key truncation limit |
INK_COMPAT_VIEWPORT_POLL_MS=<n> | Viewport poll interval |
INK_COMPAT_IDLE_REPAINT_MS=<n> | Idle repaint interval |
INK_GRADIENT_TRACE=1 | Gradient shim traces |
Use this runbook for full debug workflows and triage commands:
Testing examples
Render-to-string
import React from "react";
import { renderToString, Text } from "@rezi-ui/ink-compat";
const out = renderToString(<Text color="green">OK</Text>, { columns: 40 });Interactive frame assertions
import React from "react";
import { Text } from "@rezi-ui/ink-compat";
import { render } from "@rezi-ui/ink-compat/testing";
const ui = render(<Text>Hello</Text>);
expect(ui.lastFrame()).toContain("Hello");
ui.unmount();Known compatibility boundaries
- App/version-specific message text (for example update banners) can differ without being a renderer bug.
- Slight per-character gradient interpolation differences can exist while preserving expected visual progression.
- Terminal/OS/TTY quirks can still cause minor differences outside renderer control.
concurrentis accepted for API compatibility but does not map to upstream React concurrent scheduling semantics.
Troubleshooting checklist
- Verify package wiring first (
inkalias/import swap + shims). - Reproduce with traces enabled (
INK_COMPAT_TRACE=1). - Compare structure before color (
layoutViewport,gridViewport, static rows, overflow counts). - Then inspect color/gradient data (
FORCE_COLOR,NO_COLOR,INK_GRADIENT_TRACE). - Add focused regression tests for the failing screen.
Maintainer workflow for parity fixes
- Reproduce with trace capture.
- Identify stage of drift: host tree vs translation vs renderer vs ANSI output.
- Add or update tests in
packages/ink-compat/src/__tests__. - Keep instrumentation environment-gated and deterministic.
- Re-validate against upstream app screenshots/traces.