Skip to main content

retroglyph_core/terminal/
input.rs

1//! Input polling: [`poll`](Terminal::poll) and the queue/lookahead helpers built on it.
2//!
3//! Every event enters through [`poll_backend`](Terminal::poll_backend), the single point where a
4//! backend-sourced [`Event::Resize`] gets applied to the grids; [`poll`](Terminal::poll) and the
5//! other methods here all route through it (directly, or via `queued_events`) so a resize is
6//! never applied twice for the same logical event.
7
8use super::Terminal;
9use crate::backend::Backend;
10use crate::event::Event;
11use alloc::vec::Vec;
12use core::time::Duration;
13
14impl<B: Backend> Terminal<B> {
15    /// Polls for an input event, waiting up to `timeout`.
16    ///
17    /// If an event was previously buffered by [`has_input`](Self::has_input), it is
18    /// returned immediately. Otherwise, the backend is polled for a new event.
19    ///
20    /// [`Event::Resize`] events arriving from the backend are automatically applied: both
21    /// grids are resized before the event is returned to the caller, so the game loop can
22    /// immediately redraw at the new size. An event coming back off this terminal's own queue
23    /// (from [`requeue_events`](Self::requeue_events), or buffered by
24    /// [`wait_for_input`](Self::wait_for_input)) was already applied when it first entered, so
25    /// it is returned as-is rather than resized again. See `poll_backend`.
26    pub fn poll(&mut self, timeout: Duration) -> Option<Event> {
27        if let Some(event) = self.queued_events.pop_front() {
28            // Already passed through `poll_backend` (or was requeued from an event that did),
29            // so any `Resize` it carries was applied then; applying it again here would resize
30            // twice for one logical event. See `poll_backend`.
31            return Some(event);
32        }
33        self.poll_backend(timeout)
34    }
35
36    /// Polls the backend directly (bypassing `queued_events`), applying [`Event::Resize`]
37    /// immediately when found.
38    ///
39    /// This is the single point where a freshly-polled event enters the terminal, so it's also
40    /// the only place `resize` should be called for an event on its way in: callers handling an
41    /// event already taken from `queued_events` (via [`poll`](Self::poll)'s queue-pop branch, or
42    /// via [`requeue_events`](Self::requeue_events)) must not call `resize` again for it, or a
43    /// single resize gets applied twice.
44    ///
45    /// Skips the resize when `w, h` already matches [`size`](Self::size): a windowed driver
46    /// (`retroglyph-window`'s winit driver) now resizes the terminal itself before this event is
47    /// ever polled, purely so the app doesn't have to react to it for correctness. Without this
48    /// guard, an app that *does* poll and see that same `Event::Resize` would pay for a second,
49    /// redundant grid resize/clear of a size that was already applied.
50    fn poll_backend(&mut self, timeout: Duration) -> Option<Event> {
51        let event = self.backend.poll_event(timeout)?;
52        if let Event::Resize(w, h) = event
53            && self.size() != crate::grid::Size::new(w, h)
54        {
55            self.resize(w, h);
56        }
57        Some(event)
58    }
59
60    /// Hands `events` back to this terminal's own queue, in order, so a later
61    /// [`poll`](Self::poll)/[`drain_events`](Self::drain_events)/[`drain_events_into`](Self::drain_events_into)
62    /// call yields them again before the backend is polled for anything new.
63    ///
64    /// This is the supported way for a wrapper that drains events to intercept some of them
65    /// (e.g. `retroglyph-ui`' `PerfOverlayApp` filtering out its own toggle key) to give
66    /// the rest back: it goes through `Terminal`'s own queue, never a backend-specific input
67    /// path, so it works identically on every [`Backend`] regardless of how (or whether) that
68    /// backend implements [`Input::push_event`](crate::backend::Input::push_event).
69    pub fn requeue_events(&mut self, events: impl IntoIterator<Item = Event>) {
70        self.queued_events.extend(events);
71    }
72
73    /// Drains all available events without blocking.
74    ///
75    /// Returns an iterator that yields every pending event: the internal queued event
76    /// followed by all events buffered in the backend. The iterator polls the backend
77    /// with zero timeout repeatedly until `None` is returned.
78    ///
79    /// This is needed for frame-based game loops (e.g. software backend + WASM, where
80    /// frames are gated by `requestAnimationFrame`). Multiple keypresses can arrive
81    /// between frames; draining all of them ensures accumulated input doesn't replay in
82    /// slow motion.
83    ///
84    /// Crossterm and headless backends can also use this, but the single-event `poll`
85    /// pattern works for them because their loops aren't frame-capped.
86    ///
87    /// # One-shot semantics
88    ///
89    /// The returned iterator borrows `self` and drains the queue as it is consumed: the
90    /// first caller to iterate it gets every pending event, and a second, independent
91    /// call to `drain_events` afterward gets nothing. If more than one subsystem needs
92    /// this frame's input (e.g. persistent chrome and an active screen), collect once
93    /// and share the collected events by reference, or use
94    /// [`drain_events_into`](Self::drain_events_into) to drain into a reusable buffer
95    /// instead of allocating a fresh `Vec` every frame.
96    pub fn drain_events(&mut self) -> impl Iterator<Item = Event> + use<'_, B> {
97        struct DrainEvents<'a, B: Backend> {
98            terminal: &'a mut Terminal<B>,
99        }
100
101        impl<B: Backend> Iterator for DrainEvents<'_, B> {
102            type Item = Event;
103
104            fn next(&mut self) -> Option<Event> {
105                self.terminal.poll(Duration::ZERO)
106            }
107        }
108
109        impl<B: Backend> core::iter::FusedIterator for DrainEvents<'_, B> {}
110
111        DrainEvents { terminal: self }
112    }
113
114    /// Drains all available events without blocking, appending them to `buf`.
115    ///
116    /// `buf` is cleared first, then filled with every pending event in the same order
117    /// [`drain_events`](Self::drain_events) would yield them. Unlike `drain_events`, the
118    /// borrow of `self` ends when this call returns, so the terminal is free to draw or
119    /// be polled again afterward, and the caller can hand `buf` to multiple consumers by
120    /// shared reference without materializing a new `Vec` every frame.
121    ///
122    /// This is the same shape as `std::io::Read::read_to_end`: allocate the buffer once
123    /// at startup, reuse it every frame, and let this method manage its contents.
124    pub fn drain_events_into(&mut self, buf: &mut Vec<Event>) {
125        buf.clear();
126        while let Some(event) = self.poll(Duration::ZERO) {
127            buf.push(event);
128        }
129    }
130
131    /// Checks if a pending input event is available without blocking.
132    ///
133    /// If an event is already buffered, returns `true`. Otherwise, polls the backend
134    /// with zero timeout. If the backend returns an event, it is stored in the internal
135    /// buffer and `true` is returned; otherwise, returns `false`.
136    pub fn has_input(&mut self) -> bool {
137        self.wait_for_input(Duration::ZERO)
138    }
139
140    /// Blocks until an input event is available or `timeout` elapses, without consuming it.
141    ///
142    /// Like [`has_input`](Self::has_input), a discovered event is buffered internally so a
143    /// subsequent [`poll`](Self::poll), [`has_input`](Self::has_input), or
144    /// [`drain_events`](Self::drain_events) call still observes it: this method only answers
145    /// "did something happen", it never hands the event to the caller. That's what lets a driver
146    /// loop block between frames without stealing the event the app's own `update` reads; see
147    /// [`run_on_with`](crate::app::run_on_with)'s use of this for [`Flow::Idle`](crate::app::Flow::Idle).
148    ///
149    /// Returns `true` if an event arrived within `timeout`, `false` if `timeout` elapsed with
150    /// nothing pending. Pass [`Duration::MAX`] to block indefinitely.
151    ///
152    /// Backends that never block (e.g. [`Headless`](crate::backend::Headless), which returns
153    /// immediately regardless of `timeout`; see [`Input::poll_event`](crate::backend::Input::poll_event))
154    /// return promptly rather than actually waiting; this method is a real wait only on
155    /// backends that genuinely block (crossterm, window).
156    pub fn wait_for_input(&mut self, timeout: Duration) -> bool {
157        if !self.queued_events.is_empty() {
158            return true;
159        }
160        let Some(event) = self.poll_backend(timeout) else {
161            return false;
162        };
163        self.queued_events.push_back(event);
164        true
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::backend::{Cursor, DrawCell, Headless, Input, Output};
172    use crate::grid::{Rect, Size};
173    use crate::terminal::LayerOp;
174
175    #[test]
176    fn test_terminal_poll_and_read() {
177        let backend = Headless::new(10, 10);
178        let mut terminal = Terminal::new(backend);
179
180        assert_eq!(terminal.poll(Duration::ZERO), None);
181
182        terminal.backend_mut().push_event(Event::Close);
183        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
184
185        terminal.backend_mut().push_event(Event::Resize(80, 25));
186        assert_eq!(terminal.poll(Duration::MAX), Some(Event::Resize(80, 25)));
187    }
188
189    #[test]
190    fn test_terminal_has_input() {
191        let backend = Headless::new(10, 10);
192        let mut terminal = Terminal::new(backend);
193
194        assert!(!terminal.has_input());
195
196        terminal.backend_mut().push_event(Event::Close);
197        assert!(terminal.has_input());
198        assert!(terminal.has_input()); // Repeated calls should still be true
199
200        // Read/Poll should retrieve the buffered event
201        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
202
203        // After taking, it should be false again
204        assert!(!terminal.has_input());
205    }
206
207    #[test]
208    fn test_terminal_drain_events_into() {
209        use alloc::vec;
210
211        let backend = Headless::new(10, 10);
212        let mut terminal = Terminal::new(backend);
213
214        terminal.backend_mut().push_event(Event::Close);
215        terminal.backend_mut().push_event(Event::Resize(80, 25));
216
217        let mut buf = vec![Event::Close]; // pre-existing contents must be cleared
218        terminal.drain_events_into(&mut buf);
219        assert_eq!(buf, [Event::Close, Event::Resize(80, 25)]);
220
221        // The borrow ends at the call, so the terminal is immediately usable again.
222        assert_eq!(terminal.area(), Rect::new(0, 0, 80, 25));
223
224        // Draining again with nothing pending clears the buffer.
225        terminal.drain_events_into(&mut buf);
226        assert!(buf.is_empty());
227    }
228
229    #[test]
230    fn test_terminal_drain_events_into_applies_a_backend_resize() {
231        use alloc::vec::Vec;
232
233        let backend = Headless::new(10, 10);
234        let mut terminal = Terminal::new(backend);
235
236        terminal.backend_mut().push_event(Event::Close);
237        terminal.backend_mut().push_event(Event::Resize(4, 2));
238
239        let mut buf = Vec::new();
240        terminal.drain_events_into(&mut buf);
241        assert_eq!(buf, [Event::Close, Event::Resize(4, 2)]);
242        // The resize was applied on the way in, same as `poll`/`drain_events`, not just handed
243        // back as an inert event for the caller to notice and apply itself.
244        assert_eq!(terminal.size(), Size::new(4, 2));
245    }
246
247    #[test]
248    fn test_terminal_drain_events_into_does_not_reapply_a_requeued_resize() {
249        use alloc::vec::Vec;
250
251        let backend = ResizeCounting::new(10, 10);
252        let mut terminal = Terminal::new(backend);
253
254        terminal.backend_mut().inner.push_event(Event::Resize(4, 2));
255
256        let mut buf = Vec::new();
257        terminal.drain_events_into(&mut buf);
258        assert_eq!(buf, [Event::Resize(4, 2)]);
259        assert_eq!(terminal.backend().resize_calls, 1);
260
261        // A wrapper that drained the event and handed it back gets it again, but the resize is
262        // not applied a second time for the one logical event. See `poll_backend`.
263        terminal.requeue_events(buf.iter().cloned());
264        terminal.drain_events_into(&mut buf);
265        assert_eq!(buf, [Event::Resize(4, 2)]);
266        assert_eq!(terminal.backend().resize_calls, 1);
267    }
268
269    #[test]
270    fn test_terminal_drain_events_is_one_shot_and_fused() {
271        let backend = Headless::new(10, 10);
272        let mut terminal = Terminal::new(backend);
273
274        terminal.backend_mut().push_event(Event::Close);
275        terminal.backend_mut().push_event(Event::Close);
276
277        let mut drained = terminal.drain_events();
278        assert_eq!(drained.next(), Some(Event::Close));
279        assert_eq!(drained.next(), Some(Event::Close));
280        // Fused: repeated calls past exhaustion keep returning `None` rather than panicking.
281        assert_eq!(drained.next(), None);
282        assert_eq!(drained.next(), None);
283        drop(drained);
284
285        // One-shot: a second, independent call after the first already drained the queue sees
286        // only what was pushed since, not a second copy of anything already handed out.
287        terminal.backend_mut().push_event(Event::Close);
288        let second: Vec<_> = terminal.drain_events().collect();
289        assert_eq!(second, [Event::Close]);
290    }
291
292    #[test]
293    fn test_terminal_requeue_events_replays_before_the_backend() {
294        let backend = Headless::new(10, 10);
295        let mut terminal = Terminal::new(backend);
296
297        // Push directly to the backend first, so a requeued event has to come out ahead of it.
298        terminal.backend_mut().push_event(Event::Close);
299        terminal.requeue_events([Event::Resize(80, 25), Event::Resize(1, 1)]);
300
301        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Resize(80, 25)));
302        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Resize(1, 1)));
303        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
304        assert_eq!(terminal.poll(Duration::ZERO), None);
305    }
306
307    #[test]
308    fn test_terminal_wait_for_input_buffers_the_event_instead_of_consuming_it() {
309        let backend = Headless::new(10, 10);
310        let mut terminal = Terminal::new(backend);
311
312        // Nothing queued: returns false rather than blocking (`Headless` ignores the timeout).
313        assert!(!terminal.wait_for_input(Duration::from_millis(1)));
314
315        terminal.backend_mut().push_event(Event::Close);
316        assert!(terminal.wait_for_input(Duration::MAX));
317        // Repeated calls stay true: the event was buffered, not handed out and lost.
318        assert!(terminal.wait_for_input(Duration::MAX));
319        assert!(terminal.has_input());
320
321        // A caller reading through the normal input API (as an app's own `update` would) still
322        // observes the exact event `wait_for_input` woke up for.
323        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
324        assert!(!terminal.has_input());
325    }
326
327    #[test]
328    fn test_terminal_wait_for_input_applies_pending_resize() {
329        let backend = Headless::new(10, 10);
330        let mut terminal = Terminal::new(backend);
331
332        terminal.backend_mut().push_event(Event::Resize(4, 2));
333        assert!(terminal.wait_for_input(Duration::MAX));
334        // The grid resizes immediately, same as `poll`, even though the event is still buffered
335        // rather than consumed.
336        assert_eq!(terminal.size(), Size::new(4, 2));
337        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Resize(4, 2)));
338    }
339
340    /// Wraps [`Headless`] and counts [`resize`](Output::resize) calls, so a test can prove
341    /// `Terminal` applies a backend resize at most once per logical `Event::Resize`, even when
342    /// the event is buffered by `wait_for_input` and then consumed by `poll` (retroglyph#959).
343    struct ResizeCounting {
344        inner: Headless,
345        resize_calls: usize,
346    }
347
348    impl ResizeCounting {
349        fn new(width: u16, height: u16) -> Self {
350            Self {
351                inner: Headless::new(width, height),
352                resize_calls: 0,
353            }
354        }
355    }
356
357    impl Output for ResizeCounting {
358        type Error = core::convert::Infallible;
359
360        fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
361        where
362            I: Iterator<Item = DrawCell<'a>>,
363        {
364            self.inner.draw_layers(content)
365        }
366
367        fn resize(&mut self, size: Size) {
368            self.resize_calls += 1;
369            self.inner.resize(size);
370        }
371
372        fn flush(&mut self) -> Result<(), Self::Error> {
373            self.inner.flush()
374        }
375
376        fn size(&self) -> Size {
377            self.inner.size()
378        }
379
380        fn clear(&mut self) -> Result<(), Self::Error> {
381            self.inner.clear()
382        }
383    }
384
385    impl Input for ResizeCounting {
386        fn poll_event(&mut self, timeout: Duration) -> Option<Event> {
387            self.inner.poll_event(timeout)
388        }
389
390        fn push_event(&mut self, event: Event) {
391            self.inner.push_event(event);
392        }
393    }
394
395    impl Cursor for ResizeCounting {}
396
397    #[test]
398    fn test_terminal_poll_does_not_reapply_resize_buffered_by_wait_for_input() {
399        let backend = ResizeCounting::new(10, 10);
400        let mut terminal = Terminal::new(backend);
401
402        terminal.backend_mut().push_event(Event::Resize(8, 2));
403        assert!(terminal.wait_for_input(Duration::ZERO));
404        assert_eq!(terminal.backend().resize_calls, 1);
405
406        // The event was only buffered, not consumed, by `wait_for_input`; `poll` must return the
407        // same event without resizing again.
408        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Resize(8, 2)));
409        assert_eq!(terminal.backend().resize_calls, 1);
410        assert_eq!(terminal.size(), Size::new(8, 2));
411        assert_eq!(terminal.backend().size(), Size::new(8, 2));
412
413        // Exercise the rest of `ResizeCounting`'s `Output` passthrough too, so the mock's own
414        // plumbing (not just `resize_calls`) is covered rather than asserted by inspection.
415        terminal.present().unwrap();
416        terminal.backend_mut().clear().unwrap();
417    }
418
419    #[test]
420    fn test_terminal_poll_does_not_reapply_resize_already_matching_current_size() {
421        // Regression test for retroglyph#1291: a driver (`retroglyph-window`'s winit driver) can
422        // now call `Terminal::resize` itself before the matching `Event::Resize` is ever polled,
423        // purely so `Terminal::size` is never stale from the app's point of view. `poll_backend`
424        // must not pay for a second resize of a size that was already applied when the app later
425        // polls that same event.
426        let backend = ResizeCounting::new(10, 10);
427        let mut terminal = Terminal::new(backend);
428
429        // Simulate the driver: resize the terminal directly, then push the event a real driver
430        // would still deliver for an app that wants to react to it.
431        terminal.resize(8, 2);
432        assert_eq!(terminal.backend().resize_calls, 1);
433        terminal.backend_mut().push_event(Event::Resize(8, 2));
434
435        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Resize(8, 2)));
436        assert_eq!(
437            terminal.backend().resize_calls,
438            1,
439            "poll must not resize again for an event whose size already matches"
440        );
441        assert_eq!(terminal.size(), Size::new(8, 2));
442    }
443
444    #[test]
445    fn test_terminal_poll_does_not_reapply_resize_from_requeue_events() {
446        let backend = ResizeCounting::new(10, 10);
447        let mut terminal = Terminal::new(backend);
448
449        terminal.backend_mut().inner.push_event(Event::Resize(9, 3));
450        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Resize(9, 3)));
451        assert_eq!(terminal.backend().resize_calls, 1);
452
453        // A wrapper (e.g. `PerfOverlayApp`) hands the event straight back via `requeue_events`;
454        // the next `poll` must not resize a second time for it.
455        terminal.requeue_events([Event::Resize(9, 3)]);
456        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Resize(9, 3)));
457        assert_eq!(terminal.backend().resize_calls, 1);
458    }
459
460    #[test]
461    fn test_terminal_retain_layer_survives_wait_for_input_then_poll_of_a_resize() {
462        let backend = Headless::new(10, 10);
463        let mut terminal = Terminal::new(backend);
464
465        terminal.backend_mut().push_event(Event::Resize(8, 2));
466        assert!(terminal.wait_for_input(Duration::ZERO));
467
468        // A caller that decides retention for this frame in between `wait_for_input` waking it
469        // up and the matching `poll` that actually reads the event (a plausible ordering: e.g.
470        // deciding retention from state gathered before reading input) must not have that
471        // decision silently undone by `poll` re-applying the already-handled resize.
472        terminal.retain_layer(0u8);
473        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Resize(8, 2)));
474        assert_eq!(terminal.pending_layer_ops, [LayerOp::Retain]);
475    }
476
477    #[test]
478    fn test_terminal_requeue_events_interleaves_with_wait_for_input_lookahead() {
479        let backend = Headless::new(10, 10);
480        let mut terminal = Terminal::new(backend);
481
482        // `wait_for_input` buffers one event ahead of anything requeued afterward.
483        terminal.backend_mut().push_event(Event::Close);
484        assert!(terminal.wait_for_input(Duration::MAX));
485
486        // Requeueing now must not jump the queue ahead of the event `wait_for_input` already
487        // buffered: both go through the same internal queue, in call order.
488        terminal.requeue_events([Event::Resize(4, 2)]);
489
490        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
491        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Resize(4, 2)));
492        assert_eq!(terminal.poll(Duration::ZERO), None);
493    }
494}