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
92pub mod modes {
113 use super::ModeId;
114
115 pub const GENERAL: ModeId = ModeId::borrowed("general");
117 pub const NORMAL: ModeId = ModeId::borrowed("nor");
119 pub const INSERT: ModeId = ModeId::borrowed("ins");
121 pub const SELECT: ModeId = ModeId::borrowed("sel");
123 pub const COMMAND: ModeId = ModeId::borrowed("command");
125 pub const COMMON: ModeId = ModeId::borrowed("common");
127 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 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 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
188pub type PageFn<V, S, O = ()> = fn(&V, &S, Option<&FocusTarget<O>>) -> PageSpec<O>;
204
205pub 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#[cfg_attr(not(feature = "canvas"), allow(dead_code))]
387pub(crate) enum KeyHookRouting {
388 Handled,
389 Pending,
390}
391
392#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581pub(crate) enum LayerOwner {
582 Keymap,
584 Hook(usize),
586}
587
588pub(crate) enum LayerResult<A> {
592 Pending(TuiPagesStatus<A>),
595 Handled(TuiPagesOutput<A>),
597 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 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 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 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 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 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 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 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 let order = self.layer_order(focused_hook, page_accepts_text_input);
1008
1009 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 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 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 #[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 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 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 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 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 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 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 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 assert_eq!(reloaded.export_keybindings_toml().unwrap(), exported);
2001 }
2002}