Core Concepts - 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

# Core Concepts

Five things to understand: the generic types, `PageSpec`, `TuiEffect`, `ActionOutcome`, and modes. Get these and the rest is detail.

## The generic types

`TuiPages` is generic over your application's types. It knows nothing concrete — you supply the nouns.

    #![allow(unused)]
    fn main() {
    TuiPages<V, A, S, Pages, Handler, O, M>
    //       │  │  │  │      │        │  └─ M: modal payload      (default ())
    //       │  │  │  │      │        └──── O: overlay id          (default ())
    //       │  │  │  │      └───────────── Handler: your TuiActionHandler
    //       │  │  │  └──────────────────── Pages: your page provider (a fn)
    //       │  │  └─────────────────────── S: your app state
    //       │  └────────────────────────── A: your action enum
    //       └───────────────────────────── V: your view/page enum
    }

A concrete example of each:

| example
--- | ---
`V` | `Home`, `Settings`, `Editor`
`A` | `Save`, `FocusNext`, `Quit`
`S` | a database handle, a document, or `()` for none
`O` | `Overlay::CommandBar`
`M` | `DialogData<Purpose>`

`O` and `M` default to `()`. An app with no overlays and no modals ignores them entirely — you only see five of these.

### Use the `TuiApp` alias

Spelling all seven slots out is noisy, and the `Pages` slot forces you to repeat `V` and `S`. The `TuiApp` alias collapses the common case (pages are a plain function):

    #![allow(unused)]
    fn main() {
    pub type TuiApp<V, A, S, Handler, O = (), M = ()> =
        TuiPages<V, A, S, PageFn<V, S, O>, Handler, O, M>;

    // so your app type is just:
    pub type App = TuiApp<View, Action, AppState, Handler, Overlay>;
    }

Build it with `TuiPages::builder(...).page_fn(page_spec)` — `.page_fn` pins the function-pointer type the alias expects.

## PageSpec — what a page is

For each view, your page function returns a `PageSpec`: the focusable elements and the input modes that are live.

    #![allow(unused)]
    fn main() {
    pub struct PageSpec<O = ()> {
        pub focus_targets: Vec<FocusTarget<O>>, // what you can move focus between
        pub modes: Vec<ModeId>,                 // which keymaps are active here
        pub accepts_text_input: bool,           // does plain typing flow to a field?
        // (section item counts are filled in by PageFocusBuilder; see Focus)
    }
    }

Build the focus targets with `PageFocusBuilder` rather than by hand:

    #![allow(unused)]
    fn main() {
    fn page_spec(view: &View, _state: &State, _focus: Option<&FocusTarget>) -> PageSpec {
        let focus = match view {
            View::Home  => PageFocusBuilder::new().button(0).button(1),
            View::Notes => PageFocusBuilder::new().section_with_items(0, items.len()).button(0),
        };
        PageSpec::new()
            .focus(focus)                        // .focus() consumes the builder
            .modes(vec![modes::GENERAL, modes::GLOBAL])
    }
    }

Use `.focus(builder)` (not `.focus_targets(builder.build())`) whenever a section declares an item count — that count travels with the section so the runtime can step through its items on its own. See Focus.

## TuiEffect — the things you can ask for

Your handler returns effects. This is the complete vocabulary:

    #![allow(unused)]
    fn main() {
    pub enum TuiEffect<V, O = (), M = ()> {
        None,
        Focus(FocusIntent<O, M>),  // move/open/close focus — see Focus
        Navigate(V),               // go to a view (pushes buffer history)
        NextBuffer,                // cycle open buffers
        PreviousBuffer,
        CloseBuffer,
        SplitPane(PaneSplit),      // split the active pane
        ClosePane,
        NextPane,                  // move between panes
        PreviousPane,
        RefreshPage,               // re-read the current page spec
        Quit,
    }
    }

The runtime applies each one for you. You never call into the focus manager or the buffer state directly — you return one of these.

## ActionOutcome — wrapping effects

Your handler returns an `ActionOutcome`, which is just a list of effects applied in order:

    #![allow(unused)]
    fn main() {
    ActionOutcome::none()                                  // do nothing
    ActionOutcome::effect(TuiEffect::Quit)                 // one effect
    ActionOutcome::effects([                               // several, in order
        TuiEffect::Focus(FocusIntent::Next),
        TuiEffect::RefreshPage,
    ])
    }

## Modes

A mode is a named keymap. The same key can mean different things in different modes — that's how you get vim-style modal input. A `PageSpec` lists which modes are active, and `.bind(mode, key, action)` registers a key in a mode.

The runtime ships these names and reasons about a few of them itself:

    #![allow(unused)]
    fn main() {
    modes::GENERAL  // default page navigation (Tab, arrows, Enter on buttons)
    modes::NORMAL   // read-only navigation inside fields  ("nor")
    modes::INSERT   // typing into a text field            ("ins")
    modes::SELECT   // selection / highlighting            ("sel")
    modes::COMMAND  // command bar (`:`) is open
    modes::COMMON   // shared bindings, active alongside NORMAL and SELECT
    modes::GLOBAL   // always active, regardless of mode
    }

But a `ModeId` is just a string key — nothing is hardcoded to these names. Define a mode for any component you build and bind to it the same way:

    #![allow(unused)]
    fn main() {
    const PICKER: ModeId = ModeId::borrowed("picker");

    builder
        .bind(PICKER, "j", Action::PickerDown)
        .bind(PICKER, "k", Action::PickerUp);

    // activate it on the page where the picker is open:
    PageSpec::new().modes(vec![modes::GLOBAL, PICKER])
    }

The library gives you the mechanism; you name the modes.

## FocusWrap

Whether navigation stops at the ends of a list or wraps around. Set once on the builder; applies everywhere — page focus, section items, buffers, panes.

    #![allow(unused)]
    fn main() {
    pub enum FocusWrap {
        Clamp,  // stop at the first/last (default)
        Wrap,   // cycle around
    }

    TuiPages::builder(view).focus_wrap(FocusWrap::Wrap)
    }

## Coming from the old pipeline?

If you read the project's README, you saw the legacy flow: `InputPipeline → InputOrchestrator → FocusManager → CommandPipeline → ActionDecider → Executor`, where the system reached out and called a function living in your page. The concerns survived; the wiring changed direction.

The old design pushed: the library called into your code. This one pulls: your code returns values the library interprets. Concretely:

- `InputPipeline` is still here, same job — key into typed action.
- `InputOrchestrator`, `ActionDecider`, and `CommandPipeline` collapse into your one `Handler::handle_action` match. You are the decider now.
- `FocusManager` is still here, but you don't call it — you return `FocusIntent` and the runtime applies it.
- The `Executor` (the library calling your page's function) is gone. Your handler runs the side effect itself and returns only coordination effects (`Navigate`, `Focus`, `Quit`).

So the one-way flow `key → action → your handler → effects → runtime applies` is the whole machine.