ratflow 0.2.0

A minimalistic framework for building TUI applications using a reactive architecture.
Documentation
use crate::on_drop::OnDrop;
use crate::{Component, Events, Render, on_resize};
use crossterm::event::{Event, EventStream};
use futures_util::stream::StreamExt;
use ratatui::Terminal;
use ratatui::prelude::Backend;
use std::cell::RefCell;
use std::io::Error;
use std::mem;
use std::rc::Rc;
use sycamore_reactive::{
    RootHandle, Signal, batch, create_effect, create_root, create_signal, provide_context,
    use_context,
};
use tokio::sync::broadcast;
use tokio::sync::oneshot;

type Quit = Rc<RefCell<Option<Result<(), std::io::Error>>>>;

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

impl Runtime {
    #[inline]
    pub fn quit(&self) {
        self.quit
            .update(|result| *(result.borrow_mut()) = Some(Ok(())));
    }

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

pub fn init_app<B: Backend<Error = Error> + 'static, F: Render + 'static, C: Component<F>>(
    app: C,
    terminal: &'static mut Terminal<B>,
) -> (
    RootHandle,
    broadcast::Sender<Event>,
    oneshot::Receiver<Result<(), Error>>,
) {
    let (tx, quit) = oneshot::channel();
    let mut quit_tx = Some(tx);
    let (events, rx) = broadcast::channel(10);

    let root = create_root(move || {
        let quit = create_signal(Rc::new(RefCell::new(None)));
        let redraw = create_signal(());
        provide_context(Runtime { quit, redraw });
        provide_context(Events(Rc::new(rx)));

        create_effect(move || {
            let runtime = use_context::<Runtime>();
            if let Some(result) = runtime.quit.get_clone().borrow_mut().take()
                && let Some(quit) = quit_tx.take()
            {
                quit.send(result).unwrap()
            }
        });
        on_resize(move |_, _| redraw.set(()));

        let app = batch(move || app.create());
        create_effect(move || {
            redraw.track();
            let res = terminal.draw(|frame| {
                app.render(frame);
            });
            if let Err(err) = res {
                quit.set(Rc::new(RefCell::new(Some(Err(err)))));
            }
        });
    });

    (root, events, quit)
}

pub async fn render_loop<
    B: Backend<Error = Error> + 'static,
    F: Render + 'static,
    C: Component<F>,
>(
    app: C,
    terminal: &mut Terminal<B>,
) -> Result<(), std::io::Error> {
    // SAFETY: we can extend `terminal`'s lifetime to be static
    // for the duration of our "render_loop" since we always
    // dispose of our reactive root when we return
    let terminal: &'static mut Terminal<B> = unsafe { std::mem::transmute(terminal) };
    let (root, events_tx, mut quit) = init_app(app, terminal);
    let cleanup = OnDrop(|| root.dispose());

    let mut events = EventStream::new();
    loop {
        tokio::select! {
            Some(terminal_event) = events.next() => {
                events_tx.send(terminal_event?).unwrap();
            },
            result = &mut quit => {
                mem::drop(cleanup);
                return result.unwrap();
            }
        }
    }
}