#![doc = include_str!("../readme.md")]
#![allow(clippy::uninlined_format_args)]
use crate::framework::control_queue::ControlQueue;
use crate::tasks::{Cancel, Liveness};
use crate::thread_pool::ThreadPool;
use crate::timer::{TimerDef, TimerHandle, Timers};
#[cfg(feature = "async")]
use crate::tokio_tasks::TokioTasks;
use crossbeam::channel::{SendError, Sender};
use rat_event::{ConsumedEvent, HandleEvent, Outcome, Regular};
use rat_focus::Focus;
use ratatui_core::buffer::Buffer;
use ratatui_core::terminal::Terminal;
use ratatui_crossterm::CrosstermBackend;
use std::cell::{Cell, Ref, RefCell, RefMut};
use std::fmt::{Debug, Formatter};
#[cfg(feature = "async")]
use std::future::Future;
use std::io::Stdout;
use std::rc::Rc;
use std::time::Duration;
#[cfg(feature = "async")]
use tokio::task::AbortHandle;
#[cfg(feature = "dialog")]
pub use try_as_traits::{TryAsMut, TryAsRef, TypedContainer};
mod control;
mod framework;
mod run_config;
mod thread_pool;
#[cfg(feature = "async")]
mod tokio_tasks;
pub use control::Control;
pub use framework::run_tui;
pub use run_config::{RunConfig, TermInit};
#[cfg(feature = "dialog")]
pub mod dialog_stack;
pub mod event;
pub mod mock;
pub mod poll;
pub mod tasks;
pub mod timer;
pub trait SalsaContext<Event, Error>
where
Event: 'static,
Error: 'static,
{
fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>);
fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error>;
fn count(&self) -> usize {
self.salsa_ctx().count.get()
}
fn last_render(&self) -> Duration {
self.salsa_ctx().last_render.get()
}
fn last_event(&self) -> Duration {
self.salsa_ctx().last_event.get()
}
fn set_window_title(&self, title: String) {
self.salsa_ctx().window_title.set(Some(title));
}
fn set_screen_cursor(&self, cursor: Option<(u16, u16)>) {
if let Some(c) = cursor {
self.salsa_ctx().cursor.set(Some(c));
}
}
#[inline]
fn add_timer(&self, t: TimerDef) -> TimerHandle {
self.salsa_ctx()
.timers
.as_ref()
.expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
.add(t)
}
#[inline]
fn remove_timer(&self, tag: TimerHandle) {
self.salsa_ctx()
.timers
.as_ref()
.expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
.remove(tag);
}
#[inline]
fn replace_timer(&self, h: Option<TimerHandle>, t: TimerDef) -> TimerHandle {
if let Some(h) = h {
self.remove_timer(h);
}
self.add_timer(t)
}
#[inline]
fn spawn_ext(
&self,
task: impl FnOnce(
Cancel,
&Sender<Result<Control<Event>, Error>>,
) -> Result<Control<Event>, Error>
+ Send
+ 'static,
) -> Result<(Cancel, Liveness), SendError<()>>
where
Event: 'static + Send,
Error: 'static + Send,
{
self.salsa_ctx()
.tasks
.as_ref()
.expect(
"No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
)
.spawn(Box::new(task))
}
#[inline]
fn spawn(
&self,
task: impl FnOnce() -> Result<Control<Event>, Error> + Send + 'static,
) -> Result<(), SendError<()>>
where
Event: 'static + Send,
Error: 'static + Send,
{
_ = self
.salsa_ctx()
.tasks
.as_ref()
.expect(
"No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
)
.spawn(Box::new(|_, _| task()))?;
Ok(())
}
#[inline]
#[cfg(feature = "async")]
fn spawn_async<F>(&self, future: F)
where
F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
Event: 'static + Send,
Error: 'static + Send,
{
_ = self.salsa_ctx() .tokio
.as_ref()
.expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))")
.spawn(Box::new(future));
}
#[inline]
#[cfg(feature = "async")]
fn spawn_async_ext<C, F>(&self, cr_future: C) -> (AbortHandle, Liveness)
where
C: FnOnce(tokio::sync::mpsc::Sender<Result<Control<Event>, Error>>) -> F,
F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
Event: 'static + Send,
Error: 'static + Send,
{
let rt = self
.salsa_ctx() .tokio
.as_ref()
.expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))");
let future = cr_future(rt.sender());
rt.spawn(Box::new(future))
}
#[inline]
fn queue_event(&self, event: Event) {
self.salsa_ctx().queue.push(Ok(Control::Event(event)));
}
#[inline]
fn queue(&self, ctrl: impl Into<Control<Event>>) {
self.salsa_ctx().queue.push(Ok(ctrl.into()));
}
#[inline]
fn queue_err(&self, err: Error) {
self.salsa_ctx().queue.push(Err(err));
}
#[inline]
fn set_focus(&self, focus: Focus) {
self.salsa_ctx().focus.replace(Some(focus));
}
#[inline]
fn take_focus(&self) -> Option<Focus> {
self.salsa_ctx().focus.take()
}
#[inline]
fn clear_focus(&self) {
self.salsa_ctx().focus.replace(None);
}
#[inline]
fn focus<'a>(&'a self) -> Ref<'a, Focus> {
let borrow = self.salsa_ctx().focus.borrow();
Ref::map(borrow, |v| v.as_ref().expect("focus"))
}
#[inline]
fn focus_mut<'a>(&'a mut self) -> RefMut<'a, Focus> {
let borrow = self.salsa_ctx().focus.borrow_mut();
RefMut::map(borrow, |v| v.as_mut().expect("focus"))
}
#[inline]
fn handle_focus<E>(&mut self, event: &E) -> Outcome
where
Focus: HandleEvent<E, Regular, Outcome>,
{
let mut borrow = self.salsa_ctx().focus.borrow_mut();
let focus = borrow.as_mut().expect("focus");
let r = focus.handle(event, Regular);
if r.is_consumed() {
self.queue(r);
}
r
}
#[inline]
fn terminal(&mut self) -> Rc<RefCell<Terminal<CrosstermBackend<Stdout>>>> {
self.salsa_ctx().term.clone().expect("terminal")
}
#[inline]
fn clear_terminal(&mut self) {
self.salsa_ctx().clear_terminal.set(true);
}
#[inline]
fn insert_before(&mut self, height: u16, draw_fn: impl FnOnce(&mut Buffer) + 'static) {
self.salsa_ctx().insert_before.set(InsertBefore {
height,
draw_fn: Box::new(draw_fn),
});
}
}
pub struct SalsaAppContext<Event, Error>
where
Event: 'static,
Error: 'static,
{
pub(crate) focus: RefCell<Option<Focus>>,
pub(crate) count: Cell<usize>,
pub(crate) cursor: Cell<Option<(u16, u16)>>,
pub(crate) term: Option<Rc<RefCell<Terminal<CrosstermBackend<Stdout>>>>>,
pub(crate) window_title: Cell<Option<String>>,
pub(crate) clear_terminal: Cell<bool>,
pub(crate) insert_before: Cell<InsertBefore>,
pub(crate) last_render: Cell<Duration>,
pub(crate) last_event: Cell<Duration>,
pub(crate) timers: Option<Rc<Timers>>,
pub(crate) tasks: Option<Rc<ThreadPool<Event, Error>>>,
#[cfg(feature = "async")]
pub(crate) tokio: Option<Rc<TokioTasks<Event, Error>>>,
pub(crate) queue: ControlQueue<Event, Error>,
}
struct InsertBefore {
height: u16,
draw_fn: Box<dyn FnOnce(&mut Buffer)>,
}
impl Debug for InsertBefore {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InsertBefore")
.field("height", &self.height)
.field("draw_fn", &"dyn Fn()")
.finish()
}
}
impl Default for InsertBefore {
fn default() -> Self {
Self {
height: 0,
draw_fn: Box::new(|_| {}),
}
}
}
impl<Event, Error> Debug for SalsaAppContext<Event, Error> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut ff = f.debug_struct("AppContext");
ff.field("focus", &self.focus)
.field("count", &self.count)
.field("cursor", &self.cursor)
.field("clear_terminal", &self.clear_terminal)
.field("insert_before", &"n/a")
.field("timers", &self.timers)
.field("tasks", &self.tasks)
.field("queue", &self.queue);
#[cfg(feature = "async")]
{
ff.field("tokio", &self.tokio);
}
ff.finish()
}
}
impl<Event, Error> Default for SalsaAppContext<Event, Error>
where
Event: 'static,
Error: 'static,
{
fn default() -> Self {
Self {
focus: Default::default(),
count: Default::default(),
cursor: Default::default(),
term: Default::default(),
window_title: Default::default(),
clear_terminal: Default::default(),
insert_before: Default::default(),
last_render: Default::default(),
last_event: Default::default(),
timers: Default::default(),
tasks: Default::default(),
#[cfg(feature = "async")]
tokio: Default::default(),
queue: Default::default(),
}
}
}
impl<Event, Error> SalsaContext<Event, Error> for SalsaAppContext<Event, Error>
where
Event: 'static,
Error: 'static,
{
#[inline]
fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>) {
*self = app_ctx;
}
#[inline]
fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error> {
self
}
}
mod _private {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NonExhaustive;
}