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
24#[cfg(feature = "dialog")]
25pub mod dialog_stack;
26mod framework;
27mod run_config;
28pub mod tasks;
29pub mod terminal;
30mod thread_pool;
31pub mod timer;
32#[cfg(feature = "async")]
33mod tokio_tasks;
34
35use crate::terminal::Terminal;
36pub use framework::run_tui;
37pub use run_config::RunConfig;
38
39pub mod event {
41 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
43 pub struct TimerEvent(pub crate::timer::TimeOut);
44
45 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
47 pub struct RenderedEvent;
48
49 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
51 pub struct QuitEvent;
52}
53
54pub mod poll {
56 pub trait PollEvents<Event, Error>: std::any::Any
63 where
64 Event: 'static,
65 Error: 'static,
66 {
67 fn as_any(&self) -> &dyn std::any::Any;
68
69 fn poll(&mut self) -> Result<bool, Error>;
78
79 fn read(&mut self) -> Result<crate::Control<Event>, Error>;
84 }
85
86 mod crossterm;
87 mod quit;
88 mod rendered;
89 mod thread_pool;
90 mod timer;
91 #[cfg(feature = "async")]
92 mod tokio_tasks;
93
94 pub use crossterm::PollCrossterm;
95 pub use quit::PollQuit;
96 pub use rendered::PollRendered;
97 pub use thread_pool::PollTasks;
98 pub use timer::PollTimers;
99 #[cfg(feature = "async")]
100 pub use tokio_tasks::PollTokio;
101}
102
103pub mod mock {
104 pub fn init<State, Global, Error>(
108 _state: &mut State, _ctx: &mut Global,
110 ) -> Result<(), Error> {
111 Ok(())
112 }
113
114 pub fn error<Global, State, Event, Error>(
116 _error: Error,
117 _state: &mut State,
118 _ctx: &mut Global,
119 ) -> Result<crate::Control<Event>, Error> {
120 Ok(crate::Control::Continue)
121 }
122}
123
124#[derive(Debug, Clone, Copy)]
143#[must_use]
144#[non_exhaustive]
145pub enum Control<Event> {
146 Continue,
149 Unchanged,
152 Changed,
155 Event(Event),
166 #[cfg(feature = "dialog")]
170 Close(Event),
171 Quit,
173}
174
175impl<Event> Eq for Control<Event> {}
176
177impl<Event> PartialEq for Control<Event> {
178 fn eq(&self, other: &Self) -> bool {
179 mem::discriminant(self) == mem::discriminant(other)
180 }
181}
182
183impl<Event> Ord for Control<Event> {
184 fn cmp(&self, other: &Self) -> Ordering {
185 match self {
186 Control::Continue => match other {
187 Control::Continue => Ordering::Equal,
188 Control::Unchanged => Ordering::Less,
189 Control::Changed => Ordering::Less,
190 Control::Event(_) => Ordering::Less,
191 #[cfg(feature = "dialog")]
192 Control::Close(_) => Ordering::Less,
193 Control::Quit => Ordering::Less,
194 },
195 Control::Unchanged => match other {
196 Control::Continue => Ordering::Greater,
197 Control::Unchanged => Ordering::Equal,
198 Control::Changed => Ordering::Less,
199 Control::Event(_) => Ordering::Less,
200 #[cfg(feature = "dialog")]
201 Control::Close(_) => Ordering::Less,
202 Control::Quit => Ordering::Less,
203 },
204 Control::Changed => match other {
205 Control::Continue => Ordering::Greater,
206 Control::Unchanged => Ordering::Greater,
207 Control::Changed => Ordering::Equal,
208 Control::Event(_) => Ordering::Less,
209 #[cfg(feature = "dialog")]
210 Control::Close(_) => Ordering::Less,
211 Control::Quit => Ordering::Less,
212 },
213 Control::Event(_) => match other {
214 Control::Continue => Ordering::Greater,
215 Control::Unchanged => Ordering::Greater,
216 Control::Changed => Ordering::Greater,
217 Control::Event(_) => Ordering::Equal,
218 #[cfg(feature = "dialog")]
219 Control::Close(_) => Ordering::Less,
220 Control::Quit => Ordering::Less,
221 },
222 #[cfg(feature = "dialog")]
223 Control::Close(_) => match other {
224 Control::Continue => Ordering::Greater,
225 Control::Unchanged => Ordering::Greater,
226 Control::Changed => Ordering::Greater,
227 Control::Event(_) => Ordering::Greater,
228 Control::Close(_) => Ordering::Equal,
229 Control::Quit => Ordering::Less,
230 },
231 Control::Quit => match other {
232 Control::Continue => Ordering::Greater,
233 Control::Unchanged => Ordering::Greater,
234 Control::Changed => Ordering::Greater,
235 Control::Event(_) => Ordering::Greater,
236 #[cfg(feature = "dialog")]
237 Control::Close(_) => Ordering::Greater,
238 Control::Quit => Ordering::Equal,
239 },
240 }
241 }
242}
243
244impl<Event> PartialOrd for Control<Event> {
245 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
246 Some(self.cmp(other))
247 }
248}
249
250impl<Event> ConsumedEvent for Control<Event> {
251 fn is_consumed(&self) -> bool {
252 !matches!(self, Control::Continue)
253 }
254}
255
256impl<Event, T: Into<Outcome>> From<T> for Control<Event> {
257 fn from(value: T) -> Self {
258 let r = value.into();
259 match r {
260 Outcome::Continue => Control::Continue,
261 Outcome::Unchanged => Control::Unchanged,
262 Outcome::Changed => Control::Changed,
263 }
264 }
265}
266
267pub trait SalsaContext<Event, Error>
276where
277 Event: 'static,
278 Error: 'static,
279{
280 fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>);
284
285 fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error>;
287
288 fn count(&self) -> usize {
290 self.salsa_ctx().count.get()
291 }
292
293 fn last_render(&self) -> Duration {
295 self.salsa_ctx().last_render.get()
296 }
297
298 fn last_event(&self) -> Duration {
300 self.salsa_ctx().last_event.get()
301 }
302
303 fn set_screen_cursor(&self, cursor: Option<(u16, u16)>) {
308 if let Some(c) = cursor {
309 self.salsa_ctx().cursor.set(Some(c));
310 }
311 }
312
313 #[inline]
319 fn add_timer(&self, t: TimerDef) -> TimerHandle {
320 self.salsa_ctx()
321 .timers
322 .as_ref()
323 .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
324 .add(t)
325 }
326
327 #[inline]
333 fn remove_timer(&self, tag: TimerHandle) {
334 self.salsa_ctx()
335 .timers
336 .as_ref()
337 .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
338 .remove(tag);
339 }
340
341 #[inline]
349 fn replace_timer(&self, h: Option<TimerHandle>, t: TimerDef) -> TimerHandle {
350 if let Some(h) = h {
351 self.remove_timer(h);
352 }
353 self.add_timer(t)
354 }
355
356 #[inline]
384 fn spawn_ext(
385 &self,
386 task: impl FnOnce(Cancel, &Sender<Result<Control<Event>, Error>>) -> Result<Control<Event>, Error>
387 + Send
388 + 'static,
389 ) -> Result<(Cancel, Liveness), SendError<()>>
390 where
391 Event: 'static + Send,
392 Error: 'static + Send,
393 {
394 self.salsa_ctx()
395 .tasks
396 .as_ref()
397 .expect(
398 "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
399 )
400 .spawn(Box::new(task))
401 }
402
403 #[inline]
416 fn spawn(
417 &self,
418 task: impl FnOnce() -> Result<Control<Event>, Error> + Send + 'static,
419 ) -> Result<(), SendError<()>>
420 where
421 Event: 'static + Send,
422 Error: 'static + Send,
423 {
424 _ = self
425 .salsa_ctx()
426 .tasks
427 .as_ref()
428 .expect(
429 "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
430 )
431 .spawn(Box::new(|_, _| task()))?;
432 Ok(())
433 }
434
435 #[inline]
441 #[cfg(feature = "async")]
442 fn spawn_async<F>(&self, future: F)
443 where
444 F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
445 Event: 'static + Send,
446 Error: 'static + Send,
447 {
448 _ = self.salsa_ctx() .tokio
450 .as_ref()
451 .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))")
452 .spawn(Box::new(future));
453 }
454
455 #[inline]
471 #[cfg(feature = "async")]
472 fn spawn_async_ext<C, F>(&self, cr_future: C) -> (AbortHandle, Liveness)
473 where
474 C: FnOnce(tokio::sync::mpsc::Sender<Result<Control<Event>, Error>>) -> F,
475 F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
476 Event: 'static + Send,
477 Error: 'static + Send,
478 {
479 let rt = self
480 .salsa_ctx().tokio
482 .as_ref()
483 .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))");
484 let future = cr_future(rt.sender());
485 rt.spawn(Box::new(future))
486 }
487
488 #[inline]
490 fn queue_event(&self, event: Event) {
491 self.salsa_ctx().queue.push(Ok(Control::Event(event)));
492 }
493
494 #[inline]
496 fn queue(&self, ctrl: impl Into<Control<Event>>) {
497 self.salsa_ctx().queue.push(Ok(ctrl.into()));
498 }
499
500 #[inline]
502 fn queue_err(&self, err: Error) {
503 self.salsa_ctx().queue.push(Err(err));
504 }
505
506 #[inline]
508 fn set_focus(&self, focus: Focus) {
509 self.salsa_ctx().focus.replace(Some(focus));
510 }
511
512 #[inline]
514 fn take_focus(&self) -> Option<Focus> {
515 self.salsa_ctx().focus.take()
516 }
517
518 #[inline]
520 fn clear_focus(&self) {
521 self.salsa_ctx().focus.replace(None);
522 }
523
524 #[inline]
530 fn focus<'a>(&'a self) -> Ref<'a, Focus> {
531 let borrow = self.salsa_ctx().focus.borrow();
532 Ref::map(borrow, |v| v.as_ref().expect("focus"))
533 }
534
535 #[inline]
541 fn focus_mut<'a>(&'a mut self) -> RefMut<'a, Focus> {
542 let borrow = self.salsa_ctx().focus.borrow_mut();
543 RefMut::map(borrow, |v| v.as_mut().expect("focus"))
544 }
545
546 #[inline]
552 fn handle_focus<E>(&mut self, event: &E)
553 where
554 Focus: HandleEvent<E, Regular, Outcome>,
555 {
556 let mut borrow = self.salsa_ctx().focus.borrow_mut();
557 let focus = borrow.as_mut().expect("focus");
558 let r = focus.handle(event, Regular);
559 if r.is_consumed() {
560 self.queue(r);
561 }
562 }
563
564 #[inline]
566 fn terminal(&mut self) -> Rc<RefCell<dyn Terminal<Error>>> {
567 self.salsa_ctx().term.clone().expect("terminal")
568 }
569
570 #[inline]
572 fn clear_terminal(&mut self) {
573 self.salsa_ctx().clear_terminal.set(true);
574 }
575
576 #[inline]
578 fn insert_before(&mut self, height: u16, draw_fn: impl FnOnce(&mut Buffer) + 'static) {
579 self.salsa_ctx().insert_before.set(InsertBefore {
580 height,
581 draw_fn: Box::new(draw_fn),
582 });
583 }
584}
585
586pub struct SalsaAppContext<Event, Error>
595where
596 Event: 'static,
597 Error: 'static,
598{
599 pub(crate) focus: RefCell<Option<Focus>>,
601 pub(crate) count: Cell<usize>,
603 pub(crate) cursor: Cell<Option<(u16, u16)>>,
605 pub(crate) term: Option<Rc<RefCell<dyn Terminal<Error>>>>,
607 pub(crate) clear_terminal: Cell<bool>,
609 pub(crate) insert_before: Cell<InsertBefore>,
611 pub(crate) last_render: Cell<Duration>,
613 pub(crate) last_event: Cell<Duration>,
615
616 pub(crate) timers: Option<Rc<Timers>>,
618 pub(crate) tasks: Option<Rc<ThreadPool<Event, Error>>>,
620 #[cfg(feature = "async")]
622 pub(crate) tokio: Option<Rc<TokioTasks<Event, Error>>>,
623 pub(crate) queue: ControlQueue<Event, Error>,
625}
626
627struct InsertBefore {
628 height: u16,
629 draw_fn: Box<dyn FnOnce(&mut Buffer)>,
630}
631
632impl Debug for InsertBefore {
633 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
634 f.debug_struct("InsertBefore")
635 .field("height", &self.height)
636 .field("draw_fn", &"dyn Fn()")
637 .finish()
638 }
639}
640
641impl Default for InsertBefore {
642 fn default() -> Self {
643 Self {
644 height: 0,
645 draw_fn: Box::new(|_| {}),
646 }
647 }
648}
649
650impl<Event, Error> Debug for SalsaAppContext<Event, Error> {
651 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
652 let mut ff = f.debug_struct("AppContext");
653 ff.field("focus", &self.focus)
654 .field("count", &self.count)
655 .field("cursor", &self.cursor)
656 .field("clear_terminal", &self.clear_terminal)
657 .field("insert_before", &"n/a")
658 .field("timers", &self.timers)
659 .field("tasks", &self.tasks)
660 .field("queue", &self.queue);
661 #[cfg(feature = "async")]
662 {
663 ff.field("tokio", &self.tokio);
664 }
665 ff.finish()
666 }
667}
668
669impl<Event, Error> Default for SalsaAppContext<Event, Error>
670where
671 Event: 'static,
672 Error: 'static,
673{
674 fn default() -> Self {
675 Self {
676 focus: Default::default(),
677 count: Default::default(),
678 cursor: Default::default(),
679 term: Default::default(),
680 clear_terminal: Default::default(),
681 insert_before: Default::default(),
682 last_render: Default::default(),
683 last_event: Default::default(),
684 timers: Default::default(),
685 tasks: Default::default(),
686 #[cfg(feature = "async")]
687 tokio: Default::default(),
688 queue: Default::default(),
689 }
690 }
691}
692
693impl<Event, Error> SalsaContext<Event, Error> for SalsaAppContext<Event, Error>
694where
695 Event: 'static,
696 Error: 'static,
697{
698 #[inline]
699 fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>) {
700 *self = app_ctx;
701 }
702
703 #[inline]
704 fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error> {
705 self
706 }
707}
708
709mod _private {
710 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
711 pub struct NonExhaustive;
712}