rat_salsa/
lib.rs

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;
41/// Event types.
42pub mod event;
43/// Provides dummy implementations for some functions.
44pub mod mock;
45/// Event sources.
46pub mod poll;
47/// Types used for both future tasks and thread tasks.
48pub mod tasks;
49/// Support for timers.
50pub mod timer;
51
52/// This trait gives access to all facilities built into rat-salsa.
53///
54/// Your global state struct has to implement this trait. This allows
55/// rat-salsa to add its facilities to it.
56///
57/// [run_tui] sets it during initialization, it will be up and
58/// running by the time init() is called.
59///
60pub trait SalsaContext<Event, Error>
61where
62    Event: 'static,
63    Error: 'static,
64{
65    /// The AppContext struct holds all the data for the rat-salsa
66    /// functionality. [run_tui] calls this to set the initialized
67    /// struct.
68    fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>);
69
70    /// Access the AppContext previously set.
71    fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error>;
72
73    /// Get the current frame/render-count.
74    fn count(&self) -> usize {
75        self.salsa_ctx().count.get()
76    }
77
78    /// Get the last render timing.
79    fn last_render(&self) -> Duration {
80        self.salsa_ctx().last_render.get()
81    }
82
83    /// Get the last event-handling timing.
84    fn last_event(&self) -> Duration {
85        self.salsa_ctx().last_event.get()
86    }
87
88    /// Set a window title.
89    fn set_window_title(&self, title: String) {
90        self.salsa_ctx().window_title.set(Some(title));
91    }
92
93    /// Set the cursor, if the given value is something,
94    /// hides it otherwise.
95    ///
96    /// This should only be set during rendering.
97    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    /// Add a timer.
104    ///
105    /// __Panic__
106    ///
107    /// Panics if no timer support is configured.
108    #[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    /// Remove a timer.
118    ///
119    /// __Panic__
120    ///
121    /// Panics if no timer support is configured.
122    #[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    /// Replace a timer.
132    /// Remove the old timer and create a new one.
133    /// If the old timer no longer exists it just creates the new one.
134    ///
135    /// __Panic__
136    ///
137    /// Panics if no timer support is configured.
138    #[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    /// Add a background worker task.
147    ///
148    /// ```rust ignore
149    /// let cancel = ctx.spawn(|cancel, send| {
150    ///     // ... do stuff
151    ///     if cancel.is_canceled() {
152    ///         return; // return early
153    ///     }
154    ///     Ok(Control::Continue)
155    /// });
156    /// ```
157    ///
158    /// - Cancel token
159    ///
160    /// The cancel token can be used by the application to signal an early
161    /// cancellation of a long-running task. This cancellation is cooperative,
162    /// the background task must regularly check for cancellation and quit
163    /// if needed.
164    ///
165    /// - Liveness token
166    ///
167    /// This token is set whenever the given task has finished, be it
168    /// regularly or by panicking.
169    ///
170    /// __Panic__
171    ///
172    /// Panics if no worker-thread support is configured.
173    #[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    /// Add a background worker task.
197    ///
198    /// ```rust ignore
199    /// let cancel = ctx.spawn(|| {
200    ///     // ...
201    ///     Ok(Control::Continue)
202    /// });
203    /// ```
204    ///
205    /// __Panic__
206    ///
207    /// Panics if no worker-thread support is configured.
208    #[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    /// Spawn a future in the executor.
229    ///
230    /// Panic
231    ///
232    /// Panics if tokio is not configured.
233    #[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() //
242            .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    /// Spawn a future in the executor.
249    /// You get an extra channel to send back more than one result.
250    ///
251    /// - AbortHandle
252    ///
253    /// The tokio AbortHandle to abort a spawned task.
254    ///
255    /// - Liveness
256    ///
257    /// This token is set whenever the given task has finished, be it
258    /// regularly or by panicking.
259    ///
260    /// Panic
261    ///
262    /// Panics if tokio is not configured.
263    #[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() //
274            .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    /// Queue an application event.
282    #[inline]
283    fn queue_event(&self, event: Event) {
284        self.salsa_ctx().queue.push(Ok(Control::Event(event)));
285    }
286
287    /// Queue additional results.
288    #[inline]
289    fn queue(&self, ctrl: impl Into<Control<Event>>) {
290        self.salsa_ctx().queue.push(Ok(ctrl.into()));
291    }
292
293    /// Queue an error.
294    #[inline]
295    fn queue_err(&self, err: Error) {
296        self.salsa_ctx().queue.push(Err(err));
297    }
298
299    /// Set the `Focus`.
300    #[inline]
301    fn set_focus(&self, focus: Focus) {
302        self.salsa_ctx().focus.replace(Some(focus));
303    }
304
305    /// Take the `Focus` back from the Context.
306    #[inline]
307    fn take_focus(&self) -> Option<Focus> {
308        self.salsa_ctx().focus.take()
309    }
310
311    /// Clear the `Focus`.
312    #[inline]
313    fn clear_focus(&self) {
314        self.salsa_ctx().focus.replace(None);
315    }
316
317    /// Access the `Focus`.
318    ///
319    /// __Panic__
320    ///
321    /// Panics if no focus has been set.
322    #[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    /// Mutably access the focus-field.
329    ///
330    /// __Panic__
331    ///
332    /// Panics if no focus has been set.
333    #[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    /// Handle the focus-event and automatically queue the result.
340    ///
341    /// __Panic__
342    ///
343    /// Panics if no focus has been set.
344    #[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    /// Access the terminal.
359    #[inline]
360    fn terminal(&mut self) -> Rc<RefCell<Terminal<CrosstermBackend<Stdout>>>> {
361        self.salsa_ctx().term.clone().expect("terminal")
362    }
363
364    /// Clear the terminal and do a full redraw before the next draw.
365    #[inline]
366    fn clear_terminal(&mut self) {
367        self.salsa_ctx().clear_terminal.set(true);
368    }
369
370    /// Call insert_before() before the next draw.
371    #[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
380///
381/// Application context for event handling.
382///
383/// Add this to your global state and implement [SalsaContext] to
384/// access the facilities of rat-salsa. You can Default::default()
385/// initialize this field with some dummy values. It will
386/// be set correctly when calling [run_tui].
387///
388pub struct SalsaAppContext<Event, Error>
389where
390    Event: 'static,
391    Error: 'static,
392{
393    /// Can be set to hold a Focus, if needed.
394    pub(crate) focus: RefCell<Option<Focus>>,
395    /// Last frame count rendered.
396    pub(crate) count: Cell<usize>,
397    /// Output cursor position. Set to Frame after rendering is complete.
398    pub(crate) cursor: Cell<Option<(u16, u16)>>,
399    /// Terminal area
400    pub(crate) term: Option<Rc<RefCell<Terminal<CrosstermBackend<Stdout>>>>>,
401    /// Set the window title.
402    pub(crate) window_title: Cell<Option<String>>,
403    /// Clear terminal before next draw.
404    pub(crate) clear_terminal: Cell<bool>,
405    /// Call insert_before before the next draw.
406    pub(crate) insert_before: Cell<InsertBefore>,
407    /// Last render time.
408    pub(crate) last_render: Cell<Duration>,
409    /// Last event time.
410    pub(crate) last_event: Cell<Duration>,
411
412    /// Application timers.
413    pub(crate) timers: Option<Rc<Timers>>,
414    /// Background tasks.
415    pub(crate) tasks: Option<Rc<ThreadPool<Event, Error>>>,
416    /// Background tasks.
417    #[cfg(feature = "async")]
418    pub(crate) tokio: Option<Rc<TokioTasks<Event, Error>>>,
419    /// Queue foreground tasks.
420    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}