Getting Started
Install #
npm install @b9g/termdomTermDOM runs on Node, Bun, and Deno, and has no native or WASM dependency.
Usage #
import {TermDOM} from "@b9g/termdom";
const term = new TermDOM();
term.attach();
const {document} = term;
const box = document.createElement("div");
box.style.backgroundColor = "blue";
box.style.color = "white";
box.style.padding = "0 1ch";
box.textContent = "Hello, terminal";
document.body.appendChild(box);attach() takes the terminal. Construction and DOM mutation are inert
until it runs. There is no render call: mutations are observed and painted
on the next frame.
A program that only writes static output does not call attach():
term.renderANSI(html) returns an ANSI string, and term.print(html)
writes one as ordinary command output.
Units #
A terminal is a grid of character cells. 1ch is one cell wide and 1px
is one cell tall, so width: 12ch is twelve columns and height: 3px is
three rows. Lengths that land between cells resolve to whole cells.
Stylesheets #
const style = document.createElement("style");
style.textContent = `
.card {
border: 1px solid;
padding: 0 1ch;
color: cyan;
}
.card:focus {
border-color: yellow;
}
`;
document.head.appendChild(style);Selectors, specificity, inheritance, @media queries, and custom
properties behave as they do in the browser.
Quitting #
window.close() ends the session: the final frame is flushed to
scrollback, terminal modes are restored, and the process exits. An
unhandled Ctrl-C performs the same call.
document.addEventListener("keydown", (e) => {
if (e.key === "q") term.window.close();
});Frameworks #
A frontend framework renders into TermDOM's document the way it renders into a browser's. Frameworks read browser globals, so assign the ones yours expects before it loads:
import {TermDOM} from "@b9g/termdom";
const term = new TermDOM();
term.attach();
globalThis.document = term.document;
globalThis.window = term.window;React needs nothing further:
import {createRoot} from "react-dom/client";
createRoot(term.document.body).render(<App />);Vue also reads the DOM constructors for instanceof checks, and captures
document when its module loads โ so the globals go up first and Vue comes
in by dynamic import:
globalThis.Element = term.window.Element;
globalThis.Node = term.window.Node;
globalThis.Text = term.window.Text;
globalThis.Comment = term.window.Comment;
globalThis.SVGElement = term.window.SVGElement;
const {createApp} = await import("vue");
createApp(App).mount(term.document.body);Svelte is a compiler, so its components compile first (svelte/compiler
with generate: "client", or any bundler's Svelte plugin), and its package
exports resolve the client runtime under the browser condition:
node --conditions=browser app.jsglobalThis.Element = term.window.Element;
globalThis.Node = term.window.Node;
globalThis.Text = term.window.Text;
globalThis.Comment = term.window.Comment;
const {mount} = await import("svelte");
const {default: App} = await import("./App.js"); // compiled from App.svelte
mount(App, {target: term.document.body});Crank needs Node and document โ examples/todomvc.ts
and the solitaire example show it in full.
Next #
- Layout โ the box model, flexbox, and tables.
- Events and input โ keyboard, mouse, focus, and form controls.
- API โ the full surface.