1use 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
19const IDLE_POLL: Duration = Duration::from_millis(100);
21
22#[derive(Debug, PartialEq, Eq)]
24pub enum AppEvent<M> {
25 Input(InputEvent),
27 Message(M),
29 Resized(CellSize),
31 Repaint,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Control {
40 Wait,
42 Redraw,
44 Animate,
46 Quit,
48}
49
50pub trait App {
52 type Message: Send + 'static;
54
55 fn start(&mut self, waker: Waker<Self::Message>);
58
59 fn update(&mut self, event: AppEvent<Self::Message>) -> Control;
61
62 fn draw(&mut self, out: &mut Vec<u8>, size: CellSize) -> io::Result<Control>;
64}
65
66pub 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 session::disable_input(terminal.out)?;
106 terminal.out.flush()?;
107 pump.finish();
108 drop(terminal);
109 result
110}
111
112struct 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 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
135pub(crate) fn is_terminal_answer(event: &InputEvent) -> bool {
138 matches!(
139 event,
140 InputEvent::GraphicsAnswer(_) | InputEvent::DeviceAttributes
141 )
142}
143
144pub(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}