# retroglyph-terminal-wasm - Complete API Documentation > WASM/browser terminal backend for retroglyph, driven by pushed events and pulled ANSI output **Version:** 0.0.0 **Authors:** Matan Lurey **License:** MIT **Repository:** https://github.com/crates-lurey-io/retroglyph **Keywords:** roguelike, terminal, grid, gamedev Generated: 2026-08-05 20:21:10 UTC Created by: [cargo-llms-txt](https://github.com/masinc/cargo-llms-txt) ## Table of Contents ### src/lib.rs - impl io::Write for Utf8Sink - pub struct TerminalWasm - impl TerminalWasm - pub fn resize_terminal - impl Output for TerminalWasm - impl Input for TerminalWasm - impl Cursor for TerminalWasm - pub fn decode_key_event - pub fn decode_mouse_event - pub mod mouse_actions - pub const mouse_actions::DOWN - pub const mouse_actions::UP - pub const mouse_actions::MOVED - pub const mouse_actions::SCROLL_UP - pub const mouse_actions::SCROLL_DOWN - pub mod mouse_buttons - pub const mouse_buttons::LEFT - pub const mouse_buttons::MIDDLE - pub const mouse_buttons::RIGHT - pub mod key_codes - pub const key_codes::BACKSPACE - pub const key_codes::ENTER - pub const key_codes::LEFT - pub const key_codes::RIGHT - pub const key_codes::UP - pub const key_codes::DOWN - pub const key_codes::HOME - pub const key_codes::END - pub const key_codes::PAGE_UP - pub const key_codes::PAGE_DOWN - pub const key_codes::TAB - pub const key_codes::BACKTAB - pub const key_codes::DELETE - pub const key_codes::INSERT - pub const key_codes::ESCAPE - pub const key_codes::F1 - pub const key_codes::F24 - pub mod wasm - pub fn wasm::wasm_terminal_new - pub fn wasm::wasm_terminal_free - pub fn wasm::wasm_terminal_resize - pub fn wasm::wasm_terminal_push_key - pub fn wasm::wasm_terminal_push_mouse - pub fn wasm::wasm_terminal_push_paste - pub fn wasm::wasm_terminal_push_focus - pub fn wasm::wasm_terminal_take_output - impl retroglyph_core::testing::conformance::Observable for tests::TerminalWasm ### src/app_entry.rs - app_entry! --- ## README.md ### retroglyph-terminal-wasm WASM/browser terminal backend for retroglyph, driven by pushed events and pulled ANSI output Part of the [retroglyph](https://github.com/crates-lurey-io/retroglyph) workspace. --- ## src/lib.rs ### impl io::Write for Utf8Sink ```rust impl io::Write for Utf8Sink { } ``` ### TerminalWasm ```rust pub struct TerminalWasm { } ``` A [`Backend`](retroglyph_core::backend::Backend) that renders into an in-memory ANSI byte buffer and accepts pushed input, for driving a browser terminal emulator from WASM. Unlike [`retroglyph_crossterm::Crossterm`](https://docs.rs/retroglyph-crossterm), this backend: - never queries a TTY for its size: call [`resize_terminal`] (or, if the input side doesn't matter for the caller, [`Terminal::resize`](retroglyph_core::terminal::Terminal::resize) directly) whenever the host reports a new size (e.g. from xterm.js's `fit` addon); - never polls for input: input only ever arrives via [`push_event`](Input::push_event), called from a `wasm-bindgen` entry point in response to a JS event; - buffers rendered ANSI bytes in memory rather than writing to a descriptor; call [`take_output`](Self::take_output) once per animation frame to drain them. ### impl TerminalWasm ```rust impl TerminalWasm { pub fn new(width: u16, height: u16) -> Self; pub fn take_output(&mut self) -> String; pub fn take_output_into(&mut self, buf: &mut String); } ``` ### resize_terminal ```rust pub fn resize_terminal(term: &mut Terminal, width: u16, height: u16) ``` Resizes `term` to `(width, height)` cells, doing everything a correct resize needs in one call. That's [`Terminal::resize`](retroglyph_core::terminal::Terminal::resize) (which itself resizes both grid buffers and calls [`Output::resize`] on the backend), plus queuing the matching [`Event::Resize`] so a driven [`App`](retroglyph_core::app::App) observes the new size through its own input handling too, exactly as it would from a native backend's real resize event. Calling `term.resize(width, height)` directly (skipping this function) leaves nothing in the input queue: an app that reacts to `Event::Resize` rather than re-checking `term.backend().size()` every frame silently keeps its old layout. [`app_entry!`] already calls this on every `wasm_app_resize`; reach for it directly only when driving a `Terminal` by hand instead of through that macro. See retroglyph#684. #### Examples ``` use retroglyph_core::terminal::Terminal; use retroglyph_core::backend::Output as _; use retroglyph_core::event::Event; use retroglyph_terminal_wasm::{TerminalWasm, resize_terminal}; let mut term = Terminal::new(TerminalWasm::new(10, 3)); resize_terminal(&mut term, 20, 6); assert_eq!(term.backend().size(), retroglyph_core::grid::Size::new(20, 6)); assert_eq!(term.poll(std::time::Duration::ZERO), Some(Event::Resize(20, 6))); ``` [`app_entry!`]: crate::app_entry ### impl Output for TerminalWasm ```rust impl Output for TerminalWasm { } ``` ### impl Input for TerminalWasm ```rust impl Input for TerminalWasm { } ``` ### impl Cursor for TerminalWasm ```rust impl Cursor for TerminalWasm { } ``` ### decode_key_event ```rust pub fn decode_key_event(code: u32, mods: u8) -> Option ``` Decodes a `(code, mods)` pair from JS into a [`retroglyph_core::event::KeyEvent`]. `retroglyph_core::event::KeyCode`/`KeyModifiers` are not `wasm-bindgen` FFI-safe types, so JS crosses the boundary with plain integers instead: - `code`: for printable characters, the Unicode scalar value (as from `event.key.codePointAt(0)` for a single-character key); for named keys, one of [`key_codes`]'s constants (e.g. [`key_codes::LEFT`]). - `mods`: a bitmask matching [`retroglyph_core::event::KeyModifiers`]'s layout (`SHIFT = 1`, `CONTROL = 2`, `ALT = 4`, `SUPER = 8`). `SUPER` maps to the JS `metaKey` (Cmd on macOS, the Windows/Super key elsewhere). #### Examples ``` use retroglyph_core::event::KeyCode; use retroglyph_terminal_wasm::{decode_key_event, key_codes}; // A printable character: `code` is its Unicode scalar value. let key = decode_key_event(u32::from('a'), 0).unwrap(); assert_eq!(key.code, KeyCode::Char('a')); // A named key, with the Control modifier bit (0b010) set. let key = decode_key_event(key_codes::LEFT, 0b010).unwrap(); assert_eq!(key.code, KeyCode::Left); assert!(key.modifiers.contains(retroglyph_core::event::KeyModifiers::CONTROL)); // A lone UTF-16 surrogate half is neither a named key nor a valid `char`. assert!(decode_key_event(0xD800, 0).is_none()); ``` ### decode_mouse_event ```rust pub fn decode_mouse_event(x: u16, y: u16, action: u8, button: u8, mods: u8) -> Option ``` Decodes an `(x, y, action, button, mods)` tuple from JS into a [`retroglyph_core::event::MouseEvent`]. Same pattern as [`decode_key_event`]: `retroglyph_core::event::MouseEvent` (and its `MouseEventKind`/`MouseButton` fields) are not `wasm-bindgen` FFI-safe types, so JS crosses the boundary with plain integers instead: - `x`, `y`: the cell-grid column/row the pointer is over, matching [`retroglyph_core::grid::Pos`]. JS is responsible for converting a raw pixel position (e.g. from a DOM `MouseEvent`) into cell coordinates using the terminal emulator's own cell size, the same way it already tracks `cols`/`rows` for the `wasm32`-only `wasm::wasm_terminal_resize`. This backend has no sub-cell precision to report, so the returned event's [`pixel_position`](retroglyph_core::event::MouseEvent::pixel_position) is always `None`: the same convention `retroglyph-crossterm` uses for its own character-mode backend. - `action`: one of [`mouse_actions`]'s constants (`DOWN`, `UP`, `MOVED`, `SCROLL_UP`, `SCROLL_DOWN`). - `button`: which button the event applies to, one of [`mouse_buttons`]'s constants (`LEFT`, `MIDDLE`, `RIGHT`), matching the DOM `MouseEvent.button` convention JS already has on hand. Only consulted when `action` is `DOWN` or `UP`; ignored otherwise. - `mods`: the same bitmask layout as [`decode_key_event`]'s `mods` (`SHIFT = 1`, `CONTROL = 2`, `ALT = 4`, `SUPER = 8`). Returns `None` if `action` doesn't match a known [`mouse_actions`] constant, or if `action` is `DOWN`/`UP` and `button` doesn't match a known [`mouse_buttons`] constant. #### Examples ``` use retroglyph_core::event::{MouseButton, MouseEventKind}; use retroglyph_terminal_wasm::{decode_mouse_event, mouse_actions, mouse_buttons}; let mouse = decode_mouse_event(3, 4, mouse_actions::DOWN, mouse_buttons::LEFT, 0).unwrap(); assert_eq!(mouse.kind, MouseEventKind::Down(MouseButton::Left)); assert_eq!(mouse.position, retroglyph_core::grid::Pos { x: 3, y: 4 }); // `button` is ignored for `MOVED`; an out-of-range value doesn't fail decoding. let mouse = decode_mouse_event(1, 1, mouse_actions::MOVED, 0xFF, 0).unwrap(); assert_eq!(mouse.kind, MouseEventKind::Moved); // An unknown `action` fails to decode. assert!(decode_mouse_event(0, 0, 0xFF, mouse_buttons::LEFT, 0).is_none()); ``` ### DOWN ```rust pub const DOWN: u8 ``` A mouse button was pressed. `button` selects which one. ### UP ```rust pub const UP: u8 ``` A mouse button was released. `button` selects which one. ### MOVED ```rust pub const MOVED: u8 ``` The mouse moved. `button` is ignored. ### SCROLL_UP ```rust pub const SCROLL_UP: u8 ``` The mouse wheel scrolled up (away from the user). `button` is ignored. ### SCROLL_DOWN ```rust pub const SCROLL_DOWN: u8 ``` The mouse wheel scrolled down (toward the user). `button` is ignored. ### LEFT ```rust pub const LEFT: u8 ``` Left (primary) mouse button. ### MIDDLE ```rust pub const MIDDLE: u8 ``` Middle (auxiliary) mouse button. ### RIGHT ```rust pub const RIGHT: u8 ``` Right (secondary) mouse button. ### BACKSPACE ```rust pub const BACKSPACE: u32 ``` Backspace. ### ENTER ```rust pub const ENTER: u32 ``` Enter. ### LEFT ```rust pub const LEFT: u32 ``` Left arrow. ### RIGHT ```rust pub const RIGHT: u32 ``` Right arrow. ### UP ```rust pub const UP: u32 ``` Up arrow. ### DOWN ```rust pub const DOWN: u32 ``` Down arrow. ### HOME ```rust pub const HOME: u32 ``` Home. ### END ```rust pub const END: u32 ``` End. ### PAGE_UP ```rust pub const PAGE_UP: u32 ``` Page Up. ### PAGE_DOWN ```rust pub const PAGE_DOWN: u32 ``` Page Down. ### TAB ```rust pub const TAB: u32 ``` Tab. ### BACKTAB ```rust pub const BACKTAB: u32 ``` Backtab (Shift+Tab). ### DELETE ```rust pub const DELETE: u32 ``` Delete. ### INSERT ```rust pub const INSERT: u32 ``` Insert. ### ESCAPE ```rust pub const ESCAPE: u32 ``` Escape. ### F1 ```rust pub const F1: u32 ``` F1. F2-F24 follow contiguously up to [`F24`]. ### F24 ```rust pub const F24: u32 ``` F24, the last of the contiguous F1-F24 range. ### wasm_terminal_new ```rust pub fn wasm_terminal_new(width: u16, height: u16) -> u32 ``` Creates a new [`TerminalWasm`] of the given size and returns an opaque handle for use with the other `wasm_terminal_*` functions. ### wasm_terminal_free ```rust pub fn wasm_terminal_free(handle: u32) ``` Destroys the [`TerminalWasm`] identified by `handle`, freeing its memory. Further calls with `handle` are no-ops. ### wasm_terminal_resize ```rust pub fn wasm_terminal_resize(handle: u32, width: u16, height: u16) ``` Reports a new size (in cells) for the terminal identified by `handle`, e.g. after xterm.js's `fit` addon recomputes `cols`/`rows`. ### wasm_terminal_push_key ```rust pub fn wasm_terminal_push_key(handle: u32, code: u32, mods: u8) ``` Pushes a key event into the terminal identified by `handle`. See [`decode_key_event`] for the `code`/`mods` encoding. Silently ignores codes that don't decode to a known key (e.g. a lone Unicode combining mark with no assigned scalar meaning here). ### wasm_terminal_push_mouse ```rust pub fn wasm_terminal_push_mouse(handle: u32, x: u16, y: u16, action: u8, button: u8, mods: u8) ``` Pushes a mouse event into the terminal identified by `handle`. See [`decode_mouse_event`] for the `x`/`y`/`action`/`button`/`mods` encoding. Silently ignores an `action`/`button` combination that doesn't decode to a known mouse event. ### wasm_terminal_push_paste ```rust pub fn wasm_terminal_push_paste(handle: u32, text: String) ``` Pushes pasted text into the terminal identified by `handle`, delivered as a single [`retroglyph_core::event::Event::Paste`] rather than synthesized key events. Unlike [`wasm_terminal_push_key`], this takes a plain JS string directly: `String` is already `wasm-bindgen`-FFI-safe, so there's no `decode_*` step to pair with it, and no risk of a paste of `N` characters being misread as `N` individual keystrokes (which would let pasted text trigger single-key game commands one character at a time). The driver is responsible for sourcing `text`: e.g. a native browser `paste` event's `event.clipboardData.getData('text/plain')`, read synchronously and without an Async Clipboard API permission prompt. This crate has no opinion on *how* JS obtains the text, only that it arrives here as one call per paste. ### wasm_terminal_push_focus ```rust pub fn wasm_terminal_push_focus(handle: u32, focused: bool) ``` Pushes a focus-change event into the terminal identified by `handle`. `focused: true` delivers [`Event::FocusGained`](retroglyph_core::event::Event::FocusGained), `focused: false` delivers [`Event::FocusLost`](retroglyph_core::event::Event::FocusLost). Mirrors the crossterm backend's `EnableFocusChange`-driven focus-event mapping, so a browser terminal element's native `focus`/`blur` DOM events can drive the same "pause when unfocused" pattern. ### wasm_terminal_take_output ```rust pub fn wasm_terminal_take_output(handle: u32) -> String ``` Drains and returns the ANSI bytes rendered since the last call for the terminal identified by `handle`. Returns an empty string if `handle` is unknown or nothing has been drawn since the last call. ### impl retroglyph_core::testing::conformance::Observable for TerminalWasm ```rust impl retroglyph_core::testing::conformance::Observable for TerminalWasm { } ``` [`Observable::snapshot`] hashes only the bytes appended since the previous call, per that trait's docs: `TerminalWasm`'s observable state is an append-only ANSI byte buffer, and [`TerminalWasm::take_output`] already drains exactly that (nothing new to build here). ## src/app_entry.rs ### app_entry! ```rust macro_rules! app_entry { // macro definition } ``` Emits the `wasm-bindgen` FFI surface driving `$A: App + Default` from a browser terminal emulator (e.g. xterm.js), on `wasm32` only. `examples/src/wasm_entry.rs`'s `__wasm_terminal_entry!` does the same job for the examples crate's private `Example` trait, but that crate is `publish = false`, so nothing outside this repo can reach it (retroglyph#684). This macro is the generally-usable version: generic over [`App`](retroglyph_core::app::App) (public, stable, and already the update contract every other driver in `retroglyph-core` shares), not `Example`. Call it once, at the top level of a `wasm32` binary crate that depends on this crate and `retroglyph-core`: ```ignore #[derive(Default)] struct MyGame { /* ... */ } impl retroglyph_core::app::App for MyGame { fn update( &mut self, term: &mut retroglyph_core::terminal::Terminal, frame: &retroglyph_core::app::Frame, ) -> retroglyph_core::app::Flow { // ... retroglyph_core::app::Flow::Continue } } retroglyph_terminal_wasm::app_entry!(MyGame); fn main() {} ``` Expands to nothing at all off `wasm32` (a native build of the same crate just doesn't get this FFI surface, since nothing would call it). Exports, all thread-local and single-instance (one `$A` per page; construct a fresh handle-based session per instance instead via this crate's `wasm` module, only compiled for `target_arch = "wasm32"`, if a page needs more than one): - `wasm_app_init(width, height)`: builds the `Terminal` at the given size (in cells) and `$A::default()`. Call once, before the first tick, after sizing the host terminal emulator (e.g. xterm.js's `fitAddon.fit()`). - `wasm_app_resize(width, height)`: reports a new size (in cells) via [`resize_terminal`](crate::resize_terminal), so the driven `$A` sees the matching `Event::Resize` on its next `update`, not just a backend that silently changed size under it. - `wasm_app_push_key(code, mods)` / `wasm_app_push_mouse(x, y, action, button, mods)`: decode and queue input via [`decode_key_event`](crate::decode_key_event)/ [`decode_mouse_event`](crate::decode_mouse_event). - `wasm_app_push_paste(text)`: queues `text` as a single `Event::Paste`. - `wasm_app_push_focus(focused)`: queues `Event::FocusGained`/`Event::FocusLost`. - `wasm_app_tick() -> String`: runs one `App::update`, presents unless it returned `Flow::Idle` (or already presented itself), and returns the ANSI bytes rendered since the last call, the same contract [`TerminalWasm::take_output`](crate::TerminalWasm::take_output) documents. `Frame::delta` is wall-clock time since the previous tick, clamped to `MAX_TICK_DELTA` (250ms): a backgrounded tab can starve `requestAnimationFrame` for seconds or minutes, and an uncapped delta handed straight to an animation/physics step would try to simulate that entire gap in one frame (the same "spiral of death" concern [`FrameClock`](retroglyph_core::frames::FrameClock) caps steps-per-frame to avoid), just on the raw delta feeding into `Frame` instead. All FFI functions are no-ops (returning an empty string for `wasm_app_tick`) if called before `wasm_app_init`. - `wasm_app_exited() -> bool`: `true` once `$A::update` has returned `Flow::Exit` at least once. A browser tab has no native "exit the process" the way a windowed backend's event loop does, so this crate can't stop JS's `requestAnimationFrame` loop for it; check this after `wasm_app_tick` and stop calling it once it flips `true`, e.g. to show a fixed "Game Over" frame's own draw already put on screen. `wasm_app_tick` keeps calling `$A::update` (and, correctly, doing nothing useful) if the caller ignores this rather than panicking or hanging.