Skip to main content

katui_rt/
app.rs

1//! The interactive event loop: redraws when something changed and
2//! sleeps otherwise, on the alternate screen with the mouse enabled.
3
4use std::io;
5use std::time::{Duration, Instant};
6
7use katui_present::session;
8use katui_proto::input::InputEvent;
9use katui_proto::probe::Capabilities;
10use katui_scene::CellSize;
11
12use crate::RtError;
13use crate::events::{EventPump, Signal, Waker};
14use crate::pacing::{Pacer, Pacing};
15use crate::pane::PaneView;
16use crate::session::{ResizeEvent, ResizeWatch, Teardown};
17use crate::stop::StopRequest;
18
19/// How often an idle loop checks the terminal size.
20const IDLE_POLL: Duration = Duration::from_millis(100);
21
22/// What an [`App`] is told about.
23#[derive(Debug, PartialEq, Eq)]
24pub enum AppEvent<M> {
25    /// Something the terminal reported.
26    Input(InputEvent),
27    /// A message from one of the application's threads.
28    Message(M),
29    /// The terminal has a new size.
30    Resized(CellSize),
31    /// The terminal may have lost what we showed: a resize has settled,
32    /// or our multiplexer pane is back in view after the loop paused its
33    /// drawing. Everything on screen needs sending again.
34    Repaint,
35}
36
37/// What the loop does next.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Control {
40    /// Sleep until something happens.
41    Wait,
42    /// Draw once.
43    Redraw,
44    /// Keep drawing at the frame rate.
45    Animate,
46    /// End the loop.
47    Quit,
48}
49
50/// An interactive terminal application.
51pub trait App {
52    /// What the application's own threads send to it.
53    type Message: Send + 'static;
54
55    /// Called once before the first draw, with the handle for queuing
56    /// messages from other threads.
57    fn start(&mut self, waker: Waker<Self::Message>);
58
59    /// React to one event.
60    fn update(&mut self, event: AppEvent<Self::Message>) -> Control;
61
62    /// Encode the current state as terminal bytes into `out`.
63    fn draw(&mut self, out: &mut Vec<u8>, size: CellSize) -> io::Result<Control>;
64}
65
66/// Run `app` on `out` until it asks to quit, drawing at most
67/// `frames_per_second` times a second. The terminal is restored on every
68/// exit path that returns.
69pub fn run_app<A: App, W: io::Write>(
70    app: &mut A,
71    mut out: W,
72    caps: &Capabilities,
73    frames_per_second: u32,
74) -> Result<(), RtError> {
75    let mut pump = EventPump::start();
76    let pacing = if pump.reads_terminal() {
77        Pacing::for_terminal(caps)
78    } else {
79        Pacing::Timer
80    };
81    if let Some(identity) = caps.identity.as_ref().filter(|_| caps.multiplexed()) {
82        pump.watch_pane_visibility(&identity.name);
83    }
84    let size = crate::tty::size()?;
85    let stop = StopRequest::watch()?;
86    let terminal = RestoringTerminal { out: &mut out };
87    session::enter(terminal.out)?;
88    session::enable_input(terminal.out)?;
89    terminal.out.flush()?;
90    app.start(pump.waker());
91    let mut event_loop = EventLoop {
92        pacer: Pacer::new(pacing, caps.multiplexed(), caps.id_space),
93        resize: ResizeWatch::new(size),
94        size,
95        frame_time: Duration::from_secs_f64(1.0 / f64::from(frames_per_second.max(1))),
96        next_draw: Instant::now(),
97        pending: Control::Redraw,
98        pane: PaneView::Active,
99        wire: Vec::new(),
100        stop,
101    };
102    let result = event_loop.run(app, terminal.out, &mut pump);
103    // we stop mouse reports before leaving raw mode, or the ones already
104    // on their way would reach the shell as typed junk
105    session::disable_input(terminal.out)?;
106    terminal.out.flush()?;
107    pump.finish();
108    drop(terminal);
109    result
110}
111
112/// Restores the terminal when dropped, a panic in the application included.
113struct RestoringTerminal<'a, W: io::Write> {
114    out: &'a mut W,
115}
116
117impl<W: io::Write> Drop for RestoringTerminal<'_, W> {
118    fn drop(&mut self) {
119        // drop cannot report, so the restore is best effort
120        let _ = restore(self.out);
121    }
122}
123
124fn restore<W: io::Write>(out: &mut W) -> io::Result<()> {
125    Teardown {
126        graphics: true,
127        ..Teardown::default()
128    }
129    .finish(out)?;
130    session::disable_input(out)?;
131    session::leave(out)?;
132    out.flush()
133}
134
135/// Whether `event` answers one of our pacing queries, which belongs to
136/// the loop and never reaches the app.
137pub(crate) fn is_terminal_answer(event: &InputEvent) -> bool {
138    matches!(
139        event,
140        InputEvent::GraphicsAnswer(_) | InputEvent::DeviceAttributes
141    )
142}
143
144/// The most demanding of two requests made since the last frame.
145pub(crate) fn merge(pending: Control, control: Control) -> Control {
146    match (pending, control) {
147        (Control::Quit, _) | (_, Control::Quit) => Control::Quit,
148        (Control::Animate, _) | (_, Control::Animate) => Control::Animate,
149        (Control::Redraw, _) | (_, Control::Redraw) => Control::Redraw,
150        (Control::Wait, Control::Wait) => Control::Wait,
151    }
152}
153
154struct EventLoop {
155    pacer: Pacer,
156    resize: ResizeWatch,
157    size: CellSize,
158    frame_time: Duration,
159    next_draw: Instant,
160    pending: Control,
161    pane: PaneView,
162    wire: Vec<u8>,
163    stop: StopRequest,
164}
165
166impl EventLoop {
167    fn run<A: App, W: io::Write>(
168        &mut self,
169        app: &mut A,
170        out: &mut W,
171        pump: &mut EventPump<A::Message>,
172    ) -> Result<(), RtError> {
173        while self.pending != Control::Quit && !self.stop.requested() {
174            let first = pump.next(self.wait());
175            self.handle_signals(app, first, pump);
176            self.handle_resize(app);
177            let wants_frame = matches!(self.pending, Control::Redraw | Control::Animate)
178                && self.pane == PaneView::Active;
179            if self.pending != Control::Quit && wants_frame && Instant::now() >= self.next_draw {
180                self.draw(app, out, pump)?;
181            }
182        }
183        Ok(())
184    }
185
186    fn wait(&self) -> Duration {
187        if self.pane != PaneView::Active {
188            return IDLE_POLL;
189        }
190        match self.pending {
191            Control::Redraw | Control::Animate => self
192                .next_draw
193                .saturating_duration_since(Instant::now())
194                .min(IDLE_POLL),
195            Control::Wait | Control::Quit => IDLE_POLL,
196        }
197    }
198
199    fn handle_signals<A: App>(
200        &mut self,
201        app: &mut A,
202        first: Option<Signal<A::Message>>,
203        pump: &mut EventPump<A::Message>,
204    ) {
205        let mut signal = first;
206        while let Some(current) = signal {
207            match current {
208                Signal::Input(event) if is_terminal_answer(&event) => {}
209                Signal::Input(event) => self.apply(app, AppEvent::Input(event)),
210                Signal::Message(message) => self.apply(app, AppEvent::Message(message)),
211                Signal::Pane(view) => {
212                    self.pane = view;
213                    if view == PaneView::Active {
214                        self.pacer.restore();
215                        self.apply(app, AppEvent::Repaint);
216                    }
217                }
218            }
219            signal = pump.next(Duration::ZERO);
220        }
221    }
222
223    fn handle_resize<A: App>(&mut self, app: &mut A) {
224        match self.resize.observe(crate::tty::size().ok(), Instant::now()) {
225            Some(ResizeEvent::Resized(size)) => {
226                self.size = size;
227                self.apply(app, AppEvent::Resized(size));
228                self.merge(Control::Redraw);
229            }
230            Some(ResizeEvent::Settled) => {
231                self.apply(app, AppEvent::Repaint);
232                self.merge(Control::Redraw);
233            }
234            None => {}
235        }
236    }
237
238    fn apply<A: App>(&mut self, app: &mut A, event: AppEvent<A::Message>) {
239        let control = app.update(event);
240        self.merge(control);
241    }
242
243    fn merge(&mut self, control: Control) {
244        self.pending = merge(self.pending, control);
245    }
246
247    fn draw<A: App, W: io::Write>(
248        &mut self,
249        app: &mut A,
250        out: &mut W,
251        pump: &mut EventPump<A::Message>,
252    ) -> Result<(), RtError> {
253        self.next_draw = Instant::now() + self.frame_time;
254        self.wire.clear();
255        let after = app.draw(&mut self.wire, self.size)?;
256        self.pending = after;
257        self.pacer.deliver(out, &self.wire, self.size, pump)?;
258        Ok(())
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use std::time::{Duration, Instant};
265
266    use katui_scene::CellSize;
267
268    use super::{Control, EventLoop};
269    use crate::pacing::{Pacer, Pacing};
270    use crate::session::ResizeWatch;
271
272    fn event_loop(pending: Control) -> EventLoop {
273        let size = CellSize {
274            columns: 80,
275            rows: 24,
276        };
277        EventLoop {
278            pacer: Pacer::new(Pacing::Timer, false, katui_proto::kitty::IdSpace::default()),
279            resize: ResizeWatch::new(size),
280            size,
281            frame_time: Duration::from_millis(16),
282            next_draw: Instant::now(),
283            pending,
284            pane: super::PaneView::Active,
285            wire: Vec::new(),
286            stop: crate::stop::StopRequest::default(),
287        }
288    }
289
290    #[test]
291    fn the_most_demanding_request_wins() {
292        let mut state = event_loop(Control::Wait);
293        state.merge(Control::Redraw);
294        assert_eq!(state.pending, Control::Redraw);
295        state.merge(Control::Wait);
296        assert_eq!(state.pending, Control::Redraw, "a redraw is never lost");
297        state.merge(Control::Animate);
298        assert_eq!(state.pending, Control::Animate);
299        state.merge(Control::Quit);
300        state.merge(Control::Redraw);
301        assert_eq!(state.pending, Control::Quit, "a quit is never undone");
302    }
303
304    #[test]
305    fn an_idle_loop_only_polls() {
306        let state = event_loop(Control::Wait);
307        assert_eq!(state.wait(), super::IDLE_POLL);
308    }
309}