opentui-island
v0.4.0
Published
Embed OpenTUI islands inside Node host terminal UIs like pi-tui and Ink via a Bun sidecar.
Maintainers
Readme
opentui-island
Embed OpenTUI islands inside Node terminal UIs such as pi-tui and Ink.
Why
- Keep your host app in Node.
- Render the embedded OpenTUI island in a local Bun sidecar.
- Reuse one island module across
pi-tui, Ink, and lower-level hosts.
Install
Requirements:
- Node 18+
- Bun 1.3+
- React
Install with pi-tui:
npm i opentui-island react @opentui/core @opentui/react @mariozechner/pi-tuiInstall with Ink:
npm i opentui-island react @opentui/core @opentui/react inkQuick start
Create one island module:
/** @jsxImportSource @opentui/react */
import { useKeyboard } from "@opentui/react";
import { useState } from "react";
export default function CounterIsland() {
const [count, setCount] = useState(0);
useKeyboard(
(event) => {
if (event.eventType !== "release" && event.name === "a") {
setCount((value) => value + 1);
}
},
{ release: true },
);
return (
<box style={{ width: "100%", height: "100%", paddingLeft: 1 }}>
<text fg="#00ff88">{`count:${count}`}</text>
</box>
);
}Use it in Ink.
Since Ink is React-based, this is the simpler host integration:
import { render } from "ink";
import { createIslandController } from "opentui-island";
import { InkSurface } from "opentui-island/ink";
const controller = await createIslandController({
island: {
module: new URL("./counter.island.tsx", import.meta.url),
},
});
render(<InkSurface controller={controller} width={24} height={4} />);Use it in pi-tui.
pi-tui is a little more manual because you are wiring the surface into a terminal app directly:
import { matchesKey, ProcessTerminal, TUI } from "@mariozechner/pi-tui";
import { createIslandController } from "opentui-island";
import { createPiTuiSurface } from "opentui-island/pi-tui";
const terminal = new ProcessTerminal();
const tui = new TUI(terminal);
const controller = await createIslandController({
island: {
module: new URL("./counter.island.tsx", import.meta.url),
},
});
const surface = await createPiTuiSurface({
controller,
height: 4,
initialWidth: terminal.columns,
requestRender: () => tui.requestRender(),
});
tui.addChild(surface);
tui.setFocus(surface);
tui.addInputListener((data) => {
if (matchesKey(data, "q") || matchesKey(data, "ctrl+c")) {
void surface.destroy();
tui.stop();
return { consume: true };
}
return undefined;
});
tui.start();
await surface.sync(terminal.columns);
await surface.waitUntilReady();Press a inside the island to increment the counter. Press q to quit in pi-tui.
Docs
- API and advanced usage:
docs/api.md - Pi extension examples:
examples/pi/README.md - Repo examples:
examples/ - Release history: GitHub releases
Development
bun install
bun run check
bun test
bun run test:node-integrationContributing
- Bugs and feature requests: GitHub issues
- Local agent and repo guidance:
AGENTS.md
Security
Use the GitHub Security tab for sensitive reports.
