Expand description
A WASM/browser terminal backend, driven by pushed input and pulled ANSI output.
TerminalWasm implements Backend directly (like
Headless): there is no event loop
here. A browser terminal emulator (e.g. xterm.js, this crate has no
dependency on it and no opinion about which one is used) is driven from
JS, which calls into this crate once per animation frame (or on demand)
to pull freshly rendered ANSI bytes and push back any input it collected.
§Usage from Rust
use retroglyph_core::color::Style;
use retroglyph_core::terminal::Terminal;
use retroglyph_terminal_wasm::TerminalWasm;
let backend = TerminalWasm::new(80, 24);
let mut term = Terminal::new(backend);
term.draw(|s| s.put((0, 0), '@', Style::default())).unwrap();
let ansi = term.backend_mut().take_output();
assert!(ansi.contains('@'));§Usage from JS (via wasm-bindgen)
The wasm32 build additionally exposes free functions
(wasm_terminal_new, wasm_terminal_resize, wasm_terminal_push_key,
wasm_terminal_push_mouse, wasm_terminal_push_paste,
wasm_terminal_take_output, in this crate’s wasm module, only
compiled for target_arch = "wasm32", so it won’t appear in docs built
natively) that operate on an opaque handle, since
retroglyph_core::event::Event is not itself wasm-bindgen-compatible.
The example below is a complete, working driver pairing this crate’s
wasm32 build with xterm.js (any other browser
terminal emulator works the same way; this crate has no dependency on
xterm.js specifically). It assumes a wasm-pack/wasm-bindgen-generated
./pkg.js module built from a binary that re-exports this crate’s wasm
module, and an xterm.js <script> already loaded on the page (see
xterm.js’s own quick start for that half).
It’s a wiring template, not a full game: it plumbs input/output through
this crate’s generic handle-based FFI but calls no per-frame drawing
logic of its own (that’s the consumer’s job, via their own Rust code
holding the Terminal<TerminalWasm>; see “Usage from Rust” above).
A game driving a real App usually wants the single-instance-per-page
FFI app_entry! generates instead of hand-rolling a thread-local session over the
handle-based functions shown here: wasm_app_init/wasm_app_resize/wasm_app_push_key/
wasm_app_push_mouse/wasm_app_push_paste/wasm_app_push_focus/wasm_app_tick, with the
Terminal::resize-plus-Event::Resize bookkeeping
resize_terminal does and the backgrounded-tab delta clamp documented on app_entry!
itself. See that macro’s own doc comment for a complete example. The examples crate’s
WASM demo gallery (linked from the workspace README) uses an equivalent macro,
retroglyph_examples::wasm_entry!, generated over its own private Example trait instead of
App because that crate predates this one shipping a published equivalent.
This file (kept in sync with the copy in README.md by a test) is
crates/terminal-wasm/js/xterm-driver.js in the source tree:
import init, {
wasm_terminal_new,
wasm_terminal_resize,
wasm_terminal_push_key,
wasm_terminal_take_output,
} from './pkg.js';
// `code` values above 0x110000 select a named key; see this crate's `key_codes` module for the
// full list (arrows, Home/End, F1-F24, etc).
const NAMED_KEY_BASE = 0x00110000;
const KEY_ENTER = NAMED_KEY_BASE + 1;
const KEY_BACKSPACE = NAMED_KEY_BASE;
// `mods` is a bitmask: SHIFT = 1, CONTROL = 2, ALT = 4, SUPER = 8.
function decodeXtermData(data) {
if (data === '\r') return { code: KEY_ENTER, mods: 0 };
if (data === '\x7f') return { code: KEY_BACKSPACE, mods: 0 };
// A single printable character forwards as its Unicode codepoint; xterm.js already resolves
// Shift into the codepoint itself (e.g. 'A' vs 'a'), so no SHIFT bit is needed here.
if (data.length === 1) return { code: data.codePointAt(0), mods: 0 };
return null;
}
async function main() {
await init();
const term = new Terminal({ cols: 80, rows: 24 });
term.open(document.getElementById('screen'));
const handle = wasm_terminal_new(term.cols, term.rows);
term.onData((data) => {
const key = decodeXtermData(data);
if (key) wasm_terminal_push_key(handle, key.code, key.mods);
});
window.addEventListener('resize', () => {
// Call whatever fit-to-container logic resizes `term` first (e.g. xterm.js's FitAddon), then
// tell the backend to match.
wasm_terminal_resize(handle, term.cols, term.rows);
});
function frame() {
const ansi = wasm_terminal_take_output(handle);
if (ansi) term.write(ansi);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
main();§Features
This crate has no default features; every feature below is optional and off unless enabled.
§dev
⚪ Optional.
Forwards retroglyph-core’s dev feature, which forces development diagnostics on in a build
that would otherwise compile them out (see retroglyph_core::dev).
§egc
⚪ Optional.
Forwards to retroglyph-terminal’s (and retroglyph-core’s) egc feature for
grapheme-cluster-aware cell diffing.
§ANSI sequences emitted
TerminalWasm renders through retroglyph_terminal::TerminalRenderer (see that crate’s
docs for the full cell-diff renderer contract) and adds a handful of sequences of its own
(clear, set_cursor_visible,
set_cursor_style). Every
sequence below is standard ANSI X3.64 (ECMA-48) CSI (the subset xterm’s own control-sequence
reference calls plain “ANSI”/VT100-compatible), nothing proprietary or emulator-specific. The
bytes are the same regardless of what emulator eventually reads them; this crate makes no
attempt to detect or work around variance between implementations (see the quirks below for
the two places that matters).
| Sequence | Name | Emitted by | Meaning |
|---|---|---|---|
CSI Ps;Ps H | CUP (Cursor Position) | draw, for a non-adjacent cell; set_cursor_position | move the cursor, 1-indexed row;col, always absolute |
CSI 39 m / CSI 49 m | SGR reset FG/BG | draw, for Color::Default | reset foreground/background to the emulator’s default |
CSI 3n m / CSI 4n m (30-37 / 40-47) | SGR ANSI FG/BG | draw, for the standard 8 Color::Ansi values | set foreground/background to a standard ANSI color |
CSI 9n m / CSI 10n m (90-97 / 100-107) | SGR bright ANSI FG/BG | draw, for the bright 8 Color::Ansi values | set foreground/background to a bright ANSI color |
CSI 38;5;n m / CSI 48;5;n m | SGR indexed FG/BG | draw, for Color::Indexed | set foreground/background from the 256-color palette |
CSI 38;2;r;g;b m / CSI 48;2;r;g;b m | SGR truecolor FG/BG | draw, for Color::Rgb | set foreground/background to a 24-bit RGB color, unquantized |
CSI ?2026 h / CSI ?2026 l | DEC private mode 2026 (synchronized update) | every draw/flush pair | hold rendering until the matching end marker, avoiding tearing mid-frame |
CSI ?25 h / CSI ?25 l | DECTCEM (cursor visibility) | set_cursor_visible | show/hide the terminal cursor |
CSI Ps SP q | DECSCUSR (cursor shape) | set_cursor_style | set the cursor’s shape/blink behavior |
CSI 2J then CSI H | ED (erase display) + CUP home | clear | clear the screen, then move the cursor to (1, 1) |
retroglyph does not model text attributes (bold, italic, underline, etc.; see
retroglyph_core::color::Style’s docs for why), so no SGR attribute codes (1, 3, 4,
…) are ever emitted here; only the color and cursor/erase sequences above. Glyph bytes
themselves (see take_output) are plain UTF-8, not an escape
sequence.
§TerminalRenderer quirks to know before validating against a specific emulator
- Absolute positions only, never relative. Every cursor move is a full CUP with both
rowandcol, even to step one cell right or down: there is noCSI C/CSI B(cursor-relative) fallback.TerminalRenderer::drawdoes skip the move entirely when the cursor is already at the right cell from printing the previous glyph (adjacent same-row cells), but it never emits a relative move to get there. - No RGB-to-256/16-color quantization.
Color::Rgbis always written as the 24-bit38;2;.../48;2;...form, even targeting an emulator that only supports the 256-color or 16-color palette; downsampling (if any) is left entirely to the receiving emulator. Seeretroglyph-terminal’s crate-level docs (“RGB color fallback on 256-color terminals”) for the full rationale; useColor::IndexedorColor::Ansiinstead when a specific emulator’s color depth is known ahead of time. clearalways re-syncs tracked state.clearadditionally resets this renderer’s tracked cursor/color state, so the nextdrawcall re-emits a full CUP and color codes for every cell instead of (incorrectly) assuming the emulator remembers the old state through the erase.clearresets SGR attributes before erasing.clearemitsCSI 0 mahead ofCSI 2J, because most terminals implement erase-display via background color erase (BCE) and paint the erased cells with whatever background is currently active in the pen, not the emulator’s true default. Without the reset, a cell colored by the last frame leaves its tint across the whole screen afterclear.- Every
drawis wrapped in its own synchronized-update pair.drawitself emitsCSI ?2026 hbefore drawing, and the pairedflushcall emitsCSI ?2026 lafter. An emulator that doesn’t recognize DEC private mode 2026 ignores both codes per the CSI spec’s “unknown private mode” behavior, so this is safe to send unconditionally.
xterm’s own control-sequence reference
(https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) and ECMA-48 (the formal standard
behind “ANSI X3.64”, https://www.ecma-international.org/publications-and-standards/standards/ecma-48/)
are the normative references for every sequence in the table above. The synchronized-update
mode (2026) isn’t part of either: it follows the de facto convention specified at
https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036 and implemented by
xterm.js, kitty, iTerm2, and others.
Modules§
- key_
codes codevalues fordecode_key_event’s named (non-printable) keys.- mouse_
actions actionvalues fordecode_mouse_event.- mouse_
buttons buttonvalues fordecode_mouse_event, matching the DOMMouseEvent.buttonconvention (0= left,1= middle,2= right) so JS can forward its ownevent.buttonunchanged.
Macros§
- app_
entry - Emits the
wasm-bindgenFFI surface driving$A: App<TerminalWasm> + Defaultfrom a browser terminal emulator (e.g. xterm.js), onwasm32only.
Structs§
- Terminal
Wasm - A
Backendthat renders into an in-memory ANSI byte buffer and accepts pushed input, for driving a browser terminal emulator from WASM.
Functions§
- decode_
key_ event - Decodes a
(code, mods)pair from JS into aretroglyph_core::event::KeyEvent. - decode_
mouse_ event - Decodes an
(x, y, action, button, mods)tuple from JS into aretroglyph_core::event::MouseEvent. - resize_
terminal - Resizes
termto(width, height)cells, doing everything a correct resize needs in one call.