npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wterm/core

v0.5.8

Published

Headless terminal emulator core for the web — WASM bridge and WebSocket transport

Readme

@wterm/core

Headless terminal emulator core for wterm. Provides the WASM bridge, the pluggable TerminalCore contract, and WebSocket transport — no DOM dependency.

Related Packages

| Package | Description | |---|---| | @wterm/dom | DOM renderer, input handler — vanilla JS terminal | | @wterm/react | React component + useTerminal hook | | @wterm/vue | Vue 3 component + template ref API | | @wterm/svelte | Svelte component + callback API | | @wterm/ghostty | Full-featured VT emulation core powered by libghostty | | @wterm/just-bash | In-browser Bash shell powered by just-bash | | @wterm/markdown | Streaming Markdown-to-ANSI renderer for terminals |

Install

npm install @wterm/core

Pluggable Cores

@wterm/core defines a TerminalCore interface that any terminal emulation backend can implement. The built-in WasmBridge implements it using wterm's lightweight Zig WASM binary (~26 KB). For additional protocols and proper grapheme handling, use @wterm/ghostty, which implements the same interface using libghostty.

import { WTerm } from "@wterm/dom";
import { GhosttyCore } from "@wterm/ghostty";

// Default — uses built-in lightweight core
const term = new WTerm(el);

// Opt-in — uses libghostty for full VT emulation
const core = await GhosttyCore.load();
const term = new WTerm(el, { core });

Worker core

WorkerCore runs the built-in core in a Web Worker, so parsing leaves the main thread and many busy terminals can use more than one CPU core. Pair it with renderer: "webgl" for pages with many panes of heavy output.

import { WorkerCore } from "@wterm/core/worker";
import { WTerm } from "@wterm/dom";

const core = await WorkerCore.load();
const term = new WTerm(el, { core, renderer: "webgl" });

Writes return before they are parsed. Cells, replies, titles and bells arrive with the next update from the worker, which the DOM wrapper picks up through TerminalCore.setUpdateListener. Each WorkerCore owns one worker; call dispose() when the terminal is gone. Debug tooling sees no unhandled sequences from a worker core. It is a separate entry point so applications that do not use it never bundle a worker. The worker uses code splitting: with Vite, set worker: { format: "es" }. Bundlers that do not resolve new Worker(new URL(..., import.meta.url)) can pass their own worker running @wterm/core's core-worker.js.

API

Live row identity

TerminalCore.getRowId?(row) and isRowContentDirty?(row) let the DOM renderer reuse rows that only scrolled. Custom cores can omit them.

Terminal row metadata

TerminalCore optionally exposes getRowMetadata(row) for zero-based live rows and getScrollbackRowMetadata(offset) for history, where offset zero is the newest retained row. Both return a caller-owned TerminalRowMetadata object or null when the row is unavailable:

import type { TerminalCore } from "@wterm/core";

function continuesIntoViewport(core: TerminalCore): boolean | undefined {
  return core.getScrollbackRowMetadata?.(0)?.wrapsToNext;
}

wrapsToNext means the following row continues the same logical line; continuesPrevious means this row continues the preceding row. Filling the rightmost cell alone does not count as wrapping. The first retained row can still continue a row that has been discarded.

These are current physical-row relationships, not stable line IDs. Read them again after output, resize, reset, or screen changes; do not retain row indexes across those operations. Ghostty implements both methods with its native wrap flags. The lightweight core does not expose them, and older Ghostty WASM binaries return null. Missing metadata means unknown, not a hard newline.

Tracked cell positions

TerminalCore.trackPosition?({ row, col }) returns a TrackedTerminalPosition or null. Coordinates are zero-based cells, with row zero at the oldest retained row. Call resolve() for its current { row, col } after scrolling or reflow, and dispose() when finished. Resolution returns null after pruning, reset, screen switching, core reinitialization, or disposal; invalidated handles never refer to newly allocated positions. Disposal is idempotent.

Tracking follows a cell location, not its immutable contents. A caller preserving selected text must also check whether it was overwritten. Ghostty supports up to 64 simultaneous handles and returns null for invalid coordinates, exhausted capacity, or an older WASM binary without tracking exports. The built-in core does not provide this optional method.

WasmBridge

Low-level interface to the Zig/WASM terminal state machine. Implements the TerminalCore interface.

The embedded binary is imported on the first WasmBridge.load() without a URL, then decoded and compiled once per copy of @wterm/core, including concurrent loads. Every bridge gets a fresh instance and memory; terminal contents, dimensions, and modes remain independent. Bundlers with code splitting emit the embedded binary in a separate JavaScript chunk; Ghostty and custom-URL loads do not request it. Deploy that chunk with your other application assets. No separate WASM file setup is required.

Custom URLs share downloads and compiled modules for up to four recently used resolved URLs per page or worker. Failed downloads and compilations can be retried. Cached modules remain until eviction or page/worker reload; use versioned or hashed URLs when changing the binary. Serving Content-Type: application/wasm enables compilation during download in supported browsers. Other MIME types, unavailable streaming, or a streaming failure fall back to buffered compilation without another fetch.

import { WasmBridge } from "@wterm/core";

const bridge = await WasmBridge.load();
bridge.init(80, 24);
bridge.writeString("Hello, world!\r\n");

const cell = bridge.getCell(0, 0); // { char, chars?, fg, bg, flags, width, linkUri?, linkId?, linkKey? }
const cursor = bridge.getCursor();  // { row, col, visible, shape, blinking }

| Method | Description | |---|---| | WasmBridge.load(url?) | Load WASM binary and return a new bridge instance. Uses the embedded binary when no URL is given. | | init(cols, rows) | Initialize the terminal grid | | writeString(str, afterChunk?) | Write a UTF-8 string, optionally running a callback after each internal chunk | | writeRaw(data, afterChunk?) | Write raw bytes, optionally running a callback after each internal chunk | | resize(cols, rows) | Resize the terminal grid | | getCell(row, col) | Get cell data, including optional resolved OSC 8 metadata (linkUri, explicit linkId, and opaque linkKey) | | getCursor() | Get cursor state ({ row, col, visible, shape?, blinking? }) | | getCols() / getRows() | Get applied grid dimensions after initialization or resize. The built-in core clamps requests to 1–1024 columns and 1–512 rows. | | isDirtyRow(row) | Check if a row needs re-rendering | | clearDirty() | Reset all dirty-row flags | | getTitle() | Get pending title change (or null) | | getBellCount() | Read and clear the number of pending BEL controls; 0 when none | | getResponse() | Get pending host response (or null) | | getResourceState() | Get optional core resource state, including built-in hyperlink identity saturation | | getScrollbackCount() | Number of lines in the scrollback buffer | | getScrollbackDiscardedCount() | Cumulative rows discarded from the oldest end, when supported | | getScrollbackCell(offset, col) | Get cell data from scrollback | | getScrollbackLineLen(offset) | Get length of a scrollback line | | cursorKeysApp() | Whether cursor keys are in application mode | | bracketedPaste() | Whether bracketed paste mode is active | | usingAltScreen() | Whether the alternate screen buffer is active | | mouseTracking() | Active mouse tracking mode (0, 1000, 1002, or 1003) | | mouseSgr() | Whether cell-coordinate SGR mouse encoding (1006) is active | | mouseEncoding() | Active mouse wire format (x10, utf8, sgr, urxvt, or sgr-pixels). The DOM layer handles all five; sgr-pixels reports 1-based CSS pixels. | | focusEvents() | Whether focus reporting is active | | synchronizedOutput() | Whether synchronized output mode (2026) is active | | synchronizedOutputGeneration() | Monotonic generation for synchronized output blocks | | kittyKeyboardFlags() | Active Kitty keyboard protocol flags |

OSC 8 hyperlink metadata is optional so third-party TerminalCore implementations remain source-compatible. Cores should expose the resolved URI and an opaque semantic key rather than a private numeric index.

Wide CJK, fullwidth, and emoji codepoints occupy a leading cell with width: 2 and a continuation with width: 0. Character insertion (ICH) and deletion (DCH) shift the requested number of columns from the cursor, replacing split wide characters with background-colored spaces. With automatic wrapping disabled, a wide character that cannot fit at the right edge leaves the row unchanged. A one-column terminal consumes wide characters as spaces.

CellData.underlineStyle optionally selects "none", "single", "double", "curly", "dotted", or "dashed". It takes precedence over the underline flag (0x08); when omitted, that flag selects a single underline. underlineRgb is an optional resolved 0xRRGGBB color, including 0 for black. Without it, the underline follows the displayed foreground, including reverse video. Ghostty supplies both fields for viewport and scrollback cells. The built-in core and older Ghostty binaries keep flag-based single underlines. Custom cores can adopt either optional field; UnderlineStyle is exported by @wterm/core.

CellData.spacerHead optionally identifies an empty right-edge cell left when a wide glyph wraps to the next row. Ghostty supplies it in live rows and history. Text extraction should omit these cells and width-zero continuations while preserving real spaces; an absent flag does not identify a spacer.

The built-in core supports ASCII (B), British (A), and DEC Special Graphics (0) designations for G0–G3. For example, \x1b(0lqqk\x1b(B produces ┌──┐. SI/SO select G0/G1; ESC n/o select G2/G3, and ESC N/O select them for one character. Cursor save/restore preserves character-set state. Mapping affects ASCII characters only, so UTF-8 text remains intact.

TerminalCore.getColorOverrides?() returns a caller-owned TerminalColorOverrides snapshot with optional foreground, background, and cursor fields, each a 24-bit 0xRRGGBB value. Zero is black. Omit a field to use the host's current CSS theme. These are application overrides, not configured theme colors, and reading them does not consume them. A missing method is equivalent to an empty snapshot.

Ghostty supplies OSC 10/11/12 overrides and clears each with OSC 110/111/112. Its native color state survives resize, screen switches, SGR 0, and RIS; reinitialization clears it. Older WASM binaries and uninitialized/disposed Ghostty cores return an empty snapshot. The built-in core omits this method.

CursorState.shape ("block", "bar", or "underline") and blinking are optional for custom cores. The built-in and Ghostty cores expose both fields from DECSCUSR (CSI Ps SP q) and cursor blink mode (CSI ? 12 h/l). Missing fields render as a steady block unless the host sets cursorBlink. Style 0 or an omitted parameter restores the default steady block; styles 1/2, 3/4, and 5/6 select blinking/steady block, underline, and bar respectively.

TerminalCore.kittyKeyboardFlags() is also optional. The DOM input handler uses it to encode negotiated Kitty keyboard events; cores that omit it retain the existing legacy keyboard behavior.

The built-in core answers DEC private-mode status queries (CSI ? Ps $ p) through getResponse(). It reports 1 for set, 2 for reset, and 0 for unrecognized modes, including the current state of synchronized output (?2026), bracketed paste (?2004), mouse tracking and encoding, focus reporting, cursor modes, and alternate-screen modes. For example, \x1b[?2026$p receives \x1b[?2026;2$y until mode 2026 is enabled.

Primary device-attributes queries (CSI c or CSI 0 c) receive \x1b[?1;2c through getResponse(), identifying VT100 advanced-video support. Secondary and private variants receive no reply.

An operating-status query (CSI 5 n) receives \x1b[0n, indicating the terminal is ready. A cursor-position query (CSI 6 n) receives \x1b[row;colR. These replies share the same response queue as device attributes and preserve query order. Unsupported or malformed status requests receive no reply.

The built-in core reports its fixed hyperlink identity capacity through getResourceState(). When hyperlinks.saturated is true, new distinct OSC 8 links render as plain text and hyperlinks.rejected counts capacity-rejected opens. Existing identities remain valid.

Optional terminal graphics

TerminalCore also has optional getGraphicsState() and getGraphicsImage() methods. A provider returns copied image metadata and tightly packed, row-major RGBA bytes for the active screen, plus pinned placements in retained-row coordinates. Rows are zero-based from the oldest retained row; a placement's col, row, and pixel offsets are measured from the top-left of that retained terminal surface. Image versions identify replacements, and a changed generation invalidates placement and image snapshots.

These methods are optional so existing custom cores and test doubles remain compatible. Missing, malformed, or unavailable graphics return null and must not interrupt ordinary terminal writes. Returned arrays and Uint8Array buffers are caller-owned copies.

The built-in lightweight core consumes unsupported 7-bit Kitty APC graphics sequences safely but does not advertise a graphics provider. Kitty graphics are currently rendered by the Ghostty backend only; Sixel, iTerm2, virtual Unicode placements, animation, and file/shared-memory/URL media are outside this release.

WebSocketTransport

Connect to a PTY backend over WebSocket.

import { WebSocketTransport } from "@wterm/core";

const ws = new WebSocketTransport({
  url: "ws://localhost:8080/pty",
  onData: (data) => { /* handle received data */ },
});

ws.connect();
ws.send("ls\n");

Outbound buffering is bounded by default: maxBufferedBytes is 1 MiB across the transport queue and the current open socket's bufferedAmount, and maxBufferedMessages allows 1,024 messages waiting in the transport. send() throws RangeError before accepting any part of a message that would exceed either limit. Catch it and show that input was not accepted; do not silently discard the error or automatically retry commands.

highWaterMark (64 KiB) pauses socket writes; draining resumes at lowWaterMark (16 KiB). A message larger than the high-water mark is sent intact only when the socket buffer is empty. Message order and boundaries are preserved, strings use UTF-8, and queued byte arrays are copied. Each drain processes at most 64 messages, with 16 ms polling only while an open socket has pending data. Browser background throttling can delay draining.

Read bufferedAmount, queuedBytes, queuedMessages, and backpressured for queue state. Use onBackpressure(paused) to pause/resume producers; it reports transitions at the high/low byte thresholds or when the message queue is full. All limits are construction options. Byte/message caps and the high-water mark must be positive safe integers, with 0 <= lowWaterMark < highWaterMark <= maxBufferedBytes. If only the byte cap is reduced, the default high-water mark is clamped to it and the default low-water mark is one quarter of that value.

Before the first connection, sends are queued within these limits. Unexpected disconnects retain only messages not yet handed to the socket; already-sent bytes have uncertain delivery and are never replayed. close() clears the transport queue and timers and rejects further sends until connect() is called. Connecting to a different URL also discards unsent messages. Repeated connect() calls for the same connecting/open socket do nothing; callbacks from replaced sockets are ignored. These are client-side send bounds, not server acknowledgments, PTY flow control, or session recovery.

Host theme colors

TerminalThemeColors describes opaque 24-bit integer foreground, background, and cursor colors plus exactly 16 palette entries. The optional TerminalCore.setThemeColors(colors) updates host defaults without parsing bytes, resetting state, or replacing application overrides. Both the built-in core and Ghostty implement this hook and answer OSC 10/11 queries with these defaults; other cores remain compatible without it. Use WTerm.setThemeColors() for an attached browser terminal so CSS and live/retained rendering update together.

Clipboard write requests

TerminalCore.getClipboardWrite?() consumes the latest application request. null means none and "" requests a clear. Ghostty implements this optional effect; older/custom and built-in cores may omit it. Hosts own acceptance and browser permissions; there is no clipboard-read API or automatic clipboard access.

Working directories

The optional TerminalCore.getWorkingDirectory() consumes the latest OSC 7 URI, returning null when unchanged or unsupported and "" when reset. Ghostty retains up to 2047 UTF-8 bytes, coalesces pending reports, and clears the value on RIS. Treat the URI as untrusted metadata; it does not establish a local directory or authenticated host. WTerm forwards it through onWorkingDirectory.

Shell integration state

ShellIntegrationState contains phase (unknown, prompt, input, running, or complete) and exitCode (number | null). The optional TerminalCore.getShellIntegrationState() consumes the latest state change; null means unchanged or unsupported. Ghostty implements this hook for OSC 133. The last valid or absent completion code is retained across prompt redraws until command start or terminal reset. This is bounded current state, not a command history. Returned snapshots belong to the caller. Cores that omit the hook remain compatible; WTerm forwards new snapshots through onShellIntegration.

The optional findPrompt(row, direction) returns the preceding (-1) or following (1) prompt's retained row, or null when none is available. Row zero is the oldest retained row. The origin must be in the retained buffer; forward navigation skips the current prompt's continuation rows. Ghostty uses its retained OSC 133 metadata, follows reflow and pruning, and returns no prompt on the alternate screen. The hook neither moves the cursor nor consumes shell state.

License

Apache-2.0