# 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.
```rust
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:
| `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):
```rust
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.
```rust
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:
```rust
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](./focus.md).
## TuiEffect — the things you can ask for
Your handler returns effects. This is the complete vocabulary:
```rust
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:
```rust
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:
```rust
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:
```rust
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.
```rust
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.