Skip to main content

retroglyph_recorder/
frame_recorder.rs

1//! [`FrameRecorder`], its [`FrameRecorderHandle`], and [`CapturedFrame`]: the `TestHarness`/
2//! `Headless`-driven capture source.
3
4use crate::OwnedCell;
5use retroglyph_core::backend::{Compositing, Cursor, CursorStyle, DrawCell, Input, Output};
6use retroglyph_core::event::Event;
7use retroglyph_core::grid::{Pos, Size};
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10
11/// One changed frame: every cell [`Output::draw_layers`] reported changed on that call, with the
12/// wall-clock time since [`FrameRecorder::new`].
13#[derive(Debug, Clone)]
14pub struct CapturedFrame {
15    /// Time since the recorder was created that this frame's `draw_layers` call arrived.
16    pub at: Duration,
17    /// Every cell changed on this call, in the order [`Output::draw_layers`] delivered them.
18    pub cells: Vec<OwnedCell>,
19}
20
21/// State shared between a [`FrameRecorder`] and every [`FrameRecorderHandle`] cloned from it.
22struct State {
23    started: Instant,
24    frames: Vec<CapturedFrame>,
25}
26
27/// A cheap, cloneable handle onto a [`FrameRecorder`]'s captured frames.
28///
29/// Exists for the same reason [`InputRecorder`](crate::InputRecorder)'s
30/// [`RecorderHandle`](crate::RecorderHandle) does: a driver like
31/// [`retroglyph_core::app::run_on`] takes a `Terminal<B>` (and so the `FrameRecorder<B>` it
32/// wraps) by value and never hands it back, so the only way to read what was captured once the
33/// loop returns is a handle taken out beforehand. Backed by `Arc<Mutex<..>>`: cloning shares the
34/// same underlying frames, it does not fork them.
35#[derive(Clone)]
36pub struct FrameRecorderHandle {
37    state: Arc<Mutex<State>>,
38}
39
40impl FrameRecorderHandle {
41    fn new() -> Self {
42        Self {
43            state: Arc::new(Mutex::new(State {
44                started: Instant::now(),
45                frames: Vec::new(),
46            })),
47        }
48    }
49
50    fn record(&self, cells: Vec<OwnedCell>) {
51        let mut state = self.lock();
52        let at = state.started.elapsed();
53        state.frames.push(CapturedFrame { at, cells });
54    }
55
56    /// Locks the shared state, recovering it if a previous holder panicked while holding the
57    /// lock rather than poisoning every future access -- the same rationale
58    /// [`RecorderHandle`](crate::RecorderHandle)'s own lock helper uses.
59    fn lock(&self) -> std::sync::MutexGuard<'_, State> {
60        self.state
61            .lock()
62            .unwrap_or_else(std::sync::PoisonError::into_inner)
63    }
64
65    /// A clone of every frame captured so far, in the order it was drawn.
66    #[must_use]
67    pub fn frames(&self) -> Vec<CapturedFrame> {
68        self.lock().frames.clone()
69    }
70
71    /// Takes every captured frame, leaving the shared buffer empty. Use this (rather than
72    /// [`frames`](Self::frames) plus a manual clear) to hand ownership to
73    /// [`write_cast`](crate::write_cast) without cloning it.
74    #[must_use]
75    pub fn take_frames(&self) -> Vec<CapturedFrame> {
76        std::mem::take(&mut self.lock().frames)
77    }
78}
79
80/// Wraps a backend and records every frame it draws.
81///
82/// Taps [`Output::draw_layers`] -- the same [`DrawCell`] diff stream
83/// [`Terminal::present`](retroglyph_core::terminal::Terminal::present) and every backend already
84/// consume -- and buffers each call's changed cells as an owned [`CapturedFrame`], timestamped
85/// against when recording started. Works over a [`Backend`](retroglyph_core::backend::Backend),
86/// or any subset of `Output`/[`Input`]/[`Cursor`]: only `Output` is tapped, everything else is
87/// delegated unchanged, the same shape as
88/// [`InputRecorder`](crate::InputRecorder).
89///
90/// `DrawCell` borrows from the source grid, so capture converts to [`OwnedCell`] immediately on
91/// each `draw_layers` call rather than deferring to export time, when the borrow would already be
92/// gone.
93///
94/// # Known limitation
95///
96/// Buffers the whole session in memory (every frame captured, and every cell in it, lives in the
97/// shared [`FrameRecorderHandle`] until [`write_cast`](crate::write_cast) exports and
98/// [`take_frames`](FrameRecorderHandle::take_frames) drops it) rather than streaming to disk as
99/// frames arrive. Fine for docs/demo-length captures -- the ones this crate exists for -- but not
100/// a fit for an unbounded, long-running recording.
101///
102/// # Examples
103///
104/// ```
105/// use retroglyph_core::backend::{Headless, Output};
106/// use retroglyph_core::color::Style;
107/// use retroglyph_core::grid::Pos;
108/// use retroglyph_core::tile::Tile;
109/// use retroglyph_recorder::FrameRecorder;
110///
111/// let mut recorder = FrameRecorder::new(Headless::new(10, 3));
112/// let tile = Tile::new('x', Style::default());
113/// let cell = retroglyph_core::backend::DrawCell::new(Pos::new(0, 0), &tile);
114/// recorder.draw_layers(std::iter::once(cell)).unwrap();
115///
116/// assert_eq!(recorder.frames().len(), 1);
117/// ```
118pub struct FrameRecorder<B> {
119    inner: B,
120    handle: FrameRecorderHandle,
121}
122
123impl<B: Output> FrameRecorder<B> {
124    /// Wraps `inner`. Recording starts immediately: the clock [`CapturedFrame::at`] is measured
125    /// against begins ticking here, not on the first `draw_layers` call.
126    pub fn new(inner: B) -> Self {
127        Self {
128            inner,
129            handle: FrameRecorderHandle::new(),
130        }
131    }
132}
133
134impl<B> FrameRecorder<B> {
135    /// A cloneable [`FrameRecorderHandle`] onto this recorder's shared state, for reading
136    /// captured frames after a driver (e.g. [`retroglyph_core::app::run_on`]) has taken this
137    /// recorder's `Terminal` by value and won't hand it back.
138    #[must_use]
139    pub fn handle(&self) -> FrameRecorderHandle {
140        self.handle.clone()
141    }
142
143    /// [`FrameRecorderHandle::frames`] on this recorder's own handle.
144    #[must_use]
145    pub fn frames(&self) -> Vec<CapturedFrame> {
146        self.handle.frames()
147    }
148
149    /// [`FrameRecorderHandle::take_frames`] on this recorder's own handle.
150    pub fn take_frames(&mut self) -> Vec<CapturedFrame> {
151        self.handle.take_frames()
152    }
153
154    /// The wrapped backend, for anything not exposed through the decorator itself.
155    pub const fn inner(&self) -> &B {
156        &self.inner
157    }
158
159    /// The wrapped backend, mutably.
160    pub const fn inner_mut(&mut self) -> &mut B {
161        &mut self.inner
162    }
163}
164
165impl<B: Output> Output for FrameRecorder<B> {
166    type Error = B::Error;
167
168    // `Output::draw`'s default body already forwards to `draw_layers` below, so every cell this
169    // recorder ever sees -- including ones arriving through the single-layer `draw` entry point
170    // -- is captured without needing an override here.
171
172    fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
173    where
174        I: Iterator<Item = DrawCell<'a>>,
175    {
176        let cells: Vec<OwnedCell> = content.map(OwnedCell::from).collect();
177        // An empty `draw_layers` call (nothing changed since the last present) is common --
178        // `Terminal::present` calls it every frame regardless -- and recording it would pad
179        // every capture with useless idle frames. Only cells that actually changed produce a
180        // `CapturedFrame`.
181        if cells.is_empty() {
182            return self.inner.draw_layers(std::iter::empty());
183        }
184        let forwarded = cells.iter().map(|cell| {
185            DrawCell::on_layer(cell.layer, cell.pos, &cell.tile)
186                .with_grapheme(cell.grapheme.as_deref())
187                .with_tint(cell.tint)
188        });
189        self.inner.draw_layers(forwarded)?;
190        self.handle.record(cells);
191        Ok(())
192    }
193
194    fn compositing(&self) -> Compositing {
195        self.inner.compositing()
196    }
197
198    fn flush(&mut self) -> Result<(), Self::Error> {
199        self.inner.flush()
200    }
201
202    fn size(&self) -> Size {
203        self.inner.size()
204    }
205
206    fn clear(&mut self) -> Result<(), Self::Error> {
207        self.inner.clear()
208    }
209
210    fn resize(&mut self, size: Size) {
211        self.inner.resize(size);
212    }
213}
214
215impl<B: Input> Input for FrameRecorder<B> {
216    fn poll_event(&mut self, timeout: Duration) -> Option<Event> {
217        self.inner.poll_event(timeout)
218    }
219
220    fn push_event(&mut self, event: Event) {
221        self.inner.push_event(event);
222    }
223}
224
225impl<B: Cursor> Cursor for FrameRecorder<B> {
226    fn set_cursor_visible(&mut self, visible: bool) {
227        self.inner.set_cursor_visible(visible);
228    }
229
230    fn set_cursor_position(&mut self, position: Pos) {
231        self.inner.set_cursor_position(position);
232    }
233
234    fn set_cursor_style(&mut self, style: CursorStyle) {
235        self.inner.set_cursor_style(style);
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use retroglyph_core::backend::Headless;
243    use retroglyph_core::color::Style;
244    use retroglyph_core::tile::Tile;
245
246    fn cell(x: u16, y: u16, glyph: char) -> (Pos, Tile) {
247        (Pos::new(x, y), Tile::new(glyph, Style::default()))
248    }
249
250    #[test]
251    fn buffers_owned_cells_per_draw_layers_call() {
252        let mut recorder = FrameRecorder::new(Headless::new(10, 3));
253        let (pos, tile) = cell(0, 0, 'a');
254        recorder
255            .draw_layers(std::iter::once(DrawCell::new(pos, &tile)))
256            .unwrap();
257
258        let frames = recorder.frames();
259        assert_eq!(frames.len(), 1);
260        assert_eq!(frames[0].cells.len(), 1);
261        assert_eq!(frames[0].cells[0].tile.glyph(), 'a');
262    }
263
264    #[test]
265    fn empty_draw_layers_calls_produce_no_frame() {
266        let mut recorder = FrameRecorder::new(Headless::new(10, 3));
267        recorder
268            .draw_layers(std::iter::empty::<DrawCell<'_>>())
269            .unwrap();
270
271        assert!(recorder.frames().is_empty());
272    }
273
274    #[test]
275    fn take_frames_empties_the_buffer() {
276        let mut recorder = FrameRecorder::new(Headless::new(10, 3));
277        let (pos, tile) = cell(0, 0, 'a');
278        recorder
279            .draw_layers(std::iter::once(DrawCell::new(pos, &tile)))
280            .unwrap();
281
282        let taken = recorder.take_frames();
283        assert_eq!(taken.len(), 1);
284        assert!(recorder.frames().is_empty());
285    }
286
287    #[test]
288    fn forwards_to_inner_backend() {
289        let mut recorder = FrameRecorder::new(Headless::new(10, 3));
290        let (pos, tile) = cell(1, 1, 'z');
291        recorder
292            .draw_layers(std::iter::once(DrawCell::new(pos, &tile)))
293            .unwrap();
294        recorder.flush().unwrap();
295
296        assert_eq!(recorder.inner().grid()[pos].glyph(), 'z');
297    }
298
299    #[test]
300    fn a_handle_taken_before_a_move_still_reads_frames_captured_after_it() {
301        let mut recorder = FrameRecorder::new(Headless::new(10, 3));
302        let handle = recorder.handle();
303
304        let (pos, tile) = cell(0, 0, 'a');
305        recorder
306            .draw_layers(std::iter::once(DrawCell::new(pos, &tile)))
307            .unwrap();
308        drop(recorder); // the value moves on (e.g. into `Terminal`/`run_on`); the handle survives.
309
310        assert_eq!(handle.frames().len(), 1);
311    }
312}