web-terminal-engine

module
v2.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: GPL-2.0, GPL-3.0

README

web-terminal-engine

Go Reference npm JSR Test coverage Mutation Mutation (TS) OpenSSF Best Practices OpenSSF Scorecard

Cross-language terminal emulator and session engine (Go) with browser renderer (TypeScript).

A standalone library that bridges a PTY to a browser WebSocket. The Go packages provide a VT100/VT500 screen buffer with SGR support and a WebSocket-based terminal session handler with reconnect, scrollback replay, and adaptive ping. The TypeScript package provides the browser-side renderer, keyboard mapper, mouse encoder, and binary wire decoder. No app-specific dependencies — only the standard library, github.com/coder/websocket, and github.com/creack/pty.

Install

Go: go get github.com/cplieger/web-terminal-engine/v2@latest — TS: npx jsr add @cplieger/web-terminal-engine or npm i @cplieger/web-terminal-engine

Usage

import (
    "log/slog"
    "net/http"

    "github.com/cplieger/web-terminal-engine/v2/terminal"
)

h := terminal.NewHandler(
    []string{"/bin/bash"},
    terminal.WithWorkDir("/home/user"),
    terminal.WithLogger(slog.Default()),
)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
// or use h as an http.Handler directly:
// mux.Handle("/ws", h)
import { render, keyboard, mouse, decodeWireBinary } from "@cplieger/web-terminal-engine";

render.init({
  output: document.getElementById("term-output")!,
  termWrap: document.getElementById("term")!,
});
// On WebSocket binary message:
const msg = decodeWireBinary(event.data);
if (msg?.type === "screen") render.handleScreen(msg);

API

Go packages
  • vt — VT100/VT500 screen buffer: New(rows, cols), Write([]byte), Resize(rows, cols), RenderRowWire(y), DrainScrollback(), CursorPos(), HoldFlush(), ReleaseFlush(), IsFlushHeld(), RenderViewport(), RowString(y). Public fields: Cells, Width, Height, Title, MouseMode, InAltScreen, cursor/mode state.
  • terminal — WebSocket session handler: NewHandler(command, ...Option), RegisterRoutes(mux), ServeHTTP(w, r), Shutdown(). Options: WithWorkDir, WithLogger, WithEnv, WithScrollbackCapacity, WithAcceptOptions, WithOnProcessExit, WithKeepUnfocused, WithTheme. Handles PTY lifecycle, binary wire protocol, reconnect with scrollback replay, adaptive ping. SessionManager (NewSessionManager) fronts N PTY-backed sessions with WebSocketHandler() (/ws?session=<id>), RESTHandler() (/api/sessions), and EventsHandler() (SSE /api/sessions/events); status values working/idle/input/done/exited from a pluggable classifier. When several clients share one session, a live resize is last-writer-wins and the shared screen relaxes to the smallest remaining client's size on disconnect. MountSessionRoutes(mux, ws, rest, events, ...MountOption) wires the manager's documented route set — WSPath (/ws), SessionsPath (/api/sessions) + SessionsSubtreePath (both mounts), and SessionEventsPath (/api/sessions/events) — exactly and nothing else (the topology is a contract shared with the TS client's defaults; additions are release-noted API changes); WithCreateGate(mw) wraps the REST handler with a caller-supplied middleware so session creation (which forks a process per POST) can be rate-limited without the engine taking a middleware dependency; (*SessionManager).MountAPI(mux, opts...) is the one-manager convenience.
TypeScript (web/ — published as @cplieger/web-terminal-engine on NPM and JSR)
  • render — DOM renderer driven by ScreenMessage / ScrollMessage frames: init, handleScreen, handleScroll, updateFontMetrics, computeSize, getCursorPx, setPredictedCursor, resetScreen, resetScrollback, getHighestIndex, noteResumeBounds, updateReverseVideo.
  • keyboard — Translates KeyboardEvent to terminal byte sequences: mapKeyboardEvent, bracketTextForPaste, prepareTextForTerminal. Honors applicationCursor, applicationKeypad, bracketedPaste, and the kitty keyboard disambiguate flag (emits CSI u key events when the app enables the protocol).
  • mouse — SGR 1006 mouse + focus reporting encoder: init, encodeSGR, MouseInputHandler.
  • scroll — Auto-follow tracker for the scroll container: init, stickToBottom, scrollToBottom, isUserScrolledUp.
  • modes — DEC private mode state (synced from server's ModesMessage): setModes, isBracketedPaste, isApplicationCursor, getMouseMode, isMouseSGR, isFocusReporting, isApplicationKeypad, isReverseVideo, getKeyboardFlags.
  • decodeWireBinary(buf) — Top-level decoder for binary WebSocket frames; returns a ServerMessage or null for invalid/truncated frames.
  • connection — Client → server WebSocket lifecycle: socket ownership, exponential-backoff reconnect, and the resume/inputAck reliability layer (outbox + server-restart detection). init(callbacks), connect, sendBinary, sendResize, reconnectNow; wsPath callback option defaults to "/ws". Decodes frames and applies modes.setModes internally, so consumers only dispatch screen/scroll to render. Pairs with the Go terminal handler's resume protocol. (controlFrame / wsURL are also exported for advanced use.)
  • connectStatusStream and the SessionStatus / StatusStream types: the SSE client for /api/sessions/events that drives per-tab status. LineStore and CONTROL_FRAME_PREFIX are also exported.
  • Wire typesWireRun, ScreenMessage, ScrollMessage, ModesMessage, TitleMessage, ResumeAckMessage, ServerMessage, ControlMessage re-exported from the package root.

Wire Protocol

The Go server and TypeScript client communicate over a binary WebSocket frame format rather than shared code. The authoritative byte-level definition is the code itself — the Go encoder (terminal/wire_binary.go), the Go WireRun types (vt/wire.go), and the TS decoder (web/src/wire-binary.ts), all guarded by the round-trip fuzz tests and the wire-golden/*.bin fixtures. The design rationale, which a prose byte-table cannot capture and tends to drift from, is:

  • Binary, not JSON. Frames are WebSocket binary messages with little-endian integers, and stay compact to keep frame size and latency low on a full repaint. Client → server, raw terminal input flows unframed, while control messages (resize, resume) are a 0x00 prefix byte + a JSON body — no valid terminal input starts with NUL, so the prefix is unambiguous.
  • Absolute line indexing. Every line the server produces gets a monotonic absolute index that does not change as the screen scrolls, so the client keeps one buffer keyed by that index: applying a line is idempotent, resume aligns by absolute index rather than a fragile count, an eviction gap is detectable (surfaced as a "history trimmed" marker), and a server-epoch value detects restarts across reconnects. The exact index handling lives in the sources named above (vt/wire.go, terminal/wire_binary.go, web/src/wire-binary.ts).
  • Versioning by lockstep. There is no version byte in the frame header: the Go module and the npm/JSR package release together from this one repository, so a breaking wire change must land in the Go encoder/decoder and the TS decoder in a single release (feat!: / BREAKING CHANGE:). A version byte is added only if a break ever cannot be coordinated in one release.

Client → server input for the DEC modes (SGR 1006 mouse, focus reporting, application keypad) is encoded by the TS mouse / keyboard modules and consumed server-side by vt; the sequences live in those sources. For VT/DEC features intentionally absent from the wire, see Unsupported by Design.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude Opus and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

GPL-3.0 — see LICENSE.

The web-terminal family builds on this engine:

Apps built on the engine:

Unsupported by Design

The following VT/DEC features are intentionally not implemented. Input bytes for these sequences are consumed (not echoed or half-rendered) and produce no visible effect, except where a row notes a performed side effect. This is a deliberate design choice — not a TODO.

Category Sequences Rationale
Double-width/height lines DECDWL, DECDHL Requires line-level rendering attribute + renderer changes; purely legacy VT220 feature unused by modern apps.
Programmatic resize / geometry DECCOLM (132-column) width change, XTWINOPS window resize/move/iconify/maximize, DECSLPP/DECSNLS The browser viewport and PTY winsize own the terminal size, and a browser tab has no OS window to move. Only the size change is declined — DECCOLM's clear/home side effects, the title stack (22/23), and the size/label reports (18/19/20/21) are honored.
DCS device control tmux control-mode passthrough Not modeled; consumed silently. DECRQSS and the XTGETTCAP color-count query share the same DCS parser and are supported (see the note below).
Graphics protocols Sixel, ReGIS, Kitty image protocol, iTerm inline images Specialized rendering pipeline incompatible with the DOM-based renderer.
NRCS national charsets All national replacement character sets (only DEC Special Graphics + ASCII are supported) Legacy internationalization mechanism superseded by UTF-8. No modern app emits these.
Exotic SGR attributes Fonts 10-20, framed/encircled (51/52/54), superscript/subscript (73-75), ideogram (60-65) No modern terminal or app uses these attributes; they have no visual representation in standard monospace fonts.
X11 Xcms color specifications CIE Lab/Luv/XYZ/uvY/xyY, rgbi intensity, TekHVC in OSC 4/5/10-19 libX11 device-colorimetry, not the VT/ANSI spec — no CLI tool emits them. The rgb: / #hex forms and the palette + dynamic-color set/query/reset are all supported.
ZWJ emoji grapheme clustering Zero-width joiner sequences are not clustered into single cells Requires ICU-level grapheme segmentation. Individual emoji codepoints render correctly; only multi-codepoint ZWJ sequences (family emoji, skin-tone modifiers) may misalign.

Note on device queries: several report/query sequences sharing the DCS or CSI parsers are supported for conformance. DECRQSS (DCS $ q … ST) answers SGR (m), scroll region (r), cursor style (SP q), protection (" q), conformance level (" p), left/right margins (s) and lines-per-page/screen (t, * |), replying DCS 0 $ r ST for anything else. XTGETTCAP (DCS + q … ST) answers the color-count capability (256). DECRQCRA (rectangular-area checksum) and OSC 52 clipboard read-back are answered only when Screen.AllowScreenReport is enabled — both inject their reply into the PTY, so they default off. The VT model is validated against the esctest2 conformance suite (see CONTRIBUTING).

Note on the kitty keyboard protocol: the progressive-enhancement negotiation is implemented — CSI ? u (query) is answered, and CSI > u / CSI < u / CSI = u (push / pop / set) manage per-screen flag stacks — so an app that queries for keyboard enhancement (e.g. crossterm/Codex) detects support. Only the disambiguate flag (0x1) is honored: the current flag is synced to the client, which then encodes unambiguous CSI u key events (Escape, Ctrl/Alt combinations, functional keys, and the keypad's KP_* navigation codes) while plain text still flows as text. The other flags — report-event-types (0x2), report-alternate-keys (0x4), report-all-keys (0x8), report-associated-text (0x10) — are masked off (0x8/0x10 are incompatible with the browser's hidden-textarea/IME input model); the query reports only the honored flag, so an app that needs a masked-off one detects the gap and falls back. This is distinct from the kitty image protocol, which is unsupported (see the graphics row above).

Directories

Path Synopsis
internal
esctest
Package esctest runs the esctest2 VT conformance suite (github.com/ThomasDickey/esctest2, George Nachman's esctest with Thomas Dickey's fixes) against the engine's vt.Screen.
Package esctest runs the esctest2 VT conformance suite (github.com/ThomasDickey/esctest2, George Nachman's esctest with Thomas Dickey's fixes) against the engine's vt.Screen.
Package terminal bridges a PTY to a browser WebSocket.
Package terminal bridges a PTY to a browser WebSocket.
Package vt handles OSC (Operating System Command) dispatch.
Package vt handles OSC (Operating System Command) dispatch.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL