# retroglyph-recorder - Complete API Documentation > Input recording & replay for retroglyph: record a real session, replay it through a live App **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-23 16:09:34 UTC Created by: [cargo-llms-txt](https://github.com/masinc/cargo-llms-txt) ## Table of Contents ### src/rgrec.rs - pub fn write - pub fn read ### src/pty.rs - pub struct PtySession - impl PtySession - pub fn capture_pty ### src/owned_cell.rs - pub struct OwnedCell - impl From for OwnedCell ### src/frame_recorder.rs - pub struct CapturedFrame - pub struct FrameRecorderHandle - impl FrameRecorderHandle - pub struct FrameRecorder - impl FrameRecorder - impl FrameRecorder - impl Output for FrameRecorder - impl Input for FrameRecorder - impl Cursor for FrameRecorder ### src/panic_hook.rs - pub fn install_panic_recorder ### src/lib.rs - pub mod pty - pub use cast::write_cast - pub use frame_recorder::{CapturedFrame, FrameRecorder, FrameRecorderHandle} - pub use input_recorder::{InputRecorder, RecorderHandle} - pub use owned_cell::OwnedCell - pub use panic_hook::install_panic_recorder - pub use replay::replay_live - pub use rgrec::{read, write} ### src/cast.rs - pub fn write_cast ### src/input_recorder.rs - pub struct RecorderHandle - impl RecorderHandle - pub struct InputRecorder - impl InputRecorder - impl InputRecorder - impl Output for InputRecorder - impl Input for InputRecorder - impl Cursor for InputRecorder ### src/replay.rs - pub fn replay_live - impl App for tests::Counter --- ## README.md ### retroglyph-recorder Input recording & replay for retroglyph: record a real session, replay it through a live App Part of the [retroglyph](https://github.com/crates-lurey-io/retroglyph) workspace. --- ## src/rgrec.rs ### write ```rust pub fn write(recording: &InputRecording, path: impl Trait) -> io::Result<()> ``` Writes `recording` to `path` as `.rgrec`, overwriting any existing file. #### Errors Returns an error if `path` cannot be created or written to. ### read ```rust pub fn read(path: impl Trait) -> io::Result ``` Reads a `.rgrec` file written by [`write()`] back into an [`InputRecording`]. #### Errors Returns an error if `path` cannot be opened or read, the file has no header line, or any line's JSON does not parse. ## src/pty.rs ### PtySession ```rust pub struct PtySession { } ``` A live, real-pseudo-terminal-backed child process. The spawn/write/read/lifecycle plumbing [`capture_pty`] and `examples/tests/support`'s marker/settle-driven test harness both need, factored out so each keeps its own distinct polling loop on top of it. See this module's docs for why the two loops aren't shared too. ### impl PtySession ```rust impl PtySession { pub fn spawn(bin: &Path, args: &[&str], env: &[(&str, &str)], cols: u16, rows: u16) -> std::io::Result; pub fn output(&self) -> Arc>>; pub fn reader_done(&self) -> Arc>; pub fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()>; pub fn close_writer(&mut self); pub fn try_wait(&mut self) -> std::io::Result>; pub fn kill(&mut self); pub fn wait(&mut self); pub fn finish(self) -> std::io::Result>; } ``` ### capture_pty ```rust pub fn capture_pty(bin: &Path, args: &[&str], cols: u16, rows: u16, timeout: Duration) -> std::io::Result<(Size, Vec)> ``` Runs `bin` (with `args`) under a real pseudo-terminal sized `cols`x`rows`, and captures its settled frames until it exits or `timeout` elapses. Polls the accumulated PTY output through a [`vt100::Parser`] every `POLL_INTERVAL`, diffing each poll's [`vt100::Screen`] cell-by-cell against the previous one; a poll with no visible change produces no [`CapturedFrame`], the same convention [`FrameRecorder`](crate::FrameRecorder) uses for an empty `draw_layers` call. #### Errors Returns an error if the PTY can't be opened, `bin` can't be spawned, or the reader thread panics. #### Panics Panics if the internal output buffer's lock is poisoned, which only happens if the reader thread itself panics while holding it. ## src/owned_cell.rs ### OwnedCell ```rust #[derive(Debug, Clone, PartialEq, Eq)] pub struct OwnedCell { pub layer: u8, pub pos: Pos, pub tile: Tile, pub grapheme: Option, pub tint: Tint, } ``` An owned copy of a single [`DrawCell`], the shape [`FrameRecorder`](crate::FrameRecorder) buffers instead of the borrowed original. `DrawCell` borrows its grapheme text from the source [`Grid`](retroglyph_core::grid::Grid), so a capture that outlives one `draw_layers` call (every capture source in this crate) must copy it out immediately rather than defer to export time, when the grid it borrowed from has already moved on to the next frame. `grapheme` is the one field that costs an allocation here; everything else (`Tile`, [`Pos`], `layer`, [`Tint`]) is already `Copy`. ### impl From> for OwnedCell ```rust impl From> for OwnedCell { } ``` ## src/frame_recorder.rs ### CapturedFrame ```rust #[derive(Debug, Clone)] pub struct CapturedFrame { pub at: Duration, pub cells: Vec, } ``` One changed frame: every cell [`Output::draw_layers`] reported changed on that call, with the wall-clock time since [`FrameRecorder::new`]. ### FrameRecorderHandle ```rust #[derive(Clone)] pub struct FrameRecorderHandle { } ``` A cheap, cloneable handle onto a [`FrameRecorder`]'s captured frames. Exists for the same reason [`InputRecorder`](crate::InputRecorder)'s [`RecorderHandle`](crate::RecorderHandle) does: a driver like [`retroglyph_core::app::run_on`] takes a `Terminal` (and so the `FrameRecorder` it wraps) by value and never hands it back, so the only way to read what was captured once the loop returns is a handle taken out beforehand. Backed by `Arc>`: cloning shares the same underlying frames, it does not fork them. ### impl FrameRecorderHandle ```rust impl FrameRecorderHandle { pub fn frames(&self) -> Vec; pub fn take_frames(&self) -> Vec; } ``` ### FrameRecorder ```rust pub struct FrameRecorder { } ``` Wraps a backend and records every frame it draws. Taps [`Output::draw_layers`] -- the same [`DrawCell`] diff stream [`Terminal::present`](retroglyph_core::terminal::Terminal::present) and every backend already consume -- and buffers each call's changed cells as an owned [`CapturedFrame`], timestamped against when recording started. Works over a [`Backend`](retroglyph_core::backend::Backend), or any subset of `Output`/[`Input`]/[`Cursor`]: only `Output` is tapped, everything else is delegated unchanged, the same shape as [`InputRecorder`](crate::InputRecorder). `DrawCell` borrows from the source grid, so capture converts to [`OwnedCell`] immediately on each `draw_layers` call rather than deferring to export time, when the borrow would already be gone. #### Known limitation Buffers the whole session in memory (every frame captured, and every cell in it, lives in the shared [`FrameRecorderHandle`] until [`write_cast`](crate::write_cast) exports and [`take_frames`](FrameRecorderHandle::take_frames) drops it) rather than streaming to disk as frames arrive. Fine for docs/demo-length captures -- the ones this crate exists for -- but not a fit for an unbounded, long-running recording. #### Examples ``` use retroglyph_core::backend::{Headless, Output}; use retroglyph_core::color::Style; use retroglyph_core::grid::Pos; use retroglyph_core::tile::Tile; use retroglyph_recorder::FrameRecorder; let mut recorder = FrameRecorder::new(Headless::new(10, 3)); let tile = Tile::new('x', Style::default()); let cell = retroglyph_core::backend::DrawCell::new(Pos::new(0, 0), &tile); recorder.draw_layers(std::iter::once(cell)).unwrap(); assert_eq!(recorder.frames().len(), 1); ``` ### impl FrameRecorder ```rust impl FrameRecorder { pub fn new(inner: B) -> Self; } ``` ### impl FrameRecorder ```rust impl FrameRecorder { pub fn handle(&self) -> FrameRecorderHandle; pub fn frames(&self) -> Vec; pub fn take_frames(&mut self) -> Vec; pub fn inner(&self) -> &B; pub fn inner_mut(&mut self) -> &mut B; } ``` ### impl Output for FrameRecorder ```rust impl Output for FrameRecorder { } ``` ### impl Input for FrameRecorder ```rust impl Input for FrameRecorder { } ``` ### impl Cursor for FrameRecorder ```rust impl Cursor for FrameRecorder { } ``` ## src/panic_hook.rs ### install_panic_recorder ```rust pub fn install_panic_recorder(handle: RecorderHandle, path: impl Trait) ``` Installs a panic hook that saves whatever `handle`'s recorder has captured so far to `path` when the process panics, then chains to whatever hook was already installed. The saved recording is exactly what a bug report needs: the input stream leading up to the crash, ready to hand to [`TestHarness::replay`](retroglyph_core::testing::TestHarness::replay) or [`replay_live`](crate::replay_live) as a regression test, with no hand-transcription. Chaining to the previous hook (rather than replacing it) is deliberate: this must never suppress the panic message itself, or any other diagnostic (e.g. `color-backtrace`, `console_error_panic_hook`) a consumer already installed. The save happens first, so a panic *while* saving (a full disk, a bad `path`) still lets the original panic's own output reach the terminal via the chained hook, rather than replacing it with a save failure. This installs no hotkey and has no opinion on what triggers a save beyond an actual panic: deliberate, so it can never collide with a consumer's own keybindings. Call [`RecorderHandle::save`] directly wherever a consumer wants a non-panic trigger (a dedicated debug key, a signal handler, an explicit "report a bug" menu item). #### Examples ```no_run use retroglyph_core::backend::Headless; use retroglyph_recorder::{InputRecorder, install_panic_recorder}; let recorder = InputRecorder::new(Headless::new(80, 24)); install_panic_recorder(recorder.handle(), "crash.rgrec"); ``` ## src/lib.rs ### cast::write_cast ```rust pub use cast::write_cast; ``` ### frame_recorder::{CapturedFrame, FrameRecorder, FrameRecorderHandle} ```rust pub use frame_recorder::{CapturedFrame, FrameRecorder, FrameRecorderHandle}; ``` ### input_recorder::{InputRecorder, RecorderHandle} ```rust pub use input_recorder::{InputRecorder, RecorderHandle}; ``` ### owned_cell::OwnedCell ```rust pub use owned_cell::OwnedCell; ``` ### panic_hook::install_panic_recorder ```rust pub use panic_hook::install_panic_recorder; ``` ### replay::replay_live ```rust pub use replay::replay_live; ``` ### rgrec::{read, write} ```rust pub use rgrec::{read, write}; ``` ## src/cast.rs ### write_cast ```rust pub fn write_cast(writer: &mut impl Trait, size: Size, frames: &[CapturedFrame]) -> io::Result<()> ``` Exports `frames` as [asciicast v3](https://docs.asciinema.org/manual/asciicast/v3/) newline-delimited JSON, written to `writer`. `frames` comes from [`FrameRecorder`](crate::FrameRecorder) or the `pty` feature's capture source. Writes a header line (`{"version": 3, "term": {"cols", "rows"}}`) sized to `size`, then one `[interval_seconds, "o", ansi_text]` event line per captured frame: `interval_seconds` is the time since the previous event (since [`FrameRecorder::new`](crate::FrameRecorder::new) for the first), and `ansi_text` repositions the cursor to each changed cell (`\x1b[{row};{col}H`, grouping horizontally-adjacent runs to avoid a move per cell) and writes its glyph, switching [SGR](retroglyph_core::color::sgr) state only when the style actually changes -- exactly what a real terminal session emitting that frame would have sent, and exactly what an asciicast player (`asciinema-player`) or converter (`agg`, `svg-term`) expects to replay. Hand-rolled (via `serde_json`, not the `asciicast`/`asciicast-rs` crates) because this crate only ever *writes* v3, never parses any version -- a parser's surface would be pure dead weight here. No `timestamp` header field is emitted: it would be wall-clock-dependent, and this exists for deterministic, `insta`-snapshottable captures. #### Known limitation Buffers and serializes every frame at once rather than streaming events to `writer` as they're captured, so a session's full cost (memory in [`FrameRecorder`](crate::FrameRecorder), then this function's formatting work) is paid up front. Matches [`FrameRecorder`](crate::FrameRecorder)'s own whole-session-in-memory limitation; fine for docs/demo-length captures. #### Errors Returns an error if `writer` fails to write. #### Examples ``` use retroglyph_core::backend::{DrawCell, Headless, Output}; use retroglyph_core::color::Style; use retroglyph_core::grid::Pos; use retroglyph_core::tile::Tile; use retroglyph_recorder::{write_cast, FrameRecorder}; let mut recorder = FrameRecorder::new(Headless::new(3, 1)); let tile = Tile::new('a', Style::default()); recorder .draw_layers(std::iter::once(DrawCell::new(Pos::new(0, 0), &tile))) .unwrap(); let mut out = Vec::new(); write_cast(&mut out, recorder.inner().size(), &recorder.frames()).unwrap(); let text = String::from_utf8(out).unwrap(); assert!(text.contains("\"version\":3")); assert!(text.contains("\"o\"")); ``` ## src/input_recorder.rs ### RecorderHandle ```rust #[derive(Clone)] pub struct RecorderHandle { } ``` A cheap, cloneable handle onto an [`InputRecorder`]'s captured state. Exists so a call stack that doesn't own the recorder itself -- most notably [`install_panic_recorder`](crate::install_panic_recorder), which runs from inside a panic hook -- can still start/pause/stop it or save whatever it has captured so far. Backed by `Arc>`: cloning shares the same underlying recording, it does not fork one. ### impl RecorderHandle ```rust impl RecorderHandle { pub fn start(&self); pub fn pause(&self); pub fn snapshot(&self) -> InputRecording; pub fn stop(&self) -> InputRecording; pub fn save(&self, path: impl Trait) -> io::Result<()>; } ``` ### InputRecorder ```rust pub struct InputRecorder { } ``` Wraps a backend and records every event it sees. Works over a [`Backend`](retroglyph_core::backend::Backend), or any subset of [`Output`]/[`Input`]/[`Cursor`]: taps [`Input::poll_event`] to capture, while delegating everything else unchanged. Recording starts immediately on construction; use [`handle`](Self::handle) (or the convenience [`start`](Self::start)/[`pause`](Self::pause)/[`stop`](Self::stop)/ [`save`](Self::save) methods on this type, which forward to it) to control capture and save it to a `.rgrec` file. #### Examples ``` use retroglyph_core::backend::{Headless, Input}; use retroglyph_core::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use retroglyph_recorder::InputRecorder; let mut recorder = InputRecorder::new(Headless::new(10, 3)); recorder.push_event(Event::Key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE))); let event = recorder.poll_event(std::time::Duration::ZERO); assert!(event.is_some()); let recording = recorder.stop(); assert_eq!(recording.len(), 1); ``` ### impl InputRecorder ```rust impl InputRecorder { pub fn new(inner: B) -> Self; } ``` ### impl InputRecorder ```rust impl InputRecorder { pub fn handle(&self) -> RecorderHandle; pub fn start(&self); pub fn pause(&self); pub fn snapshot(&self) -> InputRecording; pub fn stop(&self) -> InputRecording; pub fn save(&self, path: impl Trait) -> io::Result<()>; pub fn inner(&self) -> &B; pub fn inner_mut(&mut self) -> &mut B; } ``` ### impl Output for InputRecorder ```rust impl Output for InputRecorder { } ``` ### impl Input for InputRecorder ```rust impl Input for InputRecorder { } ``` ### impl Cursor for InputRecorder ```rust impl Cursor for InputRecorder { } ``` ## src/replay.rs ### replay_live ```rust pub fn replay_live(term: Terminal, app: &mut A, recording: &InputRecording) -> Result<(), B::Error> where B: Backend, A: App ``` Drives `recording` into `app` through a live `term`, once, at recorded pace. Sleeps out each event's recorded delay in real wall-clock time, pushes the event into `term`'s backend, and runs one `App::update` frame for it, presenting automatically the same way [`run_on`](retroglyph_core::app::run_on) does. This is "watch it happen": an ordinary forward playback for a demo or a bug report, not an interactive scrubber (no pause, rewind, or speed control). For driving a recording back through a headless `App` in a test instead, with no real backend and no wall-clock sleeping, use [`TestHarness::replay`](retroglyph_core::testing::TestHarness::replay) directly; both ultimately drive the same `App::update` loop and so agree on the resulting state. Returns once every recorded event has been replayed, or as soon as `app` returns [`Flow::Exit`](retroglyph_core::app::Flow::Exit), whichever comes first. #### Errors Returns the backend's error if an automatic `present()` call fails. ### impl App for Counter ```rust impl App for Counter { } ```