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_core::buffer::Buffer;
13use ratatui_core::terminal::Terminal;
14use ratatui_crossterm::CrosstermBackend;
15use std::cell::{Cell, Ref, RefCell, RefMut};
16use std::fmt::{Debug, Formatter};
17#[cfg(feature = "async")]
18use std::future::Future;
19use std::io::Stdout;
20use std::rc::Rc;
21use std::time::Duration;
22#[cfg(feature = "async")]
23use tokio::task::AbortHandle;
24
25#[cfg(feature = "dialog")]
26pub use try_as_traits::{TryAsMut, TryAsRef, TypedContainer};
27
28mod control;
29mod framework;
30mod run_config;
31mod thread_pool;
32#[cfg(feature = "async")]
33mod tokio_tasks;
34
35pub use control::Control;
36pub use framework::run_tui;
37pub use run_config::{RunConfig, TermInit};
38
39#[cfg(feature = "dialog")]
40pub mod dialog_stack;
41pub mod event;
43pub mod mock;
45pub mod poll;
47pub mod tasks;
49pub mod timer;
51
52pub trait SalsaContext<Event, Error>
61where
62 Event: 'static,
63 Error: 'static,
64{
65 fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>);
69
70 fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error>;
72
73 fn count(&self) -> usize {
75 self.salsa_ctx().count.get()
76 }
77
78 fn last_render(&self) -> Duration {
80 self.salsa_ctx().last_render.get()
81 }
82
83 fn last_event(&self) -> Duration {
85 self.salsa_ctx().last_event.get()
86 }
87
88 fn set_window_title(&self, title: String) {
90 self.salsa_ctx().window_title.set(Some(title));
91 }
92
93 fn set_screen_cursor(&self, cursor: Option<(u16, u16)>) {
98 if let Some(c) = cursor {
99 self.salsa_ctx().cursor.set(Some(c));
100 }
101 }
102
103 #[inline]
109 fn add_timer(&self, t: TimerDef) -> TimerHandle {
110 self.salsa_ctx()
111 .timers
112 .as_ref()
113 .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
114 .add(t)
115 }
116
117 #[inline]
123 fn remove_timer(&self, tag: TimerHandle) {
124 self.salsa_ctx()
125 .timers
126 .as_ref()
127 .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
128 .remove(tag);
129 }
130
131 #[inline]
139 fn replace_timer(&self, h: Option<TimerHandle>, t: TimerDef) -> TimerHandle {
140 if let Some(h) = h {
141 self.remove_timer(h);
142 }
143 self.add_timer(t)
144 }
145
146 #[inline]
174 fn spawn_ext(
175 &self,
176 task: impl FnOnce(
177 Cancel,
178 &Sender<Result<Control<Event>, Error>>,
179 ) -> Result<Control<Event>, Error>
180 + Send
181 + 'static,
182 ) -> Result<(Cancel, Liveness), SendError<()>>
183 where
184 Event: 'static + Send,
185 Error: 'static + Send,
186 {
187 self.salsa_ctx()
188 .tasks
189 .as_ref()
190 .expect(
191 "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
192 )
193 .spawn(Box::new(task))
194 }
195
196 #[inline]
209 fn spawn(
210 &self,
211 task: impl FnOnce() -> Result<Control<Event>, Error> + Send + 'static,
212 ) -> Result<(), SendError<()>>
213 where
214 Event: 'static + Send,
215 Error: 'static + Send,
216 {
217 _ = self
218 .salsa_ctx()
219 .tasks
220 .as_ref()
221 .expect(
222 "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
223 )
224 .spawn(Box::new(|_, _| task()))?;
225 Ok(())
226 }
227
228 #[inline]
234 #[cfg(feature = "async")]
235 fn spawn_async<F>(&self, future: F)
236 where
237 F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
238 Event: 'static + Send,
239 Error: 'static + Send,
240 {
241 _ = self.salsa_ctx() .tokio
243 .as_ref()
244 .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))")
245 .spawn(Box::new(future));
246 }
247
248 #[inline]
264 #[cfg(feature = "async")]
265 fn spawn_async_ext<C, F>(&self, cr_future: C) -> (AbortHandle, Liveness)
266 where
267 C: FnOnce(tokio::sync::mpsc::Sender<Result<Control<Event>, Error>>) -> F,
268 F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
269 Event: 'static + Send,
270 Error: 'static + Send,
271 {
272 let rt = self
273 .salsa_ctx() .tokio
275 .as_ref()
276 .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))");
277 let future = cr_future(rt.sender());
278 rt.spawn(Box::new(future))
279 }
280
281 #[inline]
283 fn queue_event(&self, event: Event) {
284 self.salsa_ctx().queue.push(Ok(Control::Event(event)));
285 }
286
287 #[inline]
289 fn queue(&self, ctrl: impl Into<Control<Event>>) {
290 self.salsa_ctx().queue.push(Ok(ctrl.into()));
291 }
292
293 #[inline]
295 fn queue_err(&self, err: Error) {
296 self.salsa_ctx().queue.push(Err(err));
297 }
298
299 #[inline]
301 fn set_focus(&self, focus: Focus) {
302 self.salsa_ctx().focus.replace(Some(focus));
303 }
304
305 #[inline]
307 fn take_focus(&self) -> Option<Focus> {
308 self.salsa_ctx().focus.take()
309 }
310
311 #[inline]
313 fn clear_focus(&self) {
314 self.salsa_ctx().focus.replace(None);
315 }
316
317 #[inline]
323 fn focus<'a>(&'a self) -> Ref<'a, Focus> {
324 let borrow = self.salsa_ctx().focus.borrow();
325 Ref::map(borrow, |v| v.as_ref().expect("focus"))
326 }
327
328 #[inline]
334 fn focus_mut<'a>(&'a mut self) -> RefMut<'a, Focus> {
335 let borrow = self.salsa_ctx().focus.borrow_mut();
336 RefMut::map(borrow, |v| v.as_mut().expect("focus"))
337 }
338
339 #[inline]
345 fn handle_focus<E>(&mut self, event: &E) -> Outcome
346 where
347 Focus: HandleEvent<E, Regular, Outcome>,
348 {
349 let mut borrow = self.salsa_ctx().focus.borrow_mut();
350 let focus = borrow.as_mut().expect("focus");
351 let r = focus.handle(event, Regular);
352 if r.is_consumed() {
353 self.queue(r);
354 }
355 r
356 }
357
358 #[inline]
360 fn terminal(&mut self) -> Rc<RefCell<Terminal<CrosstermBackend<Stdout>>>> {
361 self.salsa_ctx().term.clone().expect("terminal")
362 }
363
364 #[inline]
366 fn clear_terminal(&mut self) {
367 self.salsa_ctx().clear_terminal.set(true);
368 }
369
370 #[inline]
372 fn insert_before(&mut self, height: u16, draw_fn: impl FnOnce(&mut Buffer) + 'static) {
373 self.salsa_ctx().insert_before.set(InsertBefore {
374 height,
375 draw_fn: Box::new(draw_fn),
376 });
377 }
378}
379
380pub struct SalsaAppContext<Event, Error>
389where
390 Event: 'static,
391 Error: 'static,
392{
393 pub(crate) focus: RefCell<Option<Focus>>,
395 pub(crate) count: Cell<usize>,
397 pub(crate) cursor: Cell<Option<(u16, u16)>>,
399 pub(crate) term: Option<Rc<RefCell<Terminal<CrosstermBackend<Stdout>>>>>,
401 pub(crate) window_title: Cell<Option<String>>,
403 pub(crate) clear_terminal: Cell<bool>,
405 pub(crate) insert_before: Cell<InsertBefore>,
407 pub(crate) last_render: Cell<Duration>,
409 pub(crate) last_event: Cell<Duration>,
411
412 pub(crate) timers: Option<Rc<Timers>>,
414 pub(crate) tasks: Option<Rc<ThreadPool<Event, Error>>>,
416 #[cfg(feature = "async")]
418 pub(crate) tokio: Option<Rc<TokioTasks<Event, Error>>>,
419 pub(crate) queue: ControlQueue<Event, Error>,
421}
422
423struct InsertBefore {
424 height: u16,
425 draw_fn: Box<dyn FnOnce(&mut Buffer)>,
426}
427
428impl Debug for InsertBefore {
429 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
430 f.debug_struct("InsertBefore")
431 .field("height", &self.height)
432 .field("draw_fn", &"dyn Fn()")
433 .finish()
434 }
435}
436
437impl Default for InsertBefore {
438 fn default() -> Self {
439 Self {
440 height: 0,
441 draw_fn: Box::new(|_| {}),
442 }
443 }
444}
445
446impl<Event, Error> Debug for SalsaAppContext<Event, Error> {
447 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
448 let mut ff = f.debug_struct("AppContext");
449 ff.field("focus", &self.focus)
450 .field("count", &self.count)
451 .field("cursor", &self.cursor)
452 .field("clear_terminal", &self.clear_terminal)
453 .field("insert_before", &"n/a")
454 .field("timers", &self.timers)
455 .field("tasks", &self.tasks)
456 .field("queue", &self.queue);
457 #[cfg(feature = "async")]
458 {
459 ff.field("tokio", &self.tokio);
460 }
461 ff.finish()
462 }
463}
464
465impl<Event, Error> Default for SalsaAppContext<Event, Error>
466where
467 Event: 'static,
468 Error: 'static,
469{
470 fn default() -> Self {
471 Self {
472 focus: Default::default(),
473 count: Default::default(),
474 cursor: Default::default(),
475 term: Default::default(),
476 window_title: Default::default(),
477 clear_terminal: Default::default(),
478 insert_before: Default::default(),
479 last_render: Default::default(),
480 last_event: Default::default(),
481 timers: Default::default(),
482 tasks: Default::default(),
483 #[cfg(feature = "async")]
484 tokio: Default::default(),
485 queue: Default::default(),
486 }
487 }
488}
489
490impl<Event, Error> SalsaContext<Event, Error> for SalsaAppContext<Event, Error>
491where
492 Event: 'static,
493 Error: 'static,
494{
495 #[inline]
496 fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>) {
497 *self = app_ctx;
498 }
499
500 #[inline]
501 fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error> {
502 self
503 }
504}
505
506mod _private {
507 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
508 pub struct NonExhaustive;
509}