rat_salsa/
framework.rs

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; // µs
31const BACKOFF: u64 = 10_000; // µs
32const FAST_SLEEP: u64 = 100; // µs
33
34fn _run_tui<Global, State, Event, Error>(
35    init: fn(
36        state: &mut State, //
37        ctx: &mut Global,
38    ) -> Result<(), Error>,
39    render: fn(
40        area: Rect, //
41        buf: &mut Buffer,
42        state: &mut State,
43        ctx: &mut Global,
44    ) -> Result<(), Error>,
45    event: fn(
46        event: &Event, //
47        state: &mut State,
48        ctx: &mut Global,
49    ) -> Result<Control<Event>, Error>,
50    error: fn(
51        error: Error, //
52        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
112    init(state, global)?;
113
114    // initial render
115    {
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        // panic on worker panic
148        if let Some(tasks) = &global.salsa_ctx().tasks {
149            if !tasks.check_liveness() {
150                dbg!("worker panicked");
151                break 'ui;
152            }
153        }
154
155        // No events queued, check here.
156        if global.salsa_ctx().queue.is_empty() {
157            // The events are not processed immediately, but all
158            // notifies are queued in the poll_queue.
159            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            // Sleep regime.
174            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                    // Back off slowly.
184                    poll_sleep += Duration::from_micros(BACKOFF);
185                }
186            } else {
187                // Shorter sleep immediately after an event.
188                poll_sleep = Duration::from_micros(FAST_SLEEP);
189            }
190        }
191
192        // All the fall-out of the last event has cleared.
193        // Run the next event.
194        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        // Result of event-handling.
201        if let Some(ctrl) = global.salsa_ctx().queue.take() {
202            // filter out double Changed events.
203            // no need to render twice in a row.
204            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                    // close probably demands a repaint.
263                    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) => { /* really quit now */ }
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
304/// Run the event-loop
305///
306/// The shortest version I can come up with:
307/// ```rust no_run
308/// use anyhow::{anyhow, Error};
309/// use rat_salsa::poll::PollCrossterm;
310/// use rat_salsa::{mock, run_tui, Control, RunConfig, SalsaAppContext, SalsaContext};
311/// use rat_widget::event::ct_event;
312/// use ratatui_core::buffer::Buffer;
313/// use ratatui_core::layout::Rect;
314/// use ratatui_core::style::Stylize;
315/// use ratatui_core::text::{Line, Span};
316/// use ratatui_core::widgets::Widget;
317/// use ratatui_crossterm::crossterm::event::Event;
318///
319/// fn main() -> Result<(), Error> {
320///     run_tui(
321///         mock::init,
322///         render,
323///         event,
324///         error,
325///         &mut Global::default(),
326///         &mut Ultra,
327///         RunConfig::default()?.poll(PollCrossterm),
328///     )
329/// }
330///
331/// #[derive(Debug, Default)]
332/// pub struct Global {
333///     ctx: SalsaAppContext<UltraEvent, Error>,
334///     pub err_cnt: u32,
335///     pub err_msg: String,
336/// }
337///
338/// impl SalsaContext<UltraEvent, Error> for Global {
339///     fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<UltraEvent, Error>) {
340///         self.ctx = app_ctx;
341///     }
342///
343///     fn salsa_ctx(&self) -> &SalsaAppContext<UltraEvent, Error> {
344///         &self.ctx
345///     }
346/// }
347///
348/// #[derive(Debug, PartialEq, Eq, Clone)]
349/// pub enum UltraEvent {
350///     Event(Event),
351/// }
352///
353/// impl From<Event> for UltraEvent {
354///     fn from(value: Event) -> Self {
355///         Self::Event(value)
356///     }
357/// }
358///
359/// pub struct Ultra;
360///
361/// fn render(area: Rect, buf: &mut Buffer, _state: &mut Ultra, ctx: &mut Global) -> Result<(), Error> {
362///     Line::from_iter([Span::from("'q' to quit, 'e' for error, 'r' for repair")])
363///         .render(Rect::new(area.x, area.y, area.width, 1), buf);
364///     Line::from_iter([
365///         Span::from("Hello world!").green(),
366///         Span::from(" Status: "),
367///         if ctx.err_cnt > 0 {
368///             Span::from(&ctx.err_msg).red().underlined()
369///         } else {
370///             Span::from(&ctx.err_msg).cyan().underlined()
371///         },
372///     ])
373///     .render(Rect::new(area.x, area.y + 2, area.width, 1), buf);
374///     Ok(())
375/// }
376///
377/// fn event(
378///     event: &UltraEvent,
379///     _state: &mut Ultra,
380///     ctx: &mut Global,
381/// ) -> Result<Control<UltraEvent>, Error> {
382///     match event {
383///         UltraEvent::Event(event) => match event {
384///             ct_event!(key press 'q') => Ok(Control::Quit),
385///             ct_event!(key press 'e') => return Err(anyhow!("An error occured.")),
386///             ct_event!(key press 'r') => {
387///                 if ctx.err_cnt > 1 {
388///                     ctx.err_cnt -= 1;
389///                     ctx.err_msg = format!("#{}# One error repaired.", ctx.err_cnt).to_string();
390///                 } else if ctx.err_cnt == 1 {
391///                     ctx.err_cnt -= 1;
392///                     ctx.err_msg = "All within norms.".to_string();
393///                 } else {
394///                     ctx.err_cnt = 1;
395///                     ctx.err_msg = format!("#{}# Over-repaired.", ctx.err_cnt).to_string();
396///                 }
397///                 Ok(Control::Changed)
398///             }
399///             _ => Ok(Control::Continue),
400///         },
401///     }
402/// }
403///
404/// fn error(event: Error, _state: &mut Ultra, ctx: &mut Global) -> Result<Control<UltraEvent>, Error> {
405///     ctx.err_cnt += 1;
406///     ctx.err_msg = format!("#{}# {}", ctx.err_cnt, event).to_string();
407///     Ok(Control::Changed)
408/// }
409/// ```
410///
411/// Maybe `templates/minimal.rs` is more useful.
412///
413pub fn run_tui<Global, State, Event, Error>(
414    init: fn(
415        state: &mut State, //
416        ctx: &mut Global,
417    ) -> Result<(), Error>,
418    render: fn(
419        area: Rect, //
420        buf: &mut Buffer,
421        state: &mut State,
422        ctx: &mut Global,
423    ) -> Result<(), Error>,
424    event: fn(
425        event: &Event, //
426        state: &mut State,
427        ctx: &mut Global,
428    ) -> Result<Control<Event>, Error>,
429    error: fn(
430        error: Error, //
431        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}