userland.rundocs70Try the demo
SDK

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.

javascript
import { defineNanoTerminal } from "@userland-run/nano-sdk/terminal";
defineNanoTerminal(); // registers <nano-terminal>
html
<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

OptionTypeDefaultDescription
wasm-urlstring"/nano.wasm"The image the terminal's VM boots.
service-worker-urlstring"/nano-sw.js"Service worker for previews.
shell-commandstring"sh -i"The command booted as the session (BusyBox applet + args, whitespace-split).
font-pxnumber12Initial font size in pixels.
ram-mbnumber1800VM memory. 256–512 for BusyBox; 1800 if you run Node.js on the VM.
no-catalogboolean—Hide the catalog panel.
no-paletteboolean—Disable the command palette.
no-filesboolean—Hide the files panel.
no-editorboolean—Disable the editor.
no-previewboolean—Disable the server preview.
terminal

ram-mb sets the terminal VM's memory

html
<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

loading…
Expected output
MemTotal:       262144 kB
terminal

shell-command picks the session

BusyBox's hush instead of the default sh -i; $0 shows which shell answered.

html
<nano-terminal
  wasm-url="/nano/console.wasm"
  ram-mb="256"
  shell-command="hush -i"
></nano-terminal>

Typed after the prompt: echo shell=$0

loading…
Expected output
shell=hush
terminal

font-px and the no-* switches

A bare terminal: larger font, no side panels, no editor, palette or preview.

html
<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

loading…
Expected output
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.

javascript
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 } },
});
OptionTypeDefaultDescription
wasmUrl / ramMB / shellCommand / fontPx / serviceWorkerUrlas the attributesSame meaning as the attributes above.
features.catalog / palette / files / editorbooleantrueToggle each panel.
features.previewboolean | { ports?, defaultPort? }true, ports [8080]Toggle the preview, or set the ports offered in its selector and the one selected first.

The handle

OptionTypeDefaultDescription
vmNanoVM—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.
SDK · JavaScript

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.

javascript
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);
loading…
Expected output
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.