1use crate::cmd::{Cmd, CmdResult};
2use crate::event::Event;
3use crate::model::Model;
4use crate::renderer::Renderer;
5use crate::terminal::{Terminal, TerminalOptions};
6
7use crossterm::event::EventStream;
8use futures_util::StreamExt;
9use std::io;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::Arc;
12use tokio::sync::{mpsc, Notify};
13
14fn signal_quit(quit: &Arc<AtomicBool>, quit_notify: &Arc<Notify>) {
15 quit.store(true, Ordering::Relaxed);
16 quit_notify.notify_one();
17}
18
19pub struct ProgramBuilder<M: Model> {
20 model: M,
21 alt_screen: bool,
22 mouse_support: bool,
23 fps: u32,
24}
25
26impl<M: Model> ProgramBuilder<M>
27where
28 M::Msg: From<Event>,
29{
30 pub fn new(model: M) -> Self {
31 Self {
32 model,
33 alt_screen: true,
34 mouse_support: false,
35 fps: 60,
36 }
37 }
38
39 pub fn with_alt_screen(mut self) -> Self {
40 self.alt_screen = true;
41 self
42 }
43
44 pub fn without_alt_screen(mut self) -> Self {
45 self.alt_screen = false;
46 self
47 }
48
49 pub fn with_mouse_support(mut self) -> Self {
50 self.mouse_support = true;
51 self
52 }
53
54 pub fn with_fps(mut self, fps: u32) -> Self {
55 self.fps = fps.clamp(1, 120);
56 self
57 }
58
59 pub async fn run(self) -> io::Result<()> {
60 Program::run_inner(
61 self.model,
62 TerminalOptions {
63 alt_screen: self.alt_screen,
64 mouse_support: self.mouse_support,
65 raw_mode: true,
66 },
67 self.fps,
68 )
69 .await
70 }
71}
72
73pub struct Program;
74
75impl Program {
76 pub async fn run<M: Model>(model: M) -> io::Result<()>
77 where
78 M::Msg: From<Event>,
79 {
80 Self::run_inner(model, TerminalOptions::default(), 60).await
81 }
82
83 async fn run_inner<M: Model>(mut model: M, options: TerminalOptions, fps: u32) -> io::Result<()>
84 where
85 M::Msg: From<Event>,
86 {
87 let mut terminal = Terminal::new(&options)?;
88 terminal.enter()?;
89
90 let (msg_tx, mut msg_rx) = mpsc::unbounded_channel::<M::Msg>();
91 let quit_flag = Arc::new(AtomicBool::new(false));
92 let quit_notify = Arc::new(Notify::new());
93
94 if let Some(cmd) = model.init() {
95 Self::dispatch_cmd(cmd, msg_tx.clone(), quit_flag.clone(), quit_notify.clone());
96 }
97
98 let mut event_stream = EventStream::new();
99 let mut renderer = Renderer::new(fps);
100 let mut dirty = false;
101
102 let view = model.view();
103 renderer.render(&mut terminal, &view)?;
104 match model.cursor() {
107 Some((col, row)) => terminal.show_cursor_at(col, row)?,
108 None => terminal.hide_cursor()?,
109 }
110
111 loop {
112 if quit_flag.load(Ordering::Relaxed) {
113 break;
114 }
115
116 let mut immediate = false;
120 tokio::select! {
121 event = event_stream.next() => {
122 match event {
123 Some(Ok(ct_event)) => {
124 if matches!(ct_event, crossterm::event::Event::Resize(_, _)) {
126 renderer.invalidate();
127 }
128 if let Some(ev) = Event::from_crossterm(ct_event) {
129 immediate = true;
130 let msg: M::Msg = ev.into();
131 if let Some(cmd) = model.update(msg) {
132 Self::dispatch_cmd(cmd, msg_tx.clone(), quit_flag.clone(), quit_notify.clone());
133 }
134 dirty = true;
135 }
136 }
137 Some(Err(_)) => break,
138 None => break,
139 }
140 }
141 Some(msg) = msg_rx.recv() => {
142 if let Some(cmd) = model.update(msg) {
143 Self::dispatch_cmd(cmd, msg_tx.clone(), quit_flag.clone(), quit_notify.clone());
144 }
145 dirty = true;
146 }
147 _ = quit_notify.notified() => {
148 }
149 _ = tokio::time::sleep(renderer.time_until_next_frame()), if dirty => {
150 }
151 }
152
153 if quit_flag.load(Ordering::Relaxed) {
154 break;
155 }
156
157 if immediate {
158 let view = model.view();
159 renderer.render(&mut terminal, &view)?;
160 dirty = false;
161 } else if dirty && renderer.is_frame_due() {
162 let view = model.view();
163 if renderer.is_changed(&view) {
164 renderer.render(&mut terminal, &view)?;
165 }
166 dirty = false;
167 }
168 match model.cursor() {
171 Some((col, row)) => terminal.show_cursor_at(col, row)?,
172 None => terminal.hide_cursor()?,
173 }
174 }
175
176 terminal.exit()?;
177 std::mem::forget(terminal);
178 Ok(())
179 }
180
181 fn dispatch_cmd<M: Send + 'static>(
182 cmd: Cmd<M>,
183 tx: mpsc::UnboundedSender<M>,
184 quit: Arc<AtomicBool>,
185 quit_notify: Arc<Notify>,
186 ) {
187 tokio::spawn(async move {
188 let result = cmd.await;
189 match result {
190 CmdResult::Quit => {
191 signal_quit(&quit, &quit_notify);
192 }
193 CmdResult::Msg(m) => {
194 let _ = tx.send(m);
195 }
196 CmdResult::Batch(cmds) => {
197 for c in cmds {
198 let tx2 = tx.clone();
199 let quit2 = quit.clone();
200 let quit_notify2 = quit_notify.clone();
201 tokio::spawn(async move {
202 let r = c.await;
203 match r {
204 CmdResult::Quit => {
205 signal_quit(&quit2, &quit_notify2);
206 }
207 CmdResult::Msg(m) => {
208 let _ = tx2.send(m);
209 }
210 CmdResult::Batch(inner_cmds) => {
211 for ic in inner_cmds {
212 let tx3 = tx2.clone();
213 let quit3 = quit2.clone();
214 let quit_notify3 = quit_notify2.clone();
215 tokio::spawn(async move {
216 let r = ic.await;
217 match r {
218 CmdResult::Quit => {
219 signal_quit(&quit3, &quit_notify3);
220 }
221 CmdResult::Msg(m) => {
222 let _ = tx3.send(m);
223 }
224 _ => {}
225 }
226 });
227 }
228 }
229 CmdResult::None => {}
230 }
231 });
232 }
233 }
234 CmdResult::None => {}
235 }
236 });
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use std::time::Duration;
244
245 #[tokio::test]
246 async fn signal_quit_sets_flag_and_wakes_late_waiter() {
247 let quit = Arc::new(AtomicBool::new(false));
248 let notify = Arc::new(Notify::new());
249
250 signal_quit(&quit, ¬ify);
251
252 assert!(quit.load(Ordering::Relaxed));
253 tokio::time::timeout(Duration::from_millis(50), notify.notified())
254 .await
255 .expect("quit notification should be retained until the runner awaits it");
256 }
257}