Skip to main content

Crate retroglyph_terminal_wasm

Crate retroglyph_terminal_wasm 

Source
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).

SequenceNameEmitted byMeaning
CSI Ps;Ps HCUP (Cursor Position)draw, for a non-adjacent cell; set_cursor_positionmove the cursor, 1-indexed row;col, always absolute
CSI 39 m / CSI 49 mSGR reset FG/BGdraw, for Color::Defaultreset foreground/background to the emulator’s default
CSI 3n m / CSI 4n m (30-37 / 40-47)SGR ANSI FG/BGdraw, for the standard 8 Color::Ansi valuesset foreground/background to a standard ANSI color
CSI 9n m / CSI 10n m (90-97 / 100-107)SGR bright ANSI FG/BGdraw, for the bright 8 Color::Ansi valuesset foreground/background to a bright ANSI color
CSI 38;5;n m / CSI 48;5;n mSGR indexed FG/BGdraw, for Color::Indexedset foreground/background from the 256-color palette
CSI 38;2;r;g;b m / CSI 48;2;r;g;b mSGR truecolor FG/BGdraw, for Color::Rgbset foreground/background to a 24-bit RGB color, unquantized
CSI ?2026 h / CSI ?2026 lDEC private mode 2026 (synchronized update)every draw/flush pairhold rendering until the matching end marker, avoiding tearing mid-frame
CSI ?25 h / CSI ?25 lDECTCEM (cursor visibility)set_cursor_visibleshow/hide the terminal cursor
CSI Ps SP qDECSCUSR (cursor shape)set_cursor_styleset the cursor’s shape/blink behavior
CSI 2J then CSI HED (erase display) + CUP homeclearclear 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 row and col, even to step one cell right or down: there is no CSI C/CSI B (cursor-relative) fallback. TerminalRenderer::draw does 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::Rgb is always written as the 24-bit 38;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. See retroglyph-terminal’s crate-level docs (“RGB color fallback on 256-color terminals”) for the full rationale; use Color::Indexed or Color::Ansi instead when a specific emulator’s color depth is known ahead of time.
  • clear always re-syncs tracked state. clear additionally resets this renderer’s tracked cursor/color state, so the next draw call re-emits a full CUP and color codes for every cell instead of (incorrectly) assuming the emulator remembers the old state through the erase.
  • clear resets SGR attributes before erasing. clear emits CSI 0 m ahead of CSI 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 after clear.
  • Every draw is wrapped in its own synchronized-update pair. draw itself emits CSI ?2026 h before drawing, and the paired flush call emits CSI ?2026 l after. 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
code values for decode_key_event’s named (non-printable) keys.
mouse_actions
action values for decode_mouse_event.
mouse_buttons
button values for decode_mouse_event, matching the DOM MouseEvent.button convention (0 = left, 1 = middle, 2 = right) so JS can forward its own event.button unchanged.

Macros§

app_entry
Emits the wasm-bindgen FFI surface driving $A: App<TerminalWasm> + Default from a browser terminal emulator (e.g. xterm.js), on wasm32 only.

Structs§

TerminalWasm
A Backend that 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 a retroglyph_core::event::KeyEvent.
decode_mouse_event
Decodes an (x, y, action, button, mods) tuple from JS into a retroglyph_core::event::MouseEvent.
resize_terminal
Resizes term to (width, height) cells, doing everything a correct resize needs in one call.