API

new TermDOM(options?) #

import {TermDOM} from "@b9g/termdom";

const term = new TermDOM();

Construction writes nothing to the terminal. The document is usable immediately: it can be built, styled, and measured before attach(), or without calling it at all.

Options:

term.document, term.window #

document is a Document. Setting document.title sets the terminal window title; the previous title is restored on dispose(). document.close() writes the document into the scrollback and seals it, and the next mutation starts a fresh document below it.

window has the DOM interfaces and event constructors, and these members are wired to the terminal:

Anything not listed behaves as the DOM and CSSOM standards specify, without terminal wiring.

Parts of the built-in controls #

The form controls are shadow trees, and ::part() styles their pieces:

ControlParts
<input> text types, <textarea>value, placeholder
<select>indicator, picker, option, optgroup
<progress>, <meter>track, groove, bar
<details>details-content

A <select>'s highlighted option carries data-highlighted, a disabled one data-disabled, and a <meter>'s bar carries data-level of optimum, suboptimum, or even-less-good.

term.attach(transport?) #

Puts the terminal in raw mode, starts input handling, mouse reporting, and bracketed paste, and paints whatever the document holds. Idempotent; no other call writes to the terminal. Returns a promise that resolves once the first frame has been written.

While attached to the process transport, the Node event loop stays alive until dispose() or window.close().

document.visibilityState is "hidden" before attach(), "visible" until dispose(), and "hidden" after. It is also "hidden" while the wheel has been handed to the terminal's scrollback, until the next keystroke. visibilitychange fires on each change.

Passing a transport rebinds the instance to it, only before the first attach.

term.renderANSI(html?) #

Returns the whole document as an ANSI string at the terminal's width: colors and line breaks, no cursor movement. Pass an HTML string to render that instead; the document is left alone.

const page = term.renderANSI();
const error = term.renderANSI(`<div style="color:red">error</div>`);

It works attached or not, and doesn't disturb a live session.

term.print(html?) #

Writes renderANSI(html) to the terminal as ordinary output. Await it before exiting.

term.dispose() #

Reverses attach(): flushes the document into scrollback, restores every terminal mode and the title, and releases the transport. The process continues; window.close() is the quit. Returns a promise that resolves when every queued restore has reached the transport; await it before writing further output. The process transport also restores shell-critical modes synchronously, so a caller that exits without awaiting still leaves the shell usable. using term = new TermDOM() disposes on scope exit.

TerminalTransport #

The interface between the engine and a terminal, for embedding TermDOM somewhere other than a process โ€” an SSH server, a browser terminal, a test harness:

interface TerminalTransport {
	readonly cols: number; // live: always the current size
	readonly rows: number;
	readonly colorDepth: "ansi" | "256" | "rgb";
	readonly interactive: boolean; // false: plain line output (a pipe)
	// Optional. Takes an error's text somewhere the frame does not share;
	// true when it did. The engine keeps what it cannot place and prints
	// it below the document at the end.
	logError?(text: string): boolean;
	readonly sharesScreen: boolean; // true: anchor below existing content
	readonly readable: ReadableStream<string>; // user input
	readonly writable: WritableStream<string>; // frames out
	readonly resizes: ReadableStream<{cols: number; rows: number}>;
	readonly ready: Promise<void>; // established; Promise.resolve() if born so
	readonly closed: Promise<TerminalCloseInfo>; // the terminal went away
	// Ends the medium if the transport owns it (the process transport exits
	// the process); a no-op otherwise.
	close(info?: TerminalCloseInfo): void;
}

interface TerminalCloseInfo {
	status?: number; // process-exit semantics
	signal?: string; // "SIGHUP", "SIGTERM", ... when a signal ended it
	reason?: string;
}

Chunks on readable are strings; a byte-backed wrapper must decode with a streaming decoder so code points never split. Escape sequences may split across chunks; the engine reassembles them. When closed fulfills, the engine disposes in response.

transportFromProcess(proc?, options?) #

Returns a TerminalTransport over a Node-process-shaped object.

import {TermDOM, transportFromProcess} from "@b9g/termdom";

const term = new TermDOM({transport: transportFromProcess(process)});

The wrapper owns all process-level behavior: raw mode, SIGWINCH โ†’ resizes, signals โ†’ closed, TERM/COLORTERM โ†’ colorDepth, stdout.isTTY โ†’ interactive, stderr when it is not a terminal โ†’ logError, and an exit hook that restores the cursor if the app exits without disposing.

Edit this page on GitHub