1use std::time::{Duration, Instant};
22
23use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
24use pi_tui::component::UiEvent;
25
26use super::state::{OverlayKind, StatusKind, ViewAction, ViewState};
27
28pub const DOUBLE_TAP_WINDOW: Duration = Duration::from_millis(500);
33
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum DoubleEscapeAction {
39 #[default]
41 None,
42 Tree,
44 Fork,
46}
47
48#[derive(Clone, Copy, Debug)]
53pub struct InputState {
54 last_sigint: Option<Instant>,
55 last_escape: Option<Instant>,
56 double_escape_action: DoubleEscapeAction,
57 sigint_exit_window: Duration,
58 escape_double_window: Duration,
59}
60
61impl Default for InputState {
62 fn default() -> Self {
63 Self::new(DoubleEscapeAction::default())
64 }
65}
66
67impl InputState {
68 #[must_use]
70 pub fn new(double_escape_action: DoubleEscapeAction) -> Self {
71 Self {
72 last_sigint: None,
73 last_escape: None,
74 double_escape_action,
75 sigint_exit_window: DOUBLE_TAP_WINDOW,
76 escape_double_window: DOUBLE_TAP_WINDOW,
77 }
78 }
79
80 pub fn set_double_escape_action(&mut self, action: DoubleEscapeAction) {
82 self.double_escape_action = action;
83 }
84
85 pub fn set_sigint_exit_window(&mut self, window: Duration) {
87 self.sigint_exit_window = window;
88 }
89
90 pub fn set_escape_double_window(&mut self, window: Duration) {
92 self.escape_double_window = window;
93 }
94
95 pub fn reset_taps(&mut self) {
97 self.last_sigint = None;
98 self.last_escape = None;
99 }
100
101 #[must_use]
103 pub fn last_sigint(&self) -> Option<Instant> {
104 self.last_sigint
105 }
106
107 #[must_use]
109 pub fn last_escape(&self) -> Option<Instant> {
110 self.last_escape
111 }
112
113 #[cfg(test)]
115 pub(crate) fn set_last_sigint_for_test(&mut self, instant: Option<Instant>) {
116 self.last_sigint = instant;
117 }
118}
119
120#[derive(Debug, Default)]
126pub struct InputMapper;
127
128impl InputMapper {
129 #[must_use]
131 pub fn new() -> Self {
132 Self
133 }
134
135 #[must_use]
147 pub fn map(
148 &self,
149 event: &UiEvent,
150 view: &ViewState,
151 editor_text: &str,
152 state: &mut InputState,
153 editor_consumed: bool,
154 ) -> Vec<ViewAction> {
155 let mut out = Vec::new();
156 match event {
157 UiEvent::Resize { width, height } => {
158 out.push(ViewAction::Resize {
159 width: *width,
160 height: *height,
161 });
162 }
163 UiEvent::Paste(text) => {
164 if !editor_consumed && !text.is_empty() {
168 out.push(ViewAction::Paste { text: text.clone() });
169 }
170 }
171 UiEvent::FocusGained | UiEvent::FocusLost => {
172 }
175 UiEvent::Key(key) => {
176 if !editor_consumed {
177 Self::map_key(*key, view, editor_text, state, &mut out);
178 }
179 }
180 }
181 out
182 }
183
184 fn map_key(
185 key: KeyEvent,
186 view: &ViewState,
187 editor_text: &str,
188 state: &mut InputState,
189 out: &mut Vec<ViewAction>,
190 ) {
191 let mods = key.modifiers;
192 match (mods, key.code) {
193 (KeyModifiers::CONTROL, KeyCode::Char('c')) => {
196 let now = Instant::now();
197 let double = state
198 .last_sigint
199 .is_some_and(|t| now.duration_since(t) < state.sigint_exit_window);
200 if double {
201 state.last_sigint = None;
202 out.push(ViewAction::ClearEditor);
203 out.push(ViewAction::Exit);
204 } else {
205 state.last_sigint = Some(now);
206 out.push(ViewAction::ClearEditor);
207 out.push(ViewAction::Interrupt);
208 }
209 }
210 (KeyModifiers::CONTROL, KeyCode::Char('d')) => {
213 if editor_text.trim().is_empty() && view.overlay.is_none() {
214 out.push(ViewAction::Exit);
215 }
216 }
217 (KeyModifiers::CONTROL, KeyCode::Char('z')) => {
219 out.push(ViewAction::Suspend);
220 }
221 (KeyModifiers::SHIFT, KeyCode::BackTab | KeyCode::Tab) => {
223 out.push(ViewAction::CycleThinking { forward: true });
224 }
225 (mods, KeyCode::Char('p')) if mods == KeyModifiers::CONTROL => {
231 out.push(ViewAction::CycleModel { forward: true });
232 }
233 (mods, KeyCode::Char('P'))
235 if mods.contains(KeyModifiers::CONTROL) && mods.contains(KeyModifiers::SHIFT) =>
236 {
237 out.push(ViewAction::CycleModel { forward: false });
238 }
239 (mods, KeyCode::Char('p'))
240 if mods.contains(KeyModifiers::CONTROL) && mods.contains(KeyModifiers::SHIFT) =>
241 {
242 out.push(ViewAction::CycleModel { forward: false });
243 }
244 (KeyModifiers::CONTROL, KeyCode::Char('l')) => {
246 out.push(ViewAction::OpenModelSelector);
247 }
248 (KeyModifiers::CONTROL, KeyCode::Char('o')) => {
250 out.push(ViewAction::ToggleToolExpand);
251 }
252 (KeyModifiers::CONTROL, KeyCode::Char('t')) => {
254 out.push(ViewAction::ToggleThinking);
255 }
256 (KeyModifiers::CONTROL, KeyCode::Char('g')) => {
258 out.push(ViewAction::ExternalEditor);
259 }
260 (KeyModifiers::CONTROL, KeyCode::Char('x')) => {
262 out.push(ViewAction::CopyLastAssistant);
263 }
264 (KeyModifiers::ALT, KeyCode::Enter) => {
266 let text = editor_text.trim().to_owned();
267 if text.is_empty() {
268 return;
269 }
270 if view.streaming {
271 out.push(ViewAction::QueueFollowUp { text });
272 out.push(ViewAction::ClearEditor);
273 } else {
274 out.push(ViewAction::Submit { text });
275 out.push(ViewAction::ClearEditor);
276 }
277 }
278 (KeyModifiers::ALT, KeyCode::Up) => {
280 out.push(ViewAction::DequeueFollowUp);
281 }
282 (mods, KeyCode::Char('v'))
286 if mods == KeyModifiers::CONTROL || mods == KeyModifiers::ALT =>
287 {
288 out.push(ViewAction::Paste {
289 text: String::new(),
290 });
291 }
292 (KeyModifiers::CONTROL, KeyCode::Char('r')) => {
294 out.push(ViewAction::Reload);
295 }
296 (KeyModifiers::CONTROL, KeyCode::Char('b')) => {
298 out.push(ViewAction::OpenSessionPicker);
299 }
300 (KeyModifiers::CONTROL, KeyCode::Char('f')) => {
302 out.push(ViewAction::OpenTreeSelector);
303 }
304 (KeyModifiers::CONTROL, KeyCode::Char('n')) => {
306 out.push(ViewAction::NewSession);
307 }
308 (_, KeyCode::Esc) => {
312 Self::map_escape(view, editor_text, state, out);
313 }
314 _ => {}
315 }
316 }
317
318 fn map_escape(
319 view: &ViewState,
320 editor_text: &str,
321 state: &mut InputState,
322 out: &mut Vec<ViewAction>,
323 ) {
324 if view.overlay.is_some() {
326 out.push(ViewAction::DismissOverlay);
327 state.reset_taps();
328 return;
329 }
330 if view.streaming {
333 out.push(ViewAction::Interrupt);
334 state.reset_taps();
335 return;
336 }
337 if let Some(status) = &view.status {
339 let cancel_kind = matches!(
340 status.kind,
341 StatusKind::Compaction
342 | StatusKind::Retry
343 | StatusKind::BranchSummary
344 | StatusKind::Working
345 );
346 if cancel_kind {
347 out.push(ViewAction::Interrupt);
348 state.reset_taps();
349 return;
350 }
351 }
352 if editor_text.trim().is_empty()
354 && !matches!(state.double_escape_action, DoubleEscapeAction::None)
355 {
356 let now = Instant::now();
357 let double = state
358 .last_escape
359 .is_some_and(|t| now.duration_since(t) < state.escape_double_window);
360 if double {
361 state.last_escape = None;
362 match state.double_escape_action {
363 DoubleEscapeAction::Tree => out.push(ViewAction::OpenTreeSelector),
364 DoubleEscapeAction::Fork => out.push(ViewAction::OpenForkSelector),
365 DoubleEscapeAction::None => {}
366 }
367 } else {
368 state.last_escape = Some(now);
369 }
370 return;
371 }
372 if !editor_text.trim().is_empty() {
374 out.push(ViewAction::ClearEditor);
375 }
376 }
378}
379
380#[must_use]
384pub fn double_escape_action_from_str(s: &str) -> DoubleEscapeAction {
385 match s {
386 "tree" => DoubleEscapeAction::Tree,
387 "fork" => DoubleEscapeAction::Fork,
388 _ => DoubleEscapeAction::None,
389 }
390}
391
392#[must_use]
394pub fn dismissable_overlay_kinds() -> &'static [OverlayKind] {
395 &[
396 OverlayKind::ShortcutHelp,
397 OverlayKind::Changelog,
398 OverlayKind::FirstTimeSetup,
399 OverlayKind::Login,
400 OverlayKind::Extension,
401 ]
402}
403
404#[cfg(test)]
405mod tests {
406 use std::time::Duration;
407
408 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
409 use pi_ai::AssistantMessage;
410 use pi_tui::component::UiEvent;
411
412 use super::*;
413 use crate::modes::interactive::messages::MessageView;
414 use crate::modes::interactive::state::{
415 EditorBorder, EditorView, FocusArea, Overlay, OverlayKind, SessionStatus, StatusKind,
416 ViewState,
417 };
418
419 fn key(code: KeyCode, mods: KeyModifiers) -> UiEvent {
420 UiEvent::Key(KeyEvent::new(code, mods))
421 }
422
423 fn ctrl(c: char) -> UiEvent {
424 key(KeyCode::Char(c), KeyModifiers::CONTROL)
425 }
426
427 fn alt(code: KeyCode) -> UiEvent {
428 key(code, KeyModifiers::ALT)
429 }
430
431 fn ctrl_shift(c: char) -> UiEvent {
432 key(
433 KeyCode::Char(c),
434 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
435 )
436 }
437
438 fn shift(code: KeyCode) -> UiEvent {
439 key(code, KeyModifiers::SHIFT)
440 }
441
442 fn plain(code: KeyCode) -> UiEvent {
443 key(code, KeyModifiers::NONE)
444 }
445
446 fn empty_view() -> ViewState {
447 ViewState::empty()
448 }
449
450 fn view_with_editor(text: &str) -> ViewState {
451 let mut v = empty_view();
452 v.editor = EditorView {
453 text: text.to_owned(),
454 cursor: text.chars().count(),
455 placeholder: String::new(),
456 border: EditorBorder::Muted,
457 paste_marker: None,
458 };
459 v
460 }
461
462 fn view_streaming() -> ViewState {
463 let mut v = empty_view();
464 v.streaming = true;
465 v.status = Some(SessionStatus {
466 kind: StatusKind::Working,
467 frame: 0,
468 message: "Working".to_owned(),
469 });
470 let assistant = AssistantMessage::new("anthropic", "test", "test", 0);
471 v.messages.push(MessageView::streaming_assistant(assistant));
472 v
473 }
474
475 fn view_with_overlay(kind: OverlayKind) -> ViewState {
476 let mut v = empty_view();
477 v.overlay = Some(Overlay {
478 kind,
479 lines: vec!["overlay".to_owned()],
480 height: 3,
481 });
482 v
483 }
484
485 fn view_with_status(kind: StatusKind) -> ViewState {
486 let mut v = empty_view();
487 v.status = Some(SessionStatus {
488 kind,
489 frame: 0,
490 message: "x".to_owned(),
491 });
492 v
493 }
494
495 #[test]
496 fn resize_emits_resize_action() {
497 let mapper = InputMapper::new();
498 let mut state = InputState::default();
499 let view = empty_view();
500 let actions = mapper.map(
501 &UiEvent::Resize {
502 width: 100,
503 height: 30,
504 },
505 &view,
506 "",
507 &mut state,
508 false,
509 );
510 assert_eq!(actions.len(), 1);
511 assert_eq!(
512 actions[0],
513 ViewAction::Resize {
514 width: 100,
515 height: 30
516 }
517 );
518 }
519
520 #[test]
521 fn paste_when_editor_did_not_consume_emits_paste() {
522 let mapper = InputMapper::new();
523 let mut state = InputState::default();
524 let view = empty_view();
525 let actions = mapper.map(
526 &UiEvent::Paste("hello".to_owned()),
527 &view,
528 "",
529 &mut state,
530 false,
531 );
532 assert_eq!(
533 actions,
534 vec![ViewAction::Paste {
535 text: "hello".to_owned()
536 }]
537 );
538 }
539
540 #[test]
541 fn paste_consumed_by_editor_is_ignored_by_mapper() {
542 let mapper = InputMapper::new();
543 let mut state = InputState::default();
544 let view = empty_view();
545 let actions = mapper.map(
546 &UiEvent::Paste("hello".to_owned()),
547 &view,
548 "",
549 &mut state,
550 true,
551 );
552 assert!(actions.is_empty());
553 }
554
555 #[test]
556 fn empty_paste_is_ignored() {
557 let mapper = InputMapper::new();
558 let mut state = InputState::default();
559 let view = empty_view();
560 let actions = mapper.map(&UiEvent::Paste(String::new()), &view, "", &mut state, false);
561 assert!(actions.is_empty());
562 }
563
564 #[test]
565 fn ctrl_c_first_tap_clears_and_interrupts() {
566 let mapper = InputMapper::new();
567 let mut state = InputState::default();
568 let view = view_with_editor("draft");
569 let actions = mapper.map(&ctrl('c'), &view, "draft", &mut state, false);
570 assert_eq!(
571 actions,
572 vec![ViewAction::ClearEditor, ViewAction::Interrupt]
573 );
574 assert!(state.last_sigint.is_some());
575 }
576
577 #[test]
578 fn ctrl_c_double_tap_within_window_exits() {
579 let mapper = InputMapper::new();
580 let mut state = InputState::default();
581 let view = view_with_editor("x");
582 let _ = mapper.map(&ctrl('c'), &view, "x", &mut state, false);
583 let actions = mapper.map(&ctrl('c'), &view, "x", &mut state, false);
584 assert_eq!(actions, vec![ViewAction::ClearEditor, ViewAction::Exit]);
585 assert!(state.last_sigint.is_none());
586 }
587
588 #[test]
589 fn ctrl_c_double_tap_outside_window_just_interrupts() {
590 let mapper = InputMapper::new();
591 let mut state = InputState::default();
592 state.set_sigint_exit_window(Duration::from_nanos(1));
593 let view = view_with_editor("x");
594 let _ = mapper.map(&ctrl('c'), &view, "x", &mut state, false);
595 std::thread::sleep(Duration::from_millis(2));
596 let actions = mapper.map(&ctrl('c'), &view, "x", &mut state, false);
597 assert_eq!(
598 actions,
599 vec![ViewAction::ClearEditor, ViewAction::Interrupt]
600 );
601 }
602
603 #[test]
604 fn ctrl_d_only_exits_when_editor_empty() {
605 let mapper = InputMapper::new();
606 let mut state = InputState::default();
607 let empty = empty_view();
608 let actions = mapper.map(&ctrl('d'), &empty, "", &mut state, false);
609 assert_eq!(actions, vec![ViewAction::Exit]);
610
611 let view = view_with_editor("not empty");
612 let actions = mapper.map(&ctrl('d'), &view, "not empty", &mut state, false);
613 assert!(actions.is_empty());
614 }
615
616 #[test]
617 fn ctrl_d_does_not_exit_when_overlay_open() {
618 let mapper = InputMapper::new();
619 let mut state = InputState::default();
620 let view = view_with_overlay(OverlayKind::ShortcutHelp);
621 let actions = mapper.map(&ctrl('d'), &view, "", &mut state, false);
622 assert!(actions.is_empty());
623 }
624
625 #[test]
626 fn ctrl_z_emits_suspend() {
627 let mapper = InputMapper::new();
628 let mut state = InputState::default();
629 let view = empty_view();
630 let actions = mapper.map(&ctrl('z'), &view, "", &mut state, false);
631 assert_eq!(actions, vec![ViewAction::Suspend]);
632 }
633
634 #[test]
635 fn shift_tab_cycles_thinking() {
636 let mapper = InputMapper::new();
637 let mut state = InputState::default();
638 let view = empty_view();
639 let actions = mapper.map(&shift(KeyCode::Tab), &view, "", &mut state, false);
640 assert_eq!(actions, vec![ViewAction::CycleThinking { forward: true }]);
641 }
642
643 #[test]
644 fn ctrl_p_cycles_model_forward() {
645 let mapper = InputMapper::new();
646 let mut state = InputState::default();
647 let view = empty_view();
648 let actions = mapper.map(&ctrl('p'), &view, "", &mut state, false);
649 assert_eq!(actions, vec![ViewAction::CycleModel { forward: true }]);
650 }
651
652 #[test]
653 fn ctrl_shift_p_cycles_model_backward() {
654 let mapper = InputMapper::new();
655 let mut state = InputState::default();
656 let view = empty_view();
657 let actions = mapper.map(&ctrl_shift('P'), &view, "", &mut state, false);
658 assert_eq!(actions, vec![ViewAction::CycleModel { forward: false }]);
659 }
660
661 #[test]
662 fn app_keys_dispatch_table() {
663 let mapper = InputMapper::new();
664 let mut state = InputState::default();
665 let view = empty_view();
666
667 assert_eq!(
668 mapper.map(&ctrl('l'), &view, "", &mut state, false),
669 vec![ViewAction::OpenModelSelector]
670 );
671 assert_eq!(
672 mapper.map(&ctrl('o'), &view, "", &mut state, false),
673 vec![ViewAction::ToggleToolExpand]
674 );
675 assert_eq!(
676 mapper.map(&ctrl('t'), &view, "", &mut state, false),
677 vec![ViewAction::ToggleThinking]
678 );
679 assert_eq!(
680 mapper.map(&ctrl('g'), &view, "", &mut state, false),
681 vec![ViewAction::ExternalEditor]
682 );
683 assert_eq!(
684 mapper.map(&ctrl('x'), &view, "", &mut state, false),
685 vec![ViewAction::CopyLastAssistant]
686 );
687 assert_eq!(
688 mapper.map(&ctrl('r'), &view, "", &mut state, false),
689 vec![ViewAction::Reload]
690 );
691 assert_eq!(
692 mapper.map(&ctrl('b'), &view, "", &mut state, false),
693 vec![ViewAction::OpenSessionPicker]
694 );
695 assert_eq!(
696 mapper.map(&ctrl('f'), &view, "", &mut state, false),
697 vec![ViewAction::OpenTreeSelector]
698 );
699 assert_eq!(
700 mapper.map(&ctrl('n'), &view, "", &mut state, false),
701 vec![ViewAction::NewSession]
702 );
703 }
704
705 #[test]
706 fn alt_enter_submits_when_not_streaming() {
707 let mapper = InputMapper::new();
708 let mut state = InputState::default();
709 let view = view_with_editor("hello");
710 let actions = mapper.map(&alt(KeyCode::Enter), &view, "hello", &mut state, false);
711 assert_eq!(
712 actions,
713 vec![
714 ViewAction::Submit {
715 text: "hello".to_owned()
716 },
717 ViewAction::ClearEditor,
718 ]
719 );
720 }
721
722 #[test]
723 fn alt_enter_queues_followup_when_streaming() {
724 let mapper = InputMapper::new();
725 let mut state = InputState::default();
726 let view = view_streaming();
727 let actions = mapper.map(&alt(KeyCode::Enter), &view, "more", &mut state, false);
728 assert_eq!(
729 actions,
730 vec![
731 ViewAction::QueueFollowUp {
732 text: "more".to_owned()
733 },
734 ViewAction::ClearEditor,
735 ]
736 );
737 }
738
739 #[test]
740 fn alt_enter_empty_is_noop() {
741 let mapper = InputMapper::new();
742 let mut state = InputState::default();
743 let view = empty_view();
744 let actions = mapper.map(&alt(KeyCode::Enter), &view, " ", &mut state, false);
745 assert!(actions.is_empty());
746 }
747
748 #[test]
749 fn alt_up_dequeues() {
750 let mapper = InputMapper::new();
751 let mut state = InputState::default();
752 let view = empty_view();
753 let actions = mapper.map(&alt(KeyCode::Up), &view, "", &mut state, false);
754 assert_eq!(actions, vec![ViewAction::DequeueFollowUp]);
755 }
756
757 #[test]
758 fn esc_dismisses_overlay_first() {
759 let mapper = InputMapper::new();
760 let mut state = InputState::default();
761 let view = view_with_overlay(OverlayKind::ShortcutHelp);
762 let actions = mapper.map(&plain(KeyCode::Esc), &view, "abc", &mut state, false);
763 assert_eq!(actions, vec![ViewAction::DismissOverlay]);
764 }
765
766 #[test]
767 fn esc_interrupts_when_streaming() {
768 let mapper = InputMapper::new();
769 let mut state = InputState::default();
770 let view = view_streaming();
771 let actions = mapper.map(&plain(KeyCode::Esc), &view, "x", &mut state, false);
772 assert_eq!(actions, vec![ViewAction::Interrupt]);
773 }
774
775 #[test]
776 fn esc_interrupts_when_compacting() {
777 let mapper = InputMapper::new();
778 let mut state = InputState::default();
779 let view = view_with_status(StatusKind::Compaction);
780 let actions = mapper.map(&plain(KeyCode::Esc), &view, "x", &mut state, false);
781 assert_eq!(actions, vec![ViewAction::Interrupt]);
782 }
783
784 #[test]
785 fn esc_clears_editor_when_nonempty_and_idle() {
786 let mapper = InputMapper::new();
787 let mut state = InputState::default();
788 let view = view_with_editor("draft");
789 let actions = mapper.map(&plain(KeyCode::Esc), &view, "draft", &mut state, false);
790 assert_eq!(actions, vec![ViewAction::ClearEditor]);
791 }
792
793 #[test]
794 fn esc_double_tap_on_empty_opens_tree_when_configured() {
795 let mapper = InputMapper::new();
796 let mut state = InputState::new(DoubleEscapeAction::Tree);
797 let view = empty_view();
798 let _ = mapper.map(&plain(KeyCode::Esc), &view, "", &mut state, false);
799 let actions = mapper.map(&plain(KeyCode::Esc), &view, "", &mut state, false);
800 assert_eq!(actions, vec![ViewAction::OpenTreeSelector]);
801 }
802
803 #[test]
804 fn esc_double_tap_on_empty_opens_fork_when_configured() {
805 let mapper = InputMapper::new();
806 let mut state = InputState::new(DoubleEscapeAction::Fork);
807 let view = empty_view();
808 let _ = mapper.map(&plain(KeyCode::Esc), &view, "", &mut state, false);
809 let actions = mapper.map(&plain(KeyCode::Esc), &view, "", &mut state, false);
810 assert_eq!(actions, vec![ViewAction::OpenForkSelector]);
811 }
812
813 #[test]
814 fn esc_single_tap_on_empty_when_double_disabled_is_noop() {
815 let mapper = InputMapper::new();
816 let mut state = InputState::default();
817 let view = empty_view();
818 let actions = mapper.map(&plain(KeyCode::Esc), &view, "", &mut state, false);
819 assert!(actions.is_empty());
820 assert!(state.last_escape.is_none());
821 }
822
823 #[test]
824 fn esc_double_tap_outside_window_records_new_tap() {
825 let mapper = InputMapper::new();
826 let mut state = InputState::new(DoubleEscapeAction::Tree);
827 state.set_escape_double_window(Duration::from_nanos(1));
828 let view = empty_view();
829 let _ = mapper.map(&plain(KeyCode::Esc), &view, "", &mut state, false);
830 std::thread::sleep(Duration::from_millis(2));
831 let actions = mapper.map(&plain(KeyCode::Esc), &view, "", &mut state, false);
832 assert!(actions.is_empty(), "{actions:?}");
833 assert!(state.last_escape.is_some());
834 }
835
836 #[test]
837 fn reset_taps_clears_state() {
838 let mut state = InputState {
839 last_sigint: Some(Instant::now()),
840 last_escape: Some(Instant::now()),
841 ..InputState::default()
842 };
843 state.reset_taps();
844 assert!(state.last_sigint.is_none());
845 assert!(state.last_escape.is_none());
846 }
847
848 #[test]
849 fn focus_events_produce_no_actions() {
850 let mapper = InputMapper::new();
851 let mut state = InputState::default();
852 let view = empty_view();
853 let actions = mapper.map(&UiEvent::FocusGained, &view, "", &mut state, false);
854 assert!(actions.is_empty());
855 let actions = mapper.map(&UiEvent::FocusLost, &view, "", &mut state, false);
856 assert!(actions.is_empty());
857 }
858
859 #[test]
860 fn editor_consumed_silences_app_keys() {
861 let mapper = InputMapper::new();
862 let mut state = InputState::default();
863 let view = empty_view();
864 let actions = mapper.map(&ctrl('l'), &view, "", &mut state, true);
865 assert!(actions.is_empty());
866 }
867
868 #[test]
869 fn double_escape_action_from_str_parses() {
870 assert_eq!(
871 double_escape_action_from_str("tree"),
872 DoubleEscapeAction::Tree
873 );
874 assert_eq!(
875 double_escape_action_from_str("fork"),
876 DoubleEscapeAction::Fork
877 );
878 assert_eq!(
879 double_escape_action_from_str("none"),
880 DoubleEscapeAction::None
881 );
882 assert_eq!(double_escape_action_from_str(""), DoubleEscapeAction::None);
883 assert_eq!(
884 double_escape_action_from_str("bogus"),
885 DoubleEscapeAction::None
886 );
887 }
888
889 #[test]
890 fn ctrl_v_emits_empty_paste_for_clipboard_image_path() {
891 let mapper = InputMapper::new();
892 let mut state = InputState::default();
893 let view = empty_view();
894 let actions = mapper.map(&ctrl('v'), &view, "", &mut state, false);
895 assert_eq!(
896 actions,
897 vec![ViewAction::Paste {
898 text: String::new()
899 }]
900 );
901 }
902
903 #[test]
904 fn unknown_key_is_ignored() {
905 let mapper = InputMapper::new();
906 let mut state = InputState::default();
907 let view = empty_view();
908 let actions = mapper.map(
909 &key(KeyCode::Char('a'), KeyModifiers::NONE),
910 &view,
911 "",
912 &mut state,
913 false,
914 );
915 assert!(actions.is_empty());
916 }
917
918 #[test]
919 fn focus_area_default_is_editor() {
920 let view = empty_view();
921 assert_eq!(view.focus, FocusArea::Editor);
922 }
923}