Skip to main content

tui_pages/runtime/
mod.rs

1use crate::command::{CommandHint, CommandRegistry, CommandResolver, CommandResponse};
2use crate::focus::{FocusController, FocusIntent, FocusManager, FocusTarget, FocusWrap};
3use crate::input::{parse_binding, InputHint, InputPipeline, InputRegistry, KeyChord, KeyMap};
4use crate::navigation::{BufferState, PaneSplit};
5use crossterm::event::KeyEvent;
6use std::borrow::Cow;
7use std::error::Error;
8use std::fmt;
9use std::marker::PhantomData;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
13pub struct ModeId(Cow<'static, str>);
14
15impl ModeId {
16    pub const fn borrowed(value: &'static str) -> Self {
17        Self(Cow::Borrowed(value))
18    }
19
20    pub fn owned(value: impl Into<String>) -> Self {
21        Self(Cow::Owned(value.into()))
22    }
23
24    pub fn as_str(&self) -> &str {
25        self.0.as_ref()
26    }
27}
28
29impl AsRef<str> for ModeId {
30    fn as_ref(&self) -> &str {
31        self.as_str()
32    }
33}
34
35impl fmt::Display for ModeId {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.write_str(self.as_str())
38    }
39}
40
41impl From<&'static str> for ModeId {
42    fn from(value: &'static str) -> Self {
43        Self::borrowed(value)
44    }
45}
46
47impl From<String> for ModeId {
48    fn from(value: String) -> Self {
49        Self(Cow::Owned(value))
50    }
51}
52
53/// Built-in mode identifiers shipped by the runtime.
54///
55/// These cover the input states the runtime itself reasons about. A [`ModeId`]
56/// is just a string key, so consumers are free to define their own modes for
57/// their own components — a picker, a palette, a sidebar — without the library
58/// knowing anything about them:
59///
60/// ```ignore
61/// const PICKER: ModeId = ModeId::borrowed("picker");
62///
63/// builder
64///     .bind(PICKER, "j", Action::PickerDown)
65///     .bind(PICKER, "k", Action::PickerUp);
66///
67/// // then activate it for the relevant page/overlay:
68/// PageSpec::new().modes(vec![modes::GLOBAL, PICKER])
69/// ```
70///
71/// Nothing in the runtime is hardcoded to a specific component mode; register
72/// whatever your UI needs.
73pub mod modes {
74    use super::ModeId;
75
76    /// Default page-navigation mode (Tab, arrows, Enter on buttons).
77    pub const GENERAL: ModeId = ModeId::borrowed("general");
78    /// Read-only navigation within form fields.
79    pub const NORMAL: ModeId = ModeId::borrowed("nor");
80    /// Typing into a text field; plain characters flow to the focused input.
81    pub const INSERT: ModeId = ModeId::borrowed("ins");
82    /// Text selection / highlighting.
83    pub const SELECT: ModeId = ModeId::borrowed("sel");
84    /// Command bar (`:`) is open.
85    pub const COMMAND: ModeId = ModeId::borrowed("command");
86    /// Bindings shared across non-typing modes (active alongside `nor` and `sel`).
87    pub const COMMON: ModeId = ModeId::borrowed("common");
88    /// Always active, regardless of the current mode.
89    pub const GLOBAL: ModeId = ModeId::borrowed("global");
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93#[non_exhaustive]
94pub struct PageSpec<O = ()> {
95    pub focus_targets: Vec<FocusTarget<O>>,
96    /// `(section_id, item_count)` for sections the runtime may enter on its
97    /// own. Populated by [`PageSpec::focus`] from
98    /// [`PageFocusBuilder::section_with_items`](crate::PageFocusBuilder::section_with_items);
99    /// empty when sections are registered count-less.
100    pub(crate) section_items: Vec<(usize, usize)>,
101    pub modes: Vec<ModeId>,
102    pub accepts_text_input: bool,
103}
104
105impl<O> Default for PageSpec<O> {
106    fn default() -> Self {
107        Self {
108            focus_targets: Vec::new(),
109            section_items: Vec::new(),
110            modes: vec![modes::GENERAL, modes::GLOBAL],
111            accepts_text_input: false,
112        }
113    }
114}
115
116impl<O> PageSpec<O> {
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    pub fn focus_targets(mut self, targets: Vec<FocusTarget<O>>) -> Self {
122        self.focus_targets = targets;
123        self
124    }
125
126    /// Set the focus targets *and* section item counts from a builder in one
127    /// call. Prefer this over [`focus_targets`](Self::focus_targets) when any
128    /// section is declared with
129    /// [`section_with_items`](crate::PageFocusBuilder::section_with_items), so
130    /// the runtime can enter the section on its own.
131    pub fn focus(mut self, builder: crate::PageFocusBuilder<O>) -> Self {
132        let (targets, section_items) = builder.into_parts();
133        self.focus_targets = targets;
134        self.section_items = section_items;
135        self
136    }
137
138    pub fn modes(mut self, modes: impl IntoIterator<Item = ModeId>) -> Self {
139        self.modes = modes.into_iter().collect();
140        self
141    }
142
143    pub fn accepts_text_input(mut self, accepts_text_input: bool) -> Self {
144        self.accepts_text_input = accepts_text_input;
145        self
146    }
147}
148
149/// A plain function that maps `(view, state, focus)` to a [`PageSpec`].
150///
151/// Most apps describe their pages with a free function; this alias spells out
152/// the signature so a `type App = TuiPages<…>` alias can name the page
153/// provider:
154///
155/// ```ignore
156/// type App = TuiPages<View, Action, State, PageFn<View, State>, Handler>;
157/// //                  builder: .page_fn(page_spec)   // coerces for you
158/// ```
159///
160/// Pass the function to [`page_fn`](TuiPagesBuilder::page_fn) rather than
161/// [`pages`](TuiPagesBuilder::pages): it pins this pointer type and coerces the
162/// fn item at the call site, so the application never writes
163/// `page_spec as PageFn<…>`.
164pub type PageFn<V, S, O = ()> = fn(&V, &S, Option<&FocusTarget<O>>) -> PageSpec<O>;
165
166/// The common shape of a [`TuiPages`] application: pages described by a plain
167/// [`PageFn`].
168///
169/// [`TuiPages`] carries the page provider as its own type parameter so an
170/// advanced caller can plug in any [`PageProvider`]. Almost no one needs that —
171/// pages are a free function — and spelling the provider out forces the view
172/// and state types to be repeated inside `PageFn<…>`:
173///
174/// ```ignore
175/// // the long form names View / State / Overlay twice
176/// type App = TuiPages<View, Action, State, PageFn<View, State, Overlay>, Handler, Overlay>;
177/// // this alias names each once
178/// type App = TuiApp<View, Action, State, Handler, Overlay>;
179/// ```
180///
181/// `O` (overlay) and `M` (modal payload) default to `()`, so an app with
182/// neither writes `TuiApp<View, Action, State, Handler>`. Build it with
183/// [`TuiPages::builder`] + [`page_fn`](TuiPagesBuilder::page_fn); the resulting
184/// type *is* a `TuiApp`, so `fn build() -> App` lines up with no extra effort.
185pub type TuiApp<V, A, S, Handler, O = (), M = ()> =
186    TuiPages<V, A, S, PageFn<V, S, O>, Handler, O, M>;
187
188pub trait PageProvider<V, S, O = ()> {
189    fn page_spec(&self, view: &V, state: &S, focus: Option<&FocusTarget<O>>) -> PageSpec<O>;
190}
191
192impl<V, S, O, F> PageProvider<V, S, O> for F
193where
194    F: Fn(&V, &S, Option<&FocusTarget<O>>) -> PageSpec<O>,
195{
196    fn page_spec(&self, view: &V, state: &S, focus: Option<&FocusTarget<O>>) -> PageSpec<O> {
197        self(view, state, focus)
198    }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum TuiEffect<V, O = (), M = ()> {
203    None,
204    Focus(FocusIntent<O, M>),
205    Navigate(V),
206    NextBuffer,
207    PreviousBuffer,
208    CloseBuffer,
209    SplitPane(PaneSplit),
210    ClosePane,
211    NextPane,
212    PreviousPane,
213    RefreshPage,
214    Quit,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct ActionOutcome<V, O = (), M = ()> {
219    pub effects: Vec<TuiEffect<V, O, M>>,
220}
221
222impl<V, O, M> Default for ActionOutcome<V, O, M> {
223    fn default() -> Self {
224        Self {
225            effects: Vec::new(),
226        }
227    }
228}
229
230impl<V, O, M> ActionOutcome<V, O, M> {
231    pub fn none() -> Self {
232        Self::default()
233    }
234
235    pub fn effect(effect: TuiEffect<V, O, M>) -> Self {
236        Self {
237            effects: vec![effect],
238        }
239    }
240
241    pub fn effects(effects: impl IntoIterator<Item = TuiEffect<V, O, M>>) -> Self {
242        Self {
243            effects: effects.into_iter().collect(),
244        }
245    }
246
247    pub fn push(&mut self, effect: TuiEffect<V, O, M>) {
248        self.effects.push(effect);
249    }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct ActionContext<V, O = ()> {
254    pub current_view: V,
255    pub focus: Option<FocusTarget<O>>,
256    pub has_overlay: bool,
257}
258
259pub trait TuiActionHandler<V, A, S, O = (), M = ()> {
260    type Error;
261
262    fn handle_action(
263        &mut self,
264        action: A,
265        ctx: ActionContext<V, O>,
266        state: &mut S,
267    ) -> Result<ActionOutcome<V, O, M>, Self::Error>;
268
269    fn handle_text(
270        &mut self,
271        _chord: KeyChord,
272        _ctx: ActionContext<V, O>,
273        _state: &mut S,
274    ) -> Result<ActionOutcome<V, O, M>, Self::Error> {
275        Ok(ActionOutcome::none())
276    }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub enum TuiPagesError<E> {
281    Handler(E),
282}
283
284impl<E> fmt::Display for TuiPagesError<E>
285where
286    E: fmt::Display,
287{
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        match self {
290            TuiPagesError::Handler(error) => write!(f, "handler error: {error}"),
291        }
292    }
293}
294
295impl<E> Error for TuiPagesError<E> where E: Error + 'static {}
296
297impl<E> From<E> for TuiPagesError<E> {
298    fn from(error: E) -> Self {
299        Self::Handler(error)
300    }
301}
302
303pub type TuiPagesResult<T, E> = Result<T, TuiPagesError<E>>;
304
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub enum TuiPagesStatus<A> {
307    ActionHandled,
308    TextHandled,
309    Waiting(Vec<InputHint<A>>),
310    Cancelled,
311    CommandIncomplete(Vec<CommandHint>),
312    CommandUnknown,
313    CommandEmpty,
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct TuiPagesOutput<A> {
318    pub status: TuiPagesStatus<A>,
319    pub quit_requested: bool,
320}
321
322impl<A> TuiPagesOutput<A> {
323    fn new(status: TuiPagesStatus<A>, quit_requested: bool) -> Self {
324        Self {
325            status,
326            quit_requested,
327        }
328    }
329}
330
331#[derive(Debug, Clone)]
332pub struct TuiPages<V, A, S, Pages = (), Handler = (), O = (), M = ()> {
333    pub input: InputPipeline<A>,
334    pub commands: CommandResolver<A>,
335    pub focus: FocusManager<O, M>,
336    pub buffer: BufferState<V>,
337    pages: Pages,
338    handler: Handler,
339    fallback_view: V,
340    _state: PhantomData<S>,
341}
342
343impl<V, A, S, O, M> TuiPages<V, A, S, (), (), O, M>
344where
345    V: Clone + PartialEq,
346{
347    pub fn builder(initial_view: V) -> TuiPagesBuilder<V, A, S, O, M, (), ()> {
348        TuiPagesBuilder::new(initial_view)
349    }
350}
351
352impl<V, A, S, Pages, Handler, O, M> TuiPages<V, A, S, Pages, Handler, O, M>
353where
354    V: Clone + PartialEq,
355    A: Clone,
356    O: Clone + PartialEq,
357    Pages: PageProvider<V, S, O>,
358    Handler: TuiActionHandler<V, A, S, O, M>,
359{
360    pub fn current_view(&self) -> &V {
361        self.buffer
362            .get_active_view()
363            .expect("TuiPages buffer always contains at least one view")
364    }
365
366    pub fn pages(&self) -> &Pages {
367        &self.pages
368    }
369
370    pub fn pages_mut(&mut self) -> &mut Pages {
371        &mut self.pages
372    }
373
374    pub fn handler(&self) -> &Handler {
375        &self.handler
376    }
377
378    pub fn handler_mut(&mut self) -> &mut Handler {
379        &mut self.handler
380    }
381
382    pub fn refresh_page(&mut self, state: &S) {
383        let spec = self.current_page_spec(state);
384        self.sync_focus_to_spec(spec);
385    }
386
387    /// Register a page spec's focus targets and section item counts. Targets
388    /// are only re-registered when they actually change (so focus position is
389    /// preserved across redraws), but the section item counts are always
390    /// refreshed — a list may grow or shrink while its `Section` target stays
391    /// the same.
392    fn sync_focus_to_spec(&mut self, spec: PageSpec<O>) {
393        let PageSpec {
394            focus_targets,
395            section_items,
396            ..
397        } = spec;
398        if self.focus.targets() != focus_targets.as_slice() {
399            self.focus.register_page(focus_targets);
400        }
401        self.focus.set_section_items(section_items);
402    }
403
404    pub fn handle_key(
405        &mut self,
406        key: KeyEvent,
407        state: &mut S,
408    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error> {
409        let spec = self.current_page_spec(state);
410        let modes = spec.modes.clone();
411        let accepts_text_input = spec.accepts_text_input;
412        self.sync_focus_to_spec(spec);
413
414        let response = self.input.process(key, &modes, accepts_text_input);
415        match response {
416            crate::input::PipelineResponse::Execute(action) => {
417                let quit_requested = self.dispatch_action(action, state)?;
418                Ok(TuiPagesOutput::new(
419                    TuiPagesStatus::ActionHandled,
420                    quit_requested,
421                ))
422            }
423            crate::input::PipelineResponse::Type(chord) => {
424                let quit_requested = self.dispatch_text(chord, state)?;
425                Ok(TuiPagesOutput::new(
426                    TuiPagesStatus::TextHandled,
427                    quit_requested,
428                ))
429            }
430            crate::input::PipelineResponse::Wait(hints) => {
431                Ok(TuiPagesOutput::new(TuiPagesStatus::Waiting(hints), false))
432            }
433            crate::input::PipelineResponse::Cancel => {
434                Ok(TuiPagesOutput::new(TuiPagesStatus::Cancelled, false))
435            }
436        }
437    }
438
439    pub fn submit_command(
440        &mut self,
441        input: &str,
442        state: &mut S,
443    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error> {
444        match self.commands.process(input) {
445            CommandResponse::Execute(action) => {
446                let quit_requested = self.dispatch_action(action, state)?;
447                Ok(TuiPagesOutput::new(
448                    TuiPagesStatus::ActionHandled,
449                    quit_requested,
450                ))
451            }
452            CommandResponse::Incomplete(hints) => Ok(TuiPagesOutput::new(
453                TuiPagesStatus::CommandIncomplete(hints),
454                false,
455            )),
456            CommandResponse::Unknown => {
457                Ok(TuiPagesOutput::new(TuiPagesStatus::CommandUnknown, false))
458            }
459            CommandResponse::Empty => Ok(TuiPagesOutput::new(TuiPagesStatus::CommandEmpty, false)),
460        }
461    }
462
463    pub fn apply_effect(&mut self, effect: TuiEffect<V, O, M>, state: &S) -> bool {
464        match effect {
465            TuiEffect::None => false,
466            TuiEffect::Focus(intent) => {
467                self.focus.apply_focus_intent(intent);
468                false
469            }
470            TuiEffect::Navigate(view) => {
471                self.buffer.update_history(view);
472                self.refresh_page(state);
473                false
474            }
475            TuiEffect::NextBuffer => {
476                self.switch_buffer(true, state);
477                false
478            }
479            TuiEffect::PreviousBuffer => {
480                self.switch_buffer(false, state);
481                false
482            }
483            TuiEffect::CloseBuffer => {
484                self.buffer.close_active_buffer(self.fallback_view.clone());
485                self.refresh_page(state);
486                false
487            }
488            TuiEffect::SplitPane(split) => {
489                self.buffer.split_active_pane(split);
490                false
491            }
492            TuiEffect::ClosePane => {
493                self.buffer.close_active_pane();
494                self.refresh_page(state);
495                false
496            }
497            TuiEffect::NextPane => {
498                self.buffer.focus_next_pane(self.focus.focus_wrap());
499                self.refresh_page(state);
500                false
501            }
502            TuiEffect::PreviousPane => {
503                self.buffer.focus_previous_pane(self.focus.focus_wrap());
504                self.refresh_page(state);
505                false
506            }
507            TuiEffect::RefreshPage => {
508                self.refresh_page(state);
509                false
510            }
511            TuiEffect::Quit => true,
512        }
513    }
514
515    fn current_page_spec(&self, state: &S) -> PageSpec<O> {
516        let view = self.current_view();
517        let focus = self.focus.current();
518        self.pages.page_spec(view, state, focus.as_ref())
519    }
520
521    fn dispatch_action(
522        &mut self,
523        action: A,
524        state: &mut S,
525    ) -> TuiPagesResult<bool, Handler::Error> {
526        let ctx = ActionContext {
527            current_view: self.current_view().clone(),
528            focus: self.focus.current(),
529            has_overlay: self.focus.has_overlay(),
530        };
531        let outcome = self
532            .handler
533            .handle_action(action, ctx, state)
534            .map_err(TuiPagesError::Handler)?;
535        Ok(self.apply_outcome(outcome, state))
536    }
537
538    fn dispatch_text(
539        &mut self,
540        chord: KeyChord,
541        state: &mut S,
542    ) -> TuiPagesResult<bool, Handler::Error> {
543        let ctx = ActionContext {
544            current_view: self.current_view().clone(),
545            focus: self.focus.current(),
546            has_overlay: self.focus.has_overlay(),
547        };
548        let outcome = self
549            .handler
550            .handle_text(chord, ctx, state)
551            .map_err(TuiPagesError::Handler)?;
552        Ok(self.apply_outcome(outcome, state))
553    }
554
555    fn apply_outcome(&mut self, outcome: ActionOutcome<V, O, M>, state: &S) -> bool {
556        let mut quit_requested = false;
557        for effect in outcome.effects {
558            quit_requested |= self.apply_effect(effect, state);
559        }
560        quit_requested
561    }
562
563    fn switch_buffer(&mut self, forward: bool, state: &S) {
564        if self.buffer.history.len() <= 1 {
565            return;
566        }
567
568        let len = self.buffer.history.len();
569        self.buffer.active_index =
570            self.focus
571                .focus_wrap()
572                .step(self.buffer.active_index, len, forward);
573        self.buffer.sync_active_pane_to_active_buffer();
574        self.refresh_page(state);
575    }
576}
577
578#[derive(Debug, Clone)]
579pub struct TuiPagesBuilder<V, A, S, O = (), M = (), Pages = (), Handler = ()> {
580    initial_view: V,
581    fallback_view: Option<V>,
582    input_registry: InputRegistry<A>,
583    command_registry: CommandRegistry<A>,
584    input_timeout_ms: u64,
585    command_timeout_ms: u64,
586    focus_wrap: FocusWrap,
587    pages: Pages,
588    handler: Handler,
589    _state: PhantomData<S>,
590    _overlay: PhantomData<O>,
591    _modal: PhantomData<M>,
592}
593
594impl<V, A, S, O, M> TuiPagesBuilder<V, A, S, O, M, (), ()> {
595    pub fn new(initial_view: V) -> Self {
596        Self {
597            initial_view,
598            fallback_view: None,
599            input_registry: InputRegistry::empty(),
600            command_registry: CommandRegistry::new(),
601            input_timeout_ms: 1000,
602            command_timeout_ms: 1000,
603            focus_wrap: FocusWrap::default(),
604            pages: (),
605            handler: (),
606            _state: PhantomData,
607            _overlay: PhantomData,
608            _modal: PhantomData,
609        }
610    }
611}
612
613impl<V, A, S, O, M, Pages, Handler> TuiPagesBuilder<V, A, S, O, M, Pages, Handler> {
614    pub fn fallback_view(mut self, fallback_view: V) -> Self {
615        self.fallback_view = Some(fallback_view);
616        self
617    }
618
619    pub fn input_timeout_ms(mut self, timeout_ms: u64) -> Self {
620        self.input_timeout_ms = timeout_ms;
621        self
622    }
623
624    pub fn command_timeout_ms(mut self, timeout_ms: u64) -> Self {
625        self.command_timeout_ms = timeout_ms;
626        self
627    }
628
629    pub fn pages<NextPages>(
630        self,
631        pages: NextPages,
632    ) -> TuiPagesBuilder<V, A, S, O, M, NextPages, Handler> {
633        TuiPagesBuilder {
634            initial_view: self.initial_view,
635            fallback_view: self.fallback_view,
636            input_registry: self.input_registry,
637            command_registry: self.command_registry,
638            input_timeout_ms: self.input_timeout_ms,
639            command_timeout_ms: self.command_timeout_ms,
640            focus_wrap: self.focus_wrap,
641            pages,
642            handler: self.handler,
643            _state: PhantomData,
644            _overlay: PhantomData,
645            _modal: PhantomData,
646        }
647    }
648
649    /// Set the page provider to a plain `fn`, coercing it to [`PageFn`] at the
650    /// call site so the application never writes `page_spec as PageFn<…>`.
651    ///
652    /// `.pages(f)` keeps the fn *item* type, which a `type App = TuiPages<…>`
653    /// alias cannot name; this method pins the [`PageFn`] pointer type the
654    /// alias uses, so `.page_fn(page_spec)` just works:
655    ///
656    /// ```ignore
657    /// TuiPages::builder(View::Home).page_fn(page_spec).handler(Handler).build()
658    /// ```
659    pub fn page_fn(
660        self,
661        page_fn: PageFn<V, S, O>,
662    ) -> TuiPagesBuilder<V, A, S, O, M, PageFn<V, S, O>, Handler> {
663        self.pages(page_fn)
664    }
665
666    pub fn handler<NextHandler>(
667        self,
668        handler: NextHandler,
669    ) -> TuiPagesBuilder<V, A, S, O, M, Pages, NextHandler> {
670        TuiPagesBuilder {
671            initial_view: self.initial_view,
672            fallback_view: self.fallback_view,
673            input_registry: self.input_registry,
674            command_registry: self.command_registry,
675            input_timeout_ms: self.input_timeout_ms,
676            command_timeout_ms: self.command_timeout_ms,
677            focus_wrap: self.focus_wrap,
678            pages: self.pages,
679            handler,
680            _state: PhantomData,
681            _overlay: PhantomData,
682            _modal: PhantomData,
683        }
684    }
685
686    /// Set how focus navigation behaves at the ends of a list — clamp (default)
687    /// or wrap-around. Applies to page focus and modal items.
688    pub fn focus_wrap(mut self, wrap: FocusWrap) -> Self {
689        self.focus_wrap = wrap;
690        self
691    }
692
693    pub fn keymap(
694        mut self,
695        mode: impl Into<ModeId>,
696        configure: impl FnOnce(&mut KeyMap<A>),
697    ) -> Self {
698        let mode = mode.into();
699        configure(self.input_registry.map_mut(mode.as_str()));
700        self
701    }
702
703    pub fn bind(mut self, mode: impl Into<ModeId>, binding: &str, action: A) -> Self {
704        let mode = mode.into();
705        self.input_registry
706            .map_mut(mode.as_str())
707            .bind(parse_binding(binding), action);
708        self
709    }
710
711    pub fn command<I, Alias>(
712        mut self,
713        action_name: impl Into<String>,
714        aliases: I,
715        action: A,
716    ) -> Self
717    where
718        A: Clone,
719        I: IntoIterator<Item = Alias>,
720        Alias: Into<String>,
721    {
722        self.command_registry
723            .bind_aliases(action_name, aliases, action);
724        self
725    }
726
727    pub fn build(self) -> TuiPages<V, A, S, Pages, Handler, O, M>
728    where
729        V: Clone + PartialEq,
730        Pages: PageProvider<V, S, O>,
731        Handler: TuiActionHandler<V, A, S, O, M>,
732    {
733        let fallback_view = self
734            .fallback_view
735            .unwrap_or_else(|| self.initial_view.clone());
736
737        let mut focus = FocusManager::new();
738        focus.set_focus_wrap(self.focus_wrap);
739
740        TuiPages {
741            input: InputPipeline::new(self.input_registry, self.input_timeout_ms),
742            commands: CommandResolver::new(self.command_registry, self.command_timeout_ms),
743            focus,
744            buffer: BufferState::new(self.initial_view),
745            pages: self.pages,
746            handler: self.handler,
747            fallback_view,
748            _state: PhantomData,
749        }
750    }
751}