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.