retroglyph-core 0.6.0

A 2D pseudographic terminal library -- core types, no backend
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
//! The `App`-driven game loop.
//!
//! Where [`Backend`](crate::backend::Backend) is the output contract, [`App`](crate::app::App) is the per-frame update
//! contract. A game implements [`App`](crate::app::App) once and runs on every backend unchanged.
//!
//! The loop decomposes into three pieces:
//!
//! - the contract ([`App`](crate::app::App), [`Flow`](crate::app::Flow), [`Frame`](crate::app::Frame)), here in the core;
//! - the generic blocking driver ([`run_blocking`](crate::app::run_blocking)/[`run_blocking_with`](crate::app::run_blocking_with), `std` only), which
//!   covers `Crossterm` (in `retroglyph-crossterm`) and [`Headless`](crate::backend::Headless);
//! - the inverted driver in the windowing layer (the software backend's
//!   `run_app`), which cannot be generic because winit owns the loop instead of
//!   handing control back to a shared driver function.
//!
//! ```text
//!                        +-----------------------------+
//!                        |  App, Flow, Frame (core)    |
//!                        +-----------------------------+
//!                                     |
//!                                App::update
//!                                     |
//!               +---------------------+---------------------+
//!               |                                           |
//!   run_blocking / run_blocking_with              windowing layer's run_app
//!   (std only; owns the loop)                     (winit owns the loop instead)
//!               |                                           |
//!      crossterm, headless                           software backend
//! ```
//!
//! Both drivers call [`App::update`](crate::app::App::update) as the per-frame body and present automatically after it
//! returns, skipping the present on [`Flow::Idle`](crate::app::Flow::Idle) or when `update` already presented itself. The
//! low-level [`poll`](crate::terminal::Terminal::poll) / [`present`](crate::terminal::Terminal::present) API remains
//! available for turn-based games and headless tests.

use crate::backend::Backend;
use crate::terminal::Terminal;
use core::time::Duration;

/// Whether the game loop should continue or stop after a frame, and whether that frame renders.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Flow {
    /// Run another frame, and present it.
    Continue,
    /// Run another frame, but nothing changed: skip [`present`](crate::terminal::Terminal::present) and leave the
    /// previous frame on screen.
    ///
    /// For turn-based apps that only need to redraw in response to player input, not on every
    /// tick of the driver's loop. Returning `Idle` while a `retroglyph_ui::Tween`- or
    /// [`FrameClock`](crate::frames::FrameClock)-driven animation is still in flight is an
    /// app bug, not a valid use: an in-progress animation has something new to show every frame,
    /// which is exactly what `Idle` tells the driver isn't true.
    Idle,
    /// Stop the loop. The driver returns and the terminal unwinds normally, so
    /// backend `Drop` logic (for example crossterm's terminal restore) runs.
    Exit,
}

/// Per-frame context handed to [`App::update`](crate::app::App::update).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Frame {
    /// Wall-clock time elapsed since the previous frame, supplied by the driver.
    pub delta: Duration,
    /// Monotonic frame counter, starting at 0.
    pub frame: u64,
}

/// The per-frame update contract for a game.
///
/// Implement this once, generically over the backend, to run everywhere:
///
/// ```
/// use retroglyph_core::app::{App, Flow, Frame};
/// use retroglyph_core::backend::Backend;
/// use retroglyph_core::color::Style;
/// use retroglyph_core::terminal::Terminal;
///
/// struct MyGame;
/// impl<B: Backend> App<B> for MyGame {
///     fn update(&mut self, term: &mut Terminal<B>, _frame: &Frame) -> Flow {
///         term.surface().put((0, 0), '@', Style::default());
///         Flow::Exit
///     }
/// }
/// ```
pub trait App<B: Backend> {
    /// Advance and render one frame.
    ///
    /// Draw into `term`, read input via `term`, and return [`Flow::Exit`](crate::app::Flow::Exit) to stop the loop.
    ///
    /// Draw via [`term.surface()`](crate::terminal::Terminal::surface) or [`term.draw()`](crate::terminal::Terminal::draw) (though
    /// `draw` presents itself, which usually conflicts with the driver's own automatic present
    /// below; prefer `surface()` inside `update`). Every driver ([`run_blocking`](crate::app::run_blocking) and
    /// `retroglyph-window`'s windowed drivers) presents the frame automatically right after this
    /// method returns, unless it returned [`Flow::Idle`](crate::app::Flow::Idle), in which case the driver skips
    /// [`present`](crate::terminal::Terminal::present) entirely. Calling `present` yourself inside `update` remains
    /// fine (the driver detects it already ran via [`present_count`](crate::terminal::Terminal::present_count) and
    /// skips its own call) but is never required. [`run_blocking`](crate::app::run_blocking) and [`run_blocking_with`](crate::app::run_blocking_with) link
    /// back here rather than restating this contract.
    fn update(&mut self, term: &mut Terminal<B>, frame: &Frame) -> Flow;
}

/// Drive an [`App`](crate::app::App) with a blocking, event-driven loop until it returns [`Flow::Exit`](crate::app::Flow::Exit).
///
/// Generic over the backend, so it powers every non-inverted backend
/// (`Crossterm` in `retroglyph-crossterm`, [`Headless`](crate::backend::Headless))
/// with no per-backend loop code.
/// Inverted backends (software/winit) provide their own driver.
///
/// The terminal is owned and dropped when the loop exits, so backend teardown
/// (for example crossterm's terminal restore) runs on the way out.
///
/// See [`App::update`](crate::app::App::update) for the present/idle contract this and every other driver follows.
/// Equivalent to `run_blocking_with(term, app, RunOptions::default())`: on [`Flow::Idle`](crate::app::Flow::Idle), blocks
/// on input rather than calling `update` again immediately, so a turn-based app that's idle most
/// of the time costs approximately nothing. Use [`run_blocking_with`](crate::app::run_blocking_with) with [`RunOptions::animated`](crate::app::RunOptions::animated)
/// for a continuously-rendering app instead.
///
/// # Errors
///
/// Returns the backend's error if the automatic `present()` call fails. The loop stops and the
/// terminal is dropped (running backend teardown) before the error is returned.
#[cfg(feature = "std")]
pub fn run_blocking<B, A>(term: Terminal<B>, app: A) -> Result<(), B::Error>
where
    B: Backend,
    A: App<B>,
{
    run_blocking_with(term, app, RunOptions::default())
}

/// Options controlling [`run_blocking_with`](crate::app::run_blocking_with)'s pacing and idle behavior.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct RunOptions {
    target_fps: Option<u32>,
    event_driven: bool,
    idle_wake: Option<Duration>,
}

impl RunOptions {
    /// Options for a continuously-rendering, [`target_fps`](Self::target_fps)-paced loop.
    ///
    /// [`event_driven`](Self::event_driven) is `false`: [`Flow::Idle`](crate::app::Flow::Idle) only skips `present`, it
    /// never blocks. Use this for apps that drive a `retroglyph_ui::Tween`/
    /// [`FrameClock`](crate::frames::FrameClock) from [`Frame::delta`](crate::app::Frame::delta) and need `update`
    /// called every tick regardless of input.
    ///
    /// `target_fps` becomes [`RunOptions::target_fps`](crate::app::RunOptions::target_fps) verbatim, including `0`: passing `0` here
    /// builds without panicking, but [`run_blocking_with`](crate::app::run_blocking_with) panics once it constructs the
    /// [`FrameClock`](crate::frames::FrameClock) that paces it (see that function's
    /// `# Panics` section).
    #[must_use]
    pub const fn animated(target_fps: u32) -> Self {
        Self {
            target_fps: Some(target_fps),
            event_driven: false,
            idle_wake: None,
        }
    }

    /// Caps the loop at this many [`App::update`](crate::app::App::update) calls per second whenever a frame actually
    /// runs, using a [`FrameClock`](crate::frames::FrameClock) internally to pace them
    /// evenly. `None` (the default) runs uncapped: as fast as `update` allows for back-to-back
    /// [`Flow::Continue`](crate::app::Flow::Continue) frames, or immediately after whatever woke an
    /// [`event_driven`](Self::event_driven) loop from [`Flow::Idle`](crate::app::Flow::Idle).
    #[must_use]
    pub const fn with_target_fps(mut self, target_fps: u32) -> Self {
        self.target_fps = Some(target_fps);
        self
    }

    /// Returns the configured [`target_fps`](Self::with_target_fps) cap, if any.
    #[must_use]
    pub const fn target_fps(&self) -> Option<u32> {
        self.target_fps
    }

    /// On [`Flow::Idle`](crate::app::Flow::Idle), block on input instead of calling `update` again immediately.
    ///
    /// `true` (the default) is right for turn-based, event-driven apps that are idle most of the
    /// time: an idle frame costs approximately nothing, blocked in the backend's input read
    /// rather than spinning `update` as fast as the host can manage. `false` keeps `Flow::Idle`
    /// non-blocking (skip `present`, keep looping at whatever rate
    /// [`target_fps`](Self::target_fps) allows): right for apps that animate from
    /// [`Frame::delta`](crate::app::Frame::delta) and only return `Idle` between animation-driven `Continue` frames, where
    /// blocking would freeze the animation until the next stray input event. See
    /// [`RunOptions::animated`](crate::app::RunOptions::animated) for that shape.
    #[must_use]
    pub const fn event_driven(mut self, event_driven: bool) -> Self {
        self.event_driven = event_driven;
        self
    }

    /// Returns whether [`Flow::Idle`](crate::app::Flow::Idle) blocks on input rather than looping immediately.
    #[must_use]
    pub const fn is_event_driven(&self) -> bool {
        self.event_driven
    }

    /// When [`is_event_driven`](Self::is_event_driven) is `true`, the longest an idle loop blocks
    /// before calling `update` again anyway, even with no input. `None` (the default) blocks
    /// indefinitely: right for apps with nothing to redraw until input arrives. `Some(d)`
    /// additionally wakes the loop every `d`, for apps that need a periodic idle redraw (a
    /// blinking cursor, a clock) without paying full frame-rate cost. Ignored when
    /// [`is_event_driven`](Self::is_event_driven) is `false`.
    #[must_use]
    pub const fn with_idle_wake(mut self, idle_wake: Duration) -> Self {
        self.idle_wake = Some(idle_wake);
        self
    }

    /// Returns the configured [`idle_wake`](Self::with_idle_wake) interval, if any.
    #[must_use]
    pub const fn idle_wake(&self) -> Option<Duration> {
        self.idle_wake
    }
}

impl Default for RunOptions {
    /// Event-driven, uncapped, blocks indefinitely on [`Flow::Idle`](crate::app::Flow::Idle): see [`run_blocking`](crate::app::run_blocking).
    fn default() -> Self {
        Self {
            target_fps: None,
            event_driven: true,
            idle_wake: None,
        }
    }
}

/// Drive an [`App`](crate::app::App) with a blocking loop until it returns [`Flow::Exit`](crate::app::Flow::Exit), paced by `options`.
///
/// The zero-config [`run_blocking`](crate::app::run_blocking) is equivalent to `run_blocking_with(term, app,
/// RunOptions::default())`. Pass [`RunOptions::animated`](crate::app::RunOptions::animated) for a continuously-rendering loop
/// capped at a fixed rate instead, using a [`FrameClock`](crate::frames::FrameClock)
/// internally so `update` is called at even intervals rather than however fast the host can
/// spin.
///
/// With [`RunOptions::is_event_driven`](crate::app::RunOptions::is_event_driven) `true` (the default), [`Flow::Idle`](crate::app::Flow::Idle) blocks the loop on
/// input (via [`Terminal::wait_for_input`](crate::terminal::Terminal::wait_for_input)) instead of calling `update` again immediately:
/// an idle app has nothing new to show, so there is no reason to burn CPU polling it at all,
/// let alone faster than any configured rate. With `event_driven` `false`, an idle loop still
/// waits out the remainder of the current `target_fps` interval (if set) before calling `update`
/// again, rather than looping immediately, but never blocks on input.
///
/// # Errors
///
/// Returns the backend's error if the automatic `present()` call fails. The loop stops and the
/// terminal is dropped (running backend teardown) before the error is returned.
///
/// # Panics
///
/// Panics if `options.target_fps` is `Some(0)`: pacing at a `FrameClock` internally, which
/// requires a non-zero rate (see [`FrameClock::new`](crate::frames::FrameClock::new)).
#[cfg(feature = "std")]
pub fn run_blocking_with<B, A>(
    mut term: Terminal<B>,
    mut app: A,
    options: RunOptions,
) -> Result<(), B::Error>
where
    B: Backend,
    A: App<B>,
{
    let mut clock = options.target_fps().map(crate::frames::FrameClock::new);
    let mut frame_count = 0u64;
    let mut last = std::time::Instant::now();
    loop {
        if let Some(clock) = clock.as_mut() {
            // Block out the rest of this frame's budget before ticking `update` again, so a
            // paced loop doesn't busy-spin between updates the way an uncapped one does.
            let elapsed = last.elapsed();
            if let Some(remaining) = clock.step().checked_sub(elapsed) {
                std::thread::sleep(remaining);
            }
            clock.advance(clock.step().max(elapsed));
            // A fixed-timestep `FrameClock` is meant to be drained in a `while tick()` loop for
            // logic that must run in whole steps; here it only paces wall-clock timing, so a
            // single `tick()` (there is always at least one step ready, since we just slept/
            // advanced past the threshold) resets the accumulator for the next iteration.
            let _ = clock.tick();
        }
        let now = std::time::Instant::now();
        let delta = now.duration_since(last);
        last = now;
        let frame = Frame {
            delta,
            frame: frame_count,
        };
        frame_count = frame_count.wrapping_add(1);
        let present_count_before = term.present_count();
        let flow = app.update(&mut term, &frame);
        if flow == Flow::Exit {
            return Ok(());
        }
        // A no-op if `update` already called `present()` itself (detected via `present_count`
        // rather than relying on `present()` being a safe no-op to call twice: it always presents
        // unconditionally, so a second call here would diff the just-cleared `current` against
        // the just-presented `previous` and erase the frame `update` already sent).
        if flow != Flow::Idle && term.present_count() == present_count_before {
            term.present()?;
        }
        // `Flow` is `#[non_exhaustive]`; treat any variant other than `Exit`/`Idle` the same as
        // `Continue` (keep looping and presenting) rather than exiting on an unknown future value.
        if flow == Flow::Idle && options.is_event_driven() {
            // The heart of the fix for retroglyph#603: block here instead of immediately
            // re-entering the loop, so an idle frame costs approximately nothing rather than
            // spinning `update` as fast as the host allows. `wait_for_input` buffers any event it
            // finds rather than consuming it, so the app's own `update` still observes it on the
            // next iteration; this call only answers "did something happen", it doesn't steal
            // the event. A `target_fps` clock (if set) still gets its top-of-loop sleep on the
            // next iteration; it isn't bypassed by waking early.
            term.wait_for_input(options.idle_wake().unwrap_or(Duration::MAX));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::backend::Headless;
    use crate::event::{Event, KeyCode, KeyEvent, KeyModifiers};

    struct Counter {
        frames: u64,
    }

    impl App<Headless> for Counter {
        fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
            self.frames += 1;
            term.surface()
                .put((0, 0), '#', crate::color::Style::default());
            term.present().expect("present");
            // Quit when a key is pending, or after a safety cap.
            if term.has_input() || frame.frame >= 100 {
                Flow::Exit
            } else {
                Flow::Continue
            }
        }
    }

    #[cfg(feature = "std")]
    #[test]
    fn run_blocking_exits_on_flow_exit() {
        let mut backend = Headless::new(4, 1);
        backend.push_event(Event::Key(KeyEvent::new(
            KeyCode::Char('q'),
            KeyModifiers::NONE,
        )));
        let term = Terminal::new(backend);
        let app = Counter { frames: 0 };
        // Runs until the queued key is observed. Reaching the next line proves
        // the loop terminated on Flow::Exit rather than spinning forever.
        run_blocking(term, app).expect("run_blocking");
    }

    /// An app that never draws and always returns `Idle` except on the last frame: proves
    /// `run_blocking` skips `present()` for `Idle` frames rather than erasing an untouched grid.
    struct AlwaysIdle {
        frames: u64,
    }

    impl App<Headless> for AlwaysIdle {
        fn update(&mut self, _term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
            self.frames += 1;
            if frame.frame >= 5 {
                Flow::Exit
            } else {
                Flow::Idle
            }
        }
    }

    #[cfg(feature = "std")]
    #[test]
    fn run_blocking_skips_present_on_idle() {
        let term = Terminal::new(Headless::new(2, 1));
        let app = AlwaysIdle { frames: 0 };
        // `update` never draws or presents; if the driver called `present()` on an `Idle` frame
        // anyway it would be harmless here (nothing to erase), so this mainly documents intent --
        // the presenting behavior itself is covered by `run_blocking_with_options_presents_frames`.
        run_blocking(term, app).expect("run_blocking");
    }

    /// An app that draws a distinct glyph per frame and never presents itself, so successfully
    /// reaching the backend proves the driver's automatic present ran.
    struct DrawsAndExits {
        frames: u64,
        exit_at: u64,
    }

    impl App<Headless> for DrawsAndExits {
        fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
            self.frames += 1;
            term.surface()
                .put((0, 0), 'x', crate::color::Style::default());
            if frame.frame >= self.exit_at {
                Flow::Exit
            } else {
                Flow::Continue
            }
        }
    }

    #[cfg(feature = "std")]
    #[test]
    fn run_blocking_presents_automatically() {
        let term = Terminal::new(Headless::new(2, 1));
        let app = DrawsAndExits {
            frames: 0,
            exit_at: 0,
        };
        run_blocking(term, app).expect("run_blocking");
        // No assertion on backend content is possible here: `term` is consumed by `run_blocking`.
        // Coverage that the automatic present actually reaches the backend lives in
        // `retroglyph-window`'s own driver tests, which retain the terminal after the loop.
    }

    #[cfg(feature = "std")]
    #[test]
    fn run_blocking_with_default_options_matches_run_blocking() {
        let term = Terminal::new(Headless::new(2, 1));
        let app = DrawsAndExits {
            frames: 0,
            exit_at: 2,
        };
        run_blocking_with(term, app, RunOptions::default()).expect("run_blocking_with");
    }

    #[cfg(feature = "std")]
    #[test]
    fn run_blocking_with_animated_options_runs_to_completion() {
        let term = Terminal::new(Headless::new(2, 1));
        let app = DrawsAndExits {
            frames: 0,
            exit_at: 2,
        };
        // A high cap keeps this test fast; the point is that a paced loop still terminates on
        // `Flow::Exit` and delivers the same number of updates as an uncapped loop would.
        run_blocking_with(term, app, RunOptions::animated(1000)).expect("run_blocking_with");
    }

    #[test]
    fn run_options_animated_sets_fields() {
        let animated = RunOptions::animated(30);
        assert_eq!(animated.target_fps(), Some(30));
        assert!(!animated.is_event_driven());
        assert_eq!(animated.idle_wake(), None);

        let default = RunOptions::default();
        assert_eq!(default.target_fps(), None);
        assert!(default.is_event_driven());
        assert_eq!(default.idle_wake(), None);
    }

    #[test]
    fn run_options_setters_override_defaults() {
        let options = RunOptions::default()
            .with_target_fps(60)
            .event_driven(false)
            .with_idle_wake(Duration::from_millis(250));
        assert_eq!(options.target_fps(), Some(60));
        assert!(!options.is_event_driven());
        assert_eq!(options.idle_wake(), Some(Duration::from_millis(250)));
    }

    /// An app that returns `Idle` for its first frame, then `Exit`. The queued key is only
    /// pushed into the backend *after* the driver would have already woken from the idle wait
    /// (`Headless::poll_event` ignores its timeout and returns immediately either way), so this
    /// mainly documents the contract at the type level: `event_driven: false` is accepted and the
    /// loop still terminates, i.e. the non-blocking `Idle` shape is a supported option for
    /// animated apps. Real blocking behavior (`event_driven: true` actually parking
    /// the thread) can only be observed on a backend that genuinely blocks, like crossterm --
    /// see that crate's own tests.
    struct IdleThenExit {
        frames: u64,
    }

    impl App<Headless> for IdleThenExit {
        fn update(&mut self, _term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
            self.frames += 1;
            if frame.frame == 0 {
                Flow::Idle
            } else {
                Flow::Exit
            }
        }
    }

    #[cfg(feature = "std")]
    #[test]
    fn run_blocking_with_non_event_driven_options_does_not_block_on_idle() {
        let term = Terminal::new(Headless::new(2, 1));
        let app = IdleThenExit { frames: 0 };
        let options = RunOptions {
            target_fps: None,
            event_driven: false,
            idle_wake: None,
        };
        run_blocking_with(term, app, options).expect("run_blocking_with");
    }

    /// Proves the driver's idle wait doesn't swallow the event it woke up for: `update` is only
    /// ever called again *after* `wait_for_input` observed something, so the app's own `has_input`
    /// must still see the same event on the next frame rather than the driver having consumed it.
    struct ObservesQueuedEventAfterIdle {
        frames: u64,
        saw_input_after_idle: bool,
    }

    impl App<Headless> for ObservesQueuedEventAfterIdle {
        fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
            self.frames += 1;
            if frame.frame == 0 {
                return Flow::Idle;
            }
            self.saw_input_after_idle = term.has_input();
            Flow::Exit
        }
    }

    #[test]
    fn run_blocking_event_driven_idle_wait_does_not_consume_the_waking_event() {
        let mut backend = Headless::new(2, 1);
        backend.push_event(Event::Key(KeyEvent::new(
            KeyCode::Char('x'),
            KeyModifiers::NONE,
        )));
        let term = Terminal::new(backend);
        let mut app = ObservesQueuedEventAfterIdle {
            frames: 0,
            saw_input_after_idle: false,
        };
        // Can't recover `app` through `run_blocking` (it takes the app by value and drops it with
        // the terminal), so drive the loop by hand via `step`, mirroring what `run_blocking_with`
        // does around the `Flow::Idle` branch.
        let mut term = term;
        let frame0 = Frame {
            delta: Duration::ZERO,
            frame: 0,
        };
        assert_eq!(app.update(&mut term, &frame0), Flow::Idle);
        // This is the exact call `run_blocking_with` makes on `Flow::Idle` when `event_driven` is
        // `true`: it must buffer the event, not return/consume it, so `update`'s own `has_input`
        // still finds it below.
        assert!(term.wait_for_input(Duration::MAX));
        let frame1 = Frame {
            delta: Duration::ZERO,
            frame: 1,
        };
        assert_eq!(app.update(&mut term, &frame1), Flow::Exit);
        assert!(app.saw_input_after_idle);
    }
}