Skip to content

wiedymi/restty

Repository files navigation

restty

Version Downloads Package Size CI Publish Demo

GitHub Twitter Email Discord Support me

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)

Release Status

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.

Install

npm i restty

Quick Start

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

Common Tasks

Apply a built-in theme

import { getBuiltinTheme } from "restty";

const theme = getBuiltinTheme("Aizen Dark");
if (theme) restty.applyTheme(theme);

Parse and apply a Ghostty theme file

import { parseGhosttyTheme } from "restty";

const theme = parseGhosttyTheme(`
foreground = #c0caf5
background = #1a1b26
cursor-color = #c0caf5
`);

restty.applyTheme(theme, "inline");

Split panes and operate per pane

restty.splitActivePane("vertical");
restty.splitActivePane("horizontal");

for (const pane of restty.panes()) {
  pane.connectPty("ws://localhost:8787/pty");
}

Use active-pane convenience methods

restty.setFontSize(15);
restty.sendInput("ls -la\n");
restty.copySelectionToClipboard();

Provide custom fonts

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",
  },
]);

Touch behavior (pan-first by default)

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,
  },
});

Plugin system (native)

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

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");

xterm compatibility layer

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 Restty API for long-term integrations.

API Snapshot

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)

Advanced / Internal Modules

Use these only when you need lower-level control:

  • restty/internal: full internal barrel (unstable; includes low-level modules like WASM/input/pty helpers)

Local Development

git clone https://github.com/wiedymi/restty.git
cd restty
git submodule update --init --recursive
bun install
bun run build:themes
bun run playground

Open http://localhost:5173.

Code Layout

  • 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.

Repository Commands

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 only

Documentation

  • docs/README.md - docs index
  • docs/usage.md - practical integration guide
  • docs/xterm-compat.md - xterm migration shim
  • docs/how-it-works.md - runtime flow
  • docs/internals/ - implementation notes and architecture
  • THIRD_PARTY_NOTICES.md - third-party credits and notices

About

Powerful, lightweight web terminal. Batteries included. Powered by libghostty-vt, WebGPU, and text-shaper.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages