Powerful, lightweight browser terminal. Batteries included.
Live demo: https://restty.pages.dev/
Powered by:
libghostty-vt(WASM terminal core)WebGPU(with WebGL2 fallback)text-shaper(shaping + raster)
restty is in an early release stage.
- Known issue: kitty image protocol handling can still fail in some edge cases.
- API note: high-level APIs are usable now, but some APIs may still change to improve DX.
If you hit an issue, please open one on GitHub with repro steps.
npm i restty<div id="terminal"></div>import { Restty } from "restty";
const restty = new Restty({
root: document.getElementById("terminal") as HTMLElement,
});
restty.connectPty("ws://localhost:8787/pty");That is the primary API: new Restty(...).
restty creates pane DOM, canvas, and hidden IME input for you.
import { getBuiltinTheme } from "restty";
const theme = getBuiltinTheme("Aizen Dark");
if (theme) restty.applyTheme(theme);import { parseGhosttyTheme } from "restty";
const theme = parseGhosttyTheme(`
foreground = #c0caf5
background = #1a1b26
cursor-color = #c0caf5
`);
restty.applyTheme(theme, "inline");restty.splitActivePane("vertical");
restty.splitActivePane("horizontal");
for (const pane of restty.panes()) {
pane.connectPty("ws://localhost:8787/pty");
}restty.setFontSize(15);
restty.sendInput("ls -la\n");
restty.copySelectionToClipboard();By default, restty uses a local-first font preset with CDN fallback. To fully control fonts, disable the preset and pass fontSources.
const restty = new Restty({
root: document.getElementById("terminal") as HTMLElement,
appOptions: {
fontPreset: "none",
},
fontSources: [
{
type: "url",
url: "https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono@v2.304/fonts/ttf/JetBrainsMono-Regular.ttf",
label: "JetBrains Mono",
},
{
type: "local",
matchers: ["jetbrains mono nerd font", "fira code nerd font"],
label: "Local fallback",
},
],
});Update fonts at runtime (all panes):
await restty.setFontSources([
{ type: "local", matchers: ["sf mono"], required: true },
{
type: "url",
url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/NerdFontsSymbolsOnly/SymbolsNerdFontMono-Regular.ttf",
},
]);On touch devices, restty defaults to pan-first scrolling with long-press selection.
const restty = new Restty({
root: document.getElementById("terminal") as HTMLElement,
appOptions: {
// "long-press" (default) | "drag" | "off"
touchSelectionMode: "long-press",
// Optional tuning knobs:
touchSelectionLongPressMs: 450,
touchSelectionMoveThresholdPx: 10,
},
});Use plugins when you want to extend restty behavior without patching core.
import type { ResttyPlugin } from "restty";
const metricsPlugin: ResttyPlugin = {
id: "example/metrics",
apiVersion: 1,
activate(ctx) {
const paneCreated = ctx.on("pane:created", ({ paneId }) => {
console.log("pane created", paneId);
});
const outgoing = ctx.addInputInterceptor(({ text }) => text.replace(/\t/g, " "));
const lifecycle = ctx.addLifecycleHook(({ phase, action }) => {
console.log("lifecycle", phase, action);
});
const stage = ctx.addRenderStage({
id: "metrics/tint",
mode: "after-main",
uniforms: [0.12],
shader: {
wgsl: `
fn resttyStage(color: vec4f, uv: vec2f, time: f32, params0: vec4f, params1: vec4f) -> vec4f {
return vec4f(min(vec3f(1.0), color.rgb + vec3f(params0.x, 0.0, 0.0)), color.a);
}
`,
},
});
return () => {
paneCreated.dispose();
outgoing.dispose();
lifecycle.dispose();
stage.dispose();
};
},
};
await restty.use(metricsPlugin, { sampleRate: 1 });
console.log(restty.pluginInfo("example/metrics"));
restty.unuse("example/metrics");Declarative loading (manifest + registry):
await restty.loadPlugins(
[{ id: "example/metrics", options: { sampleRate: 1 } }],
{
"example/metrics": () => metricsPlugin,
},
);See docs/plugins.md for full plugin authoring details.
Shader stages let you extend the final frame pipeline with WGSL/GLSL passes.
Global stages:
restty.setShaderStages([
{
id: "app/crt-lite",
mode: "after-main",
backend: "both",
uniforms: [0.24, 0.12],
shader: {
wgsl: `
fn resttyStage(color: vec4f, uv: vec2f, time: f32, params0: vec4f, params1: vec4f) -> vec4f {
let v = clamp(params0.x, 0.0, 0.8);
let centered = (uv - vec2f(0.5, 0.5)) * 2.0;
let vignette = max(0.0, 1.0 - v * dot(centered, centered));
return vec4f(color.rgb * vignette, color.a);
}
`,
},
},
]);
const stage = restty.addShaderStage({
id: "app/mono",
mode: "after-main",
uniforms: [1.0],
shader: {
wgsl: `
fn resttyStage(color: vec4f, uv: vec2f, time: f32, params0: vec4f, params1: vec4f) -> vec4f {
let l = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722));
return vec4f(l * 0.12, l * 0.95, l * 0.35, color.a);
}
`,
},
});
stage.setEnabled(false);
restty.removeShaderStage("app/mono");For migration from xterm.js-style app code, use restty/xterm:
import { Terminal } from "restty/xterm";
const term = new Terminal({ cols: 100, rows: 30 });
term.open(document.getElementById("terminal") as HTMLElement);
term.onData((data) => console.log("input", data));
term.onResize(({ cols, rows }) => console.log("resize", cols, rows));
term.write("hello");
term.writeln(" world");
term.resize(120, 40);
term.loadAddon({
activate() {},
dispose() {},
});Compatibility scope:
- Good for common embed/migration flows.
- Not full xterm internals parity (buffer/parser/marker ecosystem APIs are not all implemented).
- Prefer native
ResttyAPI for long-term integrations.
Primary class:
new Restty({ root, ...options })createRestty(options)
Xterm compatibility:
import { Terminal } from "restty/xterm"- Supports
open,write,writeln,resize,focus,blur,clear,reset,onData,onResize,options,loadAddon,dispose
Pane access:
panes()/pane(id)/activePane()/focusedPane()/forEachPane(visitor)splitActivePane("vertical" | "horizontal")/splitPane(id, direction)/closePane(id)
Active-pane convenience:
connectPty(url)/disconnectPty()/isPtyConnected()setRenderer("auto" | "webgpu" | "webgl2")setFontSize(number)/setFontSources([...])applyTheme(theme)/resetTheme()setMouseMode("auto" | "on" | "off")sendInput(text)/sendKeyInput(text)copySelectionToClipboard()/pasteFromClipboard()resize(cols, rows)/focus()/blur()updateSize(force?)destroy()
Plugin host:
use(plugin, options?)/loadPlugins(manifest, registry)/unuse(pluginId)/plugins()/pluginInfo(pluginId?)- plugin context supports
on(...),addInputInterceptor(...),addOutputInterceptor(...),addLifecycleHook(...),addRenderHook(...),addRenderStage(...)
Shader stages:
setShaderStages(stages)/getShaderStages()addShaderStage(stage)/removeShaderStage(id)
Use these only when you need lower-level control:
restty/internal: full internal barrel (unstable; includes low-level modules like WASM/input/pty helpers)
git clone https://github.com/wiedymi/restty.git
cd restty
git submodule update --init --recursive
bun install
bun run build:themes
bun run playgroundOpen http://localhost:5173.
src/surface/: public API (Restty), pane manager orchestration, plugin host, xterm shim.src/runtime/: terminal runtime/render loop implementation.src/renderer/,src/input/,src/pty/,src/fonts/,src/theme/,src/wasm/,src/selection/: subsystem modules.src/app/: compatibility re-export layer while internals are refactored.
bun run build # build package output
bun run test # full tests
bun run test:ci # CI-safe test target
bun run lint # lint
bun run format:check # formatting check
bun run build:assets # static playground bundle (playground/public/playground.js)
bun run playground # one-command local dev (PTY + playground dev server)
bun run pty # PTY websocket server onlydocs/README.md- docs indexdocs/usage.md- practical integration guidedocs/xterm-compat.md- xterm migration shimdocs/how-it-works.md- runtime flowdocs/internals/- implementation notes and architectureTHIRD_PARTY_NOTICES.md- third-party credits and notices