tui-pages 0.7.2

Core for TUI apps with multiple pages
Documentation
# 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:

| | 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):

```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.