Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting Started

A whole app, top to bottom. It's two pages (Home and About) with two buttons — Tab moves between them, Enter activates, Ctrl+C quits. This is the minimal example; run it with cd examples/minimal && cargo run.

It's three files: app.rs is everything that talks to the library, ui.rs just draws, main.rs is the loop. Start with app.rs.

Your types

A View is a page. An Action is something that can happen. You define both.

#![allow(unused)]
fn main() {
use tui_pages::prelude::*;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum View {
    Home,
    About
}

#[derive(Debug, Clone, Copy)]
pub enum Action {
    FocusNext,
    FocusPrev,
    Select,
    Quit
}

pub type App = TuiApp<View, Action, (), Handler>;   // () = no state in this app
}

The handler

An action comes in, you say what should happen. You don't move focus or change pages yourself — you return a TuiEffect and the runtime does it.

#![allow(unused)]
fn main() {
pub struct Handler;

impl TuiActionHandler<View, Action, ()> for Handler {
    type Error = std::convert::Infallible;

    fn handle_action(&mut self, action: Action, ctx: ActionContext<View>, _state: &mut ())
        -> Result<ActionOutcome<View>, Self::Error>
    {
        Ok(match action {
            Action::FocusNext => ActionOutcome::effect(TuiEffect::Focus(FocusIntent::Next)),
            Action::FocusPrev => ActionOutcome::effect(TuiEffect::Focus(FocusIntent::Prev)),
            Action::Quit      => ActionOutcome::effect(TuiEffect::Quit),

            // Enter means different things depending on where you are.
            // `ctx` tells you the page and what's focused.
            Action::Select => match (ctx.current_view, ctx.focus) {
                (View::Home,  Some(FocusTarget::Button(0))) => ActionOutcome::effect(TuiEffect::Navigate(View::About)),
                (View::About, Some(FocusTarget::Button(0))) => ActionOutcome::effect(TuiEffect::Navigate(View::Home)),
                (_,           Some(FocusTarget::Button(1))) => ActionOutcome::effect(TuiEffect::Quit),
                _ => ActionOutcome::none(),
            },
        })
    }
}
}

That match action { … } is your whole app's logic. Every arm returns an effect.

The page spec

For each view, say what's on it: the focusable things (two buttons) and which key modes are active.

#![allow(unused)]
fn main() {
fn page_spec(_view: &View, _state: &(), _focus: Option<&FocusTarget>) -> PageSpec {
    PageSpec::new()
        .focus(PageFocusBuilder::new().button(0).button(1))
        .modes(vec![modes::GENERAL, modes::GLOBAL])
}
}

Build it

Bind keys to actions and assemble the runtime. GENERAL is the normal mode; GLOBAL is always on, so Ctrl+C quits anywhere.

#![allow(unused)]
fn main() {
pub fn build() -> App {
    let mut app = TuiPages::builder(View::Home)
        .page_fn(page_spec)
        .handler(Handler)
        .bind(modes::GENERAL, "tab",       Action::FocusNext)
        .bind(modes::GENERAL, "shift+tab", Action::FocusPrev)
        .bind(modes::GENERAL, "enter",     Action::Select)
        .bind(modes::GLOBAL,  "ctrl+c",    Action::Quit)
        .build();
    app.refresh_page(&());   // load the first page's focus before drawing
    app
}
}

That's all of app.rs. Now the loop in main.rs.

The loop

Draw, read a key, hand it to handle_key. When an effect asks to quit, stop.

fn main() -> anyhow::Result<()> {
    let _guard = tui_pages::terminal::enter()?;   // raw mode; restores on drop/panic
    let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;

    let mut tui = app::build();
    let mut state = ();

    loop {
        terminal.draw(|frame| ui::render(frame, *tui.current_view(), tui.focus.current()))?;

        if let crossterm::event::Event::Key(key) = crossterm::event::read()? {
            if tui.handle_key(key, &mut state)?.quit_requested {
                break;
            }
        }
    }
    Ok(())
}

ui::render gets the current view and the focused element and draws them — it reads state, never changes it. (It's plain ratatui; look at examples/minimal/src/ui.rs.)

That's the loop

Read a key → handle_key turns it into an action → your handler returns an effect → the runtime applies it → draw again.

The rest of the book is just more effects you can return (focus, navigation, panes) and more you can put in a PageSpec.