1#![doc = include_str!("../readme.md")]
2#![allow(clippy::uninlined_format_args)]
3use crate::framework::control_queue::ControlQueue;
4use crate::tasks::{Cancel, Liveness};
5use crate::thread_pool::ThreadPool;
6use crate::timer::{TimerDef, TimerHandle, Timers};
7#[cfg(feature = "async")]
8use crate::tokio_tasks::TokioTasks;
9use crossbeam::channel::{SendError, Sender};
10use rat_event::{ConsumedEvent, HandleEvent, Outcome, Regular};
11use rat_focus::Focus;
12use ratatui::buffer::Buffer;
13use std::cell::{Cell, Ref, RefCell, RefMut};
14use std::cmp::Ordering;
15use std::fmt::{Debug, Formatter};
16#[cfg(feature = "async")]
17use std::future::Future;
18use std::mem;
19use std::rc::Rc;
20use std::time::Duration;
21#[cfg(feature = "async")]
22use tokio::task::AbortHandle;
23
24mod framework;
25mod run_config;
26pub mod tasks;
27pub mod terminal;
28mod thread_pool;
29pub mod timer;
30#[cfg(feature = "async")]
31mod tokio_tasks;
32
33use crate::terminal::Terminal;
34pub use framework::run_tui;
35pub use run_config::RunConfig;
36
37pub mod event {
39 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
41 pub struct TimerEvent(pub crate::timer::TimeOut);
42
43 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
45 pub struct RenderedEvent;
46
47 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
49 pub struct QuitEvent;
50}
51
52pub mod poll {
54 pub trait PollEvents<Event, Error>: std::any::Any
61 where
62 Event: 'static,
63 Error: 'static,
64 {
65 fn as_any(&self) -> &dyn std::any::Any;
66
67 fn poll(&mut self) -> Result<bool, Error>;
76
77 fn read(&mut self) -> Result<crate::Control<Event>, Error>;
82 }
83
84 mod crossterm;
85 mod quit;
86 mod rendered;
87 mod thread_pool;
88 mod timer;
89 #[cfg(feature = "async")]
90 mod tokio_tasks;
91
92 pub use crossterm::PollCrossterm;
93 pub use quit::PollQuit;
94 pub use rendered::PollRendered;
95 pub use thread_pool::PollTasks;
96 pub use timer::PollTimers;
97 #[cfg(feature = "async")]
98 pub use tokio_tasks::PollTokio;
99}
100
101pub mod mock {
102 pub fn init<State, Global, Error>(
106 _state: &mut State, _ctx: &mut Global,
108 ) -> Result<(), Error> {
109 Ok(())
110 }
111
112 pub fn error<Global, State, Event, Error>(
114 _error: Error,
115 _state: &mut State,
116 _ctx: &mut Global,
117 ) -> Result<crate::Control<Event>, Error> {
118 Ok(crate::Control::Continue)
119 }
120}
121
122#[derive(Debug, Clone, Copy)]
141#[must_use]
142#[non_exhaustive]
143pub enum Control<Event> {
144 Continue,
147 Unchanged,
150 Changed,
153 Event(Event),
164 Quit,
166}
167
168impl<Event> Eq for Control<Event> {}
169
170impl<Event> PartialEq for Control<Event> {
171 fn eq(&self, other: &Self) -> bool {
172 mem::discriminant(self) == mem::discriminant(other)
173 }
174}
175
176impl<Event> Ord for Control<Event> {
177 fn cmp(&self, other: &Self) -> Ordering {
178 match self {
179 Control::Continue => match other {
180 Control::Continue => Ordering::Equal,
181 Control::Unchanged => Ordering::Less,
182 Control::Changed => Ordering::Less,
183 Control::Event(_) => Ordering::Less,
184 Control::Quit => Ordering::Less,
185 },
186 Control::Unchanged => match other {
187 Control::Continue => Ordering::Greater,
188 Control::Unchanged => Ordering::Equal,
189 Control::Changed => Ordering::Less,
190 Control::Event(_) => Ordering::Less,
191 Control::Quit => Ordering::Less,
192 },
193 Control::Changed => match other {
194 Control::Continue => Ordering::Greater,
195 Control::Unchanged => Ordering::Greater,
196 Control::Changed => Ordering::Equal,
197 Control::Event(_) => Ordering::Less,
198 Control::Quit => Ordering::Less,
199 },
200 Control::Event(_) => match other {
201 Control::Continue => Ordering::Greater,
202 Control::Unchanged => Ordering::Greater,
203 Control::Changed => Ordering::Greater,
204 Control::Event(_) => Ordering::Equal,
205 Control::Quit => Ordering::Less,
206 },
207 Control::Quit => match other {
208 Control::Continue => Ordering::Greater,
209 Control::Unchanged => Ordering::Greater,
210 Control::Changed => Ordering::Greater,
211 Control::Event(_) => Ordering::Greater,
212 Control::Quit => Ordering::Equal,
213 },
214 }
215 }
216}
217
218impl<Event> PartialOrd for Control<Event> {
219 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
220 Some(self.cmp(other))
221 }
222}
223
224impl<Event> ConsumedEvent for Control<Event> {
225 fn is_consumed(&self) -> bool {
226 !matches!(self, Control::Continue)
227 }
228}
229
230impl<Event, T: Into<Outcome>> From<T> for Control<Event> {
231 fn from(value: T) -> Self {
232 let r = value.into();
233 match r {
234 Outcome::Continue => Control::Continue,
235 Outcome::Unchanged => Control::Unchanged,
236 Outcome::Changed => Control::Changed,
237 }
238 }
239}
240
241pub trait SalsaContext<Event, Error>
250where
251 Event: 'static,
252 Error: 'static,
253{
254 fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>);
258
259 fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error>;
261
262 fn count(&self) -> usize {
264 self.salsa_ctx().count.get()
265 }
266
267 fn last_render(&self) -> Duration {
269 self.salsa_ctx().last_render.get()
270 }
271
272 fn last_event(&self) -> Duration {
274 self.salsa_ctx().last_event.get()
275 }
276
277 fn set_screen_cursor(&self, cursor: Option<(u16, u16)>) {
282 if let Some(c) = cursor {
283 self.salsa_ctx().cursor.set(Some(c));
284 }
285 }
286
287 #[inline]
293 fn add_timer(&self, t: TimerDef) -> TimerHandle {
294 self.salsa_ctx()
295 .timers
296 .as_ref()
297 .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
298 .add(t)
299 }
300
301 #[inline]
307 fn remove_timer(&self, tag: TimerHandle) {
308 self.salsa_ctx()
309 .timers
310 .as_ref()
311 .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
312 .remove(tag);
313 }
314
315 #[inline]
323 fn replace_timer(&self, h: Option<TimerHandle>, t: TimerDef) -> TimerHandle {
324 if let Some(h) = h {
325 self.remove_timer(h);
326 }
327 self.add_timer(t)
328 }
329
330 #[inline]
358 fn spawn_ext(
359 &self,
360 task: impl FnOnce(Cancel, &Sender<Result<Control<Event>, Error>>) -> Result<Control<Event>, Error>
361 + Send
362 + 'static,
363 ) -> Result<(Cancel, Liveness), SendError<()>>
364 where
365 Event: 'static + Send,
366 Error: 'static + Send,
367 {
368 self.salsa_ctx()
369 .tasks
370 .as_ref()
371 .expect(
372 "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
373 )
374 .spawn(Box::new(task))
375 }
376
377 #[inline]
390 fn spawn(
391 &self,
392 task: impl FnOnce() -> Result<Control<Event>, Error> + Send + 'static,
393 ) -> Result<(), SendError<()>>
394 where
395 Event: 'static + Send,
396 Error: 'static + Send,
397 {
398 _ = self
399 .salsa_ctx()
400 .tasks
401 .as_ref()
402 .expect(
403 "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
404 )
405 .spawn(Box::new(|_, _| task()))?;
406 Ok(())
407 }
408
409 #[inline]
415 #[cfg(feature = "async")]
416 fn spawn_async<F>(&self, future: F)
417 where
418 F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
419 Event: 'static + Send,
420 Error: 'static + Send,
421 {
422 _ = self.salsa_ctx() .tokio
424 .as_ref()
425 .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))")
426 .spawn(Box::new(future));
427 }
428
429 #[inline]
445 #[cfg(feature = "async")]
446 fn spawn_async_ext<C, F>(&self, cr_future: C) -> (AbortHandle, Liveness)
447 where
448 C: FnOnce(tokio::sync::mpsc::Sender<Result<Control<Event>, Error>>) -> F,
449 F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
450 Event: 'static + Send,
451 Error: 'static + Send,
452 {
453 let rt = self
454 .salsa_ctx().tokio
456 .as_ref()
457 .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))");
458 let future = cr_future(rt.sender());
459 rt.spawn(Box::new(future))
460 }
461
462 #[inline]
464 fn queue_event(&self, event: Event) {
465 self.salsa_ctx().queue.push(Ok(Control::Event(event)));
466 }
467
468 #[inline]
470 fn queue(&self, ctrl: impl Into<Control<Event>>) {
471 self.salsa_ctx().queue.push(Ok(ctrl.into()));
472 }
473
474 #[inline]
476 fn queue_err(&self, err: Error) {
477 self.salsa_ctx().queue.push(Err(err));
478 }
479
480 #[inline]
482 fn set_focus(&self, focus: Focus) {
483 self.salsa_ctx().focus.replace(Some(focus));
484 }
485
486 #[inline]
488 fn take_focus(&self) -> Option<Focus> {
489 self.salsa_ctx().focus.take()
490 }
491
492 #[inline]
494 fn clear_focus(&self) {
495 self.salsa_ctx().focus.replace(None);
496 }
497
498 #[inline]
504 fn focus<'a>(&'a self) -> Ref<'a, Focus> {
505 let borrow = self.salsa_ctx().focus.borrow();
506 Ref::map(borrow, |v| v.as_ref().expect("focus"))
507 }
508
509 #[inline]
515 fn focus_mut<'a>(&'a mut self) -> RefMut<'a, Focus> {
516 let borrow = self.salsa_ctx().focus.borrow_mut();
517 RefMut::map(borrow, |v| v.as_mut().expect("focus"))
518 }
519
520 #[inline]
526 fn handle_focus<E>(&mut self, event: &E)
527 where
528 Focus: HandleEvent<E, Regular, Outcome>,
529 {
530 let mut borrow = self.salsa_ctx().focus.borrow_mut();
531 let focus = borrow.as_mut().expect("focus");
532 let r = focus.handle(event, Regular);
533 if r.is_consumed() {
534 self.queue(r);
535 }
536 }
537
538 #[inline]
540 fn terminal(&mut self) -> Rc<RefCell<dyn Terminal<Error>>> {
541 self.salsa_ctx().term.clone().expect("terminal")
542 }
543
544 #[inline]
546 fn clear_terminal(&mut self) {
547 self.salsa_ctx().clear_terminal.set(true);
548 }
549
550 #[inline]
552 fn insert_before(&mut self, height: u16, draw_fn: impl FnOnce(&mut Buffer) + 'static) {
553 self.salsa_ctx().insert_before.set(InsertBefore {
554 height,
555 draw_fn: Box::new(draw_fn),
556 });
557 }
558}
559
560pub struct SalsaAppContext<Event, Error>
569where
570 Event: 'static,
571 Error: 'static,
572{
573 pub(crate) focus: RefCell<Option<Focus>>,
575 pub(crate) count: Cell<usize>,
577 pub(crate) cursor: Cell<Option<(u16, u16)>>,
579 pub(crate) term: Option<Rc<RefCell<dyn Terminal<Error>>>>,
581 pub(crate) clear_terminal: Cell<bool>,
583 pub(crate) insert_before: Cell<InsertBefore>,
585 pub(crate) last_render: Cell<Duration>,
587 pub(crate) last_event: Cell<Duration>,
589
590 pub(crate) timers: Option<Rc<Timers>>,
592 pub(crate) tasks: Option<Rc<ThreadPool<Event, Error>>>,
594 #[cfg(feature = "async")]
596 pub(crate) tokio: Option<Rc<TokioTasks<Event, Error>>>,
597 pub(crate) queue: ControlQueue<Event, Error>,
599}
600
601struct InsertBefore {
602 height: u16,
603 draw_fn: Box<dyn FnOnce(&mut Buffer)>,
604}
605
606impl Debug for InsertBefore {
607 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
608 f.debug_struct("InsertBefore")
609 .field("height", &self.height)
610 .field("draw_fn", &"dyn Fn()")
611 .finish()
612 }
613}
614
615impl Default for InsertBefore {
616 fn default() -> Self {
617 Self {
618 height: 0,
619 draw_fn: Box::new(|_| {}),
620 }
621 }
622}
623
624impl<Event, Error> Debug for SalsaAppContext<Event, Error> {
625 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
626 let mut ff = f.debug_struct("AppContext");
627 ff.field("focus", &self.focus)
628 .field("count", &self.count)
629 .field("cursor", &self.cursor)
630 .field("clear_terminal", &self.clear_terminal)
631 .field("insert_before", &"n/a")
632 .field("timers", &self.timers)
633 .field("tasks", &self.tasks)
634 .field("queue", &self.queue);
635 #[cfg(feature = "async")]
636 {
637 ff.field("tokio", &self.tokio);
638 }
639 ff.finish()
640 }
641}
642
643impl<Event, Error> Default for SalsaAppContext<Event, Error>
644where
645 Event: 'static,
646 Error: 'static,
647{
648 fn default() -> Self {
649 Self {
650 focus: Default::default(),
651 count: Default::default(),
652 cursor: Default::default(),
653 term: Default::default(),
654 clear_terminal: Default::default(),
655 insert_before: Default::default(),
656 last_render: Default::default(),
657 last_event: Default::default(),
658 timers: Default::default(),
659 tasks: Default::default(),
660 #[cfg(feature = "async")]
661 tokio: Default::default(),
662 queue: Default::default(),
663 }
664 }
665}
666
667impl<Event, Error> SalsaContext<Event, Error> for SalsaAppContext<Event, Error>
668where
669 Event: 'static,
670 Error: 'static,
671{
672 #[inline]
673 fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>) {
674 *self = app_ctx;
675 }
676
677 #[inline]
678 fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error> {
679 self
680 }
681}
682
683mod _private {
684 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
685 pub struct NonExhaustive;
686}