Structuring a Bigger App
The handler in Getting Started is one match with four arms. That's perfect for
two pages. For ten pages with real functionality, we do it like this:
give each page its own module, and make the center dumb routing. Adding a
page becomes "write a file, add two lines" — you never touch the other pages.
One module per page
Each page owns the two things the runtime asks about it: its page_spec (what's
on the page) and its handle (what its actions do). Nothing else knows about
Home except home.rs:
#![allow(unused)] fn main() { // pages/home.rs use tui_pages::prelude::*; use crate::app::{Action, View, State}; pub fn page_spec(state: &State) -> PageSpec { PageSpec::new() .focus(PageFocusBuilder::new().button(0).button(1)) .modes(vec![modes::GENERAL, modes::GLOBAL]) } pub fn handle(action: Action, ctx: &ActionContext<View>, state: &mut State) -> ActionOutcome<View> { match action { Action::Select => match ctx.focus { Some(FocusTarget::Button(0)) => ActionOutcome::effect(TuiEffect::Navigate(View::Settings)), _ => ActionOutcome::none(), }, _ => ActionOutcome::none(), } } }
settings.rs, editor.rs, and the other seven look the same: their own focus
targets, their own logic. They're independent — you can write, read, or delete
one without opening the others.
The center just routes
Now app.rs holds no page logic at all. The page function routes to the right
module:
#![allow(unused)] fn main() { // app.rs fn page_spec(view: &View, state: &State, _focus: Option<&FocusTarget>) -> PageSpec { match view { View::Home => home::page_spec(state), View::Settings => settings::page_spec(state), View::Editor => editor::page_spec(state), // ...one arm per page } } }
and so does the handler:
#![allow(unused)] fn main() { impl TuiActionHandler<View, Action, State> for Handler { type Error = std::convert::Infallible; fn handle_action(&mut self, action: Action, ctx: ActionContext<View>, state: &mut State) -> Result<ActionOutcome<View>, Self::Error> { Ok(match ctx.current_view { View::Home => home::handle(action, &ctx, state), View::Settings => settings::handle(action, &ctx, state), View::Editor => editor::handle(action, &ctx, state), }) } } }
match ctx.current_viewassumesViewisCopy(it is in all the examples). If yours isn't, match on a reference instead —match &ctx.current_view— so the&ctxyou pass to the page is still valid.
That's the backbone. Everything below is for keeping it tidy as the app grows.
Handle global actions once
Some actions mean the same thing on every page — quit, move focus, open the command palette. Don't repeat them in ten modules. Catch them in the router before you delegate, and only fall through to the page for page-specific work:
#![allow(unused)] fn main() { fn handle_action(&mut self, action: Action, ctx: ActionContext<View>, state: &mut State) -> Result<ActionOutcome<View>, Self::Error> { // Global: handled once, the same everywhere. match action { Action::Quit => return Ok(ActionOutcome::effect(TuiEffect::Quit)), Action::FocusNext => return Ok(ActionOutcome::effect(TuiEffect::Focus(FocusIntent::Next))), Action::FocusPrev => return Ok(ActionOutcome::effect(TuiEffect::Focus(FocusIntent::Prev))), _ => {} } // Page-specific: route to wherever we are. Ok(match ctx.current_view { View::Home => home::handle(action, &ctx, state), View::Settings => settings::handle(action, &ctx, state), View::Editor => editor::handle(action, &ctx, state), }) } }
A page's handle now only deals with what's genuinely its own.
When the Action enum gets big
Ten pages with lots of functionality means a lot of action variants. A single
flat enum still works, but you can group per page and let each module own its
slice. A is your type — nest it:
#![allow(unused)] fn main() { pub enum Action { // shared across pages FocusNext, FocusPrev, Quit, // one group per page Editor(EditorAction), Settings(SettingsAction), } pub enum EditorAction { Save, Format, ToggleWrap, /* ... */ } }
Then the router hands the inner enum straight to the page, which never sees anything but its own actions:
#![allow(unused)] fn main() { match action { Action::Editor(a) => editor::handle(a, &ctx, state), // a: EditorAction Action::Settings(a) => settings::handle(a, &ctx, state), other => global::handle(other, &ctx, state), } }
Do the same for View if the page list itself gets large
(View::Editor(EditorView)), so the top-level enums stay small.
Where state lives
All your pages share one State (the S type), but each page usually cares
about its own slice. Give each one a field — or its own sub-struct — and pages
read and write only what's theirs:
#![allow(unused)] fn main() { pub struct State { pub editor: EditorState, pub settings: SettingsState, pub user: User, // shared } }
editor::handle touches state.editor; settings::handle touches
state.settings. The runtime passes the whole &mut State through; the
discipline of "each page stays in its lane" is yours to keep.
Keys: shared by default, per-page when needed
Bindings are registered once on the builder, keyed by mode — not per page. So
every page on modes::GENERAL shares Tab, Enter, and the rest for free, and
you bind them in one place. A key only needs special handling when it means
something different on one page, and you have two ways to do that:
-
Branch in that page's
handleonctx.focus/ctx.current_view(what the examples do) — good for "Enter does X here, Y there". -
Give the page its own mode when it has a whole different keymap (a text editor, a modal tool):
#![allow(unused)] fn main() { const EDITOR: ModeId = ModeId::borrowed("editor"); // bound once on the builder: .bind(EDITOR, "ctrl+s", Action::Editor(EditorAction::Save)) // and the page turns that keymap on: PageSpec::new().modes(vec![EDITOR, modes::GLOBAL]) }
Adding a page, start to finish
That's the payoff. To add page number eleven:
- Write
pages/foo.rswithpage_specandhandle. - Add one arm to the
page_specrouter and one to the handler router. - (Only if it needs them) bind its keys, or its own mode, on the builder.
You don't open any other page's file. That's the architecture the README promised — a hundred pages, each minding its own business, with no shared god object in the middle.