1#![forbid(unsafe_code)]
2
3use web_time::{Duration, Instant};
90
91use crate::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
92
93pub const DEFAULT_ESC_SEQ_TIMEOUT_MS: u64 = 250;
99
100pub const MIN_ESC_SEQ_TIMEOUT_MS: u64 = 150;
102
103pub const MAX_ESC_SEQ_TIMEOUT_MS: u64 = 400;
105
106pub const DEFAULT_ESC_DEBOUNCE_MS: u64 = 50;
108
109pub const MIN_ESC_DEBOUNCE_MS: u64 = 0;
111
112pub const MAX_ESC_DEBOUNCE_MS: u64 = 100;
114
115#[derive(Debug, Clone)]
146pub struct SequenceConfig {
147 pub esc_seq_timeout: Duration,
150
151 pub esc_debounce: Duration,
154
155 pub disable_sequences: bool,
159}
160
161impl Default for SequenceConfig {
162 fn default() -> Self {
163 Self {
164 esc_seq_timeout: Duration::from_millis(DEFAULT_ESC_SEQ_TIMEOUT_MS),
165 esc_debounce: Duration::from_millis(DEFAULT_ESC_DEBOUNCE_MS),
166 disable_sequences: false,
167 }
168 }
169}
170
171impl SequenceConfig {
172 #[must_use]
174 pub fn with_timeout(mut self, timeout: Duration) -> Self {
175 self.esc_seq_timeout = timeout;
176 self
177 }
178
179 #[must_use]
181 pub fn with_debounce(mut self, debounce: Duration) -> Self {
182 self.esc_debounce = debounce;
183 self
184 }
185
186 #[must_use]
188 pub fn disable_sequences(mut self) -> Self {
189 self.disable_sequences = true;
190 self
191 }
192
193 #[must_use]
202 pub fn from_env() -> Self {
203 let mut config = Self::default();
204
205 if let Ok(val) = std::env::var("FTUI_ESC_SEQ_TIMEOUT_MS")
206 && let Ok(ms) = val.parse::<u64>()
207 {
208 config.esc_seq_timeout = Duration::from_millis(ms);
209 }
210
211 if let Ok(val) = std::env::var("FTUI_ESC_DEBOUNCE_MS")
212 && let Ok(ms) = val.parse::<u64>()
213 {
214 config.esc_debounce = Duration::from_millis(ms);
215 }
216
217 if let Ok(val) = std::env::var("FTUI_DISABLE_ESC_SEQ") {
218 config.disable_sequences = val == "1" || val.eq_ignore_ascii_case("true");
219 }
220
221 config.validated()
222 }
223
224 #[must_use]
245 pub fn validated(mut self) -> Self {
246 let timeout_ms = self.esc_seq_timeout.as_millis() as u64;
248 let clamped_timeout = timeout_ms.clamp(MIN_ESC_SEQ_TIMEOUT_MS, MAX_ESC_SEQ_TIMEOUT_MS);
249 self.esc_seq_timeout = Duration::from_millis(clamped_timeout);
250
251 let debounce_ms = self.esc_debounce.as_millis() as u64;
253 let clamped_debounce = debounce_ms.clamp(MIN_ESC_DEBOUNCE_MS, MAX_ESC_DEBOUNCE_MS);
254
255 let final_debounce = clamped_debounce.min(clamped_timeout);
257 self.esc_debounce = Duration::from_millis(final_debounce);
258
259 self
260 }
261
262 #[must_use]
264 pub fn is_valid(&self) -> bool {
265 let timeout_ms = self.esc_seq_timeout.as_millis() as u64;
266 let debounce_ms = self.esc_debounce.as_millis() as u64;
267
268 (MIN_ESC_SEQ_TIMEOUT_MS..=MAX_ESC_SEQ_TIMEOUT_MS).contains(&timeout_ms)
269 && (MIN_ESC_DEBOUNCE_MS..=MAX_ESC_DEBOUNCE_MS).contains(&debounce_ms)
270 && debounce_ms <= timeout_ms
271 }
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub enum SequenceOutput {
281 Pending,
283
284 Esc,
286
287 EscEsc,
289
290 PassThrough,
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300enum DetectorState {
301 Idle,
303
304 AwaitingSecondEsc { first_esc_time: Instant },
306}
307
308#[derive(Debug)]
324pub struct SequenceDetector {
325 config: SequenceConfig,
326 state: DetectorState,
327}
328
329impl SequenceDetector {
330 #[must_use]
332 pub fn new(config: SequenceConfig) -> Self {
333 Self {
334 config,
335 state: DetectorState::Idle,
336 }
337 }
338
339 #[must_use]
341 pub fn with_defaults() -> Self {
342 Self::new(SequenceConfig::default())
343 }
344
345 pub fn feed(&mut self, event: &KeyEvent, now: Instant) -> SequenceOutput {
349 if event.kind != KeyEventKind::Press {
351 return SequenceOutput::PassThrough;
352 }
353
354 if self.config.disable_sequences {
356 return if event.code == KeyCode::Escape {
357 SequenceOutput::Esc
358 } else {
359 SequenceOutput::PassThrough
360 };
361 }
362
363 match self.state {
364 DetectorState::Idle => {
365 if event.code == KeyCode::Escape {
366 self.state = DetectorState::AwaitingSecondEsc {
368 first_esc_time: now,
369 };
370 SequenceOutput::Pending
371 } else {
372 SequenceOutput::PassThrough
374 }
375 }
376
377 DetectorState::AwaitingSecondEsc { first_esc_time } => {
378 let elapsed = now.saturating_duration_since(first_esc_time);
379
380 if event.code == KeyCode::Escape {
381 if elapsed <= self.config.esc_seq_timeout {
383 self.state = DetectorState::Idle;
385 SequenceOutput::EscEsc
386 } else {
387 self.state = DetectorState::AwaitingSecondEsc {
389 first_esc_time: now,
390 };
391 SequenceOutput::Esc
392 }
393 } else {
394 self.state = DetectorState::Idle;
397 SequenceOutput::Esc
399 }
400 }
401 }
402 }
403
404 pub fn check_timeout(&mut self, now: Instant) -> Option<SequenceOutput> {
412 if let DetectorState::AwaitingSecondEsc { first_esc_time } = self.state {
413 let elapsed = now.saturating_duration_since(first_esc_time);
414 if elapsed > self.config.esc_seq_timeout {
415 self.state = DetectorState::Idle;
416 return Some(SequenceOutput::Esc);
417 }
418 }
419 None
420 }
421
422 #[must_use]
424 pub fn is_pending(&self) -> bool {
425 matches!(self.state, DetectorState::AwaitingSecondEsc { .. })
426 }
427
428 pub fn reset(&mut self) {
432 self.state = DetectorState::Idle;
433 }
434
435 #[must_use]
437 pub fn config(&self) -> &SequenceConfig {
438 &self.config
439 }
440
441 pub fn set_config(&mut self, config: SequenceConfig) {
445 self.config = config;
446 }
447}
448
449#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
458pub struct AppState {
459 pub input_nonempty: bool,
461
462 pub task_running: bool,
464
465 pub modal_open: bool,
467
468 pub view_overlay: bool,
470}
471
472impl AppState {
473 #[must_use]
475 pub const fn new() -> Self {
476 Self {
477 input_nonempty: false,
478 task_running: false,
479 modal_open: false,
480 view_overlay: false,
481 }
482 }
483
484 #[must_use]
486 pub const fn with_input(mut self, nonempty: bool) -> Self {
487 self.input_nonempty = nonempty;
488 self
489 }
490
491 #[must_use]
493 pub const fn with_task(mut self, running: bool) -> Self {
494 self.task_running = running;
495 self
496 }
497
498 #[must_use]
500 pub const fn with_modal(mut self, open: bool) -> Self {
501 self.modal_open = open;
502 self
503 }
504
505 #[must_use]
507 pub const fn with_overlay(mut self, active: bool) -> Self {
508 self.view_overlay = active;
509 self
510 }
511
512 #[must_use]
514 pub const fn is_idle(&self) -> bool {
515 !self.input_nonempty && !self.task_running && !self.modal_open
516 }
517}
518
519#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
528pub enum Action {
529 ClearInput,
531
532 CancelTask,
534
535 DismissModal,
537
538 CloseOverlay,
540
541 ToggleTreeView,
543
544 Quit,
546
547 SoftQuit,
549
550 HardQuit,
552
553 Bell,
555
556 PassThrough,
560}
561
562impl Action {
563 #[must_use]
565 pub const fn consumes_event(&self) -> bool {
566 !matches!(self, Action::PassThrough)
567 }
568
569 #[must_use]
571 pub const fn is_quit(&self) -> bool {
572 matches!(self, Action::Quit | Action::SoftQuit | Action::HardQuit)
573 }
574}
575
576#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
582pub enum CtrlCIdleAction {
583 #[default]
585 Quit,
586
587 Noop,
589
590 Bell,
592}
593
594impl CtrlCIdleAction {
595 #[must_use]
597 pub fn from_str_opt(s: &str) -> Option<Self> {
598 match s.to_lowercase().as_str() {
599 "quit" => Some(Self::Quit),
600 "noop" | "none" | "ignore" => Some(Self::Noop),
601 "bell" | "beep" => Some(Self::Bell),
602 _ => None,
603 }
604 }
605
606 #[must_use]
608 pub const fn to_action(self) -> Option<Action> {
609 match self {
610 Self::Quit => Some(Action::Quit),
611 Self::Noop => None,
612 Self::Bell => Some(Action::Bell),
613 }
614 }
615}
616
617#[derive(Debug, Clone)]
663pub struct ActionConfig {
664 pub sequence_config: SequenceConfig,
666
667 pub ctrl_c_idle_action: CtrlCIdleAction,
673}
674
675impl Default for ActionConfig {
676 fn default() -> Self {
677 Self {
678 sequence_config: SequenceConfig::default(),
679 ctrl_c_idle_action: CtrlCIdleAction::Quit,
680 }
681 }
682}
683
684impl ActionConfig {
685 #[must_use]
687 pub fn with_sequence_config(mut self, config: SequenceConfig) -> Self {
688 self.sequence_config = config;
689 self
690 }
691
692 #[must_use]
694 pub fn with_ctrl_c_idle(mut self, action: CtrlCIdleAction) -> Self {
695 self.ctrl_c_idle_action = action;
696 self
697 }
698
699 #[must_use]
705 pub fn from_env() -> Self {
706 let mut config = Self {
707 sequence_config: SequenceConfig::from_env(),
708 ctrl_c_idle_action: CtrlCIdleAction::Quit,
709 };
710
711 if let Ok(val) = std::env::var("FTUI_CTRL_C_IDLE_ACTION")
712 && let Some(action) = CtrlCIdleAction::from_str_opt(&val)
713 {
714 config.ctrl_c_idle_action = action;
715 }
716
717 config
718 }
719
720 #[must_use]
724 pub fn validated(mut self) -> Self {
725 self.sequence_config = self.sequence_config.validated();
726 self
727 }
728}
729
730#[derive(Debug)]
773pub struct ActionMapper {
774 config: ActionConfig,
775 sequence_detector: SequenceDetector,
776}
777
778impl ActionMapper {
779 #[must_use]
781 pub fn new(config: ActionConfig) -> Self {
782 let sequence_detector = SequenceDetector::new(config.sequence_config.clone());
783 Self {
784 config,
785 sequence_detector,
786 }
787 }
788
789 #[must_use]
791 pub fn with_defaults() -> Self {
792 Self::new(ActionConfig::default())
793 }
794
795 #[must_use]
797 pub fn from_env() -> Self {
798 Self::new(ActionConfig::from_env())
799 }
800
801 pub fn map(&mut self, event: &KeyEvent, state: &AppState, now: Instant) -> Option<Action> {
812 if event.kind != KeyEventKind::Press {
814 return Some(Action::PassThrough);
815 }
816
817 if event.modifiers.contains(Modifiers::CTRL)
819 && let KeyCode::Char(c) = event.code
820 {
821 match c.to_ascii_lowercase() {
822 'c' => return self.resolve_ctrl_c(state),
823 'd' => return Some(Action::SoftQuit),
824 'q' => return Some(Action::HardQuit),
825 _ => {}
826 }
827 }
828
829 if event.code == KeyCode::Escape && event.modifiers == Modifiers::NONE {
831 return self.handle_esc_sequence(state, now);
832 }
833
834 let seq_output = self.sequence_detector.feed(event, now);
836 match seq_output {
837 SequenceOutput::Esc => {
838 self.resolve_single_esc(state)
843 }
844 SequenceOutput::Pending => {
845 Some(Action::PassThrough)
847 }
848 SequenceOutput::EscEsc => {
849 Some(Action::ToggleTreeView)
851 }
852 SequenceOutput::PassThrough => Some(Action::PassThrough),
853 }
854 }
855
856 fn handle_esc_sequence(&mut self, state: &AppState, now: Instant) -> Option<Action> {
858 let esc_event = KeyEvent::new(KeyCode::Escape);
859 let output = self.sequence_detector.feed(&esc_event, now);
860
861 match output {
862 SequenceOutput::Pending => {
863 None
866 }
867 SequenceOutput::Esc => {
868 self.resolve_single_esc(state)
870 }
871 SequenceOutput::EscEsc => {
872 Some(Action::ToggleTreeView)
874 }
875 SequenceOutput::PassThrough => {
876 Some(Action::PassThrough)
878 }
879 }
880 }
881
882 fn resolve_ctrl_c(&self, state: &AppState) -> Option<Action> {
884 if state.modal_open {
886 return Some(Action::DismissModal);
887 }
888
889 if state.input_nonempty {
891 return Some(Action::ClearInput);
892 }
893
894 if state.task_running {
896 return Some(Action::CancelTask);
897 }
898
899 self.config.ctrl_c_idle_action.to_action()
901 }
902
903 fn resolve_single_esc(&self, state: &AppState) -> Option<Action> {
905 if state.modal_open {
907 return Some(Action::DismissModal);
908 }
909
910 if state.view_overlay {
912 return Some(Action::CloseOverlay);
913 }
914
915 if state.input_nonempty {
917 return Some(Action::ClearInput);
918 }
919
920 if state.task_running {
922 return Some(Action::CancelTask);
923 }
924
925 Some(Action::PassThrough)
927 }
928
929 pub fn check_timeout(&mut self, state: &AppState, now: Instant) -> Option<Action> {
939 if let Some(SequenceOutput::Esc) = self.sequence_detector.check_timeout(now) {
940 return self.resolve_single_esc(state);
941 }
942 None
943 }
944
945 #[must_use]
947 pub fn is_pending_esc(&self) -> bool {
948 self.sequence_detector.is_pending()
949 }
950
951 pub fn reset(&mut self) {
955 self.sequence_detector.reset();
956 }
957
958 #[must_use]
960 pub fn config(&self) -> &ActionConfig {
961 &self.config
962 }
963
964 pub fn set_config(&mut self, config: ActionConfig) {
966 self.sequence_detector
967 .set_config(config.sequence_config.clone());
968 self.config = config;
969 }
970}
971
972#[cfg(test)]
977mod tests {
978 use super::*;
979
980 fn now() -> Instant {
981 Instant::now()
982 }
983
984 fn esc_press() -> KeyEvent {
985 KeyEvent::new(KeyCode::Escape)
986 }
987
988 fn key_press(code: KeyCode) -> KeyEvent {
989 KeyEvent::new(code)
990 }
991
992 fn esc_release() -> KeyEvent {
993 KeyEvent::new(KeyCode::Escape).with_kind(KeyEventKind::Release)
994 }
995
996 const MS_50: Duration = Duration::from_millis(50);
997 const MS_100: Duration = Duration::from_millis(100);
998 const MS_200: Duration = Duration::from_millis(200);
999 const MS_300: Duration = Duration::from_millis(300);
1000
1001 #[test]
1004 fn single_esc_returns_pending() {
1005 let mut detector = SequenceDetector::with_defaults();
1006 let t = now();
1007
1008 let output = detector.feed(&esc_press(), t);
1009 assert_eq!(output, SequenceOutput::Pending);
1010 assert!(detector.is_pending());
1011 }
1012
1013 #[test]
1014 fn esc_esc_within_timeout() {
1015 let mut detector = SequenceDetector::with_defaults();
1016 let t = now();
1017
1018 detector.feed(&esc_press(), t);
1019 let output = detector.feed(&esc_press(), t + MS_100);
1020
1021 assert_eq!(output, SequenceOutput::EscEsc);
1022 assert!(!detector.is_pending());
1023 }
1024
1025 #[test]
1026 fn esc_esc_at_timeout_boundary() {
1027 let mut detector = SequenceDetector::with_defaults();
1028 let t = now();
1029
1030 detector.feed(&esc_press(), t);
1031 let output = detector.feed(&esc_press(), t + Duration::from_millis(250));
1033
1034 assert_eq!(output, SequenceOutput::EscEsc);
1035 }
1036
1037 #[test]
1038 fn esc_esc_past_timeout() {
1039 let mut detector = SequenceDetector::with_defaults();
1040 let t = now();
1041
1042 detector.feed(&esc_press(), t);
1043 let output = detector.feed(&esc_press(), t + Duration::from_millis(251));
1045
1046 assert_eq!(output, SequenceOutput::Esc);
1048 assert!(detector.is_pending()); }
1050
1051 #[test]
1052 fn timeout_check_emits_pending_esc() {
1053 let mut detector = SequenceDetector::with_defaults();
1054 let t = now();
1055
1056 detector.feed(&esc_press(), t);
1057
1058 assert!(detector.check_timeout(t + MS_200).is_none());
1060 assert!(detector.is_pending());
1061
1062 let output = detector.check_timeout(t + Duration::from_millis(251));
1064 assert_eq!(output, Some(SequenceOutput::Esc));
1065 assert!(!detector.is_pending());
1066 }
1067
1068 #[test]
1069 fn other_key_interrupts_sequence() {
1070 let mut detector = SequenceDetector::with_defaults();
1071 let t = now();
1072
1073 detector.feed(&esc_press(), t);
1074 let output = detector.feed(&key_press(KeyCode::Char('a')), t + MS_100);
1075
1076 assert_eq!(output, SequenceOutput::Esc);
1078 assert!(!detector.is_pending());
1079 }
1080
1081 #[test]
1082 fn non_esc_key_passes_through() {
1083 let mut detector = SequenceDetector::with_defaults();
1084 let t = now();
1085
1086 let output = detector.feed(&key_press(KeyCode::Char('x')), t);
1087 assert_eq!(output, SequenceOutput::PassThrough);
1088 }
1089
1090 #[test]
1091 fn release_event_passes_through() {
1092 let mut detector = SequenceDetector::with_defaults();
1093 let t = now();
1094
1095 let output = detector.feed(&esc_release(), t);
1096 assert_eq!(output, SequenceOutput::PassThrough);
1097 assert!(!detector.is_pending());
1098 }
1099
1100 #[test]
1101 fn release_during_pending_passes_through() {
1102 let mut detector = SequenceDetector::with_defaults();
1103 let t = now();
1104
1105 detector.feed(&esc_press(), t);
1106 let output = detector.feed(&esc_release(), t + MS_50);
1107
1108 assert_eq!(output, SequenceOutput::PassThrough);
1110 assert!(detector.is_pending());
1111 }
1112
1113 #[test]
1116 fn custom_timeout() {
1117 let config = SequenceConfig::default().with_timeout(Duration::from_millis(100));
1118 let mut detector = SequenceDetector::new(config);
1119 let t = now();
1120
1121 detector.feed(&esc_press(), t);
1122 let output = detector.feed(&esc_press(), t + Duration::from_millis(150));
1124
1125 assert_eq!(output, SequenceOutput::Esc);
1126 }
1127
1128 #[test]
1129 fn disabled_sequences() {
1130 let config = SequenceConfig::default().disable_sequences();
1131 let mut detector = SequenceDetector::new(config);
1132 let t = now();
1133
1134 let output = detector.feed(&esc_press(), t);
1136 assert_eq!(output, SequenceOutput::Esc);
1137 assert!(!detector.is_pending());
1138
1139 let output = detector.feed(&esc_press(), t + MS_50);
1141 assert_eq!(output, SequenceOutput::Esc);
1142 }
1143
1144 #[test]
1145 fn disabled_sequences_passthrough() {
1146 let config = SequenceConfig::default().disable_sequences();
1147 let mut detector = SequenceDetector::new(config);
1148 let t = now();
1149
1150 let output = detector.feed(&key_press(KeyCode::Char('a')), t);
1151 assert_eq!(output, SequenceOutput::PassThrough);
1152 }
1153
1154 #[test]
1155 fn config_default_values() {
1156 let config = SequenceConfig::default();
1157 assert_eq!(config.esc_seq_timeout, Duration::from_millis(250));
1158 assert_eq!(config.esc_debounce, Duration::from_millis(50));
1159 assert!(!config.disable_sequences);
1160 }
1161
1162 #[test]
1163 fn config_builder_chain() {
1164 let config = SequenceConfig::default()
1165 .with_timeout(Duration::from_millis(300))
1166 .with_debounce(Duration::from_millis(100))
1167 .disable_sequences();
1168
1169 assert_eq!(config.esc_seq_timeout, Duration::from_millis(300));
1170 assert_eq!(config.esc_debounce, Duration::from_millis(100));
1171 assert!(config.disable_sequences);
1172 }
1173
1174 #[test]
1177 fn reset_clears_pending() {
1178 let mut detector = SequenceDetector::with_defaults();
1179 let t = now();
1180
1181 detector.feed(&esc_press(), t);
1182 assert!(detector.is_pending());
1183
1184 detector.reset();
1185 assert!(!detector.is_pending());
1186
1187 let output = detector.feed(&esc_press(), t + MS_100);
1189 assert_eq!(output, SequenceOutput::Pending);
1190 }
1191
1192 #[test]
1193 fn reset_discards_pending_esc() {
1194 let mut detector = SequenceDetector::with_defaults();
1195 let t = now();
1196
1197 detector.feed(&esc_press(), t);
1198 detector.reset();
1199
1200 assert!(detector.check_timeout(t + MS_300).is_none());
1202 }
1203
1204 #[test]
1207 fn rapid_triple_esc() {
1208 let mut detector = SequenceDetector::with_defaults();
1209 let t = now();
1210
1211 let out1 = detector.feed(&esc_press(), t);
1213 assert_eq!(out1, SequenceOutput::Pending);
1214
1215 let out2 = detector.feed(&esc_press(), t + MS_50);
1217 assert_eq!(out2, SequenceOutput::EscEsc);
1218
1219 let out3 = detector.feed(&esc_press(), t + MS_100);
1221 assert_eq!(out3, SequenceOutput::Pending);
1222 }
1223
1224 #[test]
1225 fn alternating_esc_and_key() {
1226 let mut detector = SequenceDetector::with_defaults();
1227 let t = now();
1228
1229 detector.feed(&esc_press(), t);
1231
1232 let out1 = detector.feed(&key_press(KeyCode::Char('a')), t + MS_50);
1234 assert_eq!(out1, SequenceOutput::Esc);
1235
1236 let out2 = detector.feed(&esc_press(), t + MS_100);
1238 assert_eq!(out2, SequenceOutput::Pending);
1239
1240 let out3 = detector.feed(&key_press(KeyCode::Char('b')), t + MS_200);
1242 assert_eq!(out3, SequenceOutput::Esc);
1243 }
1244
1245 #[test]
1246 fn enter_key_interrupts() {
1247 let mut detector = SequenceDetector::with_defaults();
1248 let t = now();
1249
1250 detector.feed(&esc_press(), t);
1251 let output = detector.feed(&key_press(KeyCode::Enter), t + MS_100);
1252
1253 assert_eq!(output, SequenceOutput::Esc);
1254 }
1255
1256 #[test]
1257 fn function_key_interrupts() {
1258 let mut detector = SequenceDetector::with_defaults();
1259 let t = now();
1260
1261 detector.feed(&esc_press(), t);
1262 let output = detector.feed(&key_press(KeyCode::F(1)), t + MS_100);
1263
1264 assert_eq!(output, SequenceOutput::Esc);
1265 }
1266
1267 #[test]
1268 fn arrow_key_interrupts() {
1269 let mut detector = SequenceDetector::with_defaults();
1270 let t = now();
1271
1272 detector.feed(&esc_press(), t);
1273 let output = detector.feed(&key_press(KeyCode::Up), t + MS_100);
1274
1275 assert_eq!(output, SequenceOutput::Esc);
1276 }
1277
1278 #[test]
1279 fn config_getter_and_setter() {
1280 let mut detector = SequenceDetector::with_defaults();
1281 assert_eq!(
1282 detector.config().esc_seq_timeout,
1283 Duration::from_millis(250)
1284 );
1285
1286 let new_config = SequenceConfig::default().with_timeout(Duration::from_millis(500));
1287 detector.set_config(new_config);
1288
1289 assert_eq!(
1290 detector.config().esc_seq_timeout,
1291 Duration::from_millis(500)
1292 );
1293 }
1294
1295 #[test]
1296 fn set_config_preserves_pending_state() {
1297 let mut detector = SequenceDetector::with_defaults();
1298 let t = now();
1299
1300 detector.feed(&esc_press(), t);
1301 assert!(detector.is_pending());
1302
1303 detector.set_config(SequenceConfig::default().with_timeout(Duration::from_millis(500)));
1305
1306 assert!(detector.is_pending());
1308
1309 let output = detector.feed(&esc_press(), t + MS_300);
1311 assert_eq!(output, SequenceOutput::EscEsc); }
1313
1314 #[test]
1315 fn debug_format() {
1316 let detector = SequenceDetector::with_defaults();
1317 let dbg = format!("{:?}", detector);
1318 assert!(dbg.contains("SequenceDetector"));
1319 }
1320
1321 #[test]
1322 fn config_debug_format() {
1323 let config = SequenceConfig::default();
1324 let dbg = format!("{:?}", config);
1325 assert!(dbg.contains("SequenceConfig"));
1326 }
1327
1328 #[test]
1329 fn output_debug_and_eq() {
1330 assert_eq!(SequenceOutput::Pending, SequenceOutput::Pending);
1331 assert_eq!(SequenceOutput::Esc, SequenceOutput::Esc);
1332 assert_eq!(SequenceOutput::EscEsc, SequenceOutput::EscEsc);
1333 assert_eq!(SequenceOutput::PassThrough, SequenceOutput::PassThrough);
1334 assert_ne!(SequenceOutput::Esc, SequenceOutput::EscEsc);
1335
1336 let dbg = format!("{:?}", SequenceOutput::EscEsc);
1337 assert!(dbg.contains("EscEsc"));
1338 }
1339
1340 #[test]
1343 fn no_stuck_state() {
1344 let mut detector = SequenceDetector::with_defaults();
1345 let t = now();
1346
1347 for i in 0..100 {
1349 let offset = Duration::from_millis(i * 10);
1350 if i % 3 == 0 {
1351 detector.feed(&esc_press(), t + offset);
1352 } else {
1353 detector.feed(&key_press(KeyCode::Char('x')), t + offset);
1354 }
1355 }
1356
1357 detector.check_timeout(t + Duration::from_secs(2));
1359
1360 assert!(!detector.is_pending());
1362 }
1363
1364 #[test]
1365 fn deterministic_output() {
1366 let config = SequenceConfig::default();
1368 let t = now();
1369
1370 let mut d1 = SequenceDetector::new(config.clone());
1371 let mut d2 = SequenceDetector::new(config);
1372
1373 let events = [
1374 (esc_press(), t),
1375 (esc_press(), t + MS_100),
1376 (key_press(KeyCode::Char('a')), t + MS_200),
1377 (esc_press(), t + MS_300),
1378 ];
1379
1380 for (event, time) in &events {
1381 let out1 = d1.feed(event, *time);
1382 let out2 = d2.feed(event, *time);
1383 assert_eq!(out1, out2);
1384 }
1385 }
1386
1387 mod action_mapper_tests {
1392 use super::*;
1393 use crate::event::Modifiers;
1394
1395 fn ctrl_c() -> KeyEvent {
1396 KeyEvent::new(KeyCode::Char('c')).with_modifiers(Modifiers::CTRL)
1397 }
1398
1399 fn ctrl_d() -> KeyEvent {
1400 KeyEvent::new(KeyCode::Char('d')).with_modifiers(Modifiers::CTRL)
1401 }
1402
1403 fn ctrl_q() -> KeyEvent {
1404 KeyEvent::new(KeyCode::Char('q')).with_modifiers(Modifiers::CTRL)
1405 }
1406
1407 fn idle_state() -> AppState {
1408 AppState::default()
1409 }
1410
1411 fn input_state() -> AppState {
1412 AppState::new().with_input(true)
1413 }
1414
1415 fn task_state() -> AppState {
1416 AppState::new().with_task(true)
1417 }
1418
1419 fn modal_state() -> AppState {
1420 AppState::new().with_modal(true)
1421 }
1422
1423 fn overlay_state() -> AppState {
1424 AppState::new().with_overlay(true)
1425 }
1426
1427 #[test]
1430 fn test_ctrl_c_clears_nonempty_input() {
1431 let mut mapper = ActionMapper::with_defaults();
1432 let t = now();
1433
1434 let action = mapper.map(&ctrl_c(), &input_state(), t);
1435 assert_eq!(action, Some(Action::ClearInput));
1436 }
1437
1438 #[test]
1439 fn test_ctrl_c_cancels_running_task() {
1440 let mut mapper = ActionMapper::with_defaults();
1441 let t = now();
1442
1443 let action = mapper.map(&ctrl_c(), &task_state(), t);
1444 assert_eq!(action, Some(Action::CancelTask));
1445 }
1446
1447 #[test]
1448 fn test_ctrl_c_quits_when_idle() {
1449 let mut mapper = ActionMapper::with_defaults();
1450 let t = now();
1451
1452 let action = mapper.map(&ctrl_c(), &idle_state(), t);
1453 assert_eq!(action, Some(Action::Quit));
1454 }
1455
1456 #[test]
1457 fn test_ctrl_c_dismisses_modal() {
1458 let mut mapper = ActionMapper::with_defaults();
1459 let t = now();
1460
1461 let action = mapper.map(&ctrl_c(), &modal_state(), t);
1462 assert_eq!(action, Some(Action::DismissModal));
1463 }
1464
1465 #[test]
1466 fn test_ctrl_c_modal_priority_over_input() {
1467 let mut mapper = ActionMapper::with_defaults();
1468 let t = now();
1469
1470 let state = AppState::new().with_modal(true).with_input(true);
1472 let action = mapper.map(&ctrl_c(), &state, t);
1473 assert_eq!(action, Some(Action::DismissModal));
1474 }
1475
1476 #[test]
1477 fn test_ctrl_c_input_priority_over_task() {
1478 let mut mapper = ActionMapper::with_defaults();
1479 let t = now();
1480
1481 let state = AppState::new().with_input(true).with_task(true);
1482 let action = mapper.map(&ctrl_c(), &state, t);
1483 assert_eq!(action, Some(Action::ClearInput));
1484 }
1485
1486 #[test]
1487 fn test_ctrl_c_idle_config_noop() {
1488 let config = ActionConfig::default().with_ctrl_c_idle(CtrlCIdleAction::Noop);
1489 let mut mapper = ActionMapper::new(config);
1490 let t = now();
1491
1492 let action = mapper.map(&ctrl_c(), &idle_state(), t);
1493 assert_eq!(action, None); }
1495
1496 #[test]
1497 fn test_ctrl_c_idle_config_bell() {
1498 let config = ActionConfig::default().with_ctrl_c_idle(CtrlCIdleAction::Bell);
1499 let mut mapper = ActionMapper::new(config);
1500 let t = now();
1501
1502 let action = mapper.map(&ctrl_c(), &idle_state(), t);
1503 assert_eq!(action, Some(Action::Bell));
1504 }
1505
1506 #[test]
1509 fn test_ctrl_d_soft_quit() {
1510 let mut mapper = ActionMapper::with_defaults();
1511 let t = now();
1512
1513 let action = mapper.map(&ctrl_d(), &idle_state(), t);
1514 assert_eq!(action, Some(Action::SoftQuit));
1515 }
1516
1517 #[test]
1518 fn test_ctrl_d_ignores_state() {
1519 let mut mapper = ActionMapper::with_defaults();
1520 let t = now();
1521
1522 let action = mapper.map(&ctrl_d(), &modal_state(), t);
1524 assert_eq!(action, Some(Action::SoftQuit));
1525
1526 let action = mapper.map(&ctrl_d(), &input_state(), t);
1527 assert_eq!(action, Some(Action::SoftQuit));
1528 }
1529
1530 #[test]
1531 fn test_ctrl_q_hard_quit() {
1532 let mut mapper = ActionMapper::with_defaults();
1533 let t = now();
1534
1535 let action = mapper.map(&ctrl_q(), &idle_state(), t);
1536 assert_eq!(action, Some(Action::HardQuit));
1537 }
1538
1539 #[test]
1540 fn test_ctrl_q_ignores_state() {
1541 let mut mapper = ActionMapper::with_defaults();
1542 let t = now();
1543
1544 let action = mapper.map(&ctrl_q(), &modal_state(), t);
1546 assert_eq!(action, Some(Action::HardQuit));
1547 }
1548
1549 #[test]
1552 fn test_esc_dismisses_modal() {
1553 let mut mapper = ActionMapper::with_defaults();
1554 let t = now();
1555
1556 let action1 = mapper.map(&esc_press(), &modal_state(), t);
1558 assert_eq!(action1, None);
1559
1560 let action2 = mapper.check_timeout(&modal_state(), t + MS_300);
1562 assert_eq!(action2, Some(Action::DismissModal));
1563 }
1564
1565 #[test]
1566 fn test_esc_clears_input_no_modal() {
1567 let mut mapper = ActionMapper::with_defaults();
1568 let t = now();
1569
1570 mapper.map(&esc_press(), &input_state(), t);
1571 let action = mapper.check_timeout(&input_state(), t + MS_300);
1572 assert_eq!(action, Some(Action::ClearInput));
1573 }
1574
1575 #[test]
1576 fn test_esc_cancels_task_empty_input() {
1577 let mut mapper = ActionMapper::with_defaults();
1578 let t = now();
1579
1580 mapper.map(&esc_press(), &task_state(), t);
1581 let action = mapper.check_timeout(&task_state(), t + MS_300);
1582 assert_eq!(action, Some(Action::CancelTask));
1583 }
1584
1585 #[test]
1586 fn test_esc_closes_overlay() {
1587 let mut mapper = ActionMapper::with_defaults();
1588 let t = now();
1589
1590 mapper.map(&esc_press(), &overlay_state(), t);
1591 let action = mapper.check_timeout(&overlay_state(), t + MS_300);
1592 assert_eq!(action, Some(Action::CloseOverlay));
1593 }
1594
1595 #[test]
1596 fn test_esc_modal_priority_over_overlay() {
1597 let mut mapper = ActionMapper::with_defaults();
1598 let t = now();
1599
1600 let state = AppState::new().with_modal(true).with_overlay(true);
1601 mapper.map(&esc_press(), &state, t);
1602 let action = mapper.check_timeout(&state, t + MS_300);
1603 assert_eq!(action, Some(Action::DismissModal));
1604 }
1605
1606 #[test]
1607 fn test_esc_passthrough_when_idle() {
1608 let mut mapper = ActionMapper::with_defaults();
1609 let t = now();
1610
1611 mapper.map(&esc_press(), &idle_state(), t);
1612 let action = mapper.check_timeout(&idle_state(), t + MS_300);
1613 assert_eq!(action, Some(Action::PassThrough));
1614 }
1615
1616 #[test]
1619 fn test_esc_esc_within_timeout() {
1620 let mut mapper = ActionMapper::with_defaults();
1621 let t = now();
1622
1623 mapper.map(&esc_press(), &idle_state(), t);
1624 let action = mapper.map(&esc_press(), &idle_state(), t + MS_100);
1625 assert_eq!(action, Some(Action::ToggleTreeView));
1626 }
1627
1628 #[test]
1629 fn test_esc_esc_ignores_state() {
1630 let mut mapper = ActionMapper::with_defaults();
1631 let t = now();
1632
1633 mapper.map(&esc_press(), &modal_state(), t);
1635 let action = mapper.map(&esc_press(), &modal_state(), t + MS_100);
1636 assert_eq!(action, Some(Action::ToggleTreeView));
1637 }
1638
1639 #[test]
1640 fn test_esc_esc_timeout_expired() {
1641 let mut mapper = ActionMapper::with_defaults();
1642 let t = now();
1643
1644 mapper.map(&esc_press(), &input_state(), t);
1645 let action = mapper.map(&esc_press(), &input_state(), t + MS_300);
1647
1648 assert_eq!(action, Some(Action::ClearInput));
1650 assert!(mapper.is_pending_esc());
1651 }
1652
1653 #[test]
1656 fn test_esc_then_other_key() {
1657 let mut mapper = ActionMapper::with_defaults();
1658 let t = now();
1659
1660 mapper.map(&esc_press(), &input_state(), t);
1661 let action = mapper.map(&key_press(KeyCode::Char('a')), &input_state(), t + MS_50);
1662
1663 assert_eq!(action, Some(Action::ClearInput));
1665 }
1666
1667 #[test]
1670 fn test_regular_key_passthrough() {
1671 let mut mapper = ActionMapper::with_defaults();
1672 let t = now();
1673
1674 let action = mapper.map(&key_press(KeyCode::Char('x')), &idle_state(), t);
1675 assert_eq!(action, Some(Action::PassThrough));
1676 }
1677
1678 #[test]
1679 fn test_release_event_passthrough() {
1680 let mut mapper = ActionMapper::with_defaults();
1681 let t = now();
1682
1683 let release = KeyEvent::new(KeyCode::Char('x')).with_kind(KeyEventKind::Release);
1684 let action = mapper.map(&release, &idle_state(), t);
1685 assert_eq!(action, Some(Action::PassThrough));
1686 }
1687
1688 #[test]
1691 fn test_app_state_builders() {
1692 let state = AppState::new()
1693 .with_input(true)
1694 .with_task(true)
1695 .with_modal(true)
1696 .with_overlay(true);
1697
1698 assert!(state.input_nonempty);
1699 assert!(state.task_running);
1700 assert!(state.modal_open);
1701 assert!(state.view_overlay);
1702 assert!(!state.is_idle());
1703 }
1704
1705 #[test]
1706 fn test_app_state_is_idle() {
1707 assert!(AppState::default().is_idle());
1708 assert!(!AppState::new().with_input(true).is_idle());
1709 assert!(!AppState::new().with_task(true).is_idle());
1710 assert!(!AppState::new().with_modal(true).is_idle());
1711 assert!(AppState::new().with_overlay(true).is_idle());
1713 }
1714
1715 #[test]
1718 fn test_action_consumes_event() {
1719 assert!(Action::ClearInput.consumes_event());
1720 assert!(Action::CancelTask.consumes_event());
1721 assert!(Action::Quit.consumes_event());
1722 assert!(!Action::PassThrough.consumes_event());
1723 }
1724
1725 #[test]
1726 fn test_action_is_quit() {
1727 assert!(Action::Quit.is_quit());
1728 assert!(Action::SoftQuit.is_quit());
1729 assert!(Action::HardQuit.is_quit());
1730 assert!(!Action::ClearInput.is_quit());
1731 assert!(!Action::PassThrough.is_quit());
1732 }
1733
1734 #[test]
1737 fn test_ctrl_c_idle_action_from_str() {
1738 assert_eq!(
1739 CtrlCIdleAction::from_str_opt("quit"),
1740 Some(CtrlCIdleAction::Quit)
1741 );
1742 assert_eq!(
1743 CtrlCIdleAction::from_str_opt("QUIT"),
1744 Some(CtrlCIdleAction::Quit)
1745 );
1746 assert_eq!(
1747 CtrlCIdleAction::from_str_opt("noop"),
1748 Some(CtrlCIdleAction::Noop)
1749 );
1750 assert_eq!(
1751 CtrlCIdleAction::from_str_opt("none"),
1752 Some(CtrlCIdleAction::Noop)
1753 );
1754 assert_eq!(
1755 CtrlCIdleAction::from_str_opt("ignore"),
1756 Some(CtrlCIdleAction::Noop)
1757 );
1758 assert_eq!(
1759 CtrlCIdleAction::from_str_opt("bell"),
1760 Some(CtrlCIdleAction::Bell)
1761 );
1762 assert_eq!(
1763 CtrlCIdleAction::from_str_opt("beep"),
1764 Some(CtrlCIdleAction::Bell)
1765 );
1766 assert_eq!(CtrlCIdleAction::from_str_opt("invalid"), None);
1767 }
1768
1769 #[test]
1770 fn test_ctrl_c_idle_action_to_action() {
1771 assert_eq!(CtrlCIdleAction::Quit.to_action(), Some(Action::Quit));
1772 assert_eq!(CtrlCIdleAction::Noop.to_action(), None);
1773 assert_eq!(CtrlCIdleAction::Bell.to_action(), Some(Action::Bell));
1774 }
1775
1776 #[test]
1777 fn test_action_config_builder() {
1778 let config = ActionConfig::default()
1779 .with_sequence_config(SequenceConfig::default().with_timeout(MS_100))
1780 .with_ctrl_c_idle(CtrlCIdleAction::Bell);
1781
1782 assert_eq!(config.sequence_config.esc_seq_timeout, MS_100);
1783 assert_eq!(config.ctrl_c_idle_action, CtrlCIdleAction::Bell);
1784 }
1785
1786 #[test]
1789 fn test_mapper_reset() {
1790 let mut mapper = ActionMapper::with_defaults();
1791 let t = now();
1792
1793 mapper.map(&esc_press(), &idle_state(), t);
1794 assert!(mapper.is_pending_esc());
1795
1796 mapper.reset();
1797 assert!(!mapper.is_pending_esc());
1798 }
1799
1800 #[test]
1803 fn test_deterministic_action_mapping() {
1804 let t = now();
1805
1806 let mut m1 = ActionMapper::with_defaults();
1807 let mut m2 = ActionMapper::with_defaults();
1808
1809 let events = [
1810 (ctrl_c(), input_state()),
1811 (ctrl_d(), modal_state()),
1812 (ctrl_q(), idle_state()),
1813 ];
1814
1815 for (event, state) in &events {
1816 let a1 = m1.map(event, state, t);
1817 let a2 = m2.map(event, state, t);
1818 assert_eq!(a1, a2);
1819 }
1820 }
1821
1822 #[test]
1823 fn test_uppercase_ctrl_keys() {
1824 let mut mapper = ActionMapper::with_defaults();
1825 let t = now();
1826
1827 let ctrl_c_upper = KeyEvent::new(KeyCode::Char('C')).with_modifiers(Modifiers::CTRL);
1829 let action = mapper.map(&ctrl_c_upper, &idle_state(), t);
1830 assert_eq!(action, Some(Action::Quit));
1831 }
1832
1833 #[test]
1836 fn test_sequence_config_validation_clamps_high_timeout() {
1837 let config = SequenceConfig::default()
1838 .with_timeout(Duration::from_millis(1000)) .validated();
1840
1841 assert_eq!(config.esc_seq_timeout.as_millis(), 400);
1843 }
1844
1845 #[test]
1846 fn test_sequence_config_validation_clamps_low_timeout() {
1847 let config = SequenceConfig::default()
1848 .with_timeout(Duration::from_millis(50)) .validated();
1850
1851 assert_eq!(config.esc_seq_timeout.as_millis(), 150);
1853 }
1854
1855 #[test]
1856 fn test_sequence_config_validation_clamps_high_debounce() {
1857 let config = SequenceConfig::default()
1858 .with_debounce(Duration::from_millis(200)) .validated();
1860
1861 assert_eq!(config.esc_debounce.as_millis(), 100);
1863 }
1864
1865 #[test]
1866 fn test_sequence_config_validation_debounce_not_exceeds_timeout() {
1867 let config = SequenceConfig::default()
1868 .with_timeout(Duration::from_millis(150))
1869 .with_debounce(Duration::from_millis(200)) .validated();
1871
1872 assert!(config.esc_debounce <= config.esc_seq_timeout);
1876 }
1877
1878 #[test]
1879 fn test_sequence_config_is_valid() {
1880 assert!(SequenceConfig::default().is_valid());
1881
1882 let invalid = SequenceConfig::default().with_timeout(Duration::from_millis(500));
1884 assert!(!invalid.is_valid());
1885
1886 assert!(invalid.validated().is_valid());
1888 }
1889
1890 #[test]
1891 fn test_sequence_config_constants() {
1892 assert_eq!(DEFAULT_ESC_SEQ_TIMEOUT_MS, 250);
1894 assert_eq!(MIN_ESC_SEQ_TIMEOUT_MS, 150);
1895 assert_eq!(MAX_ESC_SEQ_TIMEOUT_MS, 400);
1896 assert_eq!(DEFAULT_ESC_DEBOUNCE_MS, 50);
1897 assert_eq!(MIN_ESC_DEBOUNCE_MS, 0);
1898 assert_eq!(MAX_ESC_DEBOUNCE_MS, 100);
1899 }
1900
1901 #[test]
1902 fn test_action_config_validated() {
1903 let config = ActionConfig::default()
1904 .with_sequence_config(
1905 SequenceConfig::default().with_timeout(Duration::from_millis(1000)),
1906 )
1907 .validated();
1908
1909 assert_eq!(config.sequence_config.esc_seq_timeout.as_millis(), 400);
1911 }
1912 }
1913}