termrs_runtime 0.3.0

Runtime for termrs applications
Documentation
mod app;
mod app_event_context;
mod event_loop;

pub use app::*;
use app_event_context::*;
pub use event_loop::*;

use core::{
    render::{Position, Size},
    widget::Clipboard,
};
use std::{error::Error, io::stdout, marker::PhantomData};

use crossterm::terminal;

use core::{
    input::TerminalEvent,
    render::{RenderContext, WriteContext},
    widget::{self, Widget},
};

pub type AppRunResult = Result<(), RunFailure>;

#[derive(Debug)]
pub enum RunFailure {
    InputError(InputError),
    IOError(std::io::Error),
    Other(Box<dyn Error>),
}

#[derive(Debug)]
pub enum InputError {
    Unsupported,
    IO(std::io::Error),
}

pub trait Runtime<Message, A>
where
    A: App<Message>,
{
    fn run_app(
        &mut self,
        available_size: Size,
        clipboard: &mut impl Clipboard,
        render_context: &mut impl RenderContext,
        app: A,
    ) -> AppRunResult;
}

pub struct DefaultRuntime<Message, EL: EventLoop, A: App<Message>> {
    event_loop: EL,

    phantom_message: PhantomData<Message>,
    phantom_app: PhantomData<A>,
}

impl<Message, EL: EventLoop, A: App<Message>> DefaultRuntime<Message, EL, A> {
    pub fn new(event_loop: EL) -> Self {
        Self {
            event_loop,

            phantom_message: PhantomData,
            phantom_app: PhantomData,
        }
    }

    pub fn run_app_default(&mut self, app: A) -> AppRunResult {
        let mut stdout = stdout();

        // Set up
        {
            crossterm::execute!(
                stdout,
                crossterm::event::EnableFocusChange,
                crossterm::event::EnableMouseCapture,
                crossterm::event::EnableBracketedPaste,
            )
            .map_err(RunFailure::IOError)?;

            crossterm::terminal::enable_raw_mode().map_err(RunFailure::IOError)?;
        }

        let available_size = terminal::size()
            .map(|tuple| tuple.into())
            .map_err(RunFailure::IOError)?;
        let mut clipboard = widget::clipboard().map_err(RunFailure::Other)?;
        let mut render_context = WriteContext::new(&mut stdout);

        let result = self.run_app(available_size, &mut clipboard, &mut render_context, app);

        // Restore everyting
        {
            crossterm::terminal::disable_raw_mode().map_err(RunFailure::IOError)?;
            // TODO: Restore cursor state
        }

        result
    }
}

impl<Message, A: App<Message>> Default for DefaultRuntime<Message, DefaultEventLoop, A> {
    fn default() -> Self {
        Self {
            event_loop: DefaultEventLoop,
            phantom_message: Default::default(),
            phantom_app: Default::default(),
        }
    }
}

impl<Message, EL: EventLoop, A: App<Message>> Runtime<Message, A>
    for DefaultRuntime<Message, EL, A>
{
    fn run_app(
        &mut self,
        available_size: Size,
        clipboard: &mut impl Clipboard,
        render_context: &mut impl RenderContext,
        app: A,
    ) -> AppRunResult {
        let mut app = app;
        let mut available_size = available_size;

        let mut widget = app.view();
        let mut layout_info = widget.layout(available_size);
        let mut render_info = widget
            .render(Position::ZERO, &layout_info, render_context)
            .map_err(RunFailure::IOError)?;

        loop {
            let event = self.event_loop.poll();

            match event {
                Ok(TerminalEvent::App(event)) => {
                    let mut event_context = AppEventContext::new(
                        crossterm::cursor::position().unwrap().into(),
                        clipboard,
                    );

                    // let the app process the event
                    widget.on_event(&event, &render_info, &mut event_context);

                    // if app returned message
                    if let Some(message) = event_context.message() {
                        let mut exit_app = false;

                        drop(widget);
                        drop(layout_info);
                        drop(render_info);

                        // send the messsage and change the state
                        app.update(message, &mut exit_app);

                        // if exit was requested
                        if exit_app {
                            // do not render application, but just
                            // stop the loop and return the exit code
                            return AppRunResult::Ok(());
                        } else {
                            // Create new widget tree
                            // layout and render it
                            // handle result of rendering

                            // TODO: Do not duplicate code
                            widget = app.view();
                            layout_info = widget.layout(available_size);

                            let result =
                                widget.render(Position::ZERO, &layout_info, render_context);

                            match result {
                                // Continue to listen to events
                                Ok(value) => {
                                    render_info = value;
                                }
                                Err(error) => return Err(RunFailure::IOError(error)),
                            }
                        }
                    }
                }
                Ok(TerminalEvent::Resize(size)) => {
                    available_size = size;

                    // Create new widget tree
                    // layout and render it
                    // handle result of rendering

                    // TODO: Do not duplicate code
                    widget = app.view();
                    layout_info = widget.layout(available_size);

                    let result = widget.render(Position::ZERO, &layout_info, render_context);

                    match result {
                        // Continue to listen to events
                        Ok(value) => {
                            render_info = value;
                        }
                        Err(error) => return Err(RunFailure::IOError(error)),
                    }
                }
                Err(error) => return AppRunResult::Err(RunFailure::InputError(error)),
            };
        }
    }
}