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 use try_as_traits::{TryAsMut, TryAsRef, TypedContainer};
26
27#[cfg(feature = "dialog")]
28pub mod dialog_stack;
29mod framework;
30mod run_config;
31pub mod tasks;
32pub mod terminal;
33mod thread_pool;
34pub mod timer;
35#[cfg(feature = "async")]
36mod tokio_tasks;
37
38use crate::terminal::Terminal;
39pub use framework::run_tui;
40pub use run_config::RunConfig;
41
42pub mod event {
44 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
46 pub struct TimerEvent(pub crate::timer::TimeOut);
47
48 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
50 pub struct RenderedEvent;
51
52 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
54 pub struct QuitEvent;
55}
56
57pub mod poll {
59 pub trait PollEvents<Event, Error>: std::any::Any
66 where
67 Event: 'static,
68 Error: 'static,
69 {
70 fn as_any(&self) -> &dyn std::any::Any;
71
72 fn poll(&mut self) -> Result<bool, Error>;
81
82 fn read(&mut self) -> Result<crate::Control<Event>, Error>;
87 }
88
89 mod crossterm;
90 mod quit;
91 mod rendered;
92 mod thread_pool;
93 mod timer;
94 #[cfg(feature = "async")]
95 mod tokio_tasks;
96
97 pub use crossterm::PollCrossterm;
98 pub use quit::PollQuit;
99 pub use rendered::PollRendered;
100 pub use thread_pool::PollTasks;
101 pub use timer::PollTimers;
102 #[cfg(feature = "async")]
103 pub use tokio_tasks::PollTokio;
104}
105
106pub mod mock {
107 pub fn init<State, Global, Error>(
111 _state: &mut State, _ctx: &mut Global,
113 ) -> Result<(), Error> {
114 Ok(())
115 }
116
117 pub fn error<Global, State, Event, Error>(
119 _error: Error,
120 _state: &mut State,
121 _ctx: &mut Global,
122 ) -> Result<crate::Control<Event>, Error> {
123 Ok(crate::Control::Continue)
124 }
125}
126
127#[derive(Debug, Clone, Copy)]
146#[must_use]
147#[non_exhaustive]
148pub enum Control<Event> {
149 Continue,
152 Unchanged,
155 Changed,
158 Event(Event),
169 #[cfg(feature = "dialog")]
173 Close(Event),
174 Quit,
176}
177
178impl<Event> Eq for Control<Event> {}
179
180impl<Event> PartialEq for Control<Event> {
181 fn eq(&self, other: &Self) -> bool {
182 mem::discriminant(self) == mem::discriminant(other)
183 }
184}
185
186impl<Event> Ord for Control<Event> {
187 fn cmp(&self, other: &Self) -> Ordering {
188 match self {
189 Control::Continue => match other {
190 Control::Continue => Ordering::Equal,
191 Control::Unchanged => Ordering::Less,
192 Control::Changed => Ordering::Less,
193 Control::Event(_) => Ordering::Less,
194 #[cfg(feature = "dialog")]
195 Control::Close(_) => Ordering::Less,
196 Control::Quit => Ordering::Less,
197 },
198 Control::Unchanged => match other {
199 Control::Continue => Ordering::Greater,
200 Control::Unchanged => Ordering::Equal,
201 Control::Changed => Ordering::Less,
202 Control::Event(_) => Ordering::Less,
203 #[cfg(feature = "dialog")]
204 Control::Close(_) => Ordering::Less,
205 Control::Quit => Ordering::Less,
206 },
207 Control::Changed => match other {
208 Control::Continue => Ordering::Greater,
209 Control::Unchanged => Ordering::Greater,
210 Control::Changed => Ordering::Equal,
211 Control::Event(_) => Ordering::Less,
212 #[cfg(feature = "dialog")]
213 Control::Close(_) => Ordering::Less,
214 Control::Quit => Ordering::Less,
215 },
216 Control::Event(_) => match other {
217 Control::Continue => Ordering::Greater,
218 Control::Unchanged => Ordering::Greater,
219 Control::Changed => Ordering::Greater,
220 Control::Event(_) => Ordering::Equal,
221 #[cfg(feature = "dialog")]
222 Control::Close(_) => Ordering::Less,
223 Control::Quit => Ordering::Less,
224 },
225 #[cfg(feature = "dialog")]
226 Control::Close(_) => match other {
227 Control::Continue => Ordering::Greater,
228 Control::Unchanged => Ordering::Greater,
229 Control::Changed => Ordering::Greater,
230 Control::Event(_) => Ordering::Greater,
231 Control::Close(_) => Ordering::Equal,
232 Control::Quit => Ordering::Less,
233 },
234 Control::Quit => match other {
235 Control::Continue => Ordering::Greater,
236 Control::Unchanged => Ordering::Greater,
237 Control::Changed => Ordering::Greater,
238 Control::Event(_) => Ordering::Greater,
239 #[cfg(feature = "dialog")]
240 Control::Close(_) => Ordering::Greater,
241 Control::Quit => Ordering::Equal,
242 },
243 }
244 }
245}
246
247impl<Event> PartialOrd for Control<Event> {
248 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
249 Some(self.cmp(other))
250 }
251}
252
253impl<Event> ConsumedEvent for Control<Event> {
254 fn is_consumed(&self) -> bool {
255 !matches!(self, Control::Continue)
256 }
257}
258
259impl<Event, T: Into<Outcome>> From<T> for Control<Event> {
260 fn from(value: T) -> Self {
261 let r = value.into();
262 match r {
263 Outcome::Continue => Control::Continue,
264 Outcome::Unchanged => Control::Unchanged,
265 Outcome::Changed => Control::Changed,
266 }
267 }
268}
269
270pub trait SalsaContext<Event, Error>
279where
280 Event: 'static,
281 Error: 'static,
282{
283 fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>);
287
288 fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error>;
290
291 fn count(&self) -> usize {
293 self.salsa_ctx().count.get()
294 }
295
296 fn last_render(&self) -> Duration {
298 self.salsa_ctx().last_render.get()
299 }
300
301 fn last_event(&self) -> Duration {
303 self.salsa_ctx().last_event.get()
304 }
305
306 fn set_screen_cursor(&self, cursor: Option<(u16, u16)>) {
311 if let Some(c) = cursor {
312 self.salsa_ctx().cursor.set(Some(c));
313 }
314 }
315
316 #[inline]
322 fn add_timer(&self, t: TimerDef) -> TimerHandle {
323 self.salsa_ctx()
324 .timers
325 .as_ref()
326 .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
327 .add(t)
328 }
329
330 #[inline]
336 fn remove_timer(&self, tag: TimerHandle) {
337 self.salsa_ctx()
338 .timers
339 .as_ref()
340 .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
341 .remove(tag);
342 }
343
344 #[inline]
352 fn replace_timer(&self, h: Option<TimerHandle>, t: TimerDef) -> TimerHandle {
353 if let Some(h) = h {
354 self.remove_timer(h);
355 }
356 self.add_timer(t)
357 }
358
359 #[inline]
387 fn spawn_ext(
388 &self,
389 task: impl FnOnce(Cancel, &Sender<Result<Control<Event>, Error>>) -> Result<Control<Event>, Error>
390 + Send
391 + 'static,
392 ) -> Result<(Cancel, Liveness), SendError<()>>
393 where
394 Event: 'static + Send,
395 Error: 'static + Send,
396 {
397 self.salsa_ctx()
398 .tasks
399 .as_ref()
400 .expect(
401 "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
402 )
403 .spawn(Box::new(task))
404 }
405
406 #[inline]
419 fn spawn(
420 &self,
421 task: impl FnOnce() -> Result<Control<Event>, Error> + Send + 'static,
422 ) -> Result<(), SendError<()>>
423 where
424 Event: 'static + Send,
425 Error: 'static + Send,
426 {
427 _ = self
428 .salsa_ctx()
429 .tasks
430 .as_ref()
431 .expect(
432 "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
433 )
434 .spawn(Box::new(|_, _| task()))?;
435 Ok(())
436 }
437
438 #[inline]
444 #[cfg(feature = "async")]
445 fn spawn_async<F>(&self, future: F)
446 where
447 F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
448 Event: 'static + Send,
449 Error: 'static + Send,
450 {
451 _ = self.salsa_ctx() .tokio
453 .as_ref()
454 .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))")
455 .spawn(Box::new(future));
456 }
457
458 #[inline]
474 #[cfg(feature = "async")]
475 fn spawn_async_ext<C, F>(&self, cr_future: C) -> (AbortHandle, Liveness)
476 where
477 C: FnOnce(tokio::sync::mpsc::Sender<Result<Control<Event>, Error>>) -> F,
478 F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
479 Event: 'static + Send,
480 Error: 'static + Send,
481 {
482 let rt = self
483 .salsa_ctx().tokio
485 .as_ref()
486 .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))");
487 let future = cr_future(rt.sender());
488 rt.spawn(Box::new(future))
489 }
490
491 #[inline]
493 fn queue_event(&self, event: Event) {
494 self.salsa_ctx().queue.push(Ok(Control::Event(event)));
495 }
496
497 #[inline]
499 fn queue(&self, ctrl: impl Into<Control<Event>>) {
500 self.salsa_ctx().queue.push(Ok(ctrl.into()));
501 }
502
503 #[inline]
505 fn queue_err(&self, err: Error) {
506 self.salsa_ctx().queue.push(Err(err));
507 }
508
509 #[inline]
511 fn set_focus(&self, focus: Focus) {
512 self.salsa_ctx().focus.replace(Some(focus));
513 }
514
515 #[inline]
517 fn take_focus(&self) -> Option<Focus> {
518 self.salsa_ctx().focus.take()
519 }
520
521 #[inline]
523 fn clear_focus(&self) {
524 self.salsa_ctx().focus.replace(None);
525 }
526
527 #[inline]
533 fn focus<'a>(&'a self) -> Ref<'a, Focus> {
534 let borrow = self.salsa_ctx().focus.borrow();
535 Ref::map(borrow, |v| v.as_ref().expect("focus"))
536 }
537
538 #[inline]
544 fn focus_mut<'a>(&'a mut self) -> RefMut<'a, Focus> {
545 let borrow = self.salsa_ctx().focus.borrow_mut();
546 RefMut::map(borrow, |v| v.as_mut().expect("focus"))
547 }
548
549 #[inline]
555 fn handle_focus<E>(&mut self, event: &E) -> Outcome
556 where
557 Focus: HandleEvent<E, Regular, Outcome>,
558 {
559 let mut borrow = self.salsa_ctx().focus.borrow_mut();
560 let focus = borrow.as_mut().expect("focus");
561 let r = focus.handle(event, Regular);
562 if r.is_consumed() {
563 self.queue(r);
564 }
565 r
566 }
567
568 #[inline]
570 fn terminal(&mut self) -> Rc<RefCell<dyn Terminal<Error>>> {
571 self.salsa_ctx().term.clone().expect("terminal")
572 }
573
574 #[inline]
576 fn clear_terminal(&mut self) {
577 self.salsa_ctx().clear_terminal.set(true);
578 }
579
580 #[inline]
582 fn insert_before(&mut self, height: u16, draw_fn: impl FnOnce(&mut Buffer) + 'static) {
583 self.salsa_ctx().insert_before.set(InsertBefore {
584 height,
585 draw_fn: Box::new(draw_fn),
586 });
587 }
588}
589
590pub struct SalsaAppContext<Event, Error>
599where
600 Event: 'static,
601 Error: 'static,
602{
603 pub(crate) focus: RefCell<Option<Focus>>,
605 pub(crate) count: Cell<usize>,
607 pub(crate) cursor: Cell<Option<(u16, u16)>>,
609 pub(crate) term: Option<Rc<RefCell<dyn Terminal<Error>>>>,
611 pub(crate) clear_terminal: Cell<bool>,
613 pub(crate) insert_before: Cell<InsertBefore>,
615 pub(crate) last_render: Cell<Duration>,
617 pub(crate) last_event: Cell<Duration>,
619
620 pub(crate) timers: Option<Rc<Timers>>,
622 pub(crate) tasks: Option<Rc<ThreadPool<Event, Error>>>,
624 #[cfg(feature = "async")]
626 pub(crate) tokio: Option<Rc<TokioTasks<Event, Error>>>,
627 pub(crate) queue: ControlQueue<Event, Error>,
629}
630
631struct InsertBefore {
632 height: u16,
633 draw_fn: Box<dyn FnOnce(&mut Buffer)>,
634}
635
636impl Debug for InsertBefore {
637 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
638 f.debug_struct("InsertBefore")
639 .field("height", &self.height)
640 .field("draw_fn", &"dyn Fn()")
641 .finish()
642 }
643}
644
645impl Default for InsertBefore {
646 fn default() -> Self {
647 Self {
648 height: 0,
649 draw_fn: Box::new(|_| {}),
650 }
651 }
652}
653
654impl<Event, Error> Debug for SalsaAppContext<Event, Error> {
655 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
656 let mut ff = f.debug_struct("AppContext");
657 ff.field("focus", &self.focus)
658 .field("count", &self.count)
659 .field("cursor", &self.cursor)
660 .field("clear_terminal", &self.clear_terminal)
661 .field("insert_before", &"n/a")
662 .field("timers", &self.timers)
663 .field("tasks", &self.tasks)
664 .field("queue", &self.queue);
665 #[cfg(feature = "async")]
666 {
667 ff.field("tokio", &self.tokio);
668 }
669 ff.finish()
670 }
671}
672
673impl<Event, Error> Default for SalsaAppContext<Event, Error>
674where
675 Event: 'static,
676 Error: 'static,
677{
678 fn default() -> Self {
679 Self {
680 focus: Default::default(),
681 count: Default::default(),
682 cursor: Default::default(),
683 term: Default::default(),
684 clear_terminal: Default::default(),
685 insert_before: Default::default(),
686 last_render: Default::default(),
687 last_event: Default::default(),
688 timers: Default::default(),
689 tasks: Default::default(),
690 #[cfg(feature = "async")]
691 tokio: Default::default(),
692 queue: Default::default(),
693 }
694 }
695}
696
697impl<Event, Error> SalsaContext<Event, Error> for SalsaAppContext<Event, Error>
698where
699 Event: 'static,
700 Error: 'static,
701{
702 #[inline]
703 fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>) {
704 *self = app_ctx;
705 }
706
707 #[inline]
708 fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error> {
709 self
710 }
711}
712
713mod _private {
714 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
715 pub struct NonExhaustive;
716}