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::{InputHint, InputPipeline, InputRegistry, KeyChord, KeyMap, parse_binding};
4use crate::keybindings::{
5    BindingStore, KeybindingConfig, KeybindingConfigError, KeybindingInheritance, KeybindingReport,
6    NavigationAction,
7};
8use crate::navigation::{BufferState, PaneSplit};
9use crossterm::event::{Event, KeyEvent};
10#[cfg(feature = "command-line")]
11use ratatui::layout::{Constraint, Layout, Rect};
12use std::borrow::Cow;
13#[cfg(feature = "canvas")]
14use std::cell::RefCell;
15use std::error::Error;
16use std::fmt;
17use std::marker::PhantomData;
18#[cfg(feature = "canvas")]
19use std::rc::Rc;
20
21#[cfg(feature = "canvas")]
22#[derive(Debug, Clone)]
23pub(crate) struct CanvasKeybindingProfileState {
24    pub profile: crate::canvas::CanvasKeybindingProfile,
25    pub generation: u64,
26}
27
28#[cfg(feature = "canvas")]
29impl CanvasKeybindingProfileState {
30    pub fn new(profile: crate::canvas::CanvasKeybindingProfile) -> Self {
31        Self {
32            profile,
33            generation: 0,
34        }
35    }
36
37    pub fn replace(&mut self, profile: crate::canvas::CanvasKeybindingProfile) {
38        self.profile = profile;
39        self.generation = self.generation.wrapping_add(1);
40    }
41
42    pub fn bump(&mut self) {
43        self.generation = self.generation.wrapping_add(1);
44    }
45}
46
47#[cfg(feature = "canvas")]
48pub(crate) type CanvasKeybindingProfileHandle = Rc<RefCell<CanvasKeybindingProfileState>>;
49
50#[derive(Debug, Clone, PartialEq, Eq, Hash)]
51#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
52pub struct ModeId(Cow<'static, str>);
53
54impl ModeId {
55    pub const fn borrowed(value: &'static str) -> Self {
56        Self(Cow::Borrowed(value))
57    }
58
59    pub fn owned(value: impl Into<String>) -> Self {
60        Self(Cow::Owned(value.into()))
61    }
62
63    pub fn as_str(&self) -> &str {
64        self.0.as_ref()
65    }
66}
67
68impl AsRef<str> for ModeId {
69    fn as_ref(&self) -> &str {
70        self.as_str()
71    }
72}
73
74impl fmt::Display for ModeId {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.write_str(self.as_str())
77    }
78}
79
80impl From<&'static str> for ModeId {
81    fn from(value: &'static str) -> Self {
82        Self::borrowed(value)
83    }
84}
85
86impl From<String> for ModeId {
87    fn from(value: String) -> Self {
88        Self(Cow::Owned(value))
89    }
90}
91
92/// Built-in mode identifiers shipped by the runtime.
93///
94/// These cover the input states the runtime itself reasons about. A [`ModeId`]
95/// is just a string key, so consumers are free to define their own modes for
96/// their own components — a picker, a palette, a sidebar — without the library
97/// knowing anything about them:
98///
99/// ```ignore
100/// const PICKER: ModeId = ModeId::borrowed("picker");
101///
102/// builder
103///     .bind(PICKER, "j", Action::PickerDown)
104///     .bind(PICKER, "k", Action::PickerUp);
105///
106/// // then activate it for the relevant page/overlay:
107/// PageSpec::new().modes(vec![modes::GLOBAL, PICKER])
108/// ```
109///
110/// Nothing in the runtime is hardcoded to a specific component mode; register
111/// whatever your UI needs.
112pub mod modes {
113    use super::ModeId;
114
115    /// Default page-navigation mode (Tab, arrows, Enter on buttons).
116    pub const GENERAL: ModeId = ModeId::borrowed("general");
117    /// Read-only navigation within form fields.
118    pub const NORMAL: ModeId = ModeId::borrowed("nor");
119    /// Typing into a text field; plain characters flow to the focused input.
120    pub const INSERT: ModeId = ModeId::borrowed("ins");
121    /// Text selection / highlighting.
122    pub const SELECT: ModeId = ModeId::borrowed("sel");
123    /// Command bar (`:`) is open.
124    pub const COMMAND: ModeId = ModeId::borrowed("command");
125    /// Bindings shared across non-typing modes (active alongside `nor` and `sel`).
126    pub const COMMON: ModeId = ModeId::borrowed("common");
127    /// Always active, regardless of the current mode.
128    pub const GLOBAL: ModeId = ModeId::borrowed("global");
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
132#[non_exhaustive]
133pub struct PageSpec<O = ()> {
134    pub focus_targets: Vec<FocusTarget<O>>,
135    /// `(section_id, item_count)` for sections the runtime may enter on its
136    /// own. Populated by [`PageSpec::focus`] from
137    /// [`PageFocusBuilder::section_with_items`](crate::PageFocusBuilder::section_with_items);
138    /// empty when sections are registered count-less.
139    pub(crate) section_items: Vec<(usize, usize)>,
140    pub modes: Vec<ModeId>,
141    pub accepts_text_input: bool,
142}
143
144impl<O> Default for PageSpec<O> {
145    fn default() -> Self {
146        Self {
147            focus_targets: Vec::new(),
148            section_items: Vec::new(),
149            modes: vec![modes::GENERAL, modes::GLOBAL],
150            accepts_text_input: false,
151        }
152    }
153}
154
155impl<O> PageSpec<O> {
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    pub fn focus_targets(mut self, targets: Vec<FocusTarget<O>>) -> Self {
161        self.focus_targets = targets;
162        self
163    }
164
165    /// Set the focus targets *and* section item counts from a builder in one
166    /// call. Prefer this over [`focus_targets`](Self::focus_targets) when any
167    /// section is declared with
168    /// [`section_with_items`](crate::PageFocusBuilder::section_with_items), so
169    /// the runtime can enter the section on its own.
170    pub fn focus(mut self, builder: crate::PageFocusBuilder<O>) -> Self {
171        let (targets, section_items) = builder.into_parts();
172        self.focus_targets = targets;
173        self.section_items = section_items;
174        self
175    }
176
177    pub fn modes(mut self, modes: impl IntoIterator<Item = ModeId>) -> Self {
178        self.modes = modes.into_iter().collect();
179        self
180    }
181
182    pub fn accepts_text_input(mut self, accepts_text_input: bool) -> Self {
183        self.accepts_text_input = accepts_text_input;
184        self
185    }
186}
187
188/// A plain function that maps `(view, state, focus)` to a [`PageSpec`].
189///
190/// Most apps describe their pages with a free function; this alias spells out
191/// the signature so a `type App = TuiPages<…>` alias can name the page
192/// provider:
193///
194/// ```ignore
195/// type App = TuiPages<View, Action, PageFn<View, State>, Handler>;
196/// //                  builder: .page_fn(page_spec)   // coerces for you
197/// ```
198///
199/// Pass the function to [`page_fn`](TuiPagesBuilder::page_fn) rather than
200/// [`pages`](TuiPagesBuilder::pages): it pins this pointer type and coerces the
201/// fn item at the call site, so the application never writes
202/// `page_spec as PageFn<…>`.
203pub type PageFn<V, S, O = ()> = fn(&V, &S, Option<&FocusTarget<O>>) -> PageSpec<O>;
204
205/// The common shape of a [`TuiPages`] application: pages described by a plain
206/// [`PageFn`].
207///
208/// [`TuiPages`] carries the page provider as its own type parameter so an
209/// advanced caller can plug in any [`PageProvider`]. Almost no one needs that —
210/// pages are a free function — and spelling the provider out forces the view
211/// and state types to be repeated inside `PageFn<…>`:
212///
213/// ```ignore
214/// // the long form names View / State / Overlay twice
215/// type App = TuiPages<View, Action, PageFn<View, State, Overlay>, Handler, Overlay>;
216/// // this alias names each once
217/// type App = TuiApp<View, Action, State, Handler, Overlay>;
218/// ```
219///
220/// `O` (overlay) and `M` (modal payload) default to `()`, so an app with
221/// neither writes `TuiApp<View, Action, State, Handler>`. Build it with
222/// [`TuiPages::builder`] + [`page_fn`](TuiPagesBuilder::page_fn); the resulting
223/// type *is* a `TuiApp`, so `fn build() -> App` lines up with no extra effort.
224pub type TuiApp<V, A, S, Handler, O = (), M = (), Hooks = NoCanvasHooks> =
225    TuiPages<V, A, PageFn<V, S, O>, Handler, O, M, Hooks>;
226
227pub trait PageProvider<V, S: ?Sized, O = ()> {
228    fn page_spec(&self, view: &V, state: &S, focus: Option<&FocusTarget<O>>) -> PageSpec<O>;
229}
230
231impl<V, S: ?Sized, O, F> PageProvider<V, S, O> for F
232where
233    F: Fn(&V, &S, Option<&FocusTarget<O>>) -> PageSpec<O>,
234{
235    fn page_spec(&self, view: &V, state: &S, focus: Option<&FocusTarget<O>>) -> PageSpec<O> {
236        self(view, state, focus)
237    }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub enum TuiEffect<V, O = (), M = ()> {
242    None,
243    Focus(FocusIntent<O, M>),
244    Navigate(V),
245    NextBuffer,
246    PreviousBuffer,
247    CloseBuffer,
248    SplitPane(PaneSplit),
249    ClosePane,
250    NextPane,
251    PreviousPane,
252    RefreshPage,
253    Quit,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct ActionOutcome<V, O = (), M = ()> {
258    pub effects: Vec<TuiEffect<V, O, M>>,
259}
260
261impl<V, O, M> Default for ActionOutcome<V, O, M> {
262    fn default() -> Self {
263        Self {
264            effects: Vec::new(),
265        }
266    }
267}
268
269impl<V, O, M> ActionOutcome<V, O, M> {
270    pub fn none() -> Self {
271        Self::default()
272    }
273
274    pub fn effect(effect: TuiEffect<V, O, M>) -> Self {
275        Self {
276            effects: vec![effect],
277        }
278    }
279
280    pub fn effects(effects: impl IntoIterator<Item = TuiEffect<V, O, M>>) -> Self {
281        Self {
282            effects: effects.into_iter().collect(),
283        }
284    }
285
286    pub fn push(&mut self, effect: TuiEffect<V, O, M>) {
287        self.effects.push(effect);
288    }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
292pub struct ActionContext<V, O = ()> {
293    pub current_view: V,
294    pub focus: Option<FocusTarget<O>>,
295    pub has_overlay: bool,
296}
297
298pub struct RuntimeContext<'a, A, O = (), M = ()> {
299    pub focus: &'a mut FocusManager<O, M>,
300    pub commands: &'a mut CommandResolver<A>,
301}
302
303pub trait TuiActionHandler<V, A, S: ?Sized, O = (), M = ()> {
304    type Error;
305
306    fn handle_action(
307        &mut self,
308        action: A,
309        ctx: ActionContext<V, O>,
310        state: &mut S,
311        runtime: RuntimeContext<'_, A, O, M>,
312    ) -> Result<ActionOutcome<V, O, M>, Self::Error>;
313
314    fn handle_text(
315        &mut self,
316        _chord: KeyChord,
317        _ctx: ActionContext<V, O>,
318        _state: &mut S,
319        _runtime: RuntimeContext<'_, A, O, M>,
320    ) -> Result<ActionOutcome<V, O, M>, Self::Error> {
321        Ok(ActionOutcome::none())
322    }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub enum TuiPagesError<E> {
327    Handler(E),
328}
329
330impl<E> fmt::Display for TuiPagesError<E>
331where
332    E: fmt::Display,
333{
334    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335        match self {
336            TuiPagesError::Handler(error) => write!(f, "handler error: {error}"),
337        }
338    }
339}
340
341impl<E> Error for TuiPagesError<E> where E: Error + 'static {}
342
343impl<E> From<E> for TuiPagesError<E> {
344    fn from(error: E) -> Self {
345        Self::Handler(error)
346    }
347}
348
349pub type TuiPagesResult<T, E> = Result<T, TuiPagesError<E>>;
350
351#[derive(Debug, Clone, PartialEq, Eq)]
352pub enum TuiPagesStatus<A> {
353    ActionHandled,
354    TextHandled,
355    Waiting(Vec<InputHint<A>>),
356    Cancelled,
357    CommandIncomplete(Vec<CommandHint>),
358    CommandUnknown,
359    CommandEmpty,
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
363pub struct TuiPagesOutput<A> {
364    pub status: TuiPagesStatus<A>,
365    pub quit_requested: bool,
366}
367
368impl<A> TuiPagesOutput<A> {
369    fn new(status: TuiPagesStatus<A>, quit_requested: bool) -> Self {
370        Self {
371            status,
372            quit_requested,
373        }
374    }
375}
376
377pub(crate) struct KeyHookOutcome<V, A, O, M> {
378    pub status: TuiPagesStatus<A>,
379    pub outcome: ActionOutcome<V, O, M>,
380    pub routing: KeyHookRouting,
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384// Both variants are only *constructed* by the canvas key hooks; without the
385// `canvas` feature they're still matched in `run_layer` but never built.
386#[cfg_attr(not(feature = "canvas"), allow(dead_code))]
387pub(crate) enum KeyHookRouting {
388    Handled,
389    Pending,
390}
391
392/// Which input context a focused widget is in. `Command`-context keys are
393/// routed to the global keymap first; `Text`-context keys flow to the canvas
394/// editing layer first. Exposed so [`crate::canvas::analyze_canvas_overlaps`]
395/// can explain which layer wins for an overlapping sequence.
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub enum InputLayerContext {
398    Command,
399    Text,
400}
401
402#[cfg(feature = "command-line")]
403#[derive(Debug, Clone, Copy, PartialEq, Eq)]
404pub struct CommandLineAreas {
405    pub page: Rect,
406    pub command_line: Option<Rect>,
407}
408
409#[cfg(feature = "command-line")]
410impl CommandLineAreas {
411    pub fn split(area: Rect, reserve_command_line: bool) -> Self {
412        if !reserve_command_line {
413            return Self {
414                page: area,
415                command_line: None,
416            };
417        }
418
419        let [page, command_line] =
420            Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).areas(area);
421
422        Self {
423            page,
424            command_line: Some(command_line),
425        }
426    }
427}
428
429#[derive(Debug, Clone)]
430pub(crate) enum KeyHookKind {
431    #[cfg(feature = "canvas")]
432    CanvasFormEditor {
433        id: usize,
434        profile: CanvasKeybindingProfileHandle,
435        installed_generation: Option<u64>,
436    },
437    #[cfg(feature = "canvas")]
438    CanvasTextArea {
439        focus_index: usize,
440        profile: CanvasKeybindingProfileHandle,
441        installed_generation: Option<u64>,
442    },
443    #[cfg(feature = "canvas")]
444    CanvasTextInput {
445        focus_index: usize,
446        profile: CanvasKeybindingProfileHandle,
447        installed_generation: Option<u64>,
448    },
449}
450
451#[derive(Debug, Clone)]
452#[cfg_attr(not(feature = "canvas"), allow(dead_code))]
453pub(crate) struct KeyHook {
454    pub kind: KeyHookKind,
455}
456
457#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
458pub struct NoCanvasHooks;
459
460#[cfg(feature = "canvas")]
461#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
462pub struct CanvasHooks;
463
464pub(crate) trait HookDispatch<S: ?Sized, V, A, O, M> {
465    fn focused_hook_context(
466        hooks: &[KeyHook],
467        ctx: &ActionContext<V, O>,
468        state: &S,
469    ) -> Option<(usize, InputLayerContext)>;
470
471    #[cfg(feature = "canvas")]
472    fn cursor_behavior(
473        hooks: &[KeyHook],
474        ctx: &ActionContext<V, O>,
475        state: &S,
476    ) -> crate::canvas::DefaultCursorBehavior;
477
478    fn dispatch_hook(
479        hook: &mut KeyHook,
480        key: KeyEvent,
481        ctx: ActionContext<V, O>,
482        state: &mut S,
483    ) -> Option<KeyHookOutcome<V, A, O, M>>;
484
485    fn paste_hook(
486        hook: &mut KeyHook,
487        text: &str,
488        ctx: ActionContext<V, O>,
489        state: &mut S,
490    ) -> Option<KeyHookOutcome<V, A, O, M>>;
491}
492
493impl<S: ?Sized, V, A, O, M> HookDispatch<S, V, A, O, M> for NoCanvasHooks {
494    fn focused_hook_context(
495        _hooks: &[KeyHook],
496        _ctx: &ActionContext<V, O>,
497        _state: &S,
498    ) -> Option<(usize, InputLayerContext)> {
499        None
500    }
501
502    #[cfg(feature = "canvas")]
503    fn cursor_behavior(
504        _hooks: &[KeyHook],
505        _ctx: &ActionContext<V, O>,
506        _state: &S,
507    ) -> crate::canvas::DefaultCursorBehavior {
508        crate::canvas::DefaultCursorBehavior::Hidden
509    }
510
511    fn dispatch_hook(
512        _hook: &mut KeyHook,
513        _key: KeyEvent,
514        _ctx: ActionContext<V, O>,
515        _state: &mut S,
516    ) -> Option<KeyHookOutcome<V, A, O, M>> {
517        None
518    }
519
520    fn paste_hook(
521        _hook: &mut KeyHook,
522        _text: &str,
523        _ctx: ActionContext<V, O>,
524        _state: &mut S,
525    ) -> Option<KeyHookOutcome<V, A, O, M>> {
526        None
527    }
528}
529
530#[cfg(feature = "canvas")]
531impl<S, V, A, O, M> HookDispatch<S, V, A, O, M> for CanvasHooks
532where
533    S: crate::canvas::CanvasWidgetState + ?Sized,
534{
535    fn focused_hook_context(
536        hooks: &[KeyHook],
537        ctx: &ActionContext<V, O>,
538        state: &S,
539    ) -> Option<(usize, InputLayerContext)> {
540        hooks.iter().enumerate().find_map(|(index, hook)| {
541            crate::canvas::canvas_hook_context(&hook.kind, ctx, state)
542                .map(|context| (index, context))
543        })
544    }
545
546    fn cursor_behavior(
547        hooks: &[KeyHook],
548        ctx: &ActionContext<V, O>,
549        state: &S,
550    ) -> crate::canvas::DefaultCursorBehavior {
551        hooks
552            .iter()
553            .find_map(|hook| crate::canvas::canvas_hook_cursor_behavior(&hook.kind, ctx, state))
554            .unwrap_or(crate::canvas::DefaultCursorBehavior::Hidden)
555    }
556
557    fn dispatch_hook(
558        hook: &mut KeyHook,
559        key: KeyEvent,
560        ctx: ActionContext<V, O>,
561        state: &mut S,
562    ) -> Option<KeyHookOutcome<V, A, O, M>> {
563        crate::canvas::dispatch_canvas_key_hook(&mut hook.kind, key, ctx, state)
564    }
565
566    fn paste_hook(
567        hook: &mut KeyHook,
568        text: &str,
569        ctx: ActionContext<V, O>,
570        state: &mut S,
571    ) -> Option<KeyHookOutcome<V, A, O, M>> {
572        crate::canvas::dispatch_canvas_paste_hook(&mut hook.kind, text, ctx, state)
573    }
574}
575
576/// Identifies which input layer the orchestrator is talking to. The global
577/// keymap (`self.input`) and each registered canvas [`KeyHook`] are layers; the
578/// orchestrator in [`TuiPages::handle_key`] drives them as an ordered stack and
579/// remembers which one owns an in-flight multi-key sequence.
580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581pub(crate) enum LayerOwner {
582    /// The global keymap pipeline (`self.input`).
583    Keymap,
584    /// The canvas key hook at this index in `key_hooks`.
585    Hook(usize),
586}
587
588/// The result of offering a key to a single input layer. Unifies the keymap's
589/// `PipelineResponse` and a canvas hook's `KeyHookOutcome` so the orchestrator
590/// can treat every layer the same way.
591pub(crate) enum LayerResult<A> {
592    /// The layer began (or continued) a multi-key sequence and now owns
593    /// subsequent keys until it resolves. Carries the status to report.
594    Pending(TuiPagesStatus<A>),
595    /// The layer fully handled the key; nothing else should see it.
596    Handled(TuiPagesOutput<A>),
597    /// The layer declined the key; try the next layer. Carries the typed chord
598    /// when the keymap produced one, so the final text fallback can use it.
599    Ignored(Option<KeyChord>),
600}
601
602#[derive(Debug, Clone)]
603pub struct TuiPages<V, A, Pages = (), Handler = (), O = (), M = (), Hooks = NoCanvasHooks> {
604    pub input: InputPipeline<A>,
605    pub commands: CommandResolver<A>,
606    pub focus: FocusManager<O, M>,
607    pub buffer: BufferState<V>,
608    pages: Pages,
609    handler: Handler,
610    fallback_view: V,
611    reserve_command_line: bool,
612    pub(crate) text_input_mapper: Option<fn(KeyChord) -> Option<A>>,
613    pub(crate) key_hooks: Vec<KeyHook>,
614    /// The layer that owns the in-flight multi-key sequence, if any. Set when a
615    /// layer returns [`LayerResult::Pending`]; subsequent keys route straight to
616    /// it until it resolves. `None` means the next key is freshly arbitrated.
617    pub(crate) active_owner: Option<LayerOwner>,
618    keybinding_store: Option<BindingStore<A>>,
619    keybinding_report: Option<KeybindingReport<A>>,
620    keybinding_inheritances: Vec<KeybindingInheritance<A>>,
621    action_registry: Option<crate::keybindings::ActionRegistry<A>>,
622    #[cfg(feature = "canvas")]
623    canvas_keybinding_profile: CanvasKeybindingProfileHandle,
624    _hooks: PhantomData<Hooks>,
625}
626
627impl<V, A, O, M> TuiPages<V, A, (), (), O, M>
628where
629    V: Clone + PartialEq,
630{
631    pub fn builder(initial_view: V) -> TuiPagesBuilder<V, A, O, M, (), (), NoCanvasHooks> {
632        TuiPagesBuilder::new(initial_view)
633    }
634}
635
636impl<V, A, Pages, Handler, O, M, Hooks> TuiPages<V, A, Pages, Handler, O, M, Hooks>
637where
638    V: Clone + PartialEq,
639    A: Clone,
640    O: Clone + PartialEq,
641{
642    pub fn current_view(&self) -> &V {
643        self.buffer
644            .get_active_view()
645            .expect("TuiPages buffer always contains at least one view")
646    }
647
648    pub fn pages(&self) -> &Pages {
649        &self.pages
650    }
651
652    pub fn pages_mut(&mut self) -> &mut Pages {
653        &mut self.pages
654    }
655
656    pub fn handler(&self) -> &Handler {
657        &self.handler
658    }
659
660    pub fn handler_mut(&mut self) -> &mut Handler {
661        &mut self.handler
662    }
663
664    pub fn reserve_command_line(&self) -> bool {
665        self.reserve_command_line
666    }
667
668    #[cfg(feature = "command-line")]
669    pub fn render_areas(&self, area: Rect) -> CommandLineAreas {
670        CommandLineAreas::split(area, self.reserve_command_line)
671    }
672
673    pub fn refresh_page<S: ?Sized>(&mut self, state: &S)
674    where
675        Pages: PageProvider<V, S, O>,
676    {
677        let spec = self.current_page_spec(state);
678        self.sync_focus_to_spec(spec);
679    }
680
681    /// Drop all in-flight input-routing state in one place: the keymap's
682    /// partial sequence *and* the sticky [`active_owner`](Self) that a canvas
683    /// hook (or the keymap) may hold. Call this whenever the world shifts out
684    /// from under a pending sequence — page navigation, buffer switch, or after
685    /// remapping bindings at runtime — so a half-typed chord can't resolve
686    /// against the new context or be delivered to a layer that no longer owns
687    /// the focus.
688    pub fn reset_input_routing(&mut self) {
689        self.input.reset();
690        self.active_owner = None;
691    }
692
693    pub fn take_keybinding_report(&mut self) -> Option<KeybindingReport<A>> {
694        self.keybinding_report.take()
695    }
696
697    pub fn keybinding_store(&self) -> Option<&BindingStore<A>> {
698        self.keybinding_store.as_ref()
699    }
700
701    /// The key currently bound to `action`, formatted for display (e.g.
702    /// `"Ctrl+b"`, or `"g d"` for a chord sequence), or `None` if it is
703    /// unbound. The one-liner behind a footer/help hint — "which key does X?".
704    ///
705    /// If the action is bound to several sequences, the lexicographically first
706    /// is returned; use [`keys_for`](Self::keys_for) to get them all.
707    ///
708    /// ```ignore
709    /// let hint = app.key_for(&Action::ToggleSidebar).unwrap_or_else(|| "(unbound)".into());
710    /// ```
711    pub fn key_for(&self, action: &A) -> Option<String>
712    where
713        A: PartialEq,
714    {
715        self.keys_for(action).into_iter().next()
716    }
717
718    /// Every key sequence currently bound to `action`, each formatted for
719    /// display and sorted for stable output. Use for actions that may carry
720    /// more than one binding; for the common single-binding case reach for
721    /// [`key_for`](Self::key_for).
722    pub fn keys_for(&self, action: &A) -> Vec<String>
723    where
724        A: PartialEq,
725    {
726        let mut keys: Vec<String> = self
727            .input
728            .registry
729            .maps
730            .values()
731            .flat_map(|map| map.bindings_for(action))
732            .map(|sequence| {
733                sequence
734                    .iter()
735                    .map(|chord| chord.display_string())
736                    .collect::<Vec<_>>()
737                    .join(" ")
738            })
739            .collect();
740        keys.sort();
741        keys.dedup();
742        keys
743    }
744
745    fn keybinding_builtin_registry(&self) -> InputRegistry<A>
746    where
747        A: Clone + PartialEq,
748    {
749        self.keybinding_store
750            .as_ref()
751            .map(BindingStore::builtin_registry)
752            .unwrap_or_else(|| self.input.registry.clone())
753    }
754
755    fn set_keybinding_store_and_registry(
756        &mut self,
757        store: BindingStore<A>,
758        report: KeybindingReport<A>,
759    ) where
760        A: Clone + PartialEq,
761    {
762        self.input.registry = store.effective_registry();
763        self.keybinding_store = Some(store);
764        self.keybinding_report = Some(report);
765        self.reset_input_routing();
766    }
767
768    pub fn apply_keybindings_toml(
769        &mut self,
770        source: &str,
771    ) -> Result<KeybindingReport<A>, KeybindingConfigError>
772    where
773        A: Clone + PartialEq + From<NavigationAction>,
774    {
775        let config = KeybindingConfig::from_toml(source)?;
776        let builtin = self.keybinding_builtin_registry();
777        let actions = self
778            .action_registry
779            .clone()
780            .unwrap_or_else(crate::keybindings::ActionRegistry::navigation);
781        let (store, _, report) = BindingStore::with_user_config_and_inheritances(
782            &builtin,
783            &config,
784            &actions,
785            self.keybinding_inheritances.clone(),
786        )?;
787        #[cfg(feature = "canvas")]
788        {
789            let profile = config.canvas_profile()?;
790            self.canvas_keybinding_profile.borrow_mut().replace(profile);
791        }
792        self.set_keybinding_store_and_registry(store, report.clone());
793        Ok(report)
794    }
795
796    /// Serialize the current live keybindings (config + runtime rebinds) to the
797    /// unified TOML schema — the inverse of [`Self::apply_keybindings_toml`] and
798    /// the builder's `keybindings_toml`. Persisting the string is the caller's
799    /// job, e.g. `std::fs::write(path, app.export_keybindings_toml()?)?;`, and a
800    /// later launch loads it back via `builder.keybindings_toml(&contents)`.
801    pub fn export_keybindings_toml(&self) -> Result<String, KeybindingConfigError>
802    where
803        A: Clone + PartialEq + From<NavigationAction>,
804    {
805        let actions = self
806            .action_registry
807            .clone()
808            .unwrap_or_else(crate::keybindings::ActionRegistry::navigation);
809        let store = self.keybinding_store.clone().unwrap_or_default();
810        #[cfg(feature = "canvas")]
811        {
812            let profile = self.canvas_keybinding_profile.borrow().profile.clone();
813            crate::keybindings::export_to_toml(&store, &actions, &profile)
814        }
815        #[cfg(not(feature = "canvas"))]
816        {
817            crate::keybindings::export_to_toml(&store, &actions)
818        }
819    }
820
821    pub fn rebind_keymap(
822        &mut self,
823        mode: impl Into<String>,
824        sequence: &str,
825        action: A,
826    ) -> Result<KeybindingReport<A>, KeybindingConfigError>
827    where
828        A: Clone + PartialEq,
829    {
830        let mode = mode.into();
831        let sequence =
832            crate::input::try_parse_binding(sequence).map_err(KeybindingConfigError::KeyBinding)?;
833        let mut store = self.keybinding_store.clone().unwrap_or_else(|| {
834            let mut store = BindingStore::default();
835            store.builtin_keymap = crate::input::BindingCatalog::from_registry(
836                &self.input.registry,
837                crate::input::BindingSource::Builtin,
838            );
839            store
840        });
841        store
842            .runtime_keymap
843            .bindings
844            .retain(|binding| !(binding.mode == mode && binding.action == action));
845        store.runtime_keymap.push(crate::input::BindingInfo {
846            layer: crate::input::BindingLayer::Keymap,
847            mode,
848            sequence,
849            action,
850            source: crate::input::BindingSource::Runtime,
851        });
852        let report = store.report(&["global", "general", "nor", "ins", "sel"]);
853        self.set_keybinding_store_and_registry(store, report.clone());
854        Ok(report)
855    }
856
857    pub fn reset_keybindings_to_defaults(&mut self)
858    where
859        A: Clone + PartialEq,
860    {
861        if let Some(mut store) = self.keybinding_store.clone() {
862            store.user_keymap.bindings.clear();
863            store.runtime_keymap.bindings.clear();
864            #[cfg(feature = "canvas")]
865            {
866                store.user_canvas.bindings.clear();
867                store.runtime_canvas.bindings.clear();
868                if let Some(first) = store.builtin_canvas.bindings.first() {
869                    let preset = match first.source {
870                        crate::input::BindingSource::CanvasBuiltin => {
871                            self.canvas_keybinding_profile.borrow().profile.preset()
872                        }
873                        _ => crate::canvas::BuiltinCanvasKeybindingPreset::Vim,
874                    };
875                    self.canvas_keybinding_profile
876                        .borrow_mut()
877                        .replace(preset.profile());
878                }
879            }
880            let report = store.report(&["global", "general", "nor", "ins", "sel"]);
881            self.set_keybinding_store_and_registry(store, report);
882        } else {
883            self.reset_input_routing();
884        }
885    }
886
887    #[cfg(feature = "canvas")]
888    pub fn rebind_canvas(
889        &mut self,
890        mode: crate::canvas::AppMode,
891        action_name: &str,
892        sequences: Vec<String>,
893    ) -> Result<KeybindingReport<A>, KeybindingConfigError>
894    where
895        A: Clone + PartialEq,
896    {
897        let action = crate::canvas::CanvasKeyAction::from_name(action_name);
898        if matches!(action, crate::canvas::CanvasKeyAction::Unknown(_)) {
899            return Err(KeybindingConfigError::CanvasAction {
900                action: action_name.to_string(),
901            });
902        }
903        // Parse every sequence with tui-pages' parser up front, before mutating
904        // anything, so a parse error can't leave the live profile half-changed.
905        let parsed_sequences = sequences
906            .iter()
907            .map(|sequence| crate::input::try_parse_binding(sequence))
908            .collect::<Result<Vec<_>, _>>()
909            .map_err(KeybindingConfigError::KeyBinding)?;
910        {
911            let mut profile = self.canvas_keybinding_profile.borrow_mut();
912            profile
913                .profile
914                .remap_action(mode, action.clone(), sequences.clone())
915                .map_err(KeybindingConfigError::Canvas)?;
916            profile.bump();
917        }
918
919        let mut store = self.keybinding_store.clone().unwrap_or_else(|| {
920            let mut store = BindingStore::default();
921            store.builtin_keymap = crate::input::BindingCatalog::from_registry(
922                &self.input.registry,
923                crate::input::BindingSource::Builtin,
924            );
925            store.builtin_canvas = crate::canvas::canvas_default_binding_catalog(
926                self.canvas_keybinding_profile.borrow().profile.preset(),
927            );
928            store
929        });
930        store.runtime_canvas.bindings.retain(|binding| {
931            !(binding.mode == crate::canvas::mode_for_app_mode(mode).as_str()
932                && crate::canvas::canvas_action_name(&binding.action) == Some(action_name))
933        });
934        if let Some(canvas_action) = action.to_canvas_action() {
935            for sequence in parsed_sequences {
936                store.runtime_canvas.push(crate::input::BindingInfo {
937                    layer: crate::input::BindingLayer::Canvas,
938                    mode: crate::canvas::mode_for_app_mode(mode).as_str().to_string(),
939                    sequence,
940                    action: canvas_action.clone(),
941                    source: crate::input::BindingSource::Runtime,
942                });
943            }
944        }
945        let report = store.report(&["global", "general", "nor", "ins", "sel"]);
946        self.keybinding_store = Some(store);
947        self.keybinding_report = Some(report.clone());
948        self.reset_input_routing();
949        Ok(report)
950    }
951
952    /// Register a page spec's focus targets and section item counts. Targets
953    /// are only re-registered when they actually change (so focus position is
954    /// preserved across redraws), but the section item counts are always
955    /// refreshed — a list may grow or shrink while its `Section` target stays
956    /// the same.
957    fn sync_focus_to_spec(&mut self, spec: PageSpec<O>) {
958        let PageSpec {
959            focus_targets,
960            section_items,
961            ..
962        } = spec;
963        if self.focus.targets() != focus_targets.as_slice() {
964            self.focus.register_page(focus_targets);
965        }
966        self.focus.set_section_items(section_items);
967    }
968
969    fn handle_key_inner<S: ?Sized>(
970        &mut self,
971        key: KeyEvent,
972        state: &mut S,
973    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
974    where
975        Pages: PageProvider<V, S, O>,
976        Handler: TuiActionHandler<V, A, S, O, M>,
977        Hooks: HookDispatch<S, V, A, O, M>,
978    {
979        let spec = self.current_page_spec(state);
980        let modes = spec.modes.clone();
981        let page_accepts_text_input = spec.accepts_text_input;
982        self.sync_focus_to_spec(spec);
983
984        let ctx = ActionContext {
985            current_view: self.current_view().clone(),
986            focus: self.focus.current(),
987            has_overlay: self.focus.has_overlay(),
988        };
989        let focused_hook = self.focused_hook_context(&ctx, state);
990        let focused_canvas_accepts_text =
991            matches!(focused_hook, Some((_, InputLayerContext::Text)));
992        let accepts_text_input = page_accepts_text_input || focused_canvas_accepts_text;
993        let focus_accepts_mapped_text = focused_canvas_accepts_text
994            || (page_accepts_text_input
995                && self
996                    .focus
997                    .current()
998                    .as_ref()
999                    .map(FocusTarget::is_canvas)
1000                    .unwrap_or(false));
1001
1002        // The orchestrator drives the input layers as an ordered stack. The
1003        // global keymap (`self.input`) and each canvas hook are layers; the
1004        // first to claim the key wins, and whichever begins a multi-key
1005        // sequence becomes the sticky `active_owner` for the keys that follow.
1006        //
1007        let order = self.layer_order(focused_hook, page_accepts_text_input);
1008
1009        // A sequence in flight routes its continuation straight to its owner.
1010        if let Some(owner) = self.active_owner {
1011            match self.run_layer(
1012                owner,
1013                key,
1014                &ctx,
1015                &modes,
1016                accepts_text_input,
1017                focus_accepts_mapped_text,
1018                state,
1019            )? {
1020                LayerResult::Pending(status) => return Ok(TuiPagesOutput::new(status, false)),
1021                LayerResult::Handled(output) => {
1022                    self.active_owner = None;
1023                    return Ok(output);
1024                }
1025                // The owner unexpectedly let go of the key; clear it and fall
1026                // through to a fresh arbitration below.
1027                LayerResult::Ignored(_) => self.active_owner = None,
1028            }
1029        }
1030
1031        let mut text_chord = None;
1032        for owner in order {
1033            match self.run_layer(
1034                owner,
1035                key,
1036                &ctx,
1037                &modes,
1038                accepts_text_input,
1039                focus_accepts_mapped_text,
1040                state,
1041            )? {
1042                LayerResult::Pending(status) => {
1043                    self.active_owner = Some(owner);
1044                    return Ok(TuiPagesOutput::new(status, false));
1045                }
1046                LayerResult::Handled(output) => return Ok(output),
1047                LayerResult::Ignored(chord) => {
1048                    if chord.is_some() {
1049                        text_chord = chord;
1050                    }
1051                }
1052            }
1053        }
1054
1055        // No layer claimed the key: it is plain text for the focused widget.
1056        let chord = text_chord.unwrap_or_else(|| KeyChord::from_event(&key));
1057        let quit_requested = self.dispatch_text(chord, state)?;
1058        Ok(TuiPagesOutput::new(
1059            TuiPagesStatus::TextHandled,
1060            quit_requested,
1061        ))
1062    }
1063
1064    fn focused_hook_context<S: ?Sized>(
1065        &self,
1066        ctx: &ActionContext<V, O>,
1067        state: &S,
1068    ) -> Option<(usize, InputLayerContext)>
1069    where
1070        Hooks: HookDispatch<S, V, A, O, M>,
1071    {
1072        Hooks::focused_hook_context(&self.key_hooks, ctx, state)
1073    }
1074
1075    fn layer_order(
1076        &self,
1077        focused_hook: Option<(usize, InputLayerContext)>,
1078        page_accepts_text_input: bool,
1079    ) -> Vec<LayerOwner> {
1080        let mut order = Vec::with_capacity(self.key_hooks.len() + 1);
1081        let focused_index = focused_hook.map(|(index, _)| index);
1082        let text_context =
1083            page_accepts_text_input || matches!(focused_hook, Some((_, InputLayerContext::Text)));
1084
1085        if text_context {
1086            if let Some(index) = focused_index {
1087                order.push(LayerOwner::Hook(index));
1088            } else {
1089                order.extend((0..self.key_hooks.len()).map(LayerOwner::Hook));
1090            }
1091            order.push(LayerOwner::Keymap);
1092        } else {
1093            order.push(LayerOwner::Keymap);
1094            if let Some(index) = focused_index {
1095                order.push(LayerOwner::Hook(index));
1096            }
1097        }
1098
1099        let remaining = (0..self.key_hooks.len())
1100            .map(LayerOwner::Hook)
1101            .filter(|owner| !order.contains(owner))
1102            .collect::<Vec<_>>();
1103        order.extend(remaining);
1104        order
1105    }
1106
1107    /// Offer a key to a single input layer and normalise its outcome into a
1108    /// [`LayerResult`] the orchestrator can act on uniformly.
1109    #[allow(clippy::too_many_arguments)]
1110    fn run_layer<S: ?Sized>(
1111        &mut self,
1112        owner: LayerOwner,
1113        key: KeyEvent,
1114        ctx: &ActionContext<V, O>,
1115        modes: &[ModeId],
1116        accepts_text_input: bool,
1117        focus_accepts_mapped_text: bool,
1118        state: &mut S,
1119    ) -> TuiPagesResult<LayerResult<A>, Handler::Error>
1120    where
1121        Handler: TuiActionHandler<V, A, S, O, M>,
1122        Pages: PageProvider<V, S, O>,
1123        Hooks: HookDispatch<S, V, A, O, M>,
1124    {
1125        match owner {
1126            LayerOwner::Hook(index) => {
1127                let response =
1128                    Hooks::dispatch_hook(&mut self.key_hooks[index], key, ctx.clone(), state);
1129                match response {
1130                    None => Ok(LayerResult::Ignored(None)),
1131                    Some(KeyHookOutcome {
1132                        status,
1133                        outcome,
1134                        routing,
1135                    }) => {
1136                        let quit_requested = self.apply_outcome(outcome, state);
1137                        if matches!(routing, KeyHookRouting::Pending) {
1138                            Ok(LayerResult::Pending(status))
1139                        } else {
1140                            Ok(LayerResult::Handled(TuiPagesOutput::new(
1141                                status,
1142                                quit_requested,
1143                            )))
1144                        }
1145                    }
1146                }
1147            }
1148            LayerOwner::Keymap => {
1149                let response = match self.input.process(key, modes, accepts_text_input) {
1150                    crate::input::PipelineResponse::Type(chord) if focus_accepts_mapped_text => {
1151                        self.text_input_mapper
1152                            .and_then(|mapper| mapper(chord))
1153                            .map(crate::input::PipelineResponse::Execute)
1154                            .unwrap_or(crate::input::PipelineResponse::Type(chord))
1155                    }
1156                    response => response,
1157                };
1158                match response {
1159                    crate::input::PipelineResponse::Execute(action) => {
1160                        let quit_requested = self.dispatch_action(action, state)?;
1161                        Ok(LayerResult::Handled(TuiPagesOutput::new(
1162                            TuiPagesStatus::ActionHandled,
1163                            quit_requested,
1164                        )))
1165                    }
1166                    crate::input::PipelineResponse::Wait(hints) => {
1167                        Ok(LayerResult::Pending(TuiPagesStatus::Waiting(hints)))
1168                    }
1169                    crate::input::PipelineResponse::Cancel => Ok(LayerResult::Handled(
1170                        TuiPagesOutput::new(TuiPagesStatus::Cancelled, false),
1171                    )),
1172                    crate::input::PipelineResponse::Type(chord) => {
1173                        Ok(LayerResult::Ignored(Some(chord)))
1174                    }
1175                }
1176            }
1177        }
1178    }
1179
1180    /// Route a bracketed-paste payload to the focused canvas widget, if any.
1181    ///
1182    /// Mirrors [`handle_key`](Self::handle_key): it walks the registered hooks
1183    /// and lets the first one whose widget currently holds focus consume the
1184    /// pasted text. Returns [`TuiPagesStatus::TextHandled`] when a widget took
1185    /// the paste, and [`TuiPagesStatus::Cancelled`] when nothing was focused to
1186    /// receive it.
1187    fn handle_paste_inner<S: ?Sized>(
1188        &mut self,
1189        text: &str,
1190        state: &mut S,
1191    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
1192    where
1193        Pages: PageProvider<V, S, O>,
1194        Handler: TuiActionHandler<V, A, S, O, M>,
1195        Hooks: HookDispatch<S, V, A, O, M>,
1196    {
1197        let spec = self.current_page_spec(state);
1198        self.sync_focus_to_spec(spec);
1199
1200        let ctx = ActionContext {
1201            current_view: self.current_view().clone(),
1202            focus: self.focus.current(),
1203            has_overlay: self.focus.has_overlay(),
1204        };
1205
1206        let mut paste_response: Option<KeyHookOutcome<V, A, O, M>> = None;
1207        for hook in &mut self.key_hooks {
1208            let response = Hooks::paste_hook(hook, text, ctx.clone(), state);
1209            if let Some(response) = response {
1210                paste_response = Some(response);
1211                break;
1212            }
1213        }
1214
1215        if let Some(response) = paste_response {
1216            let quit_requested = self.apply_outcome(response.outcome, state);
1217            return Ok(TuiPagesOutput::new(response.status, quit_requested));
1218        }
1219
1220        Ok(TuiPagesOutput::new(TuiPagesStatus::Cancelled, false))
1221    }
1222
1223    fn submit_command_inner<S: ?Sized>(
1224        &mut self,
1225        input: &str,
1226        state: &mut S,
1227    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
1228    where
1229        Pages: PageProvider<V, S, O>,
1230        Handler: TuiActionHandler<V, A, S, O, M>,
1231        Hooks: HookDispatch<S, V, A, O, M>,
1232    {
1233        match self.commands.process(input) {
1234            CommandResponse::Execute(action) => {
1235                let quit_requested = self.dispatch_action(action, state)?;
1236                Ok(TuiPagesOutput::new(
1237                    TuiPagesStatus::ActionHandled,
1238                    quit_requested,
1239                ))
1240            }
1241            CommandResponse::Incomplete(hints) => Ok(TuiPagesOutput::new(
1242                TuiPagesStatus::CommandIncomplete(hints),
1243                false,
1244            )),
1245            CommandResponse::Unknown => {
1246                Ok(TuiPagesOutput::new(TuiPagesStatus::CommandUnknown, false))
1247            }
1248            CommandResponse::Empty => Ok(TuiPagesOutput::new(TuiPagesStatus::CommandEmpty, false)),
1249        }
1250    }
1251
1252    pub fn apply_effect<S: ?Sized>(
1253        &mut self,
1254        effect: TuiEffect<V, O, M>,
1255        state: &S,
1256    ) -> bool
1257    where
1258        Pages: PageProvider<V, S, O>,
1259    {
1260        match effect {
1261            TuiEffect::None => false,
1262            TuiEffect::Focus(intent) => {
1263                self.focus.apply_focus_intent(intent);
1264                false
1265            }
1266            TuiEffect::Navigate(view) => {
1267                self.reset_input_routing();
1268                self.buffer.update_history(view);
1269                self.refresh_page(state);
1270                false
1271            }
1272            TuiEffect::NextBuffer => {
1273                self.reset_input_routing();
1274                self.switch_buffer(true, state);
1275                false
1276            }
1277            TuiEffect::PreviousBuffer => {
1278                self.reset_input_routing();
1279                self.switch_buffer(false, state);
1280                false
1281            }
1282            TuiEffect::CloseBuffer => {
1283                self.reset_input_routing();
1284                self.buffer.close_active_buffer(self.fallback_view.clone());
1285                self.refresh_page(state);
1286                false
1287            }
1288            TuiEffect::SplitPane(split) => {
1289                self.buffer.split_active_pane(split);
1290                false
1291            }
1292            TuiEffect::ClosePane => {
1293                self.buffer.close_active_pane();
1294                self.refresh_page(state);
1295                false
1296            }
1297            TuiEffect::NextPane => {
1298                self.buffer.focus_next_pane(self.focus.focus_wrap());
1299                self.refresh_page(state);
1300                false
1301            }
1302            TuiEffect::PreviousPane => {
1303                self.buffer.focus_previous_pane(self.focus.focus_wrap());
1304                self.refresh_page(state);
1305                false
1306            }
1307            TuiEffect::RefreshPage => {
1308                self.refresh_page(state);
1309                false
1310            }
1311            TuiEffect::Quit => true,
1312        }
1313    }
1314
1315    fn current_page_spec<S: ?Sized>(&self, state: &S) -> PageSpec<O>
1316    where
1317        Pages: PageProvider<V, S, O>,
1318    {
1319        let view = self.current_view();
1320        let focus = self.focus.current();
1321        self.pages.page_spec(view, state, focus.as_ref())
1322    }
1323
1324    fn dispatch_action<S: ?Sized>(
1325        &mut self,
1326        action: A,
1327        state: &mut S,
1328    ) -> TuiPagesResult<bool, Handler::Error>
1329    where
1330        Pages: PageProvider<V, S, O>,
1331        Handler: TuiActionHandler<V, A, S, O, M>,
1332        Hooks: HookDispatch<S, V, A, O, M>,
1333    {
1334        let ctx = ActionContext {
1335            current_view: self.current_view().clone(),
1336            focus: self.focus.current(),
1337            has_overlay: self.focus.has_overlay(),
1338        };
1339        let runtime = RuntimeContext {
1340            focus: &mut self.focus,
1341            commands: &mut self.commands,
1342        };
1343        let outcome = self
1344            .handler
1345            .handle_action(action, ctx, state, runtime)
1346            .map_err(TuiPagesError::Handler)?;
1347        Ok(self.apply_outcome(outcome, state))
1348    }
1349
1350    fn dispatch_text<S: ?Sized>(
1351        &mut self,
1352        chord: KeyChord,
1353        state: &mut S,
1354    ) -> TuiPagesResult<bool, Handler::Error>
1355    where
1356        Pages: PageProvider<V, S, O>,
1357        Handler: TuiActionHandler<V, A, S, O, M>,
1358        Hooks: HookDispatch<S, V, A, O, M>,
1359    {
1360        let ctx = ActionContext {
1361            current_view: self.current_view().clone(),
1362            focus: self.focus.current(),
1363            has_overlay: self.focus.has_overlay(),
1364        };
1365        let runtime = RuntimeContext {
1366            focus: &mut self.focus,
1367            commands: &mut self.commands,
1368        };
1369        let outcome = self
1370            .handler
1371            .handle_text(chord, ctx, state, runtime)
1372            .map_err(TuiPagesError::Handler)?;
1373        Ok(self.apply_outcome(outcome, state))
1374    }
1375
1376    fn apply_outcome<S: ?Sized>(
1377        &mut self,
1378        outcome: ActionOutcome<V, O, M>,
1379        state: &S,
1380    ) -> bool
1381    where
1382        Pages: PageProvider<V, S, O>,
1383    {
1384        let mut quit_requested = false;
1385        for effect in outcome.effects {
1386            quit_requested |= self.apply_effect(effect, state);
1387        }
1388        quit_requested
1389    }
1390
1391    fn switch_buffer<S: ?Sized>(&mut self, forward: bool, state: &S)
1392    where
1393        Pages: PageProvider<V, S, O>,
1394    {
1395        if self.buffer.history.len() <= 1 {
1396            return;
1397        }
1398
1399        let len = self.buffer.history.len();
1400        self.buffer.active_index =
1401            self.focus
1402                .focus_wrap()
1403                .step(self.buffer.active_index, len, forward);
1404        self.buffer.sync_active_pane_to_active_buffer();
1405        self.refresh_page(state);
1406    }
1407}
1408
1409impl<V, A, Pages, Handler, O, M> TuiPages<V, A, Pages, Handler, O, M, NoCanvasHooks>
1410where
1411    V: Clone + PartialEq,
1412    A: Clone,
1413    O: Clone + PartialEq,
1414{
1415    #[cfg(feature = "canvas")]
1416    pub fn default_cursor_behavior<S: ?Sized>(
1417        &mut self,
1418        state: &S,
1419    ) -> crate::canvas::DefaultCursorBehavior
1420    where
1421        Pages: PageProvider<V, S, O>,
1422    {
1423        let spec = self.current_page_spec(state);
1424        self.sync_focus_to_spec(spec);
1425        crate::canvas::DefaultCursorBehavior::Hidden
1426    }
1427
1428    pub fn handle_key<S: ?Sized>(
1429        &mut self,
1430        key: KeyEvent,
1431        state: &mut S,
1432    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
1433    where
1434        Pages: PageProvider<V, S, O>,
1435        Handler: TuiActionHandler<V, A, S, O, M>,
1436    {
1437        self.handle_key_inner(key, state)
1438    }
1439
1440    pub fn handle_paste<S: ?Sized>(
1441        &mut self,
1442        text: &str,
1443        state: &mut S,
1444    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
1445    where
1446        Pages: PageProvider<V, S, O>,
1447        Handler: TuiActionHandler<V, A, S, O, M>,
1448    {
1449        self.handle_paste_inner(text, state)
1450    }
1451
1452    pub fn handle_event<S: ?Sized>(
1453        &mut self,
1454        event: Event,
1455        state: &mut S,
1456    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
1457    where
1458        Pages: PageProvider<V, S, O>,
1459        Handler: TuiActionHandler<V, A, S, O, M>,
1460    {
1461        match event {
1462            Event::Key(key) => self.handle_key_inner(key, state),
1463            Event::Paste(text) => self.handle_paste_inner(&text, state),
1464            _ => Ok(TuiPagesOutput::new(TuiPagesStatus::Cancelled, false)),
1465        }
1466    }
1467
1468    pub fn submit_command<S: ?Sized>(
1469        &mut self,
1470        input: &str,
1471        state: &mut S,
1472    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
1473    where
1474        Pages: PageProvider<V, S, O>,
1475        Handler: TuiActionHandler<V, A, S, O, M>,
1476    {
1477        self.submit_command_inner(input, state)
1478    }
1479}
1480
1481#[cfg(feature = "canvas")]
1482impl<V, A, Pages, Handler, O, M> TuiPages<V, A, Pages, Handler, O, M, CanvasHooks>
1483where
1484    V: Clone + PartialEq,
1485    A: Clone,
1486    O: Clone + PartialEq,
1487{
1488    pub fn default_cursor_behavior<S>(
1489        &mut self,
1490        state: &S,
1491    ) -> crate::canvas::DefaultCursorBehavior
1492    where
1493        S: crate::canvas::CanvasWidgetState + ?Sized,
1494        Pages: PageProvider<V, S, O>,
1495    {
1496        let spec = self.current_page_spec(state);
1497        self.sync_focus_to_spec(spec);
1498        let ctx = ActionContext {
1499            current_view: self.current_view().clone(),
1500            focus: self.focus.current(),
1501            has_overlay: self.focus.has_overlay(),
1502        };
1503        <CanvasHooks as HookDispatch<S, V, A, O, M>>::cursor_behavior(&self.key_hooks, &ctx, state)
1504    }
1505
1506    pub fn handle_key<S>(
1507        &mut self,
1508        key: KeyEvent,
1509        state: &mut S,
1510    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
1511    where
1512        S: crate::canvas::CanvasWidgetState + ?Sized,
1513        Pages: PageProvider<V, S, O>,
1514        Handler: TuiActionHandler<V, A, S, O, M>,
1515    {
1516        self.handle_key_inner(key, state)
1517    }
1518
1519    pub fn handle_paste<S>(
1520        &mut self,
1521        text: &str,
1522        state: &mut S,
1523    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
1524    where
1525        S: crate::canvas::CanvasWidgetState + ?Sized,
1526        Pages: PageProvider<V, S, O>,
1527        Handler: TuiActionHandler<V, A, S, O, M>,
1528    {
1529        self.handle_paste_inner(text, state)
1530    }
1531
1532    pub fn handle_event<S>(
1533        &mut self,
1534        event: Event,
1535        state: &mut S,
1536    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
1537    where
1538        S: crate::canvas::CanvasWidgetState + ?Sized,
1539        Pages: PageProvider<V, S, O>,
1540        Handler: TuiActionHandler<V, A, S, O, M>,
1541    {
1542        match event {
1543            Event::Key(key) => self.handle_key_inner(key, state),
1544            Event::Paste(text) => self.handle_paste_inner(&text, state),
1545            _ => Ok(TuiPagesOutput::new(TuiPagesStatus::Cancelled, false)),
1546        }
1547    }
1548
1549    pub fn submit_command<S>(
1550        &mut self,
1551        input: &str,
1552        state: &mut S,
1553    ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error>
1554    where
1555        S: crate::canvas::CanvasWidgetState + ?Sized,
1556        Pages: PageProvider<V, S, O>,
1557        Handler: TuiActionHandler<V, A, S, O, M>,
1558    {
1559        self.submit_command_inner(input, state)
1560    }
1561}
1562
1563#[derive(Debug, Clone)]
1564pub struct TuiPagesBuilder<
1565    V,
1566    A,
1567    O = (),
1568    M = (),
1569    Pages = (),
1570    Handler = (),
1571    Hooks = NoCanvasHooks,
1572> {
1573    pub(crate) initial_view: V,
1574    pub(crate) fallback_view: Option<V>,
1575    pub(crate) input_registry: InputRegistry<A>,
1576    pub(crate) command_registry: CommandRegistry<A>,
1577    pub(crate) input_timeout_ms: u64,
1578    pub(crate) command_timeout_ms: u64,
1579    pub(crate) focus_wrap: FocusWrap,
1580    pub(crate) reserve_command_line: bool,
1581    pub(crate) text_input_mapper: Option<fn(KeyChord) -> Option<A>>,
1582    pub(crate) key_hooks: Vec<KeyHook>,
1583    pub(crate) keybinding_store: Option<BindingStore<A>>,
1584    pub(crate) keybinding_report: Option<KeybindingReport<A>>,
1585    pub(crate) keybinding_inheritances: Vec<KeybindingInheritance<A>>,
1586    pub(crate) action_registry: Option<crate::keybindings::ActionRegistry<A>>,
1587    #[cfg(feature = "canvas")]
1588    pub(crate) canvas_keybinding_profile: CanvasKeybindingProfileHandle,
1589    pub(crate) pages: Pages,
1590    pub(crate) handler: Handler,
1591    pub(crate) _overlay: PhantomData<O>,
1592    pub(crate) _modal: PhantomData<M>,
1593    pub(crate) _hooks: PhantomData<Hooks>,
1594}
1595
1596impl<V, A, O, M> TuiPagesBuilder<V, A, O, M, (), (), NoCanvasHooks> {
1597    pub fn new(initial_view: V) -> Self {
1598        Self {
1599            initial_view,
1600            fallback_view: None,
1601            input_registry: InputRegistry::empty(),
1602            command_registry: CommandRegistry::new(),
1603            input_timeout_ms: 1000,
1604            command_timeout_ms: 1000,
1605            focus_wrap: FocusWrap::default(),
1606            reserve_command_line: cfg!(feature = "command-line"),
1607            text_input_mapper: None,
1608            key_hooks: Vec::new(),
1609            keybinding_store: None,
1610            keybinding_report: None,
1611            keybinding_inheritances: Vec::new(),
1612            action_registry: None,
1613            #[cfg(feature = "canvas")]
1614            canvas_keybinding_profile: Rc::new(RefCell::new(CanvasKeybindingProfileState::new(
1615                crate::canvas::BuiltinCanvasKeybindingPreset::Vim.profile(),
1616            ))),
1617            pages: (),
1618            handler: (),
1619            _overlay: PhantomData,
1620            _modal: PhantomData,
1621            _hooks: PhantomData,
1622        }
1623    }
1624}
1625
1626impl<V, A, O, M, Pages, Handler, Hooks>
1627    TuiPagesBuilder<V, A, O, M, Pages, Handler, Hooks>
1628{
1629    /// Supply the table that maps `[keymap.*]` action *names* to the app's
1630    /// action type `A`, used to load and export keybindings. When unset, the
1631    /// crate defaults to [`ActionRegistry::navigation`](crate::keybindings::ActionRegistry::navigation).
1632    /// Build one from `navigation_bindable_actions()` /
1633    /// `canvas_bindable_actions()` plus the app's own bindable actions.
1634    pub fn action_registry(mut self, registry: crate::keybindings::ActionRegistry<A>) -> Self {
1635        self.action_registry = Some(registry);
1636        self
1637    }
1638
1639    pub fn fallback_view(mut self, fallback_view: V) -> Self {
1640        self.fallback_view = Some(fallback_view);
1641        self
1642    }
1643
1644    pub fn input_timeout_ms(mut self, timeout_ms: u64) -> Self {
1645        self.input_timeout_ms = timeout_ms;
1646        self
1647    }
1648
1649    pub fn command_timeout_ms(mut self, timeout_ms: u64) -> Self {
1650        self.command_timeout_ms = timeout_ms;
1651        self
1652    }
1653
1654    pub fn pages<NextPages>(
1655        self,
1656        pages: NextPages,
1657    ) -> TuiPagesBuilder<V, A, O, M, NextPages, Handler, Hooks> {
1658        TuiPagesBuilder {
1659            initial_view: self.initial_view,
1660            fallback_view: self.fallback_view,
1661            input_registry: self.input_registry,
1662            command_registry: self.command_registry,
1663            input_timeout_ms: self.input_timeout_ms,
1664            command_timeout_ms: self.command_timeout_ms,
1665            focus_wrap: self.focus_wrap,
1666            reserve_command_line: self.reserve_command_line,
1667            text_input_mapper: self.text_input_mapper,
1668            key_hooks: self.key_hooks,
1669            keybinding_store: self.keybinding_store,
1670            keybinding_report: self.keybinding_report,
1671            keybinding_inheritances: self.keybinding_inheritances,
1672            action_registry: self.action_registry,
1673            #[cfg(feature = "canvas")]
1674            canvas_keybinding_profile: self.canvas_keybinding_profile,
1675            pages,
1676            handler: self.handler,
1677            _overlay: PhantomData,
1678            _modal: PhantomData,
1679            _hooks: PhantomData,
1680        }
1681    }
1682
1683    /// Set the page provider to a plain `fn`, coercing it to [`PageFn`] at the
1684    /// call site so the application never writes `page_spec as PageFn<…>`.
1685    ///
1686    /// `.pages(f)` keeps the fn *item* type, which a `type App = TuiPages<…>`
1687    /// alias cannot name; this method pins the [`PageFn`] pointer type the
1688    /// alias uses, so `.page_fn(page_spec)` just works:
1689    ///
1690    /// ```ignore
1691    /// TuiPages::builder(View::Home).page_fn(page_spec).handler(Handler).build()
1692    /// ```
1693    pub fn page_fn<S>(
1694        self,
1695        page_fn: PageFn<V, S, O>,
1696    ) -> TuiPagesBuilder<V, A, O, M, PageFn<V, S, O>, Handler, Hooks> {
1697        self.pages(page_fn)
1698    }
1699
1700    pub fn handler<NextHandler>(
1701        self,
1702        handler: NextHandler,
1703    ) -> TuiPagesBuilder<V, A, O, M, Pages, NextHandler, Hooks> {
1704        TuiPagesBuilder {
1705            initial_view: self.initial_view,
1706            fallback_view: self.fallback_view,
1707            input_registry: self.input_registry,
1708            command_registry: self.command_registry,
1709            input_timeout_ms: self.input_timeout_ms,
1710            command_timeout_ms: self.command_timeout_ms,
1711            focus_wrap: self.focus_wrap,
1712            reserve_command_line: self.reserve_command_line,
1713            text_input_mapper: self.text_input_mapper,
1714            key_hooks: self.key_hooks,
1715            keybinding_store: self.keybinding_store,
1716            keybinding_report: self.keybinding_report,
1717            keybinding_inheritances: self.keybinding_inheritances,
1718            action_registry: self.action_registry,
1719            #[cfg(feature = "canvas")]
1720            canvas_keybinding_profile: self.canvas_keybinding_profile,
1721            pages: self.pages,
1722            handler,
1723            _overlay: PhantomData,
1724            _modal: PhantomData,
1725            _hooks: PhantomData,
1726        }
1727    }
1728
1729    /// Set how focus navigation behaves at the ends of a list — clamp (default)
1730    /// or wrap-around. Applies to page focus and modal items.
1731    pub fn focus_wrap(mut self, wrap: FocusWrap) -> Self {
1732        self.focus_wrap = wrap;
1733        self
1734    }
1735
1736    pub fn reserve_command_line(mut self, reserve: bool) -> Self {
1737        self.reserve_command_line = reserve;
1738        self
1739    }
1740
1741    /// Map raw text-input chords into application actions before
1742    /// [`TuiActionHandler::handle_text`] is called.
1743    ///
1744    /// The mapper only runs when the current focus target is a canvas target
1745    /// and the current [`PageSpec`] accepts text input. This keeps command bars,
1746    /// palettes, and other text overlays on their normal `handle_text` path.
1747    pub fn text_input_mapper(mut self, mapper: fn(KeyChord) -> Option<A>) -> Self {
1748        self.text_input_mapper = Some(mapper);
1749        self
1750    }
1751
1752    pub fn keymap(
1753        mut self,
1754        mode: impl Into<ModeId>,
1755        configure: impl FnOnce(&mut KeyMap<A>),
1756    ) -> Self {
1757        let mode = mode.into();
1758        configure(self.input_registry.map_mut(mode.as_str()));
1759        self
1760    }
1761
1762    pub fn bind(mut self, mode: impl Into<ModeId>, binding: &str, action: A) -> Self {
1763        let mode = mode.into();
1764        self.input_registry
1765            .map_mut(mode.as_str())
1766            .bind(parse_binding(binding), action);
1767        self
1768    }
1769
1770    /// Replace the built-in keymap layer with a pre-built registry. This is the
1771    /// app's default bindings, on top of which `[keymap.*]` config overrides and
1772    /// runtime rebinds layer. Use when the app builds its defaults as a whole
1773    /// [`InputRegistry`] rather than one `.bind()` at a time; call it before
1774    /// [`keybindings_toml`](Self::keybindings_toml) so the config layers on top.
1775    pub fn input_registry(mut self, registry: InputRegistry<A>) -> Self {
1776        self.input_registry = registry;
1777        self
1778    }
1779
1780    pub fn inherit_keybinding(
1781        mut self,
1782        target_mode: impl Into<ModeId>,
1783        target_action: A,
1784        source_mode: impl Into<ModeId>,
1785        source_action: A,
1786    ) -> Self {
1787        self.keybinding_inheritances
1788            .push(KeybindingInheritance::new(
1789                target_mode,
1790                target_action,
1791                source_mode,
1792                source_action,
1793            ));
1794        self
1795    }
1796
1797    pub fn command<I, Alias>(
1798        mut self,
1799        action_name: impl Into<String>,
1800        aliases: I,
1801        action: A,
1802    ) -> Self
1803    where
1804        A: Clone,
1805        I: IntoIterator<Item = Alias>,
1806        Alias: Into<String>,
1807    {
1808        self.command_registry
1809            .bind_aliases(action_name, aliases, action);
1810        self
1811    }
1812
1813    pub fn build(self) -> TuiPages<V, A, Pages, Handler, O, M, Hooks>
1814    where
1815        V: Clone + PartialEq,
1816    {
1817        let fallback_view = self
1818            .fallback_view
1819            .unwrap_or_else(|| self.initial_view.clone());
1820
1821        let mut focus = FocusManager::new();
1822        focus.set_focus_wrap(self.focus_wrap);
1823
1824        TuiPages {
1825            input: InputPipeline::new(self.input_registry, self.input_timeout_ms),
1826            commands: CommandResolver::new(self.command_registry, self.command_timeout_ms),
1827            focus,
1828            buffer: BufferState::new(self.initial_view),
1829            pages: self.pages,
1830            handler: self.handler,
1831            fallback_view,
1832            reserve_command_line: self.reserve_command_line,
1833            text_input_mapper: self.text_input_mapper,
1834            key_hooks: self.key_hooks,
1835            active_owner: None,
1836            keybinding_store: self.keybinding_store,
1837            keybinding_report: self.keybinding_report,
1838            keybinding_inheritances: self.keybinding_inheritances,
1839            action_registry: self.action_registry,
1840            #[cfg(feature = "canvas")]
1841            canvas_keybinding_profile: self.canvas_keybinding_profile,
1842            _hooks: PhantomData,
1843        }
1844    }
1845}
1846
1847impl<V, A, O, M, Pages, Handler, Hooks> TuiPagesBuilder<V, A, O, M, Pages, Handler, Hooks>
1848where
1849    A: Clone + PartialEq + From<NavigationAction>,
1850{
1851    pub fn keybindings_toml(mut self, source: &str) -> Result<Self, KeybindingConfigError> {
1852        let config = KeybindingConfig::from_toml(source)?;
1853        self = self.keybindings_config(config)?;
1854        Ok(self)
1855    }
1856
1857    pub fn keybindings_config(
1858        mut self,
1859        config: KeybindingConfig,
1860    ) -> Result<Self, KeybindingConfigError> {
1861        let actions = self
1862            .action_registry
1863            .clone()
1864            .unwrap_or_else(crate::keybindings::ActionRegistry::navigation);
1865        let (store, registry, report) = BindingStore::with_user_config_and_inheritances(
1866            &self.input_registry,
1867            &config,
1868            &actions,
1869            self.keybinding_inheritances.clone(),
1870        )?;
1871        self.input_registry = registry;
1872        #[cfg(feature = "canvas")]
1873        {
1874            let profile = config.canvas_profile()?;
1875            self.canvas_keybinding_profile.borrow_mut().replace(profile);
1876        }
1877        self.keybinding_store = Some(store);
1878        self.keybinding_report = Some(report);
1879        Ok(self)
1880    }
1881}
1882
1883#[cfg(test)]
1884mod tests {
1885    use super::*;
1886    use crate::keybindings::NavigationAction;
1887
1888    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1889    enum View {
1890        Main,
1891    }
1892
1893    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1894    enum Action {
1895        Nav(NavigationAction),
1896    }
1897
1898    impl From<NavigationAction> for Action {
1899        fn from(value: NavigationAction) -> Self {
1900            Self::Nav(value)
1901        }
1902    }
1903
1904    struct Handler;
1905
1906    impl TuiActionHandler<View, Action, ()> for Handler {
1907        type Error = std::convert::Infallible;
1908
1909        fn handle_action(
1910            &mut self,
1911            _action: Action,
1912            _ctx: ActionContext<View>,
1913            _state: &mut (),
1914            _runtime: RuntimeContext<'_, Action>,
1915        ) -> Result<ActionOutcome<View>, Self::Error> {
1916            Ok(ActionOutcome::none())
1917        }
1918    }
1919
1920    fn page_spec(_view: &View, _state: &(), _focus: Option<&FocusTarget>) -> PageSpec {
1921        PageSpec::new()
1922    }
1923
1924    #[test]
1925    fn runtime_rebind_keymap_and_reset_restore_defaults() {
1926        let mut app = TuiPages::<View, Action>::builder(View::Main)
1927            .page_fn(page_spec)
1928            .handler(Handler)
1929            .bind(modes::GLOBAL, "ctrl+c", Action::Nav(NavigationAction::Quit))
1930            .build();
1931
1932        let report = app
1933            .rebind_keymap("global", "ctrl+q", Action::Nav(NavigationAction::Quit))
1934            .unwrap();
1935        assert!(report.notices.is_empty());
1936        let global = app.input.registry.maps.get("global").unwrap();
1937        assert!(
1938            global
1939                .bindings
1940                .contains_key(&crate::input::try_parse_binding("ctrl+q").unwrap())
1941        );
1942        assert!(
1943            !global
1944                .bindings
1945                .contains_key(&crate::input::try_parse_binding("ctrl+c").unwrap())
1946        );
1947
1948        app.reset_keybindings_to_defaults();
1949        let global = app.input.registry.maps.get("global").unwrap();
1950        assert!(
1951            global
1952                .bindings
1953                .contains_key(&crate::input::try_parse_binding("ctrl+c").unwrap())
1954        );
1955        assert!(
1956            !global
1957                .bindings
1958                .contains_key(&crate::input::try_parse_binding("ctrl+q").unwrap())
1959        );
1960    }
1961
1962    #[test]
1963    fn export_keybindings_toml_round_trips_and_is_idempotent() {
1964        // Config override (`focus_next = j` in general) + a runtime rebind
1965        // (`quit = ctrl+q` in global), exported and reloaded into a fresh app.
1966        let mut app = TuiPages::<View, Action>::builder(View::Main)
1967            .page_fn(page_spec)
1968            .handler(Handler)
1969            .bind(modes::GLOBAL, "ctrl+c", Action::Nav(NavigationAction::Quit))
1970            .keybindings_toml("[keymap.general]\nfocus_next = [\"j\"]\n")
1971            .unwrap()
1972            .build();
1973        app.rebind_keymap("global", "ctrl+q", Action::Nav(NavigationAction::Quit))
1974            .unwrap();
1975
1976        let exported = app.export_keybindings_toml().unwrap();
1977
1978        let reloaded = TuiPages::<View, Action>::builder(View::Main)
1979            .page_fn(page_spec)
1980            .handler(Handler)
1981            .bind(modes::GLOBAL, "ctrl+c", Action::Nav(NavigationAction::Quit))
1982            .keybindings_toml(&exported)
1983            .unwrap()
1984            .build();
1985
1986        let general = reloaded.input.registry.maps.get("general").unwrap();
1987        assert!(
1988            general
1989                .bindings
1990                .contains_key(&crate::input::try_parse_binding("j").unwrap())
1991        );
1992        let global = reloaded.input.registry.maps.get("global").unwrap();
1993        assert!(
1994            global
1995                .bindings
1996                .contains_key(&crate::input::try_parse_binding("ctrl+q").unwrap())
1997        );
1998
1999        // Re-exporting the reloaded state yields the identical document.
2000        assert_eq!(reloaded.export_keybindings_toml().unwrap(), exported);
2001    }
2002}