cocotte 0.1.1

A convenient way to make a Ratatui
Documentation

cocotte

A convenient way to build Ratatui terminal applications by composing independent sub-apps.

Inspiration

--- You look sad, Grug. What is the matter?

--- Grug likes Ratatui. But Ratatui big! When Grug uses Ratatui, it takes all place in Grug's brain! Grug cannot do the things anymore!

--- Well Grug, you know, it's normal, tuis are kinda complicated when you think about it and...

--- Grug do not care! Grug only wants to care about what the little pieces do!

--- Mhhm. I think I know what you need, Grug. You need a Cocotte!

--- What is Cocotte?

--- A Cocotte is a good way to make a good Ratatui.

--- Is it dumb?

--- Yes Grug, do not worry.

Presentation

Split your TUI into vertical panes. Each pane handles its own input, rendering and manages its state. Each pane also manages the shared app state.

+-----------------------------+
|  Input (Constraint::Length) |
+-----------------------------+
|                             |
|  Browser (Constraint::Fill) |
|                             |
+-----------------------------+

Usage

Add to Cargo.toml:

[dependencies]
cocotte = "0.1"

Define an event type, a state type, one or more SubApp implementations, then wire them together with define_sub_apps!:

use cocotte::{define_sub_apps, SubApp};
use cocotte::eyre::Result;
use cocotte::ratatui::Frame;
use cocotte::ratatui::layout::{Constraint, Rect};
use cocotte::ratatui::widgets::{Block, Borders};
use crossterm::event::{self, Event, KeyCode, KeyEvent};

#[derive(Clone, Debug)]
enum AppEvent {
    Tick,
    Key(KeyEvent),
}

#[derive(Default)]
struct State {
    should_quit: bool,
}

struct Pane;

impl Pane {
    fn new() -> Self {
        Self
    }
}

impl SubApp<AppEvent, State> for Pane {
    fn handle_input(&mut self, event: &mut AppEvent, state: &mut State) {
        if let AppEvent::Key(KeyEvent {
            code: KeyCode::Char('q') | KeyCode::Esc,
            ..
        }) = event
        {
            state.should_quit = true;
        }
    }

    fn render(&self, frame: &mut Frame, area: Rect, _state: &mut State) {
        let block = Block::default().borders(Borders::ALL).title("Pane");
        frame.render_widget(block, area);
    }

    fn constraints(&self) -> Constraint {
        Constraint::Fill(1)
    }
}

define_sub_apps! {
    event = AppEvent;
    state = State;
    Pane(Pane) => Pane::new(),
}

fn main() -> Result<()> {
    let mut app = make_app()?;
    let mut state = State::default();
    let mut setup = AppEvent::Tick;
    app.handle_input(&mut setup, &mut state);

    while !state.should_quit {
        app.draw(&mut state)?;

        if let Event::Key(key) = event::read()? {
            let mut event = AppEvent::Key(key);
            app.handle_input(&mut event, &mut state);
        }
    }

    Ok(())
}

See examples/simple (cargo run --example simple) for a full file-browser example.

References

Grug by https://grugbrain.dev/

License

MIT