Skip to content

ynqa/lanterm

Repository files navigation

Lanterm

Not another web terminal server. Lanterm is a library toolkit for building your own PTY-backed terminal UX.

Unlike web terminal servers such as ttyd, Lanterm is designed for teams that want to:

  • embedding real PTY-backed terminals without adopting a full terminal server
  • control session lifecycle, transport, and UI from their own application
  • build shell workspaces for coding agents, browser IDEs, ops tools, and dashboards

Packages

  • @lanterm/protocol: shared message types and codecs
  • @lanterm/server: server-side PTY and WebSocket utilities
    • @lanterm/pty: native PTY binding used by @lanterm/server
  • @lanterm/react: xterm-backed React surface

Embed a terminal in your UI

Lanterm fills the gap between full web terminal servers and low-level terminal building blocks, letting you easily add a real PTY-backed terminal to your own UI with a regular React component.

Client:

React
import "@xterm/xterm/css/xterm.css";
import { LantermTerminal } from "@lanterm/react";
import type { ReactElement } from "react";

export function WorkspaceTerminal(): ReactElement {
  return (
    <main className="workspace">
      <aside>{/* Your existing files, tools, agent timeline, etc. */}</aside>
      <LantermTerminal
        className="terminal"
        endpoint="ws://localhost:8790/terminal"
      />
    </main>
  );
}

Server:

Node
import { createServer } from "node:http";
import { createPtyEnv, createPtySession, type PtySessionHandle } from "@lanterm/server";
import { WebSocket, WebSocketServer } from "ws";

const sessions = new WeakMap<WebSocket, PtySessionHandle>();
const server = createServer();
const wss = new WebSocketServer({ noServer: true });

server.on("upgrade", (request, socket, head) => {
  if (request.url !== "/terminal") {
    socket.destroy();
    return;
  }

  wss.handleUpgrade(request, socket, head, (websocket) => {
    wss.emit("connection", websocket, request);
  });
});

wss.on("connection", (socket) => {
  const session = createPtySession(socket, {
    args: [],
    cwd: process.cwd(),
    env: createPtyEnv(process.env),
    shell: process.env.SHELL ?? "/bin/sh"
  });

  sessions.set(socket, session);

  socket.on("message", (data) => {
    session.handle(data.toString());
  });

  socket.on("close", () => {
    sessions.delete(socket);
    session.close();
  });
});

server.listen(8790, "127.0.0.1", () => {
  console.log("Lanterm Node backend listening on ws://127.0.0.1:8790/terminal");
});
Bun
import { createPtyEnv, createPtySession, type PtySessionHandle } from "@lanterm/server";

type WebSocketData = {
  session?: PtySessionHandle;
};

Bun.serve<WebSocketData>({
  fetch(request, server) {
    const url = new URL(request.url);

    if (url.pathname === "/terminal" && server.upgrade(request, { data: {} })) {
      return undefined;
    }

    return new Response("Lanterm Bun backend", { status: 200 });
  },
  hostname: "127.0.0.1",
  port: 8790,
  websocket: {
    close(socket) {
      socket.data.session?.close();
      socket.data.session = undefined;
    },
    message(socket, data) {
      socket.data.session?.handle(data);
    },
    open(socket) {
      socket.data.session = createPtySession(socket, {
        args: [],
        cwd: process.cwd(),
        env: createPtyEnv(process.env),
        shell: process.env.SHELL ?? "/bin/sh"
      });
    }
  }
});

Run Examples

The React client source is shared from examples/client; each backend example owns its Vite setup.

Run the shared client with the Bun backend:

bun install --cwd examples/bun
bun run --cwd examples/bun dev

Run the shared client with the Node backend:

pnpm --dir examples/node install
pnpm --dir examples/node dev

About

Toolkit for building PTY-backed terminal experiences in the browser.

Topics

Resources

License

Code of conduct

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors