Skip to main content

retroglyph_recorder/
replay.rs

1//! [`replay_live`]: forward replay of an [`InputRecording`] into a live backend.
2
3use retroglyph_core::app::{App, Flow, Frame};
4use retroglyph_core::backend::Backend;
5use retroglyph_core::terminal::Terminal;
6use retroglyph_core::testing::InputRecording;
7
8/// Drives `recording` into `app` through a live `term`, once, at recorded pace.
9///
10/// Sleeps out each event's recorded delay in real wall-clock time, pushes the event into
11/// `term`'s backend, and runs one `App::update` frame for it, presenting automatically the same
12/// way [`run_on`](retroglyph_core::app::run_on) does.
13///
14/// This is "watch it happen": an ordinary forward playback for a demo or a bug report, not an
15/// interactive scrubber (no pause, rewind, or speed control). For driving a recording back
16/// through a headless `App` in a test instead, with no real backend and no wall-clock sleeping,
17/// use [`TestHarness::replay`](retroglyph_core::testing::TestHarness::replay) directly; both
18/// ultimately drive the same `App::update` loop and so agree on the resulting state.
19///
20/// Returns once every recorded event has been replayed, or as soon as `app` returns
21/// [`Flow::Exit`](retroglyph_core::app::Flow::Exit), whichever comes first.
22///
23/// # Errors
24///
25/// Returns the backend's error if an automatic `present()` call fails.
26pub fn replay_live<B, A>(
27    mut term: Terminal<B>,
28    app: &mut A,
29    recording: &InputRecording,
30) -> Result<(), B::Error>
31where
32    B: Backend,
33    A: App<B>,
34{
35    app.init(&mut term);
36    let mut frame_count = 0u64;
37    for (delay, event) in recording.events() {
38        if !delay.is_zero() {
39            std::thread::sleep(*delay);
40        }
41        term.backend_mut().push_event(event.clone());
42
43        let frame = Frame::new(*delay, frame_count);
44        frame_count = frame_count.wrapping_add(1);
45
46        let present_count_before = term.present_count();
47        let flow = app.update(&mut term, &frame);
48        if flow != Flow::Idle && term.present_count() == present_count_before {
49            term.present()?;
50        }
51        if flow == Flow::Exit {
52            return Ok(());
53        }
54    }
55    Ok(())
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use retroglyph_core::backend::Headless;
62    use retroglyph_core::color::Style;
63    use retroglyph_core::event::{Event, KeyCode, KeyEvent, KeyModifiers};
64    use retroglyph_core::testing::TestHarness;
65    use std::time::Duration;
66
67    /// Counts key presses it has seen, and draws that count as a single digit -- enough state to
68    /// compare `replay_live` against `TestHarness::replay` for the same recording.
69    struct Counter {
70        presses: u32,
71    }
72
73    impl<B: Backend> App<B> for Counter {
74        fn update(&mut self, term: &mut Terminal<B>, _frame: &Frame) -> Flow {
75            for event in term.drain_events() {
76                if matches!(event, Event::Key(_)) {
77                    self.presses += 1;
78                }
79            }
80            term.surface().put(
81                (0, 0),
82                char::from_digit(self.presses, 10).unwrap_or('?'),
83                Style::default(),
84            );
85            Flow::Continue
86        }
87    }
88
89    fn recording() -> InputRecording {
90        let mut recording = InputRecording::new(4, 1);
91        recording.push(
92            Duration::ZERO,
93            Event::Key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)),
94        );
95        recording.push(
96            Duration::from_millis(1),
97            Event::Key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE)),
98        );
99        recording
100    }
101
102    #[test]
103    fn replay_live_reaches_the_same_state_as_test_harness_replay() {
104        let recording = recording();
105
106        let term = Terminal::new(Headless::new(recording.width(), recording.height()));
107        let mut live_app = Counter { presses: 0 };
108        replay_live(term, &mut live_app, &recording).expect("replay_live");
109
110        let mut harness = TestHarness::from_recording(&recording);
111        let mut harness_app = Counter { presses: 0 };
112        harness.replay(&recording, &mut harness_app);
113
114        assert_eq!(live_app.presses, harness_app.presses);
115        assert_eq!(live_app.presses, 2);
116    }
117
118    #[test]
119    fn replay_live_stops_early_on_flow_exit() {
120        struct ExitsAfterFirst {
121            updates: u32,
122        }
123        impl<B: Backend> App<B> for ExitsAfterFirst {
124            fn update(&mut self, _term: &mut Terminal<B>, _frame: &Frame) -> Flow {
125                self.updates += 1;
126                Flow::Exit
127            }
128        }
129
130        let recording = recording();
131        let term = Terminal::new(Headless::new(recording.width(), recording.height()));
132        let mut app = ExitsAfterFirst { updates: 0 };
133        replay_live(term, &mut app, &recording).expect("replay_live");
134
135        assert_eq!(app.updates, 1, "must stop after the first Flow::Exit");
136    }
137
138    #[test]
139    fn replay_live_calls_init_once_before_the_first_update() {
140        struct RecordsInit {
141            init_calls: u32,
142            updates: u32,
143        }
144        impl<B: Backend> App<B> for RecordsInit {
145            fn init(&mut self, _term: &mut Terminal<B>) {
146                self.init_calls += 1;
147            }
148            fn update(&mut self, _term: &mut Terminal<B>, _frame: &Frame) -> Flow {
149                self.updates += 1;
150                Flow::Continue
151            }
152        }
153
154        let recording = recording();
155        let term = Terminal::new(Headless::new(recording.width(), recording.height()));
156        let mut app = RecordsInit {
157            init_calls: 0,
158            updates: 0,
159        };
160        replay_live(term, &mut app, &recording).expect("replay_live");
161
162        assert_eq!(app.init_calls, 1, "init must run exactly once");
163        assert_eq!(app.updates, 2);
164    }
165}