Skip to content
/ restty Public

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

License

Notifications You must be signed in to change notification settings

wiedymi/restty

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

115 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

No packages published

Languages