1use crate::command::{CommandHint, CommandRegistry, CommandResolver, CommandResponse};
2use crate::focus::{FocusController, FocusIntent, FocusManager, FocusTarget, FocusWrap};
3use crate::input::{parse_binding, InputHint, InputPipeline, InputRegistry, KeyChord, KeyMap};
4use crate::navigation::{BufferState, PaneSplit};
5use crossterm::event::KeyEvent;
6use std::borrow::Cow;
7use std::error::Error;
8use std::fmt;
9use std::marker::PhantomData;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
13pub struct ModeId(Cow<'static, str>);
14
15impl ModeId {
16 pub const fn borrowed(value: &'static str) -> Self {
17 Self(Cow::Borrowed(value))
18 }
19
20 pub fn owned(value: impl Into<String>) -> Self {
21 Self(Cow::Owned(value.into()))
22 }
23
24 pub fn as_str(&self) -> &str {
25 self.0.as_ref()
26 }
27}
28
29impl AsRef<str> for ModeId {
30 fn as_ref(&self) -> &str {
31 self.as_str()
32 }
33}
34
35impl fmt::Display for ModeId {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 f.write_str(self.as_str())
38 }
39}
40
41impl From<&'static str> for ModeId {
42 fn from(value: &'static str) -> Self {
43 Self::borrowed(value)
44 }
45}
46
47impl From<String> for ModeId {
48 fn from(value: String) -> Self {
49 Self(Cow::Owned(value))
50 }
51}
52
53pub mod modes {
74 use super::ModeId;
75
76 pub const GENERAL: ModeId = ModeId::borrowed("general");
78 pub const NORMAL: ModeId = ModeId::borrowed("nor");
80 pub const INSERT: ModeId = ModeId::borrowed("ins");
82 pub const SELECT: ModeId = ModeId::borrowed("sel");
84 pub const COMMAND: ModeId = ModeId::borrowed("command");
86 pub const COMMON: ModeId = ModeId::borrowed("common");
88 pub const GLOBAL: ModeId = ModeId::borrowed("global");
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93#[non_exhaustive]
94pub struct PageSpec<O = ()> {
95 pub focus_targets: Vec<FocusTarget<O>>,
96 pub(crate) section_items: Vec<(usize, usize)>,
101 pub modes: Vec<ModeId>,
102 pub accepts_text_input: bool,
103}
104
105impl<O> Default for PageSpec<O> {
106 fn default() -> Self {
107 Self {
108 focus_targets: Vec::new(),
109 section_items: Vec::new(),
110 modes: vec![modes::GENERAL, modes::GLOBAL],
111 accepts_text_input: false,
112 }
113 }
114}
115
116impl<O> PageSpec<O> {
117 pub fn new() -> Self {
118 Self::default()
119 }
120
121 pub fn focus_targets(mut self, targets: Vec<FocusTarget<O>>) -> Self {
122 self.focus_targets = targets;
123 self
124 }
125
126 pub fn focus(mut self, builder: crate::PageFocusBuilder<O>) -> Self {
132 let (targets, section_items) = builder.into_parts();
133 self.focus_targets = targets;
134 self.section_items = section_items;
135 self
136 }
137
138 pub fn modes(mut self, modes: impl IntoIterator<Item = ModeId>) -> Self {
139 self.modes = modes.into_iter().collect();
140 self
141 }
142
143 pub fn accepts_text_input(mut self, accepts_text_input: bool) -> Self {
144 self.accepts_text_input = accepts_text_input;
145 self
146 }
147}
148
149pub type PageFn<V, S, O = ()> = fn(&V, &S, Option<&FocusTarget<O>>) -> PageSpec<O>;
165
166pub type TuiApp<V, A, S, Handler, O = (), M = ()> =
186 TuiPages<V, A, S, PageFn<V, S, O>, Handler, O, M>;
187
188pub trait PageProvider<V, S, O = ()> {
189 fn page_spec(&self, view: &V, state: &S, focus: Option<&FocusTarget<O>>) -> PageSpec<O>;
190}
191
192impl<V, S, O, F> PageProvider<V, S, O> for F
193where
194 F: Fn(&V, &S, Option<&FocusTarget<O>>) -> PageSpec<O>,
195{
196 fn page_spec(&self, view: &V, state: &S, focus: Option<&FocusTarget<O>>) -> PageSpec<O> {
197 self(view, state, focus)
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum TuiEffect<V, O = (), M = ()> {
203 None,
204 Focus(FocusIntent<O, M>),
205 Navigate(V),
206 NextBuffer,
207 PreviousBuffer,
208 CloseBuffer,
209 SplitPane(PaneSplit),
210 ClosePane,
211 NextPane,
212 PreviousPane,
213 RefreshPage,
214 Quit,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct ActionOutcome<V, O = (), M = ()> {
219 pub effects: Vec<TuiEffect<V, O, M>>,
220}
221
222impl<V, O, M> Default for ActionOutcome<V, O, M> {
223 fn default() -> Self {
224 Self {
225 effects: Vec::new(),
226 }
227 }
228}
229
230impl<V, O, M> ActionOutcome<V, O, M> {
231 pub fn none() -> Self {
232 Self::default()
233 }
234
235 pub fn effect(effect: TuiEffect<V, O, M>) -> Self {
236 Self {
237 effects: vec![effect],
238 }
239 }
240
241 pub fn effects(effects: impl IntoIterator<Item = TuiEffect<V, O, M>>) -> Self {
242 Self {
243 effects: effects.into_iter().collect(),
244 }
245 }
246
247 pub fn push(&mut self, effect: TuiEffect<V, O, M>) {
248 self.effects.push(effect);
249 }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct ActionContext<V, O = ()> {
254 pub current_view: V,
255 pub focus: Option<FocusTarget<O>>,
256 pub has_overlay: bool,
257}
258
259pub trait TuiActionHandler<V, A, S, O = (), M = ()> {
260 type Error;
261
262 fn handle_action(
263 &mut self,
264 action: A,
265 ctx: ActionContext<V, O>,
266 state: &mut S,
267 ) -> Result<ActionOutcome<V, O, M>, Self::Error>;
268
269 fn handle_text(
270 &mut self,
271 _chord: KeyChord,
272 _ctx: ActionContext<V, O>,
273 _state: &mut S,
274 ) -> Result<ActionOutcome<V, O, M>, Self::Error> {
275 Ok(ActionOutcome::none())
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub enum TuiPagesError<E> {
281 Handler(E),
282}
283
284impl<E> fmt::Display for TuiPagesError<E>
285where
286 E: fmt::Display,
287{
288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289 match self {
290 TuiPagesError::Handler(error) => write!(f, "handler error: {error}"),
291 }
292 }
293}
294
295impl<E> Error for TuiPagesError<E> where E: Error + 'static {}
296
297impl<E> From<E> for TuiPagesError<E> {
298 fn from(error: E) -> Self {
299 Self::Handler(error)
300 }
301}
302
303pub type TuiPagesResult<T, E> = Result<T, TuiPagesError<E>>;
304
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub enum TuiPagesStatus<A> {
307 ActionHandled,
308 TextHandled,
309 Waiting(Vec<InputHint<A>>),
310 Cancelled,
311 CommandIncomplete(Vec<CommandHint>),
312 CommandUnknown,
313 CommandEmpty,
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct TuiPagesOutput<A> {
318 pub status: TuiPagesStatus<A>,
319 pub quit_requested: bool,
320}
321
322impl<A> TuiPagesOutput<A> {
323 fn new(status: TuiPagesStatus<A>, quit_requested: bool) -> Self {
324 Self {
325 status,
326 quit_requested,
327 }
328 }
329}
330
331#[derive(Debug, Clone)]
332pub struct TuiPages<V, A, S, Pages = (), Handler = (), O = (), M = ()> {
333 pub input: InputPipeline<A>,
334 pub commands: CommandResolver<A>,
335 pub focus: FocusManager<O, M>,
336 pub buffer: BufferState<V>,
337 pages: Pages,
338 handler: Handler,
339 fallback_view: V,
340 _state: PhantomData<S>,
341}
342
343impl<V, A, S, O, M> TuiPages<V, A, S, (), (), O, M>
344where
345 V: Clone + PartialEq,
346{
347 pub fn builder(initial_view: V) -> TuiPagesBuilder<V, A, S, O, M, (), ()> {
348 TuiPagesBuilder::new(initial_view)
349 }
350}
351
352impl<V, A, S, Pages, Handler, O, M> TuiPages<V, A, S, Pages, Handler, O, M>
353where
354 V: Clone + PartialEq,
355 A: Clone,
356 O: Clone + PartialEq,
357 Pages: PageProvider<V, S, O>,
358 Handler: TuiActionHandler<V, A, S, O, M>,
359{
360 pub fn current_view(&self) -> &V {
361 self.buffer
362 .get_active_view()
363 .expect("TuiPages buffer always contains at least one view")
364 }
365
366 pub fn pages(&self) -> &Pages {
367 &self.pages
368 }
369
370 pub fn pages_mut(&mut self) -> &mut Pages {
371 &mut self.pages
372 }
373
374 pub fn handler(&self) -> &Handler {
375 &self.handler
376 }
377
378 pub fn handler_mut(&mut self) -> &mut Handler {
379 &mut self.handler
380 }
381
382 pub fn refresh_page(&mut self, state: &S) {
383 let spec = self.current_page_spec(state);
384 self.sync_focus_to_spec(spec);
385 }
386
387 fn sync_focus_to_spec(&mut self, spec: PageSpec<O>) {
393 let PageSpec {
394 focus_targets,
395 section_items,
396 ..
397 } = spec;
398 if self.focus.targets() != focus_targets.as_slice() {
399 self.focus.register_page(focus_targets);
400 }
401 self.focus.set_section_items(section_items);
402 }
403
404 pub fn handle_key(
405 &mut self,
406 key: KeyEvent,
407 state: &mut S,
408 ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error> {
409 let spec = self.current_page_spec(state);
410 let modes = spec.modes.clone();
411 let accepts_text_input = spec.accepts_text_input;
412 self.sync_focus_to_spec(spec);
413
414 let response = self.input.process(key, &modes, accepts_text_input);
415 match response {
416 crate::input::PipelineResponse::Execute(action) => {
417 let quit_requested = self.dispatch_action(action, state)?;
418 Ok(TuiPagesOutput::new(
419 TuiPagesStatus::ActionHandled,
420 quit_requested,
421 ))
422 }
423 crate::input::PipelineResponse::Type(chord) => {
424 let quit_requested = self.dispatch_text(chord, state)?;
425 Ok(TuiPagesOutput::new(
426 TuiPagesStatus::TextHandled,
427 quit_requested,
428 ))
429 }
430 crate::input::PipelineResponse::Wait(hints) => {
431 Ok(TuiPagesOutput::new(TuiPagesStatus::Waiting(hints), false))
432 }
433 crate::input::PipelineResponse::Cancel => {
434 Ok(TuiPagesOutput::new(TuiPagesStatus::Cancelled, false))
435 }
436 }
437 }
438
439 pub fn submit_command(
440 &mut self,
441 input: &str,
442 state: &mut S,
443 ) -> TuiPagesResult<TuiPagesOutput<A>, Handler::Error> {
444 match self.commands.process(input) {
445 CommandResponse::Execute(action) => {
446 let quit_requested = self.dispatch_action(action, state)?;
447 Ok(TuiPagesOutput::new(
448 TuiPagesStatus::ActionHandled,
449 quit_requested,
450 ))
451 }
452 CommandResponse::Incomplete(hints) => Ok(TuiPagesOutput::new(
453 TuiPagesStatus::CommandIncomplete(hints),
454 false,
455 )),
456 CommandResponse::Unknown => {
457 Ok(TuiPagesOutput::new(TuiPagesStatus::CommandUnknown, false))
458 }
459 CommandResponse::Empty => Ok(TuiPagesOutput::new(TuiPagesStatus::CommandEmpty, false)),
460 }
461 }
462
463 pub fn apply_effect(&mut self, effect: TuiEffect<V, O, M>, state: &S) -> bool {
464 match effect {
465 TuiEffect::None => false,
466 TuiEffect::Focus(intent) => {
467 self.focus.apply_focus_intent(intent);
468 false
469 }
470 TuiEffect::Navigate(view) => {
471 self.buffer.update_history(view);
472 self.refresh_page(state);
473 false
474 }
475 TuiEffect::NextBuffer => {
476 self.switch_buffer(true, state);
477 false
478 }
479 TuiEffect::PreviousBuffer => {
480 self.switch_buffer(false, state);
481 false
482 }
483 TuiEffect::CloseBuffer => {
484 self.buffer.close_active_buffer(self.fallback_view.clone());
485 self.refresh_page(state);
486 false
487 }
488 TuiEffect::SplitPane(split) => {
489 self.buffer.split_active_pane(split);
490 false
491 }
492 TuiEffect::ClosePane => {
493 self.buffer.close_active_pane();
494 self.refresh_page(state);
495 false
496 }
497 TuiEffect::NextPane => {
498 self.buffer.focus_next_pane(self.focus.focus_wrap());
499 self.refresh_page(state);
500 false
501 }
502 TuiEffect::PreviousPane => {
503 self.buffer.focus_previous_pane(self.focus.focus_wrap());
504 self.refresh_page(state);
505 false
506 }
507 TuiEffect::RefreshPage => {
508 self.refresh_page(state);
509 false
510 }
511 TuiEffect::Quit => true,
512 }
513 }
514
515 fn current_page_spec(&self, state: &S) -> PageSpec<O> {
516 let view = self.current_view();
517 let focus = self.focus.current();
518 self.pages.page_spec(view, state, focus.as_ref())
519 }
520
521 fn dispatch_action(
522 &mut self,
523 action: A,
524 state: &mut S,
525 ) -> TuiPagesResult<bool, Handler::Error> {
526 let ctx = ActionContext {
527 current_view: self.current_view().clone(),
528 focus: self.focus.current(),
529 has_overlay: self.focus.has_overlay(),
530 };
531 let outcome = self
532 .handler
533 .handle_action(action, ctx, state)
534 .map_err(TuiPagesError::Handler)?;
535 Ok(self.apply_outcome(outcome, state))
536 }
537
538 fn dispatch_text(
539 &mut self,
540 chord: KeyChord,
541 state: &mut S,
542 ) -> TuiPagesResult<bool, Handler::Error> {
543 let ctx = ActionContext {
544 current_view: self.current_view().clone(),
545 focus: self.focus.current(),
546 has_overlay: self.focus.has_overlay(),
547 };
548 let outcome = self
549 .handler
550 .handle_text(chord, ctx, state)
551 .map_err(TuiPagesError::Handler)?;
552 Ok(self.apply_outcome(outcome, state))
553 }
554
555 fn apply_outcome(&mut self, outcome: ActionOutcome<V, O, M>, state: &S) -> bool {
556 let mut quit_requested = false;
557 for effect in outcome.effects {
558 quit_requested |= self.apply_effect(effect, state);
559 }
560 quit_requested
561 }
562
563 fn switch_buffer(&mut self, forward: bool, state: &S) {
564 if self.buffer.history.len() <= 1 {
565 return;
566 }
567
568 let len = self.buffer.history.len();
569 self.buffer.active_index =
570 self.focus
571 .focus_wrap()
572 .step(self.buffer.active_index, len, forward);
573 self.buffer.sync_active_pane_to_active_buffer();
574 self.refresh_page(state);
575 }
576}
577
578#[derive(Debug, Clone)]
579pub struct TuiPagesBuilder<V, A, S, O = (), M = (), Pages = (), Handler = ()> {
580 initial_view: V,
581 fallback_view: Option<V>,
582 input_registry: InputRegistry<A>,
583 command_registry: CommandRegistry<A>,
584 input_timeout_ms: u64,
585 command_timeout_ms: u64,
586 focus_wrap: FocusWrap,
587 pages: Pages,
588 handler: Handler,
589 _state: PhantomData<S>,
590 _overlay: PhantomData<O>,
591 _modal: PhantomData<M>,
592}
593
594impl<V, A, S, O, M> TuiPagesBuilder<V, A, S, O, M, (), ()> {
595 pub fn new(initial_view: V) -> Self {
596 Self {
597 initial_view,
598 fallback_view: None,
599 input_registry: InputRegistry::empty(),
600 command_registry: CommandRegistry::new(),
601 input_timeout_ms: 1000,
602 command_timeout_ms: 1000,
603 focus_wrap: FocusWrap::default(),
604 pages: (),
605 handler: (),
606 _state: PhantomData,
607 _overlay: PhantomData,
608 _modal: PhantomData,
609 }
610 }
611}
612
613impl<V, A, S, O, M, Pages, Handler> TuiPagesBuilder<V, A, S, O, M, Pages, Handler> {
614 pub fn fallback_view(mut self, fallback_view: V) -> Self {
615 self.fallback_view = Some(fallback_view);
616 self
617 }
618
619 pub fn input_timeout_ms(mut self, timeout_ms: u64) -> Self {
620 self.input_timeout_ms = timeout_ms;
621 self
622 }
623
624 pub fn command_timeout_ms(mut self, timeout_ms: u64) -> Self {
625 self.command_timeout_ms = timeout_ms;
626 self
627 }
628
629 pub fn pages<NextPages>(
630 self,
631 pages: NextPages,
632 ) -> TuiPagesBuilder<V, A, S, O, M, NextPages, Handler> {
633 TuiPagesBuilder {
634 initial_view: self.initial_view,
635 fallback_view: self.fallback_view,
636 input_registry: self.input_registry,
637 command_registry: self.command_registry,
638 input_timeout_ms: self.input_timeout_ms,
639 command_timeout_ms: self.command_timeout_ms,
640 focus_wrap: self.focus_wrap,
641 pages,
642 handler: self.handler,
643 _state: PhantomData,
644 _overlay: PhantomData,
645 _modal: PhantomData,
646 }
647 }
648
649 pub fn page_fn(
660 self,
661 page_fn: PageFn<V, S, O>,
662 ) -> TuiPagesBuilder<V, A, S, O, M, PageFn<V, S, O>, Handler> {
663 self.pages(page_fn)
664 }
665
666 pub fn handler<NextHandler>(
667 self,
668 handler: NextHandler,
669 ) -> TuiPagesBuilder<V, A, S, O, M, Pages, NextHandler> {
670 TuiPagesBuilder {
671 initial_view: self.initial_view,
672 fallback_view: self.fallback_view,
673 input_registry: self.input_registry,
674 command_registry: self.command_registry,
675 input_timeout_ms: self.input_timeout_ms,
676 command_timeout_ms: self.command_timeout_ms,
677 focus_wrap: self.focus_wrap,
678 pages: self.pages,
679 handler,
680 _state: PhantomData,
681 _overlay: PhantomData,
682 _modal: PhantomData,
683 }
684 }
685
686 pub fn focus_wrap(mut self, wrap: FocusWrap) -> Self {
689 self.focus_wrap = wrap;
690 self
691 }
692
693 pub fn keymap(
694 mut self,
695 mode: impl Into<ModeId>,
696 configure: impl FnOnce(&mut KeyMap<A>),
697 ) -> Self {
698 let mode = mode.into();
699 configure(self.input_registry.map_mut(mode.as_str()));
700 self
701 }
702
703 pub fn bind(mut self, mode: impl Into<ModeId>, binding: &str, action: A) -> Self {
704 let mode = mode.into();
705 self.input_registry
706 .map_mut(mode.as_str())
707 .bind(parse_binding(binding), action);
708 self
709 }
710
711 pub fn command<I, Alias>(
712 mut self,
713 action_name: impl Into<String>,
714 aliases: I,
715 action: A,
716 ) -> Self
717 where
718 A: Clone,
719 I: IntoIterator<Item = Alias>,
720 Alias: Into<String>,
721 {
722 self.command_registry
723 .bind_aliases(action_name, aliases, action);
724 self
725 }
726
727 pub fn build(self) -> TuiPages<V, A, S, Pages, Handler, O, M>
728 where
729 V: Clone + PartialEq,
730 Pages: PageProvider<V, S, O>,
731 Handler: TuiActionHandler<V, A, S, O, M>,
732 {
733 let fallback_view = self
734 .fallback_view
735 .unwrap_or_else(|| self.initial_view.clone());
736
737 let mut focus = FocusManager::new();
738 focus.set_focus_wrap(self.focus_wrap);
739
740 TuiPages {
741 input: InputPipeline::new(self.input_registry, self.input_timeout_ms),
742 commands: CommandResolver::new(self.command_registry, self.command_timeout_ms),
743 focus,
744 buffer: BufferState::new(self.initial_view),
745 pages: self.pages,
746 handler: self.handler,
747 fallback_view,
748 _state: PhantomData,
749 }
750 }
751}