cocotte 0.1.1

A convenient way to make a Ratatui
Documentation
//! Main application orchestration and terminal lifecycle management.

use crate::sub_app::SubApp;
use crossterm::{
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use eyre::Result;
use ratatui::Frame;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Direction, Layout};
use std::io::Stdout;

struct RawModeGuard;

impl RawModeGuard {
    fn enter() -> eyre::Result<Self> {
        enable_raw_mode()?;
        Ok(Self)
    }
}

impl Drop for RawModeGuard {
    fn drop(&mut self) {
        let _ = disable_raw_mode();
    }
}

struct AlternateScreenGuard;

impl AlternateScreenGuard {
    fn enter() -> eyre::Result<Self> {
        execute!(std::io::stdout(), EnterAlternateScreen)?;
        Ok(Self)
    }
}

impl Drop for AlternateScreenGuard {
    fn drop(&mut self) {
        let _ = execute!(std::io::stdout(), LeaveAlternateScreen);
    }
}

struct TerminalModeGuard {
    _raw: RawModeGuard,
    _alt: AlternateScreenGuard,
}

impl TerminalModeGuard {
    fn enter() -> eyre::Result<(Self, Terminal<CrosstermBackend<Stdout>>)> {
        let raw = RawModeGuard::enter()?;
        let alt = AlternateScreenGuard::enter()?;

        let stdout = std::io::stdout();
        let backend = CrosstermBackend::new(stdout);
        let terminal = Terminal::new(backend)?;

        Ok((
            Self {
                _raw: raw,
                _alt: alt,
            },
            terminal,
        ))
    }
}

/// A terminal application composed of multiple [`SubApp`] views.
///
/// `App` owns the terminal lifecycle and forwards input and rendering to each
/// sub-app in order.
pub struct App<E, V, S>
where
    V: SubApp<E, S>,
{
    _terminal_guard: TerminalModeGuard,
    terminal: Terminal<CrosstermBackend<Stdout>>,
    sub_apps: Vec<V>,
    _phantom: std::marker::PhantomData<(E, S)>,
}

impl<E, V, S> App<E, V, S>
where
    V: SubApp<E, S>,
{
    /// Creates a new application and enters terminal raw mode and the
    /// alternate screen.
    pub fn new(sub_apps: Vec<V>) -> Result<Self> {
        let (_terminal_guard, terminal) = TerminalModeGuard::enter()?;
        Ok(Self {
            _terminal_guard,
            sub_apps,
            _phantom: std::marker::PhantomData,
            terminal,
        })
    }

    /// Forwards an event to each sub-app.
    pub fn handle_input(&mut self, event: &mut E, state: &mut S) {
        for sub_app in &mut self.sub_apps {
            sub_app.handle_input(event, state);
        }
    }

    /// Draws all sub-apps using their declared layout constraints.
    pub fn draw(&mut self, state: &mut S) -> Result<()> {
        let sub_apps = &self.sub_apps;
        self.terminal
            .draw(|frame| render_app::<V, E, S>(sub_apps, state, frame))?;
        Ok(())
    }
}

fn render_app<V, E, S>(sub_apps: &[V], state: &mut S, frame: &mut Frame)
where
    V: SubApp<E, S>,
{
    let area = frame.area();
    let constraints: Vec<Constraint> = sub_apps.iter().map(|v| v.constraints()).collect();
    let areas = Layout::default()
        .direction(Direction::Vertical)
        .constraints(constraints)
        .split(area);

    for (sub_app, area) in sub_apps.iter().zip(areas.iter().copied()) {
        sub_app.render(frame, area, state);
    }
}