Structuring a Bigger App - tui-pages

## 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

# 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_view` assumes `View` is `Copy` (it is in all the examples). If yours isn't, match on a reference instead — `match &ctx.current_view` — so the `&ctx` you 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 `handle` on `ctx.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:

1. Write `pages/foo.rs` with `page_spec` and `handle`.
2. Add one arm to the `page_spec` router and one to the handler router.
3. (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.