ratflow 0.4.0

A minimalistic framework for building TUI applications using a reactive architecture.
Documentation
use crate::channel::broadcast;
use crate::channel::oneshot;
use crate::{Component, Events, Render};
use futures_lite::Stream;
use futures_lite::StreamExt;
use futures_lite::future::or;
use futures_lite::future::yield_now;
use ratatui::Terminal;
use ratatui::prelude::Backend;
use std::cell::Ref;
use std::cell::RefCell;
use std::cell::RefMut;
use std::fmt::Debug;
use std::rc::Rc;
use sycamore_reactive::{
    RootHandle, Signal, batch, create_effect, create_root, create_signal, provide_context,
};

#[derive(Debug, Clone, Copy)]
pub struct Runtime {
    quit: Signal<bool>,
    redraw: Signal<()>,
}

impl Runtime {
    #[inline]
    pub fn quit(&self) {
        self.quit.set(true);
    }

    #[inline]
    pub fn force_redraw(&self) {
        self.redraw.set(());
    }
}

pub struct ReactiveApp<B: Backend, E> {
    root: RootHandle,
    event_tx: broadcast::Sender<E>,
    quit_rx: oneshot::Receiver<Result<(), B::Error>>,
    terminal: Rc<RefCell<Terminal<B>>>,
}

impl<B: Backend, E> ReactiveApp<B, E> {
    pub fn new<F: Render + 'static, C: Component<F>>(
        app: C,
        terminal: Terminal<B>,
    ) -> ReactiveApp<B, E>
    where
        B: 'static,
        E: 'static,
    {
        let (quit_tx, quit_rx) = oneshot::channel();
        let (event_tx, event_rx) = broadcast::channel();

        let terminal = Rc::new(RefCell::new(terminal));
        let root = create_root(|| {
            let quit = create_signal(false);
            let redraw = create_signal(());

            provide_context(Runtime { quit, redraw });
            provide_context(Events(event_rx));

            {
                let quit_tx = quit_tx.clone();
                create_effect(move || {
                    if quit.get() {
                        quit_tx.send(Ok(()));
                    }
                });
            }

            let app = batch(move || app.create());

            let terminal = terminal.clone();
            create_effect(move || {
                redraw.track();
                let mut terminal = terminal.borrow_mut();
                let res = terminal.draw(|frame| {
                    app.render(frame);
                });
                if let Err(err) = res {
                    quit_tx.send(Err(err));
                }
            });
        });

        ReactiveApp {
            root,
            event_tx,
            quit_rx,
            terminal,
        }
    }

    #[inline]
    pub fn terminal(&self) -> Ref<'_, Terminal<B>> {
        self.terminal.borrow()
    }

    #[inline]
    pub fn terminal_mut(&mut self) -> RefMut<'_, Terminal<B>> {
        self.terminal.borrow_mut()
    }

    #[inline]
    pub async fn handle_event(&self, event: E) {
        self.event_tx.send(event);
        yield_now().await;
    }

    #[inline]
    pub async fn wait(&mut self) -> Result<(), B::Error> {
        (&mut self.quit_rx).await
    }

    #[inline]
    pub async fn run<S: Stream<Item = Result<E, B::Error>> + Unpin>(
        mut self,
        mut event_stream: S,
    ) -> Result<(), B::Error> {
        or(&mut self.quit_rx, async {
            while let Some(event) = event_stream.next().await {
                self.event_tx.send(event?);
            }
            Ok(())
        })
        .await
    }
}

impl<B: Backend, E> Drop for ReactiveApp<B, E> {
    #[inline]
    fn drop(&mut self) {
        self.root.dispose();
    }
}