Just Another Browser Terminal — run your real terminal in browser
- Single
<JabTerm>React component - Multiple independent terminals on one page without conflicts
- Node.js server powered by
node-pty— real shell, real colors - Same-origin WebSocket proxy for HTTPS / Cloudflare / tunnel deployments
- CLI binary:
npx jabterm-server --port 3223
pnpm add jabterm @xterm/xtermnpx jabterm-server --port 3223Or programmatically:
import { createJabtermServer } from "jabterm/server";
const server = createJabtermServer({ port: 3223, host: "127.0.0.1", path: "/ws" });
await server.listen();
// server.address() -> { address, family, port }
// server.close() to shut down deterministicallyimport { JabTerm } from "jabterm/react";
import "@xterm/xterm/css/xterm.css";
function App() {
return (
<div style={{ width: "100%", height: 400 }}>
<JabTerm wsUrl="ws://localhost:3223" />
</div>
);
}pnpm install
pnpm dev:demoThis starts:
- the terminal WebSocket server on
ws://127.0.0.1:3223 - the demo page on
http://127.0.0.1:3224
CI also regenerates these assets on pushes to main (so contributors typically don't have to).
| Prop | Type | Default | Description |
|---|---|---|---|
wsUrl |
string |
required | Full WebSocket URL (ws:// or wss://) |
onTitleChange |
(title: string) => void |
— | Fires when the shell sets a window title |
onOpen |
() => void |
— | Fires when the WebSocket becomes open |
onClose |
(ev: CloseEvent) => void |
— | Fires when the WebSocket closes |
onError |
(ev: Event) => void |
— | Fires on WebSocket errors |
captureOutput |
boolean |
true |
Capture output for imperative read*() methods |
maxCaptureChars |
number |
200000 |
Max captured output size (characters) |
className |
string |
— | CSS class for the outer container |
fontSize |
number |
13 |
Font size in pixels |
fontFamily |
string |
system monospace | Font family |
accessibilitySupport |
"on" | "off" | "auto" |
— | xterm accessibility support mode (set to "on" if you need to read terminal text from the DOM for automation) |
theme |
{ background?, foreground?, cursor? } |
{ background: "#1e1e1e" } |
xterm.js theme overrides |
The outer container also exposes data-jabterm-state="connecting|open|closed" to make UI tests (e.g. Playwright) wait reliably.
<JabTerm ref={...} /> exposes:
focus(),fit(),resize(cols, rows),paste(text),send(data)getXterm()to access the underlying xterm instancereadAll(),readLast(n),readNew(),getNewCount()for testing/automation
const server = createJabtermServer({
port: 3223, // default: 3223 (use 0 for ephemeral)
host: "127.0.0.1", // default: 127.0.0.1
path: "/ws", // default: "/"
shell: "/bin/bash", // default: resolves from $SHELL / OS defaults
cwd: "/home/user", // default: $HOME
env: { FOO: "bar" }, // extra env for spawned PTYs
strictPort: false, // default: false — fail if port is busy (ignored for port 0)
});
await server.listen();
console.log(server.address()); // { address, family, port }The WebSocket endpoint supports per-session routing: connect to ${path}/:terminalId (e.g. /ws/my-terminal).
Optional client-side helper:
import { setDocumentTitle } from "jabterm/react";jabterm/server spawns real shell processes. For production deployments:
- Keep it bound to loopback (
127.0.0.1) and expose only behind an authenticated app/reverse-proxy. - Consider adding origin checks and/or a token handshake for WebSocket connections.
Creates a WebSocketServer in noServer mode for same-origin proxying:
import { createTerminalProxy } from "jabterm/server";
const proxyWss = createTerminalProxy({
upstreamUrl: "ws://127.0.0.1:3223",
});
httpServer.on("upgrade", (req, socket, head) => {
if (new URL(req.url, "http://localhost").pathname === "/ws/terminal") {
proxyWss.handleUpgrade(req, socket, head, (ws) => {
proxyWss.emit("connection", ws, req);
});
}
});See docs/ARCHITECTURE.md for internals
MIT