Embedded terminal
One element, <nano-terminal>, embeds a full terminal with file tree, editor, app catalog, command palette and server preview. It renders in Shadow DOM, boots its own VM and ships as a separate subpath so headless apps never download it.
import { defineNanoTerminal } from "@userland-run/nano-sdk/terminal";
defineNanoTerminal(); // registers <nano-terminal><nano-terminal wasm-url="/nano/console.wasm" service-worker-url="/nano-sw.js" ram-mb="512" style="display:block; height:560px" ></nano-terminal>
Point wasm-url at console.wasm: the interactive shell needs BusyBox, which the slim nano.wasm doesn't embed. The element reads its attributes once, on connect. el.config (a TerminalConfig) overrides them; el.ready resolves to the handle once the shell is live.
Attributes
| Option | Type | Default | Description |
|---|---|---|---|
wasm-url | string | "/nano.wasm" | The image the terminal's VM boots. |
service-worker-url | string | "/nano-sw.js" | Service worker for previews. |
shell-command | string | "sh -i" | The command booted as the session (BusyBox applet + args, whitespace-split). |
font-px | number | 12 | Initial font size in pixels. |
ram-mb | number | 1800 | VM memory. 256–512 for BusyBox; 1800 if you run Node.js on the VM. |
no-catalog | boolean | — | Hide the catalog panel. |
no-palette | boolean | — | Disable the command palette. |
no-files | boolean | — | Hide the files panel. |
no-editor | boolean | — | Disable the editor. |
no-preview | boolean | — | Disable the server preview. |
ram-mb sets the terminal VM's memory
<nano-terminal wasm-url="/nano/console.wasm" service-worker-url="/nano-sw.js" ram-mb="256" ></nano-terminal>
Typed after the prompt: head -1 /proc/meminfo
MemTotal: 262144 kB
shell-command picks the session
BusyBox's hush instead of the default sh -i; $0 shows which shell answered.
<nano-terminal wasm-url="/nano/console.wasm" ram-mb="256" shell-command="hush -i" ></nano-terminal>
Typed after the prompt: echo shell=$0
shell=hush
font-px and the no-* switches
A bare terminal: larger font, no side panels, no editor, palette or preview.
<nano-terminal wasm-url="/nano/console.wasm" ram-mb="256" font-px="16" no-catalog no-palette no-files no-editor no-preview ></nano-terminal>
Typed after the prompt: uname -sm | tr a-z A-Z
LINUX RISCV64
createTerminal
createTerminal(target, config) mounts into a selector, element or shadow root and resolves to the same handle; the element is a thin wrapper around it. Every field is optional and deep-merged over the defaults.
import { createTerminal } from "@userland-run/nano-sdk/terminal";
const term = await createTerminal("#app", {
wasmUrl: "/nano/console.wasm",
ramMB: 512,
fontPx: 13,
shellCommand: "sh -i",
serviceWorkerUrl: "/nano-sw.js",
features: { catalog: true, palette: true, files: true, editor: true, preview: { ports: [8080, 3000], defaultPort: 3000 } },
});| Option | Type | Default | Description |
|---|---|---|---|
wasmUrl / ramMB / shellCommand / fontPx / serviceWorkerUrl | | as the attributes | Same meaning as the attributes above. |
features.catalog / palette / files / editor | boolean | true | Toggle each panel. |
features.preview | boolean | { ports?, defaultPort? } | true, ports [8080] | Toggle the preview, or set the ports offered in its selector and the one selected first. |
The handle
| Option | Type | Default | Description |
|---|---|---|---|
vm | NanoVM | — | The terminal's raw VM (addFile, readFileString, listDir, writeStdin, …; see the raw API). |
installApp(ref, opts?) | Promise<boolean> | — | Install a catalog app into the terminal's VM. opts.onProgress, opts.quiet (no echo in the shell). Rejects when the catalog feature is off. |
openFile(path) | void | — | Open a file in the editor tab. |
refreshFiles() | void | — | Re-list the files panel after outside changes. |
showPreview(port?) | void | — | Open the preview tab on a port. |
dispose() | void | — | Tear the terminal down: render loop, timers, listeners, the VM. Idempotent. <nano-terminal> calls it when the element stays detached; el.dispose() does the same. |
config, ready and the handle
Creates a second terminal from code (the element is registered by the terminal samples above, so run one of them first), drives it through its handle, then disposes it.
if (!customElements.get("nano-terminal")) throw new Error("run a terminal sample above first; it registers <nano-terminal>");
const el = document.createElement("nano-terminal");
el.setAttribute("no-assistant", "");
el.config = { wasmUrl: "/nano/console.wasm", ramMB: 256, features: { palette: false, preview: false } };
el.style.cssText = "display:block;height:360px";
document.body.append(el);
const term = await el.ready;
print("handle:", Object.keys(term).sort().join(", "));
print("installApp:", await term.installApp("hexyl", { quiet: true }));
term.vm.addFile("/root/notes.md", "# opened with openFile()\n");
term.refreshFiles();
term.openFile("/root/notes.md");
await new Promise((r) => setTimeout(r, 300));
print("files panel lists notes.md:", el.shadowRoot.textContent.includes("notes.md"));
print("editor shows it:", el.shadowRoot.textContent.includes("opened with openFile()"));
el.dispose(); // runs the handle dispose() and resets the element
el.remove();
print("after dispose, el.ready:", el.ready);handle: dispose, installApp, openFile, refreshFiles, showPreview, vm installApp: true files panel lists notes.md: true editor shows it: true after dispose, el.ready: null
showPreview() needs a server listening in the terminal's own VM. We couldn't make a reliable click-to-run sample of it on this page (another terminal mounted earlier on the same page answered the preview), so there is none here; see the dev servers feature.Styling and isolation
Everything lives in the element's shadow root: the terminal's CSS can't leak into your page and your CSS can't break it. Size it with display:block and a height. The renderer uses WebGPU and falls back to a 2D canvas; a visually hidden text mirror inside the shadow root keeps the screen readable by assistive technology.
More: the embeddable terminal feature, Web IDE and demos, and createHeadless() for the same features without UI.