1use crate::framework::control_queue::ControlQueue;
2#[cfg(feature = "async")]
3use crate::poll::PollTokio;
4use crate::poll::{PollQuit, PollRendered, PollTasks, PollTimers};
5use crate::run_config::{RunConfig, TermInit};
6use crate::{Control, SalsaAppContext, SalsaContext};
7use poll_queue::PollQueue;
8use rat_event::util::set_have_keyboard_enhancement;
9use ratatui_core::buffer::Buffer;
10use ratatui_core::layout::Rect;
11use ratatui_core::terminal::Frame;
12use ratatui_crossterm::crossterm::ExecutableCommand;
13use ratatui_crossterm::crossterm::cursor::{DisableBlinking, EnableBlinking, SetCursorStyle};
14use ratatui_crossterm::crossterm::event::{
15 DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
16};
17use ratatui_crossterm::crossterm::terminal::{
18 EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode,
19};
20use std::any::TypeId;
21use std::cmp::min;
22use std::io::stdout;
23use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
24use std::time::{Duration, SystemTime};
25use std::{io, thread};
26
27pub(crate) mod control_queue;
28mod poll_queue;
29
30const SLEEP: u64 = 250_000; const BACKOFF: u64 = 10_000; const FAST_SLEEP: u64 = 100; fn _run_tui<Global, State, Event, Error>(
35 init: fn(
36 state: &mut State, ctx: &mut Global,
38 ) -> Result<(), Error>,
39 render: fn(
40 area: Rect, buf: &mut Buffer,
42 state: &mut State,
43 ctx: &mut Global,
44 ) -> Result<(), Error>,
45 event: fn(
46 event: &Event, state: &mut State,
48 ctx: &mut Global,
49 ) -> Result<Control<Event>, Error>,
50 error: fn(
51 error: Error, state: &mut State,
53 ctx: &mut Global,
54 ) -> Result<Control<Event>, Error>,
55 global: &mut Global,
56 state: &mut State,
57 cfg: RunConfig<Event, Error>,
58) -> Result<(), Error>
59where
60 Global: SalsaContext<Event, Error>,
61 Event: 'static,
62 Error: 'static + From<io::Error>,
63{
64 let term = cfg.term;
65 let mut poll = cfg.poll;
66
67 let timers = poll.iter().find_map(|v| {
68 v.as_any()
69 .downcast_ref::<PollTimers>()
70 .map(|t| t.get_timers())
71 });
72 let tasks = poll.iter().find_map(|v| {
73 v.as_any()
74 .downcast_ref::<PollTasks<Event, Error>>()
75 .map(|t| t.get_tasks())
76 });
77 let rendered_event = poll
78 .iter()
79 .position(|v| v.as_ref().type_id() == TypeId::of::<PollRendered>());
80 let quit = poll
81 .iter()
82 .position(|v| v.as_ref().type_id() == TypeId::of::<PollQuit>());
83 #[cfg(feature = "async")]
84 let tokio = poll.iter().find_map(|v| {
85 v.as_any()
86 .downcast_ref::<PollTokio<Event, Error>>()
87 .map(|t| t.get_tasks())
88 });
89
90 global.set_salsa_ctx(SalsaAppContext {
91 focus: Default::default(),
92 count: Default::default(),
93 cursor: Default::default(),
94 term: Some(term.clone()),
95 window_title: Default::default(),
96 clear_terminal: Default::default(),
97 insert_before: Default::default(),
98 last_render: Default::default(),
99 last_event: Default::default(),
100 timers,
101 tasks,
102 #[cfg(feature = "async")]
103 tokio,
104 queue: ControlQueue::default(),
105 });
106
107 let poll_queue = PollQueue::default();
108 let mut poll_sleep = Duration::from_micros(SLEEP);
109 let mut was_changed = false;
110
111 init(state, global)?;
113
114 {
116 let ib = global.salsa_ctx().insert_before.take();
117 if ib.height > 0 {
118 term.borrow_mut().insert_before(ib.height, ib.draw_fn)?;
119 }
120 if let Some(title) = global.salsa_ctx().window_title.replace(None) {
121 stdout().execute(SetTitle(title))?;
122 }
123 let mut r = Ok(());
124 term.borrow_mut().draw(&mut |frame: &mut Frame| -> () {
125 let frame_area = frame.area();
126 let ttt = SystemTime::now();
127
128 r = render(frame_area, frame.buffer_mut(), state, global);
129
130 global
131 .salsa_ctx()
132 .last_render
133 .set(ttt.elapsed().unwrap_or_default());
134 if let Some((cursor_x, cursor_y)) = global.salsa_ctx().cursor.get() {
135 frame.set_cursor_position((cursor_x, cursor_y));
136 }
137 global.salsa_ctx().count.set(frame.count());
138 global.salsa_ctx().cursor.set(None);
139 })?;
140 r?;
141 if let Some(idx) = rendered_event {
142 global.salsa_ctx().queue.push(poll[idx].read());
143 }
144 }
145
146 'ui: loop {
147 if let Some(tasks) = &global.salsa_ctx().tasks {
149 if !tasks.check_liveness() {
150 dbg!("worker panicked");
151 break 'ui;
152 }
153 }
154
155 if global.salsa_ctx().queue.is_empty() {
157 if poll_queue.is_empty() {
160 for (n, p) in poll.iter_mut().enumerate() {
161 match p.poll() {
162 Ok(true) => {
163 poll_queue.push(n);
164 }
165 Ok(false) => {}
166 Err(e) => {
167 global.salsa_ctx().queue.push(Err(e));
168 }
169 }
170 }
171 }
172
173 if poll_queue.is_empty() {
175 let mut t = poll_sleep;
176 for p in poll.iter_mut() {
177 if let Some(timer_sleep) = p.sleep_time() {
178 t = min(timer_sleep, t);
179 }
180 }
181 thread::sleep(t);
182 if poll_sleep < Duration::from_micros(SLEEP) {
183 poll_sleep += Duration::from_micros(BACKOFF);
185 }
186 } else {
187 poll_sleep = Duration::from_micros(FAST_SLEEP);
189 }
190 }
191
192 if global.salsa_ctx().queue.is_empty() {
195 if let Some(h) = poll_queue.take() {
196 global.salsa_ctx().queue.push(poll[h].read());
197 }
198 }
199
200 if let Some(ctrl) = global.salsa_ctx().queue.take() {
202 if matches!(ctrl, Ok(Control::Changed)) {
205 if was_changed {
206 continue;
207 }
208 was_changed = true;
209 } else {
210 was_changed = false;
211 }
212
213 match ctrl {
214 Err(e) => {
215 let r = error(e, state, global);
216 global.salsa_ctx().queue.push(r);
217 }
218 Ok(Control::Continue) => {}
219 Ok(Control::Unchanged) => {}
220 Ok(Control::Changed) => {
221 if global.salsa_ctx().clear_terminal.get() {
222 global.salsa_ctx().clear_terminal.set(false);
223 if let Err(e) = term.borrow_mut().clear() {
224 global.salsa_ctx().queue.push(Err(e.into()));
225 }
226 }
227 let ib = global.salsa_ctx().insert_before.take();
228 if ib.height > 0 {
229 term.borrow_mut().insert_before(ib.height, ib.draw_fn)?;
230 }
231 if let Some(title) = global.salsa_ctx().window_title.replace(None) {
232 stdout().execute(SetTitle(title))?;
233 }
234 let mut r = Ok(());
235 term.borrow_mut().draw(&mut |frame: &mut Frame| -> () {
236 let frame_area = frame.area();
237 let ttt = SystemTime::now();
238
239 r = render(frame_area, frame.buffer_mut(), state, global);
240
241 global
242 .salsa_ctx()
243 .last_render
244 .set(ttt.elapsed().unwrap_or_default());
245 if let Some((cursor_x, cursor_y)) = global.salsa_ctx().cursor.get() {
246 frame.set_cursor_position((cursor_x, cursor_y));
247 }
248 global.salsa_ctx().count.set(frame.count());
249 global.salsa_ctx().cursor.set(None);
250 })?;
251 match r {
252 Ok(_) => {
253 if let Some(h) = rendered_event {
254 global.salsa_ctx().queue.push(poll[h].read());
255 }
256 }
257 Err(e) => global.salsa_ctx().queue.push(Err(e)),
258 }
259 }
260 #[cfg(feature = "dialog")]
261 Ok(Control::Close(a)) => {
262 global.salsa_ctx().queue.push(Ok(Control::Event(a)));
264 global.salsa_ctx().queue.push(Ok(Control::Changed));
265 }
266 Ok(Control::Event(a)) => {
267 let ttt = SystemTime::now();
268 let r = event(&a, state, global);
269 global
270 .salsa_ctx()
271 .last_event
272 .set(ttt.elapsed().unwrap_or_default());
273 global.salsa_ctx().queue.push(r);
274 }
275 Ok(Control::Quit) => {
276 if let Some(quit) = quit {
277 match poll[quit].read() {
278 Ok(Control::Event(a)) => {
279 match event(&a, state, global) {
280 Ok(Control::Quit) => { }
281 v => {
282 global.salsa_ctx().queue.push(v);
283 continue;
284 }
285 }
286 }
287 Err(_) => unreachable!(),
288 Ok(_) => unreachable!(),
289 }
290 }
291 break 'ui;
292 }
293 }
294 }
295 }
296
297 if cfg.term_init.clear_area {
298 term.borrow_mut().clear()?;
299 }
300
301 Ok(())
302}
303
304pub fn run_tui<Global, State, Event, Error>(
414 init: fn(
415 state: &mut State, ctx: &mut Global,
417 ) -> Result<(), Error>,
418 render: fn(
419 area: Rect, buf: &mut Buffer,
421 state: &mut State,
422 ctx: &mut Global,
423 ) -> Result<(), Error>,
424 event: fn(
425 event: &Event, state: &mut State,
427 ctx: &mut Global,
428 ) -> Result<Control<Event>, Error>,
429 error: fn(
430 error: Error, state: &mut State,
432 ctx: &mut Global,
433 ) -> Result<Control<Event>, Error>,
434 global: &mut Global,
435 state: &mut State,
436 cfg: RunConfig<Event, Error>,
437) -> Result<(), Error>
438where
439 Global: SalsaContext<Event, Error>,
440 Event: 'static,
441 Error: 'static + From<io::Error>,
442{
443 let t = cfg.term_init;
444
445 if !t.manual {
446 init_terminal(t)?;
447 }
448
449 let r = match catch_unwind(AssertUnwindSafe(|| {
450 _run_tui(init, render, event, error, global, state, cfg)
451 })) {
452 Ok(v) => v,
453 Err(e) => {
454 if !t.manual {
455 _ = shutdown_terminal(t);
456 }
457 resume_unwind(e);
458 }
459 };
460
461 if !t.manual {
462 shutdown_terminal(t)?;
463 }
464
465 r
466}
467
468fn init_terminal(cfg: TermInit) -> io::Result<()> {
469 if cfg.alternate_screen {
470 stdout().execute(EnterAlternateScreen)?;
471 }
472 if cfg.mouse_capture {
473 stdout().execute(EnableMouseCapture)?;
474 }
475 if cfg.bracketed_paste {
476 stdout().execute(EnableBracketedPaste)?;
477 }
478 if cfg.cursor_blinking {
479 stdout().execute(EnableBlinking)?;
480 }
481 stdout().execute(cfg.cursor)?;
482 #[cfg(not(windows))]
483 {
484 stdout().execute(PushKeyboardEnhancementFlags(cfg.keyboard_enhancements))?;
485 let enhanced = supports_keyboard_enhancement().unwrap_or_default();
486 set_have_keyboard_enhancement(enhanced);
487 }
488 #[cfg(windows)]
489 {
490 set_have_keyboard_enhancement(true);
491 }
492
493 enable_raw_mode()?;
494
495 Ok(())
496}
497
498fn shutdown_terminal(cfg: TermInit) -> io::Result<()> {
499 disable_raw_mode()?;
500
501 #[cfg(not(windows))]
502 stdout().execute(PopKeyboardEnhancementFlags)?;
503 stdout().execute(SetCursorStyle::DefaultUserShape)?;
504 if cfg.cursor_blinking {
505 stdout().execute(DisableBlinking)?;
506 }
507 if cfg.bracketed_paste {
508 stdout().execute(DisableBracketedPaste)?;
509 }
510 if cfg.mouse_capture {
511 stdout().execute(DisableMouseCapture)?;
512 }
513 if cfg.alternate_screen {
514 stdout().execute(LeaveAlternateScreen)?;
515 }
516
517 Ok(())
518}