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;
20#[cfg(feature = "async")]
21use tokio::task::AbortHandle;
22
23mod framework;
24mod run_config;
25pub mod tasks;
26pub mod terminal;
27mod thread_pool;
28pub mod timer;
29#[cfg(feature = "async")]
30mod tokio_tasks;
31
32use crate::terminal::Terminal;
33pub use framework::run_tui;
34pub use run_config::RunConfig;
35
36pub mod event {
38 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
40 pub struct TimerEvent(pub crate::timer::TimeOut);
41
42 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
44 pub struct RenderedEvent;
45
46 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
48 pub struct QuitEvent;
49}
50
51pub mod poll {
53 pub trait PollEvents<Event, Error>: std::any::Any
60 where
61 Event: 'static,
62 Error: 'static,
63 {
64 fn as_any(&self) -> &dyn std::any::Any;
65
66 fn poll(&mut self) -> Result<bool, Error>;
75
76 fn read(&mut self) -> Result<crate::Control<Event>, Error>;
81 }
82
83 mod crossterm;
84 mod quit;
85 mod rendered;
86 mod thread_pool;
87 mod timer;
88 #[cfg(feature = "async")]
89 mod tokio_tasks;
90
91 pub use crossterm::PollCrossterm;
92 pub use quit::PollQuit;
93 pub use rendered::PollRendered;
94 pub use thread_pool::PollTasks;
95 pub use timer::PollTimers;
96 #[cfg(feature = "async")]
97 pub use tokio_tasks::PollTokio;
98}
99
100pub mod mock {
101 pub fn init<State, Global, Error>(
105 _state: &mut State, _ctx: &mut Global,
107 ) -> Result<(), Error> {
108 Ok(())
109 }
110
111 pub fn error<Global, State, Event, Error>(
113 _error: Error,
114 _state: &mut State,
115 _ctx: &mut Global,
116 ) -> Result<crate::Control<Event>, Error> {
117 Ok(crate::Control::Continue)
118 }
119}
120
121#[derive(Debug, Clone, Copy)]
140#[must_use]
141#[non_exhaustive]
142pub enum Control<Event> {
143 Continue,
146 Unchanged,
149 Changed,
152 Event(Event),
163 Quit,
165}
166
167impl<Event> Eq for Control<Event> {}
168
169impl<Event> PartialEq for Control<Event> {
170 fn eq(&self, other: &Self) -> bool {
171 mem::discriminant(self) == mem::discriminant(other)
172 }
173}
174
175impl<Event> Ord for Control<Event> {
176 fn cmp(&self, other: &Self) -> Ordering {
177 match self {
178 Control::Continue => match other {
179 Control::Continue => Ordering::Equal,
180 Control::Unchanged => Ordering::Less,
181 Control::Changed => Ordering::Less,
182 Control::Event(_) => Ordering::Less,
183 Control::Quit => Ordering::Less,
184 },
185 Control::Unchanged => match other {
186 Control::Continue => Ordering::Greater,
187 Control::Unchanged => Ordering::Equal,
188 Control::Changed => Ordering::Less,
189 Control::Event(_) => Ordering::Less,
190 Control::Quit => Ordering::Less,
191 },
192 Control::Changed => match other {
193 Control::Continue => Ordering::Greater,
194 Control::Unchanged => Ordering::Greater,
195 Control::Changed => Ordering::Equal,
196 Control::Event(_) => Ordering::Less,
197 Control::Quit => Ordering::Less,
198 },
199 Control::Event(_) => match other {
200 Control::Continue => Ordering::Greater,
201 Control::Unchanged => Ordering::Greater,
202 Control::Changed => Ordering::Greater,
203 Control::Event(_) => Ordering::Equal,
204 Control::Quit => Ordering::Less,
205 },
206 Control::Quit => match other {
207 Control::Continue => Ordering::Greater,
208 Control::Unchanged => Ordering::Greater,
209 Control::Changed => Ordering::Greater,
210 Control::Event(_) => Ordering::Greater,
211 Control::Quit => Ordering::Equal,
212 },
213 }
214 }
215}
216
217impl<Event> PartialOrd for Control<Event> {
218 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
219 Some(self.cmp(other))
220 }
221}
222
223impl<Event> ConsumedEvent for Control<Event> {
224 fn is_consumed(&self) -> bool {
225 !matches!(self, Control::Continue)
226 }
227}
228
229impl<Event, T: Into<Outcome>> From<T> for Control<Event> {
230 fn from(value: T) -> Self {
231 let r = value.into();
232 match r {
233 Outcome::Continue => Control::Continue,
234 Outcome::Unchanged => Control::Unchanged,
235 Outcome::Changed => Control::Changed,
236 }
237 }
238}
239
240pub trait SalsaContext<Event, Error>
249where
250 Event: 'static,
251 Error: 'static,
252{
253 fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>);
257
258 fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error>;
260
261 fn count(&self) -> usize {
263 self.salsa_ctx().count.get()
264 }
265
266 fn set_screen_cursor(&self, cursor: Option<(u16, u16)>) {
271 if let Some(c) = cursor {
272 self.salsa_ctx().cursor.set(Some(c));
273 }
274 }
275
276 #[inline]
282 fn add_timer(&self, t: TimerDef) -> TimerHandle {
283 self.salsa_ctx()
284 .timers
285 .as_ref()
286 .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
287 .add(t)
288 }
289
290 #[inline]
296 fn remove_timer(&self, tag: TimerHandle) {
297 self.salsa_ctx()
298 .timers
299 .as_ref()
300 .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
301 .remove(tag);
302 }
303
304 #[inline]
312 fn replace_timer(&self, h: Option<TimerHandle>, t: TimerDef) -> TimerHandle {
313 if let Some(h) = h {
314 self.remove_timer(h);
315 }
316 self.add_timer(t)
317 }
318
319 #[inline]
347 fn spawn_ext(
348 &self,
349 task: impl FnOnce(Cancel, &Sender<Result<Control<Event>, Error>>) -> Result<Control<Event>, Error>
350 + Send
351 + 'static,
352 ) -> Result<(Cancel, Liveness), SendError<()>>
353 where
354 Event: 'static + Send,
355 Error: 'static + Send,
356 {
357 self.salsa_ctx()
358 .tasks
359 .as_ref()
360 .expect(
361 "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
362 )
363 .spawn(Box::new(task))
364 }
365
366 #[inline]
379 fn spawn(
380 &self,
381 task: impl FnOnce() -> Result<Control<Event>, Error> + Send + 'static,
382 ) -> Result<(), SendError<()>>
383 where
384 Event: 'static + Send,
385 Error: 'static + Send,
386 {
387 _ = self
388 .salsa_ctx()
389 .tasks
390 .as_ref()
391 .expect(
392 "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
393 )
394 .spawn(Box::new(|_, _| task()))?;
395 Ok(())
396 }
397
398 #[inline]
404 #[cfg(feature = "async")]
405 fn spawn_async<F>(&self, future: F)
406 where
407 F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
408 Event: 'static + Send,
409 Error: 'static + Send,
410 {
411 _ = self.salsa_ctx() .tokio
413 .as_ref()
414 .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))")
415 .spawn(Box::new(future));
416 }
417
418 #[inline]
434 #[cfg(feature = "async")]
435 fn spawn_async_ext<C, F>(&self, cr_future: C) -> (AbortHandle, Liveness)
436 where
437 C: FnOnce(tokio::sync::mpsc::Sender<Result<Control<Event>, Error>>) -> F,
438 F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
439 Event: 'static + Send,
440 Error: 'static + Send,
441 {
442 let rt = self
443 .salsa_ctx().tokio
445 .as_ref()
446 .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))");
447 let future = cr_future(rt.sender());
448 rt.spawn(Box::new(future))
449 }
450
451 #[inline]
453 fn queue_event(&self, event: Event) {
454 self.salsa_ctx().queue.push(Ok(Control::Event(event)));
455 }
456
457 #[inline]
459 fn queue(&self, ctrl: impl Into<Control<Event>>) {
460 self.salsa_ctx().queue.push(Ok(ctrl.into()));
461 }
462
463 #[inline]
465 fn queue_err(&self, err: Error) {
466 self.salsa_ctx().queue.push(Err(err));
467 }
468
469 #[inline]
471 fn set_focus(&self, focus: Focus) {
472 self.salsa_ctx().focus.replace(Some(focus));
473 }
474
475 #[inline]
477 fn take_focus(&self) -> Option<Focus> {
478 self.salsa_ctx().focus.take()
479 }
480
481 #[inline]
483 fn clear_focus(&self) {
484 self.salsa_ctx().focus.replace(None);
485 }
486
487 #[inline]
493 fn focus<'a>(&'a self) -> Ref<'a, Focus> {
494 let borrow = self.salsa_ctx().focus.borrow();
495 Ref::map(borrow, |v| v.as_ref().expect("focus"))
496 }
497
498 #[inline]
504 fn focus_mut<'a>(&'a mut self) -> RefMut<'a, Focus> {
505 let borrow = self.salsa_ctx().focus.borrow_mut();
506 RefMut::map(borrow, |v| v.as_mut().expect("focus"))
507 }
508
509 #[inline]
515 fn handle_focus<E>(&mut self, event: &E)
516 where
517 Focus: HandleEvent<E, Regular, Outcome>,
518 {
519 let mut borrow = self.salsa_ctx().focus.borrow_mut();
520 let focus = borrow.as_mut().expect("focus");
521 let r = focus.handle(event, Regular);
522 if r.is_consumed() {
523 self.queue(r);
524 }
525 }
526
527 #[inline]
529 fn terminal(&mut self) -> Rc<RefCell<dyn Terminal<Error>>> {
530 self.salsa_ctx().term.clone().expect("terminal")
531 }
532
533 #[inline]
535 fn clear_terminal(&mut self) {
536 self.salsa_ctx().clear_terminal.set(true);
537 }
538
539 #[inline]
541 fn insert_before(&mut self, height: u16, draw_fn: impl FnOnce(&mut Buffer) + 'static) {
542 self.salsa_ctx().insert_before.set(InsertBefore {
543 height,
544 draw_fn: Box::new(draw_fn),
545 });
546 }
547}
548
549pub struct SalsaAppContext<Event, Error>
558where
559 Event: 'static,
560 Error: 'static,
561{
562 pub(crate) focus: RefCell<Option<Focus>>,
564 pub(crate) count: Cell<usize>,
566 pub(crate) cursor: Cell<Option<(u16, u16)>>,
568 pub(crate) term: Option<Rc<RefCell<dyn Terminal<Error>>>>,
570 pub(crate) clear_terminal: Cell<bool>,
572 pub(crate) insert_before: Cell<InsertBefore>,
574
575 pub(crate) timers: Option<Rc<Timers>>,
577 pub(crate) tasks: Option<Rc<ThreadPool<Event, Error>>>,
579 #[cfg(feature = "async")]
581 pub(crate) tokio: Option<Rc<TokioTasks<Event, Error>>>,
582 pub(crate) queue: ControlQueue<Event, Error>,
584}
585
586struct InsertBefore {
587 height: u16,
588 draw_fn: Box<dyn FnOnce(&mut Buffer)>,
589}
590
591impl Debug for InsertBefore {
592 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
593 f.debug_struct("InsertBefore")
594 .field("height", &self.height)
595 .field("draw_fn", &"dyn Fn()")
596 .finish()
597 }
598}
599
600impl Default for InsertBefore {
601 fn default() -> Self {
602 Self {
603 height: 0,
604 draw_fn: Box::new(|_| {}),
605 }
606 }
607}
608
609impl<Event, Error> Debug for SalsaAppContext<Event, Error> {
610 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
611 let mut ff = f.debug_struct("AppContext");
612 ff.field("focus", &self.focus)
613 .field("count", &self.count)
614 .field("cursor", &self.cursor)
615 .field("clear_terminal", &self.clear_terminal)
616 .field("insert_before", &"n/a")
617 .field("timers", &self.timers)
618 .field("tasks", &self.tasks)
619 .field("queue", &self.queue);
620 #[cfg(feature = "async")]
621 {
622 ff.field("tokio", &self.tokio);
623 }
624 ff.finish()
625 }
626}
627
628impl<Event, Error> Default for SalsaAppContext<Event, Error>
629where
630 Event: 'static,
631 Error: 'static,
632{
633 fn default() -> Self {
634 Self {
635 focus: Default::default(),
636 count: Default::default(),
637 cursor: Default::default(),
638 term: Default::default(),
639 clear_terminal: Default::default(),
640 insert_before: Default::default(),
641 timers: Default::default(),
642 tasks: Default::default(),
643 #[cfg(feature = "async")]
644 tokio: Default::default(),
645 queue: Default::default(),
646 }
647 }
648}
649
650impl<Event, Error> SalsaContext<Event, Error> for SalsaAppContext<Event, Error>
651where
652 Event: 'static,
653 Error: 'static,
654{
655 #[inline]
656 fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>) {
657 *self = app_ctx;
658 }
659
660 #[inline]
661 fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error> {
662 self
663 }
664}
665
666mod _private {
667 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
668 pub struct NonExhaustive;
669}