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
972use std::fmt;
1002use std::str::FromStr;
1003
1004#[derive(Debug, Clone, PartialEq, Eq)]
1006pub enum KeyParseError {
1007 EmptyKey,
1009 UnknownKey(String),
1011 UnknownModifier(String),
1013 TooManyKeys(usize),
1015}
1016
1017impl fmt::Display for KeyParseError {
1018 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1019 match self {
1020 Self::EmptyKey => f.write_str("empty key"),
1021 Self::UnknownKey(name) => write!(f, "unknown key `{name}`"),
1022 Self::UnknownModifier(name) => write!(f, "unknown modifier `{name}`"),
1023 Self::TooManyKeys(n) => {
1024 write!(f, "chord has {n} keys; the maximum is {}", Chord::MAX_LEN)
1025 }
1026 }
1027 }
1028}
1029
1030impl std::error::Error for KeyParseError {}
1031
1032#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1038pub struct KeyCombo {
1039 pub code: KeyCode,
1041 pub modifiers: Modifiers,
1043}
1044
1045impl KeyCombo {
1046 #[must_use]
1048 pub fn new(code: KeyCode, modifiers: Modifiers) -> Self {
1049 match code {
1050 KeyCode::Char(c) if c.is_alphabetic() && c.is_uppercase() => Self {
1051 code: KeyCode::Char(c.to_lowercase().next().unwrap_or(c)),
1052 modifiers: modifiers | Modifiers::SHIFT,
1053 },
1054 _ => Self { code, modifiers },
1055 }
1056 }
1057
1058 #[must_use]
1060 pub fn key(code: KeyCode) -> Self {
1061 Self::new(code, Modifiers::NONE)
1062 }
1063
1064 #[must_use]
1066 pub fn from_event(event: &KeyEvent) -> Self {
1067 Self::new(event.code, event.modifiers)
1068 }
1069
1070 #[must_use]
1072 pub fn matches(&self, event: &KeyEvent) -> bool {
1073 Self::from_event(event) == *self
1074 }
1075}
1076
1077impl fmt::Display for KeyCombo {
1078 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1079 let mut modifiers = self.modifiers;
1080 let key = match self.code {
1081 KeyCode::Char(c) if c.is_alphabetic() && modifiers.contains(Modifiers::SHIFT) => {
1082 modifiers.remove(Modifiers::SHIFT);
1083 c.to_uppercase().collect::<String>()
1084 }
1085 KeyCode::Char(' ') => "Space".to_string(),
1086 KeyCode::Char(c) => c.to_string(),
1087 KeyCode::Enter => "Enter".to_string(),
1088 KeyCode::Escape => "Esc".to_string(),
1089 KeyCode::Backspace => "Backspace".to_string(),
1090 KeyCode::Tab => "Tab".to_string(),
1091 KeyCode::BackTab => "BackTab".to_string(),
1092 KeyCode::Delete => "Delete".to_string(),
1093 KeyCode::Insert => "Insert".to_string(),
1094 KeyCode::Home => "Home".to_string(),
1095 KeyCode::End => "End".to_string(),
1096 KeyCode::PageUp => "PageUp".to_string(),
1097 KeyCode::PageDown => "PageDown".to_string(),
1098 KeyCode::Up => "Up".to_string(),
1099 KeyCode::Down => "Down".to_string(),
1100 KeyCode::Left => "Left".to_string(),
1101 KeyCode::Right => "Right".to_string(),
1102 KeyCode::F(n) => format!("F{n}"),
1103 KeyCode::Null => "Null".to_string(),
1104 KeyCode::MediaPlayPause => "MediaPlayPause".to_string(),
1105 KeyCode::MediaStop => "MediaStop".to_string(),
1106 KeyCode::MediaNextTrack => "MediaNextTrack".to_string(),
1107 KeyCode::MediaPrevTrack => "MediaPrevTrack".to_string(),
1108 };
1109 for (flag, name) in [
1110 (Modifiers::CTRL, "Ctrl"),
1111 (Modifiers::ALT, "Alt"),
1112 (Modifiers::SHIFT, "Shift"),
1113 (Modifiers::SUPER, "Super"),
1114 ] {
1115 if modifiers.contains(flag) {
1116 write!(f, "{name}+")?;
1117 }
1118 }
1119 f.write_str(&key)
1120 }
1121}
1122
1123fn parse_key_name(name: &str) -> Result<KeyCode, KeyParseError> {
1128 let mut chars = name.chars();
1129 if let (Some(c), None) = (chars.next(), chars.next()) {
1130 return Ok(KeyCode::Char(c));
1131 }
1132 let lower = name.to_ascii_lowercase();
1133 let code = match lower.as_str() {
1134 "enter" | "return" => KeyCode::Enter,
1135 "esc" | "escape" => KeyCode::Escape,
1136 "backspace" => KeyCode::Backspace,
1137 "tab" => KeyCode::Tab,
1138 "backtab" => KeyCode::BackTab,
1139 "delete" | "del" => KeyCode::Delete,
1140 "insert" | "ins" => KeyCode::Insert,
1141 "home" => KeyCode::Home,
1142 "end" => KeyCode::End,
1143 "pageup" | "pgup" => KeyCode::PageUp,
1144 "pagedown" | "pgdn" => KeyCode::PageDown,
1145 "up" => KeyCode::Up,
1146 "down" => KeyCode::Down,
1147 "left" => KeyCode::Left,
1148 "right" => KeyCode::Right,
1149 "space" => KeyCode::Char(' '),
1150 "null" => KeyCode::Null,
1151 "mediaplaypause" => KeyCode::MediaPlayPause,
1152 "mediastop" => KeyCode::MediaStop,
1153 "medianexttrack" => KeyCode::MediaNextTrack,
1154 "mediaprevtrack" => KeyCode::MediaPrevTrack,
1155 other => {
1156 if let Some(digits) = other.strip_prefix('f')
1157 && let Ok(n) = digits.parse::<u8>()
1158 && (1..=24).contains(&n)
1159 {
1160 KeyCode::F(n)
1161 } else {
1162 return Err(KeyParseError::UnknownKey(name.to_string()));
1163 }
1164 }
1165 };
1166 Ok(code)
1167}
1168
1169impl FromStr for KeyCombo {
1170 type Err = KeyParseError;
1171
1172 fn from_str(s: &str) -> Result<Self, Self::Err> {
1176 let s = s.trim();
1177 if s.is_empty() {
1178 return Err(KeyParseError::EmptyKey);
1179 }
1180 let (modifier_part, key_part) = if s == "+" {
1181 ("", "+")
1182 } else if let Some(stripped) = s.strip_suffix('+') {
1183 (stripped.trim_end_matches('+'), "+")
1184 } else if let Some((modifiers, key)) = s.rsplit_once('+') {
1185 (modifiers, key)
1186 } else {
1187 ("", s)
1188 };
1189 let mut modifiers = Modifiers::NONE;
1190 for part in modifier_part.split('+').filter(|p| !p.is_empty()) {
1191 modifiers |= match part.to_ascii_lowercase().as_str() {
1192 "ctrl" | "control" => Modifiers::CTRL,
1193 "alt" | "opt" | "option" => Modifiers::ALT,
1194 "shift" => Modifiers::SHIFT,
1195 "super" | "cmd" | "meta" | "win" => Modifiers::SUPER,
1196 _ => return Err(KeyParseError::UnknownModifier(part.to_string())),
1197 };
1198 }
1199 if key_part.is_empty() {
1200 return Err(KeyParseError::EmptyKey);
1201 }
1202 Ok(Self::new(parse_key_name(key_part)?, modifiers))
1203 }
1204}
1205
1206#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1208pub struct Chord(Vec<KeyCombo>);
1209
1210impl Chord {
1211 pub const MAX_LEN: usize = 4;
1213
1214 #[must_use]
1216 pub fn single(combo: KeyCombo) -> Self {
1217 Self(vec![combo])
1218 }
1219
1220 pub fn new(combos: Vec<KeyCombo>) -> Result<Self, KeyParseError> {
1222 if combos.is_empty() {
1223 Err(KeyParseError::EmptyKey)
1224 } else if combos.len() > Self::MAX_LEN {
1225 Err(KeyParseError::TooManyKeys(combos.len()))
1226 } else {
1227 Ok(Self(combos))
1228 }
1229 }
1230
1231 pub fn parse(s: &str) -> Result<Self, KeyParseError> {
1233 s.parse()
1234 }
1235
1236 #[must_use]
1238 pub fn combos(&self) -> &[KeyCombo] {
1239 &self.0
1240 }
1241
1242 #[must_use]
1244 pub fn len(&self) -> usize {
1245 self.0.len()
1246 }
1247
1248 #[must_use]
1251 pub fn is_empty(&self) -> bool {
1252 self.0.is_empty()
1253 }
1254
1255 #[must_use]
1257 pub fn is_prefix_of(&self, other: &Self) -> bool {
1258 self.0.len() < other.0.len() && other.0.starts_with(&self.0)
1259 }
1260}
1261
1262impl FromStr for Chord {
1263 type Err = KeyParseError;
1264
1265 fn from_str(s: &str) -> Result<Self, Self::Err> {
1266 let combos = s
1267 .split_whitespace()
1268 .map(str::parse)
1269 .collect::<Result<Vec<KeyCombo>, _>>()?;
1270 Self::new(combos)
1271 }
1272}
1273
1274impl fmt::Display for Chord {
1275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1276 for (i, combo) in self.0.iter().enumerate() {
1277 if i > 0 {
1278 f.write_str(" ")?;
1279 }
1280 write!(f, "{combo}")?;
1281 }
1282 Ok(())
1283 }
1284}
1285
1286#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1288#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1289pub enum Priority {
1290 #[default]
1292 Global = 0,
1293 Mode = 1,
1295 Widget = 2,
1297}
1298
1299#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1301pub struct ContextId(pub u32);
1302
1303#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1305pub struct BindingId(pub u32);
1306
1307impl fmt::Display for BindingId {
1308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1309 write!(f, "#{}", self.0)
1310 }
1311}
1312
1313#[derive(Debug, Clone)]
1315pub struct Binding<A> {
1316 pub id: BindingId,
1318 pub chord: Chord,
1320 pub action: A,
1322 pub priority: Priority,
1324 pub context: Option<ContextId>,
1326 pub label: Option<String>,
1328}
1329
1330pub const MIN_CHORD_TIMEOUT_MS: u64 = 200;
1332pub const MAX_CHORD_TIMEOUT_MS: u64 = 5000;
1334pub const DEFAULT_CHORD_TIMEOUT_MS: u64 = 1000;
1336
1337#[derive(Debug, Clone)]
1339pub struct KeyMapConfig {
1340 pub chord_timeout: Duration,
1342 pub esc: SequenceConfig,
1344}
1345
1346impl Default for KeyMapConfig {
1347 fn default() -> Self {
1348 Self {
1349 chord_timeout: Duration::from_millis(DEFAULT_CHORD_TIMEOUT_MS),
1350 esc: SequenceConfig::default(),
1351 }
1352 }
1353}
1354
1355impl KeyMapConfig {
1356 #[must_use]
1358 pub fn with_chord_timeout(mut self, timeout: Duration) -> Self {
1359 let ms = timeout.as_millis().clamp(
1360 u128::from(MIN_CHORD_TIMEOUT_MS),
1361 u128::from(MAX_CHORD_TIMEOUT_MS),
1362 );
1363 self.chord_timeout = Duration::from_millis(ms as u64);
1364 self
1365 }
1366
1367 #[must_use]
1369 pub fn with_esc(mut self, esc: SequenceConfig) -> Self {
1370 self.esc = esc;
1371 self
1372 }
1373}
1374
1375#[derive(Debug, Clone, Copy)]
1377pub struct Lookup<'a, A> {
1378 pub exact: Option<&'a Binding<A>>,
1380 pub longer: usize,
1383}
1384
1385impl<A> Lookup<'_, A> {
1386 #[must_use]
1388 pub fn is_none(&self) -> bool {
1389 self.exact.is_none() && self.longer == 0
1390 }
1391}
1392
1393#[derive(Debug, Clone, PartialEq, Eq)]
1395pub enum Conflict {
1396 Shadowed {
1398 winner: BindingId,
1399 loser: BindingId,
1400 chord: Chord,
1401 },
1402 PrefixCollision {
1405 short: BindingId,
1406 long: BindingId,
1407 short_chord: Chord,
1408 long_chord: Chord,
1409 },
1410 Duplicate {
1412 first: BindingId,
1413 second: BindingId,
1414 chord: Chord,
1415 },
1416}
1417
1418#[derive(Debug, Clone, Default, PartialEq, Eq)]
1420pub struct ConflictReport {
1421 pub items: Vec<Conflict>,
1423}
1424
1425impl ConflictReport {
1426 #[must_use]
1428 pub fn is_empty(&self) -> bool {
1429 self.items.is_empty()
1430 }
1431
1432 #[must_use]
1434 pub fn len(&self) -> usize {
1435 self.items.len()
1436 }
1437}
1438
1439impl fmt::Display for ConflictReport {
1440 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1441 for item in &self.items {
1442 match item {
1443 Conflict::Shadowed {
1444 winner,
1445 loser,
1446 chord,
1447 } => writeln!(
1448 f,
1449 "warning: binding {winner} shadows binding {loser} on `{chord}` (higher priority)"
1450 )?,
1451 Conflict::PrefixCollision {
1452 short,
1453 long,
1454 short_chord,
1455 long_chord,
1456 } => writeln!(
1457 f,
1458 "warning: binding {short} (`{short_chord}`) is a prefix of binding {long} (`{long_chord}`); it fires only after the chord timeout or a non-extending key"
1459 )?,
1460 Conflict::Duplicate {
1461 first,
1462 second,
1463 chord,
1464 } => writeln!(
1465 f,
1466 "warning: bindings {first} and {second} both bind `{chord}` at the same priority; the later one wins"
1467 )?,
1468 }
1469 }
1470 Ok(())
1471 }
1472}
1473
1474#[derive(Debug, Clone)]
1477pub struct KeyMap<A> {
1478 bindings: Vec<Binding<A>>,
1479 contexts: Vec<String>,
1480 config: KeyMapConfig,
1481 next_id: u32,
1482}
1483
1484impl<A> Default for KeyMap<A> {
1485 fn default() -> Self {
1486 Self::new()
1487 }
1488}
1489
1490impl<A> KeyMap<A> {
1491 #[must_use]
1493 pub fn new() -> Self {
1494 Self::with_config(KeyMapConfig::default())
1495 }
1496
1497 #[must_use]
1499 pub fn with_config(config: KeyMapConfig) -> Self {
1500 Self {
1501 bindings: Vec::new(),
1502 contexts: Vec::new(),
1503 config,
1504 next_id: 0,
1505 }
1506 }
1507
1508 #[must_use]
1510 pub fn config(&self) -> &KeyMapConfig {
1511 &self.config
1512 }
1513
1514 pub fn context(&mut self, name: &str) -> ContextId {
1516 if let Some(index) = self.contexts.iter().position(|n| n == name) {
1517 return ContextId(index as u32);
1518 }
1519 self.contexts.push(name.to_string());
1520 ContextId((self.contexts.len() - 1) as u32)
1521 }
1522
1523 #[must_use]
1525 pub fn context_name(&self, id: ContextId) -> Option<&str> {
1526 self.contexts.get(id.0 as usize).map(String::as_str)
1527 }
1528
1529 pub fn bind(&mut self, chord: Chord, action: A) -> BindingId {
1531 self.bind_in(chord, action, Priority::Global, None)
1532 }
1533
1534 pub fn bind_in(
1536 &mut self,
1537 chord: Chord,
1538 action: A,
1539 priority: Priority,
1540 context: Option<ContextId>,
1541 ) -> BindingId {
1542 let id = BindingId(self.next_id);
1543 self.next_id += 1;
1544 self.bindings.push(Binding {
1545 id,
1546 chord,
1547 action,
1548 priority,
1549 context,
1550 label: None,
1551 });
1552 id
1553 }
1554
1555 pub fn set_label(&mut self, id: BindingId, label: impl Into<String>) -> bool {
1557 match self.bindings.iter_mut().find(|b| b.id == id) {
1558 Some(binding) => {
1559 binding.label = Some(label.into());
1560 true
1561 }
1562 None => false,
1563 }
1564 }
1565
1566 pub fn unbind(&mut self, id: BindingId) -> Option<Binding<A>> {
1568 let index = self.bindings.iter().position(|b| b.id == id)?;
1569 Some(self.bindings.remove(index))
1570 }
1571
1572 #[must_use]
1574 pub fn bindings(&self) -> &[Binding<A>] {
1575 &self.bindings
1576 }
1577
1578 #[must_use]
1580 pub fn get(&self, id: BindingId) -> Option<&Binding<A>> {
1581 self.bindings.iter().find(|b| b.id == id)
1582 }
1583
1584 #[must_use]
1586 pub fn len(&self) -> usize {
1587 self.bindings.len()
1588 }
1589
1590 #[must_use]
1592 pub fn is_empty(&self) -> bool {
1593 self.bindings.is_empty()
1594 }
1595
1596 fn applies(binding: &Binding<A>, active: &[ContextId]) -> bool {
1597 binding
1598 .context
1599 .is_none_or(|context| active.contains(&context))
1600 }
1601
1602 fn rank(binding: &Binding<A>) -> (bool, Priority, BindingId) {
1605 (binding.context.is_some(), binding.priority, binding.id)
1606 }
1607
1608 #[must_use]
1612 pub fn lookup(&self, chord: &Chord, active: &[ContextId]) -> Lookup<'_, A> {
1613 let mut exact: Option<&Binding<A>> = None;
1614 let mut longer = 0;
1615 for binding in &self.bindings {
1616 if !Self::applies(binding, active) {
1617 continue;
1618 }
1619 if binding.chord == *chord {
1620 if exact.is_none_or(|current| Self::rank(binding) > Self::rank(current)) {
1621 exact = Some(binding);
1622 }
1623 } else if chord.is_prefix_of(&binding.chord) {
1624 longer += 1;
1625 }
1626 }
1627 Lookup { exact, longer }
1628 }
1629
1630 #[must_use]
1632 pub fn conflicts(&self) -> ConflictReport {
1633 let mut items = Vec::new();
1634 for (i, a) in self.bindings.iter().enumerate() {
1635 for b in &self.bindings[i + 1..] {
1636 if a.chord == b.chord {
1637 if a.context != b.context {
1638 continue;
1640 }
1641 if a.priority == b.priority {
1642 items.push(Conflict::Duplicate {
1643 first: a.id,
1644 second: b.id,
1645 chord: a.chord.clone(),
1646 });
1647 } else {
1648 let (winner, loser) = if a.priority > b.priority {
1649 (a.id, b.id)
1650 } else {
1651 (b.id, a.id)
1652 };
1653 items.push(Conflict::Shadowed {
1654 winner,
1655 loser,
1656 chord: a.chord.clone(),
1657 });
1658 }
1659 } else if a.chord.is_prefix_of(&b.chord) {
1660 items.push(Conflict::PrefixCollision {
1661 short: a.id,
1662 long: b.id,
1663 short_chord: a.chord.clone(),
1664 long_chord: b.chord.clone(),
1665 });
1666 } else if b.chord.is_prefix_of(&a.chord) {
1667 items.push(Conflict::PrefixCollision {
1668 short: b.id,
1669 long: a.id,
1670 short_chord: b.chord.clone(),
1671 long_chord: a.chord.clone(),
1672 });
1673 }
1674 }
1675 }
1676 ConflictReport { items }
1677 }
1678}
1679
1680#[derive(Debug, Clone, PartialEq, Eq)]
1682pub enum Dispatch<A> {
1683 Action {
1685 action: A,
1686 binding: BindingId,
1687 chord: Chord,
1688 },
1689 Pending { prefix: Chord },
1691 Unbound(KeyEvent),
1693 Expired { prefix: Chord },
1695 Esc(SequenceOutput),
1697}
1698
1699#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1701pub struct DispatchStats {
1702 pub dispatched: u64,
1704 pub pending: u64,
1706 pub expired: u64,
1708 pub unbound: u64,
1710 pub esc: u64,
1712}
1713
1714fn action_dispatch<A: Clone>(binding: &Binding<A>, chord: Chord) -> Dispatch<A> {
1715 Dispatch::Action {
1716 action: binding.action.clone(),
1717 binding: binding.id,
1718 chord,
1719 }
1720}
1721
1722#[derive(Debug)]
1729pub struct KeyDispatcher<A> {
1730 map: KeyMap<A>,
1731 pending: Vec<KeyCombo>,
1732 pending_since: Option<Instant>,
1733 esc: SequenceDetector,
1734 active_contexts: Vec<ContextId>,
1735 stats: DispatchStats,
1736}
1737
1738impl<A: Clone> KeyDispatcher<A> {
1739 #[must_use]
1741 pub fn new(map: KeyMap<A>) -> Self {
1742 let esc = SequenceDetector::new(map.config().esc.clone());
1743 Self {
1744 map,
1745 pending: Vec::new(),
1746 pending_since: None,
1747 esc,
1748 active_contexts: Vec::new(),
1749 stats: DispatchStats::default(),
1750 }
1751 }
1752
1753 #[must_use]
1755 pub fn map(&self) -> &KeyMap<A> {
1756 &self.map
1757 }
1758
1759 pub fn map_mut(&mut self) -> &mut KeyMap<A> {
1761 &mut self.map
1762 }
1763
1764 pub fn set_active_contexts(&mut self, contexts: &[ContextId]) {
1766 self.active_contexts.clear();
1767 self.active_contexts.extend_from_slice(contexts);
1768 }
1769
1770 #[must_use]
1772 pub fn active_contexts(&self) -> &[ContextId] {
1773 &self.active_contexts
1774 }
1775
1776 #[must_use]
1778 pub fn pending_prefix(&self) -> Option<Chord> {
1779 Chord::new(self.pending.clone()).ok()
1780 }
1781
1782 #[must_use]
1784 pub fn stats(&self) -> DispatchStats {
1785 self.stats
1786 }
1787
1788 pub fn reset(&mut self) {
1790 self.pending.clear();
1791 self.pending_since = None;
1792 self.esc.reset();
1793 }
1794
1795 pub fn feed(&mut self, key: &KeyEvent, now: Instant) -> Vec<Dispatch<A>> {
1797 let mut out = Vec::with_capacity(2);
1798
1799 if key.code == KeyCode::Escape {
1800 if key.kind == KeyEventKind::Press {
1801 self.flush_pending(&mut out, false);
1802 }
1803 match self.esc.feed(key, now) {
1804 SequenceOutput::PassThrough => {}
1806 output => {
1807 self.dispatch_esc(output, &mut out);
1808 return out;
1809 }
1810 }
1811 }
1812
1813 match key.kind {
1814 KeyEventKind::Release => {
1815 self.stats.unbound += 1;
1816 out.push(Dispatch::Unbound(*key));
1817 return out;
1818 }
1819 KeyEventKind::Repeat => {
1820 let single = Chord::single(KeyCombo::from_event(key));
1822 let fired = if self.pending.is_empty() {
1823 self.map
1824 .lookup(&single, &self.active_contexts)
1825 .exact
1826 .map(|binding| action_dispatch(binding, single))
1827 } else {
1828 None
1829 };
1830 match fired {
1831 Some(dispatch) => {
1832 self.stats.dispatched += 1;
1833 out.push(dispatch);
1834 }
1835 None => {
1836 self.stats.unbound += 1;
1837 out.push(Dispatch::Unbound(*key));
1838 }
1839 }
1840 return out;
1841 }
1842 KeyEventKind::Press => {}
1843 }
1844
1845 let combo = KeyCombo::from_event(key);
1846 if self.try_extend(combo, now, &mut out) {
1847 return out;
1848 }
1849
1850 if !self.pending.is_empty() {
1853 self.flush_pending(&mut out, false);
1854 if self.try_extend(combo, now, &mut out) {
1855 return out;
1856 }
1857 }
1858
1859 self.stats.unbound += 1;
1860 out.push(Dispatch::Unbound(*key));
1861 out
1862 }
1863
1864 pub fn tick(&mut self, now: Instant) -> Vec<Dispatch<A>> {
1866 let mut out = Vec::new();
1867 if let Some(since) = self.pending_since
1868 && now.saturating_duration_since(since) >= self.map.config.chord_timeout
1869 {
1870 self.flush_pending(&mut out, true);
1871 }
1872 if let Some(output) = self.esc.check_timeout(now) {
1873 self.dispatch_esc(output, &mut out);
1874 }
1875 out
1876 }
1877
1878 fn try_extend(&mut self, combo: KeyCombo, now: Instant, out: &mut Vec<Dispatch<A>>) -> bool {
1881 if self.pending.len() >= Chord::MAX_LEN {
1882 return false;
1883 }
1884 let mut candidate = self.pending.clone();
1885 candidate.push(combo);
1886 let chord = Chord(candidate);
1887 let lookup = self.map.lookup(&chord, &self.active_contexts);
1888 if let Some(binding) = lookup.exact
1889 && lookup.longer == 0
1890 {
1891 let dispatch = action_dispatch(binding, chord);
1892 self.pending.clear();
1893 self.pending_since = None;
1894 self.stats.dispatched += 1;
1895 out.push(dispatch);
1896 return true;
1897 }
1898 if lookup.exact.is_some() || lookup.longer > 0 {
1899 self.pending.clone_from(&chord.0);
1900 self.pending_since = Some(now);
1901 self.stats.pending += 1;
1902 out.push(Dispatch::Pending { prefix: chord });
1903 return true;
1904 }
1905 false
1906 }
1907
1908 fn flush_pending(&mut self, out: &mut Vec<Dispatch<A>>, timed_out: bool) {
1911 if self.pending.is_empty() {
1912 return;
1913 }
1914 let prefix = Chord(std::mem::take(&mut self.pending));
1915 self.pending_since = None;
1916 let fired = self
1917 .map
1918 .lookup(&prefix, &self.active_contexts)
1919 .exact
1920 .map(|binding| action_dispatch(binding, prefix.clone()));
1921 match fired {
1922 Some(dispatch) => {
1923 if timed_out {
1924 self.stats.expired += 1;
1925 out.push(Dispatch::Expired { prefix });
1926 }
1927 self.stats.dispatched += 1;
1928 out.push(dispatch);
1929 }
1930 None => {
1931 self.stats.expired += 1;
1932 out.push(Dispatch::Expired { prefix });
1933 }
1934 }
1935 }
1936
1937 fn dispatch_esc(&mut self, output: SequenceOutput, out: &mut Vec<Dispatch<A>>) {
1940 let esc = KeyCombo::key(KeyCode::Escape);
1941 let bound = match output {
1942 SequenceOutput::Esc => Some(Chord::single(esc)),
1943 SequenceOutput::EscEsc => Chord::new(vec![esc, esc]).ok(),
1944 SequenceOutput::Pending | SequenceOutput::PassThrough => None,
1945 };
1946 let fired = bound.and_then(|chord| {
1947 self.map
1948 .lookup(&chord, &self.active_contexts)
1949 .exact
1950 .map(|binding| action_dispatch(binding, chord.clone()))
1951 });
1952 match fired {
1953 Some(dispatch) => {
1954 self.stats.dispatched += 1;
1955 out.push(dispatch);
1956 }
1957 None => {
1958 self.stats.esc += 1;
1959 out.push(Dispatch::Esc(output));
1960 }
1961 }
1962 }
1963}
1964
1965#[cfg(feature = "serde")]
1989#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1990#[serde(deny_unknown_fields)]
1991pub struct KeyMapFile<A> {
1992 #[serde(default = "default_chord_timeout_ms")]
1994 pub chord_timeout_ms: u64,
1995 #[serde(default = "Vec::new")]
1997 pub bindings: Vec<BindingFile<A>>,
1998}
1999
2000#[cfg(feature = "serde")]
2001fn default_chord_timeout_ms() -> u64 {
2002 DEFAULT_CHORD_TIMEOUT_MS
2003}
2004
2005#[cfg(feature = "serde")]
2007#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2008#[serde(deny_unknown_fields)]
2009pub struct BindingFile<A> {
2010 pub chord: String,
2012 pub action: A,
2014 #[serde(default)]
2016 pub priority: Priority,
2017 #[serde(default, skip_serializing_if = "Option::is_none")]
2019 pub context: Option<String>,
2020 #[serde(default, skip_serializing_if = "Option::is_none")]
2022 pub label: Option<String>,
2023}
2024
2025#[cfg(feature = "serde")]
2027#[derive(Debug, Clone, PartialEq, Eq)]
2028pub enum KeyMapFileError {
2029 Chord {
2031 index: usize,
2033 chord: String,
2035 source: KeyParseError,
2037 },
2038}
2039
2040#[cfg(feature = "serde")]
2041impl fmt::Display for KeyMapFileError {
2042 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2043 match self {
2044 Self::Chord {
2045 index,
2046 chord,
2047 source,
2048 } => write!(f, "binding {index} (`{chord}`): {source}"),
2049 }
2050 }
2051}
2052
2053#[cfg(feature = "serde")]
2054impl std::error::Error for KeyMapFileError {}
2055
2056#[cfg(feature = "serde")]
2057impl<A: Clone> KeyMap<A> {
2058 #[must_use]
2060 pub fn to_file(&self) -> KeyMapFile<A> {
2061 KeyMapFile {
2062 chord_timeout_ms: self.config.chord_timeout.as_millis() as u64,
2063 bindings: self
2064 .bindings
2065 .iter()
2066 .map(|binding| BindingFile {
2067 chord: binding.chord.to_string(),
2068 action: binding.action.clone(),
2069 priority: binding.priority,
2070 context: binding
2071 .context
2072 .and_then(|id| self.context_name(id))
2073 .map(str::to_string),
2074 label: binding.label.clone(),
2075 })
2076 .collect(),
2077 }
2078 }
2079
2080 pub fn from_file(file: KeyMapFile<A>) -> Result<Self, KeyMapFileError> {
2083 let config = KeyMapConfig::default()
2084 .with_chord_timeout(Duration::from_millis(file.chord_timeout_ms));
2085 let mut map = Self::with_config(config);
2086 for (index, entry) in file.bindings.into_iter().enumerate() {
2087 let chord = Chord::parse(&entry.chord).map_err(|source| KeyMapFileError::Chord {
2088 index,
2089 chord: entry.chord.clone(),
2090 source,
2091 })?;
2092 let context = entry.context.as_deref().map(|name| map.context(name));
2093 let id = map.bind_in(chord, entry.action, entry.priority, context);
2094 if let Some(label) = entry.label {
2095 map.set_label(id, label);
2096 }
2097 }
2098 Ok(map)
2099 }
2100}
2101
2102#[cfg(feature = "serde")]
2103impl<A: Clone + serde::Serialize> serde::Serialize for KeyMap<A> {
2104 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2105 self.to_file().serialize(serializer)
2106 }
2107}
2108
2109#[cfg(feature = "serde")]
2110impl<'de, A: Clone + serde::Deserialize<'de>> serde::Deserialize<'de> for KeyMap<A> {
2111 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2112 let file = KeyMapFile::<A>::deserialize(deserializer)?;
2113 Self::from_file(file).map_err(serde::de::Error::custom)
2114 }
2115}
2116
2117#[cfg(test)]
2118mod keymap_tests {
2119 use super::*;
2120 use proptest::prelude::*;
2121
2122 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
2123 enum Act {
2124 GoTop,
2125 Help,
2126 Save,
2127 Quit,
2128 Submit,
2129 Newline,
2130 Global,
2131 Mode,
2132 Widget,
2133 Down,
2134 }
2135
2136 fn press(c: char) -> KeyEvent {
2137 KeyEvent::new(KeyCode::Char(c))
2138 }
2139
2140 fn kind(mut event: KeyEvent, kind: KeyEventKind) -> KeyEvent {
2141 event.kind = kind;
2142 event
2143 }
2144
2145 fn ms(n: u64) -> Duration {
2146 Duration::from_millis(n)
2147 }
2148
2149 fn chord(s: &str) -> Chord {
2150 Chord::parse(s).unwrap_or_else(|e| panic!("{s}: {e}"))
2151 }
2152
2153 fn actions<A: Clone>(dispatches: &[Dispatch<A>]) -> Vec<A> {
2154 dispatches
2155 .iter()
2156 .filter_map(|d| match d {
2157 Dispatch::Action { action, .. } => Some(action.clone()),
2158 _ => None,
2159 })
2160 .collect()
2161 }
2162
2163 #[test]
2164 fn combo_parse_display_and_normalization() {
2165 let ctrl_x: KeyCombo = "Ctrl+x".parse().unwrap();
2166 assert_eq!(ctrl_x, KeyCombo::new(KeyCode::Char('x'), Modifiers::CTRL));
2167 assert_eq!(ctrl_x.to_string(), "Ctrl+x");
2168
2169 let shift_a: KeyCombo = "shift+a".parse().unwrap();
2171 assert_eq!(shift_a, "A".parse().unwrap());
2172 assert_eq!(shift_a, KeyCombo::new(KeyCode::Char('A'), Modifiers::SHIFT));
2173 assert_eq!(shift_a.to_string(), "A");
2174
2175 assert_eq!("F12".parse::<KeyCombo>().unwrap().code, KeyCode::F(12));
2176 assert_eq!(
2177 "Space".parse::<KeyCombo>().unwrap().code,
2178 KeyCode::Char(' ')
2179 );
2180 assert_eq!(
2181 "Ctrl+Alt+Delete".parse::<KeyCombo>().unwrap().to_string(),
2182 "Ctrl+Alt+Delete"
2183 );
2184 assert_eq!(
2185 "Shift+Tab".parse::<KeyCombo>().unwrap().to_string(),
2186 "Shift+Tab"
2187 );
2188 assert_eq!("+".parse::<KeyCombo>().unwrap().code, KeyCode::Char('+'));
2190 let ctrl_plus: KeyCombo = "Ctrl++".parse().unwrap();
2191 assert_eq!(
2192 ctrl_plus,
2193 KeyCombo::new(KeyCode::Char('+'), Modifiers::CTRL)
2194 );
2195
2196 assert_eq!(
2197 "Hyper+x".parse::<KeyCombo>(),
2198 Err(KeyParseError::UnknownModifier("Hyper".into()))
2199 );
2200 assert_eq!(
2201 "Banana".parse::<KeyCombo>(),
2202 Err(KeyParseError::UnknownKey("Banana".into()))
2203 );
2204 assert_eq!("".parse::<KeyCombo>(), Err(KeyParseError::EmptyKey));
2205 assert_eq!(
2206 "F0".parse::<KeyCombo>(),
2207 Err(KeyParseError::UnknownKey("F0".into()))
2208 );
2209 }
2210
2211 #[test]
2212 fn chord_parse_prefix_and_limits() {
2213 let gg = chord("g g");
2214 let g = chord("g");
2215 assert_eq!(gg.len(), 2);
2216 assert_eq!(gg.to_string(), "g g");
2217 assert!(g.is_prefix_of(&gg));
2218 assert!(!gg.is_prefix_of(&g));
2219 assert!(!g.is_prefix_of(&g), "a chord is not its own prefix");
2220 assert_eq!(chord("Ctrl+x Ctrl+s").to_string(), "Ctrl+x Ctrl+s");
2221 assert_eq!(Chord::parse(""), Err(KeyParseError::EmptyKey));
2222 assert_eq!(
2223 Chord::parse("a b c d e"),
2224 Err(KeyParseError::TooManyKeys(5))
2225 );
2226 }
2227
2228 #[test]
2229 fn chord_completes_within_timeout() {
2230 let mut map = KeyMap::new();
2231 map.bind(chord("g g"), Act::GoTop);
2232 map.bind(chord("x"), Act::Save);
2233 let mut dispatcher = KeyDispatcher::new(map);
2234 let t0 = Instant::now();
2235
2236 let first = dispatcher.feed(&press('g'), t0);
2237 assert_eq!(first, vec![Dispatch::Pending { prefix: chord("g") }]);
2238 assert_eq!(dispatcher.pending_prefix(), Some(chord("g")));
2239 assert!(
2240 dispatcher.tick(t0 + ms(300)).is_empty(),
2241 "still inside the timeout"
2242 );
2243
2244 let second = dispatcher.feed(&press('g'), t0 + ms(300));
2245 assert_eq!(actions(&second), vec![Act::GoTop]);
2246 assert_eq!(dispatcher.pending_prefix(), None);
2247 assert_eq!(dispatcher.stats().dispatched, 1);
2248 assert_eq!(dispatcher.stats().pending, 1);
2249 }
2250
2251 #[test]
2252 fn chord_expires_after_timeout() {
2253 let mut map = KeyMap::new();
2255 map.bind(chord("g g"), Act::GoTop);
2256 map.bind(chord("g"), Act::Help);
2257 let mut dispatcher = KeyDispatcher::new(map);
2258 let t0 = Instant::now();
2259 assert_eq!(
2260 dispatcher.feed(&press('g'), t0),
2261 vec![Dispatch::Pending { prefix: chord("g") }]
2262 );
2263 assert!(dispatcher.tick(t0 + ms(999)).is_empty());
2264 let expired = dispatcher.tick(t0 + ms(1000));
2265 assert_eq!(expired[0], Dispatch::Expired { prefix: chord("g") });
2266 assert_eq!(actions(&expired), vec![Act::Help]);
2267 assert_eq!(dispatcher.stats().expired, 1);
2268
2269 let mut map = KeyMap::new();
2271 map.bind(chord("g g"), Act::GoTop);
2272 let mut dispatcher = KeyDispatcher::new(map);
2273 dispatcher.feed(&press('g'), t0);
2274 assert_eq!(
2275 dispatcher.tick(t0 + ms(5000)),
2276 vec![Dispatch::Expired { prefix: chord("g") }]
2277 );
2278 assert_eq!(dispatcher.pending_prefix(), None);
2279 }
2280
2281 #[test]
2282 fn single_key_fires_while_chord_pending() {
2283 let mut map = KeyMap::new();
2284 map.bind(chord("g g"), Act::GoTop);
2285 map.bind(chord("x"), Act::Save);
2286 let mut dispatcher = KeyDispatcher::new(map);
2287 let t0 = Instant::now();
2288 dispatcher.feed(&press('g'), t0);
2289 let out = dispatcher.feed(&press('x'), t0 + ms(10));
2290 assert_eq!(out[0], Dispatch::Expired { prefix: chord("g") });
2291 assert_eq!(
2292 actions(&out),
2293 vec![Act::Save],
2294 "x is never blocked by the pending g"
2295 );
2296
2297 let mut map = KeyMap::new();
2299 map.bind(chord("g g"), Act::GoTop);
2300 map.bind(chord("g"), Act::Help);
2301 map.bind(chord("x"), Act::Save);
2302 let mut dispatcher = KeyDispatcher::new(map);
2303 dispatcher.feed(&press('g'), t0);
2304 let out = dispatcher.feed(&press('x'), t0 + ms(10));
2305 assert_eq!(actions(&out), vec![Act::Help, Act::Save]);
2306
2307 let mut map = KeyMap::new();
2309 map.bind(chord("g g"), Act::GoTop);
2310 map.bind(chord("z z"), Act::Quit);
2311 let mut dispatcher = KeyDispatcher::new(map);
2312 dispatcher.feed(&press('g'), t0);
2313 let out = dispatcher.feed(&press('z'), t0 + ms(10));
2314 assert_eq!(
2315 out,
2316 vec![
2317 Dispatch::Expired { prefix: chord("g") },
2318 Dispatch::Pending { prefix: chord("z") }
2319 ]
2320 );
2321 }
2322
2323 #[test]
2324 fn prefix_with_own_binding_fires_on_flush() {
2325 let mut map = KeyMap::new();
2330 let one = map.bind(chord("g"), Act::Help);
2331 map.bind(chord("g g"), Act::GoTop);
2332 let mut dispatcher = KeyDispatcher::new(map);
2333 let t0 = Instant::now();
2334
2335 assert_eq!(
2336 dispatcher.feed(&press('g'), t0),
2337 vec![Dispatch::Pending { prefix: chord("g") }]
2338 );
2339 let out = dispatcher.feed(&press('x'), t0 + ms(10));
2340 assert_eq!(
2341 out,
2342 vec![
2343 Dispatch::Action {
2344 action: Act::Help,
2345 binding: one,
2346 chord: chord("g"),
2347 },
2348 Dispatch::Unbound(press('x')),
2349 ]
2350 );
2351 assert_eq!(dispatcher.pending_prefix(), None);
2352 assert_eq!(
2353 dispatcher.stats().expired,
2354 0,
2355 "a bound prefix flushed by a non-extending key does not expire"
2356 );
2357 assert_eq!(dispatcher.stats().dispatched, 1);
2358 }
2359
2360 #[test]
2361 fn widget_beats_mode_beats_global() {
2362 let mut map = KeyMap::new();
2363 let g = map.bind_in(chord("s"), Act::Global, Priority::Global, None);
2364 let m = map.bind_in(chord("s"), Act::Mode, Priority::Mode, None);
2365 let w = map.bind_in(chord("s"), Act::Widget, Priority::Widget, None);
2366 let lookup = map.lookup(&chord("s"), &[]);
2367 assert_eq!(lookup.exact.map(|b| b.id), Some(w));
2368 assert_eq!(lookup.longer, 0);
2369
2370 let mut dispatcher = KeyDispatcher::new(map);
2371 assert_eq!(
2372 actions(&dispatcher.feed(&press('s'), Instant::now())),
2373 vec![Act::Widget]
2374 );
2375
2376 let report = dispatcher.map().conflicts();
2377 assert_eq!(report.len(), 3, "{report}");
2378 assert!(report.items.contains(&Conflict::Shadowed {
2379 winner: w,
2380 loser: g,
2381 chord: chord("s")
2382 }));
2383 assert!(report.items.contains(&Conflict::Shadowed {
2384 winner: m,
2385 loser: g,
2386 chord: chord("s")
2387 }));
2388 assert!(report.items.contains(&Conflict::Shadowed {
2389 winner: w,
2390 loser: m,
2391 chord: chord("s")
2392 }));
2393 assert_eq!(report.to_string().lines().count(), 3);
2394
2395 dispatcher.map_mut().unbind(w);
2397 assert_eq!(
2398 actions(&dispatcher.feed(&press('s'), Instant::now())),
2399 vec![Act::Mode]
2400 );
2401 }
2402
2403 #[test]
2404 fn active_context_beats_contextless_even_at_lower_priority() {
2405 let mut map = KeyMap::new();
2406 let text_input = map.context("text_input");
2407 assert_eq!(map.context("text_input"), text_input, "interned once");
2408 assert_eq!(map.context_name(text_input), Some("text_input"));
2409 map.bind_in(chord("Enter"), Act::Submit, Priority::Widget, None);
2410 map.bind_in(
2411 chord("Enter"),
2412 Act::Newline,
2413 Priority::Global,
2414 Some(text_input),
2415 );
2416 assert!(
2417 map.conflicts().is_empty(),
2418 "a context override is not a conflict"
2419 );
2420
2421 let mut dispatcher = KeyDispatcher::new(map);
2422 let enter = KeyEvent::new(KeyCode::Enter);
2423 let t0 = Instant::now();
2424 assert_eq!(actions(&dispatcher.feed(&enter, t0)), vec![Act::Submit]);
2425 dispatcher.set_active_contexts(&[text_input]);
2426 assert_eq!(actions(&dispatcher.feed(&enter, t0)), vec![Act::Newline]);
2427 dispatcher.set_active_contexts(&[]);
2428 assert_eq!(actions(&dispatcher.feed(&enter, t0)), vec![Act::Submit]);
2429 }
2430
2431 #[test]
2432 fn conflicts_reports_shadowed_prefix_and_duplicate() {
2433 let mut map = KeyMap::new();
2434 let long = map.bind(chord("g g"), Act::GoTop);
2435 let short = map.bind(chord("g"), Act::Help);
2436 let q1 = map.bind(chord("q"), Act::Quit);
2437 let q2 = map.bind(chord("q"), Act::Quit);
2438 map.set_label(q2, "quit");
2439 assert_eq!(map.get(q2).and_then(|b| b.label.as_deref()), Some("quit"));
2440
2441 let report = map.conflicts();
2442 assert_eq!(report.len(), 2, "{report}");
2443 assert_eq!(
2444 report.items[0],
2445 Conflict::PrefixCollision {
2446 short,
2447 long,
2448 short_chord: chord("g"),
2449 long_chord: chord("g g"),
2450 }
2451 );
2452 assert_eq!(
2453 report.items[1],
2454 Conflict::Duplicate {
2455 first: q1,
2456 second: q2,
2457 chord: chord("q")
2458 }
2459 );
2460 let text = report.to_string();
2461 assert_eq!(text.lines().count(), 2);
2462 assert!(
2463 text.contains("warning: binding #1 (`g`) is a prefix of binding #0 (`g g`)"),
2464 "{text}"
2465 );
2466 assert!(text.contains("the later one wins"), "{text}");
2467
2468 assert_eq!(map.lookup(&chord("q"), &[]).exact.map(|b| b.id), Some(q2));
2470 }
2471
2472 #[test]
2473 fn repeat_refires_single_key_binding_but_never_extends_a_chord() {
2474 let mut map = KeyMap::new();
2475 map.bind(chord("j"), Act::Down);
2476 map.bind(chord("g g"), Act::GoTop);
2477 let mut dispatcher = KeyDispatcher::new(map);
2478 let t0 = Instant::now();
2479
2480 let held = kind(press('j'), KeyEventKind::Repeat);
2481 assert_eq!(actions(&dispatcher.feed(&held, t0)), vec![Act::Down]);
2482
2483 dispatcher.feed(&press('g'), t0);
2484 let repeat_g = kind(press('g'), KeyEventKind::Repeat);
2485 assert_eq!(
2486 dispatcher.feed(&repeat_g, t0 + ms(10)),
2487 vec![Dispatch::Unbound(repeat_g)]
2488 );
2489 assert_eq!(
2490 dispatcher.pending_prefix(),
2491 Some(chord("g")),
2492 "repeat left the prefix alone"
2493 );
2494
2495 let released = kind(press('j'), KeyEventKind::Release);
2496 assert_eq!(
2497 dispatcher.feed(&released, t0 + ms(20)),
2498 vec![Dispatch::Unbound(released)]
2499 );
2500 }
2501
2502 #[test]
2503 fn esc_goes_through_the_sequence_detector() {
2504 let mut map = KeyMap::new();
2505 map.bind(chord("Esc"), Act::Quit);
2506 map.bind(chord("Esc Esc"), Act::Help);
2507 map.bind(chord("g g"), Act::GoTop);
2508 let mut dispatcher = KeyDispatcher::new(map);
2509 let esc = KeyEvent::new(KeyCode::Escape);
2510 let t0 = Instant::now();
2511
2512 assert_eq!(
2514 dispatcher.feed(&esc, t0),
2515 vec![Dispatch::Esc(SequenceOutput::Pending)]
2516 );
2517 assert_eq!(actions(&dispatcher.tick(t0 + ms(300))), vec![Act::Quit]);
2518
2519 let t1 = t0 + ms(1000);
2521 dispatcher.feed(&esc, t1);
2522 assert_eq!(
2523 actions(&dispatcher.feed(&esc, t1 + ms(100))),
2524 vec![Act::Help]
2525 );
2526
2527 let t2 = t0 + ms(3000);
2529 dispatcher.feed(&press('g'), t2);
2530 let out = dispatcher.feed(&esc, t2 + ms(10));
2531 assert_eq!(out[0], Dispatch::Expired { prefix: chord("g") });
2532 assert_eq!(dispatcher.pending_prefix(), None);
2533
2534 let mut plain = KeyDispatcher::new(KeyMap::<Act>::new());
2536 plain.feed(&esc, t0);
2537 assert_eq!(
2538 plain.tick(t0 + ms(300)),
2539 vec![Dispatch::Esc(SequenceOutput::Esc)]
2540 );
2541 }
2542
2543 #[cfg(feature = "serde")]
2547 #[test]
2548 fn keymap_round_trips_through_toml_and_json() {
2549 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2550 enum Action {
2551 Quit,
2552 Save,
2553 Newline,
2554 }
2555
2556 let mut map = KeyMap::with_config(KeyMapConfig::default().with_chord_timeout(ms(750)));
2557 let editor = map.context("editor");
2558 let quit = map.bind(chord("q"), Action::Quit);
2559 map.set_label(quit, "quit");
2560 map.bind_in(chord("Ctrl+x Ctrl+s"), Action::Save, Priority::Mode, None);
2561 map.bind_in(
2562 chord("Enter"),
2563 Action::Newline,
2564 Priority::Widget,
2565 Some(editor),
2566 );
2567
2568 let text = toml::to_string(&map).expect("serialize to TOML");
2569 assert!(text.contains("chord_timeout_ms = 750"), "{text}");
2570 assert!(text.contains("chord = \"Ctrl+x Ctrl+s\""), "{text}");
2571 assert!(text.contains("context = \"editor\""), "{text}");
2572 assert!(text.contains("label = \"quit\""), "{text}");
2573
2574 let back: KeyMap<Action> = toml::from_str(&text).expect("parse TOML");
2575 assert_eq!(back.config().chord_timeout, ms(750));
2576 assert_eq!(back.len(), 3);
2577 assert_eq!(back.bindings()[0].label.as_deref(), Some("quit"));
2578 assert_eq!(back.bindings()[1].priority, Priority::Mode);
2579 assert_eq!(back.bindings()[1].chord, chord("Ctrl+x Ctrl+s"));
2580 let editor_back = back.bindings()[2].context.expect("context restored");
2581 assert_eq!(back.context_name(editor_back), Some("editor"));
2582 assert_eq!(
2583 back.lookup(&chord("Enter"), &[editor_back])
2584 .exact
2585 .map(|b| &b.action),
2586 Some(&Action::Newline)
2587 );
2588 assert!(
2589 back.lookup(&chord("Enter"), &[]).is_none(),
2590 "the context binding stays inactive outside its context"
2591 );
2592
2593 let json = serde_json::to_string(&map).expect("serialize to JSON");
2594 let back_json: KeyMap<Action> = serde_json::from_str(&json).expect("parse JSON");
2595 assert_eq!(back_json.len(), 3);
2596 assert_eq!(back_json.bindings()[2].action, Action::Newline);
2597
2598 let bad =
2599 "chord_timeout_ms = 500\n\n[[bindings]]\nchord = \"Hyper+q\"\naction = \"Quit\"\n";
2600 let err = toml::from_str::<KeyMap<Action>>(bad)
2601 .expect_err("bad chord must fail")
2602 .to_string();
2603 assert!(err.contains("binding 0") && err.contains("Hyper"), "{err}");
2604
2605 let minimal: KeyMap<Action> =
2606 toml::from_str("[[bindings]]\nchord = \"q\"\naction = \"Quit\"\n")
2607 .expect("defaults fill in");
2608 assert_eq!(minimal.config().chord_timeout, ms(DEFAULT_CHORD_TIMEOUT_MS));
2609 assert_eq!(minimal.bindings()[0].priority, Priority::Global);
2610 }
2611
2612 #[cfg(feature = "serde")]
2615 #[test]
2616 fn toml_rejects_unknown_field_and_bad_chord() {
2617 #[derive(Debug, Clone, serde::Deserialize)]
2618 enum Action {
2619 Quit,
2620 }
2621
2622 let unknown_top =
2623 toml::from_str::<KeyMap<Action>>("chord_timeout_ms = 500\ntypo_field = 3\n")
2624 .expect_err("an unknown top-level field must be rejected")
2625 .to_string();
2626 assert!(unknown_top.contains("typo_field"), "{unknown_top}");
2627
2628 let unknown_binding = toml::from_str::<KeyMap<Action>>(
2629 "[[bindings]]\nchord = \"q\"\naction = \"Quit\"\nchrod = \"x\"\n",
2630 )
2631 .expect_err("an unknown binding field must be rejected")
2632 .to_string();
2633 assert!(unknown_binding.contains("chrod"), "{unknown_binding}");
2634
2635 let bad_chord = toml::from_str::<KeyMap<Action>>(
2636 "[[bindings]]\nchord = \"Nope+q\"\naction = \"Quit\"\n",
2637 )
2638 .expect_err("a bad chord must be rejected")
2639 .to_string();
2640 assert!(
2641 bad_chord.contains("binding 0") && bad_chord.contains("Nope"),
2642 "{bad_chord}"
2643 );
2644 }
2645
2646 #[cfg(feature = "serde")]
2650 #[test]
2651 fn toml_example_in_docs_parses() {
2652 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
2653 enum Action {
2654 Save,
2655 Newline,
2656 Top,
2657 }
2658
2659 const EXAMPLE: &str = include_str!("../tests/fixtures/keymap_example.toml");
2660 let map: KeyMap<Action> =
2661 toml::from_str(EXAMPLE).expect("documented keymap example must parse");
2662
2663 assert_eq!(map.config().chord_timeout, ms(750));
2664 assert_eq!(map.len(), 3);
2665
2666 let save = &map.bindings()[0];
2667 assert_eq!(save.action, Action::Save);
2668 assert_eq!(save.chord, chord("Ctrl+x Ctrl+s"));
2669 assert_eq!(save.priority, Priority::Mode);
2670 assert_eq!(save.label.as_deref(), Some("save"));
2671
2672 let newline = &map.bindings()[1];
2673 assert_eq!(newline.action, Action::Newline);
2674 assert_eq!(newline.priority, Priority::Widget);
2675 let editor = newline.context.expect("editor context restored");
2676 assert_eq!(map.context_name(editor), Some("editor"));
2677 assert_eq!(
2678 map.lookup(&chord("Enter"), &[editor])
2679 .exact
2680 .map(|binding| &binding.action),
2681 Some(&Action::Newline)
2682 );
2683 assert!(
2684 map.lookup(&chord("Enter"), &[]).is_none(),
2685 "the editor binding stays inactive outside its context"
2686 );
2687
2688 assert_eq!(map.bindings()[2].chord, chord("g g"));
2689 assert_eq!(map.bindings()[2].action, Action::Top);
2690 let report = map.conflicts();
2691 assert!(report.is_empty(), "{report}");
2692 }
2693
2694 fn arb_code() -> impl Strategy<Value = KeyCode> {
2695 prop_oneof![
2696 prop::sample::select(vec![
2697 'a', 'b', 'q', 'x', 'z', 'A', 'Q', '1', '9', '+', '-', '.', '/', ' ',
2698 ])
2699 .prop_map(KeyCode::Char),
2700 (1u8..=24).prop_map(KeyCode::F),
2701 prop::sample::select(vec![
2702 KeyCode::Enter,
2703 KeyCode::Escape,
2704 KeyCode::Backspace,
2705 KeyCode::Tab,
2706 KeyCode::BackTab,
2707 KeyCode::Delete,
2708 KeyCode::Insert,
2709 KeyCode::Home,
2710 KeyCode::End,
2711 KeyCode::PageUp,
2712 KeyCode::PageDown,
2713 KeyCode::Up,
2714 KeyCode::Down,
2715 KeyCode::Left,
2716 KeyCode::Right,
2717 KeyCode::Null,
2718 KeyCode::MediaPlayPause,
2719 KeyCode::MediaStop,
2720 KeyCode::MediaNextTrack,
2721 KeyCode::MediaPrevTrack,
2722 ]),
2723 ]
2724 }
2725
2726 fn arb_mods() -> impl Strategy<Value = Modifiers> {
2727 (0u8..16).prop_map(|bits| {
2728 let mut modifiers = Modifiers::NONE;
2729 if bits & 0b0001 != 0 {
2730 modifiers |= Modifiers::CTRL;
2731 }
2732 if bits & 0b0010 != 0 {
2733 modifiers |= Modifiers::ALT;
2734 }
2735 if bits & 0b0100 != 0 {
2736 modifiers |= Modifiers::SHIFT;
2737 }
2738 if bits & 0b1000 != 0 {
2739 modifiers |= Modifiers::SUPER;
2740 }
2741 modifiers
2742 })
2743 }
2744
2745 fn arb_small_chord() -> impl Strategy<Value = Chord> {
2746 prop::collection::vec(
2747 prop::sample::select(vec!['a', 'b', 'c', 'd'])
2748 .prop_map(|c| KeyCombo::key(KeyCode::Char(c))),
2749 1..=3usize,
2750 )
2751 .prop_map(|combos| Chord::new(combos).expect("1..=3 combos is a valid chord"))
2752 }
2753
2754 fn arb_key() -> impl Strategy<Value = KeyEvent> {
2755 let code = prop_oneof![
2756 Just(KeyCode::Char('a')),
2757 Just(KeyCode::Char('b')),
2758 Just(KeyCode::Char('c')),
2759 Just(KeyCode::Enter),
2760 Just(KeyCode::Escape),
2761 ];
2762 let kind = prop_oneof![
2763 Just(KeyEventKind::Press),
2764 Just(KeyEventKind::Repeat),
2765 Just(KeyEventKind::Release),
2766 ];
2767 (code, kind).prop_map(|(code, kind)| KeyEvent {
2768 code,
2769 modifiers: Modifiers::NONE,
2770 kind,
2771 })
2772 }
2773
2774 proptest! {
2775 #![proptest_config(ProptestConfig::with_cases(1000))]
2776
2777 #[test]
2780 fn every_fed_key_yields_a_dispatch(
2781 keys in proptest::collection::vec(arb_key(), 1..20),
2782 gaps in proptest::collection::vec(0u64..1500, 1..20),
2783 ) {
2784 let mut map = KeyMap::new();
2785 map.bind(chord("a b"), Act::GoTop);
2786 map.bind(chord("a"), Act::Help);
2787 map.bind(chord("c c c"), Act::Save);
2788 map.bind(chord("Enter"), Act::Submit);
2789 let mut dispatcher = KeyDispatcher::new(map);
2790 let mut now = Instant::now();
2791 for (key, gap) in keys.iter().zip(gaps.iter().cycle()) {
2792 now += ms(*gap);
2793 let out = dispatcher.feed(key, now);
2794 prop_assert!(!out.is_empty(), "{key:?} produced nothing");
2795 let _ = dispatcher.tick(now);
2796 }
2797 let _ = dispatcher.tick(now + ms(10_000));
2798 prop_assert_eq!(dispatcher.pending_prefix(), None);
2799 let stats = dispatcher.stats();
2800 prop_assert!(
2801 stats.dispatched + stats.pending + stats.expired + stats.unbound + stats.esc > 0
2802 );
2803 }
2804
2805 #[test]
2808 fn combo_display_parse_round_trip(code in arb_code(), mods in arb_mods()) {
2809 let combo = KeyCombo::new(code, mods);
2810 let text = combo.to_string();
2811 let parsed: KeyCombo = text
2812 .parse()
2813 .unwrap_or_else(|e| panic!("`{text}` did not re-parse: {e}"));
2814 prop_assert_eq!(parsed, combo, "text = `{}`", text);
2815 }
2816
2817 #[test]
2820 fn lookup_is_deterministic_under_shuffle(
2821 raw in prop::collection::vec((arb_small_chord(), any::<u64>()), 1..12),
2822 queries in prop::collection::vec(arb_small_chord(), 1..8),
2823 ) {
2824 let mut seen = std::collections::BTreeSet::new();
2827 let mut entries: Vec<(Chord, u64)> = Vec::new();
2828 for (ch, key) in raw {
2829 if seen.insert(ch.to_string()) {
2830 entries.push((ch, key));
2831 }
2832 }
2833 let build = |order: &[(Chord, u64)]| {
2834 let mut map = KeyMap::new();
2835 for (ch, _) in order {
2836 map.bind(ch.clone(), ch.to_string());
2837 }
2838 map
2839 };
2840 let in_order = build(&entries);
2841 let mut shuffled = entries.clone();
2842 shuffled.sort_by_key(|(_, key)| *key);
2843 let reordered = build(&shuffled);
2844 for query in &queries {
2845 let a = in_order.lookup(query, &[]);
2846 let b = reordered.lookup(query, &[]);
2847 prop_assert_eq!(
2848 a.exact.map(|binding| binding.action.clone()),
2849 b.exact.map(|binding| binding.action.clone()),
2850 "winner changed for `{}`",
2851 query
2852 );
2853 prop_assert_eq!(a.longer, b.longer, "longer count changed for `{}`", query);
2854 }
2855 }
2856 }
2857}
2858
2859#[cfg(test)]
2860mod tests {
2861 use super::*;
2862
2863 fn now() -> Instant {
2864 Instant::now()
2865 }
2866
2867 fn esc_press() -> KeyEvent {
2868 KeyEvent::new(KeyCode::Escape)
2869 }
2870
2871 fn key_press(code: KeyCode) -> KeyEvent {
2872 KeyEvent::new(code)
2873 }
2874
2875 fn esc_release() -> KeyEvent {
2876 KeyEvent::new(KeyCode::Escape).with_kind(KeyEventKind::Release)
2877 }
2878
2879 const MS_50: Duration = Duration::from_millis(50);
2880 const MS_100: Duration = Duration::from_millis(100);
2881 const MS_200: Duration = Duration::from_millis(200);
2882 const MS_300: Duration = Duration::from_millis(300);
2883
2884 #[test]
2887 fn single_esc_returns_pending() {
2888 let mut detector = SequenceDetector::with_defaults();
2889 let t = now();
2890
2891 let output = detector.feed(&esc_press(), t);
2892 assert_eq!(output, SequenceOutput::Pending);
2893 assert!(detector.is_pending());
2894 }
2895
2896 #[test]
2897 fn esc_esc_within_timeout() {
2898 let mut detector = SequenceDetector::with_defaults();
2899 let t = now();
2900
2901 detector.feed(&esc_press(), t);
2902 let output = detector.feed(&esc_press(), t + MS_100);
2903
2904 assert_eq!(output, SequenceOutput::EscEsc);
2905 assert!(!detector.is_pending());
2906 }
2907
2908 #[test]
2909 fn esc_esc_at_timeout_boundary() {
2910 let mut detector = SequenceDetector::with_defaults();
2911 let t = now();
2912
2913 detector.feed(&esc_press(), t);
2914 let output = detector.feed(&esc_press(), t + Duration::from_millis(250));
2916
2917 assert_eq!(output, SequenceOutput::EscEsc);
2918 }
2919
2920 #[test]
2921 fn esc_esc_past_timeout() {
2922 let mut detector = SequenceDetector::with_defaults();
2923 let t = now();
2924
2925 detector.feed(&esc_press(), t);
2926 let output = detector.feed(&esc_press(), t + Duration::from_millis(251));
2928
2929 assert_eq!(output, SequenceOutput::Esc);
2931 assert!(detector.is_pending()); }
2933
2934 #[test]
2935 fn timeout_check_emits_pending_esc() {
2936 let mut detector = SequenceDetector::with_defaults();
2937 let t = now();
2938
2939 detector.feed(&esc_press(), t);
2940
2941 assert!(detector.check_timeout(t + MS_200).is_none());
2943 assert!(detector.is_pending());
2944
2945 let output = detector.check_timeout(t + Duration::from_millis(251));
2947 assert_eq!(output, Some(SequenceOutput::Esc));
2948 assert!(!detector.is_pending());
2949 }
2950
2951 #[test]
2952 fn other_key_interrupts_sequence() {
2953 let mut detector = SequenceDetector::with_defaults();
2954 let t = now();
2955
2956 detector.feed(&esc_press(), t);
2957 let output = detector.feed(&key_press(KeyCode::Char('a')), t + MS_100);
2958
2959 assert_eq!(output, SequenceOutput::Esc);
2961 assert!(!detector.is_pending());
2962 }
2963
2964 #[test]
2965 fn non_esc_key_passes_through() {
2966 let mut detector = SequenceDetector::with_defaults();
2967 let t = now();
2968
2969 let output = detector.feed(&key_press(KeyCode::Char('x')), t);
2970 assert_eq!(output, SequenceOutput::PassThrough);
2971 }
2972
2973 #[test]
2974 fn release_event_passes_through() {
2975 let mut detector = SequenceDetector::with_defaults();
2976 let t = now();
2977
2978 let output = detector.feed(&esc_release(), t);
2979 assert_eq!(output, SequenceOutput::PassThrough);
2980 assert!(!detector.is_pending());
2981 }
2982
2983 #[test]
2984 fn release_during_pending_passes_through() {
2985 let mut detector = SequenceDetector::with_defaults();
2986 let t = now();
2987
2988 detector.feed(&esc_press(), t);
2989 let output = detector.feed(&esc_release(), t + MS_50);
2990
2991 assert_eq!(output, SequenceOutput::PassThrough);
2993 assert!(detector.is_pending());
2994 }
2995
2996 #[test]
2999 fn custom_timeout() {
3000 let config = SequenceConfig::default().with_timeout(Duration::from_millis(100));
3001 let mut detector = SequenceDetector::new(config);
3002 let t = now();
3003
3004 detector.feed(&esc_press(), t);
3005 let output = detector.feed(&esc_press(), t + Duration::from_millis(150));
3007
3008 assert_eq!(output, SequenceOutput::Esc);
3009 }
3010
3011 #[test]
3012 fn disabled_sequences() {
3013 let config = SequenceConfig::default().disable_sequences();
3014 let mut detector = SequenceDetector::new(config);
3015 let t = now();
3016
3017 let output = detector.feed(&esc_press(), t);
3019 assert_eq!(output, SequenceOutput::Esc);
3020 assert!(!detector.is_pending());
3021
3022 let output = detector.feed(&esc_press(), t + MS_50);
3024 assert_eq!(output, SequenceOutput::Esc);
3025 }
3026
3027 #[test]
3028 fn disabled_sequences_passthrough() {
3029 let config = SequenceConfig::default().disable_sequences();
3030 let mut detector = SequenceDetector::new(config);
3031 let t = now();
3032
3033 let output = detector.feed(&key_press(KeyCode::Char('a')), t);
3034 assert_eq!(output, SequenceOutput::PassThrough);
3035 }
3036
3037 #[test]
3038 fn config_default_values() {
3039 let config = SequenceConfig::default();
3040 assert_eq!(config.esc_seq_timeout, Duration::from_millis(250));
3041 assert_eq!(config.esc_debounce, Duration::from_millis(50));
3042 assert!(!config.disable_sequences);
3043 }
3044
3045 #[test]
3046 fn config_builder_chain() {
3047 let config = SequenceConfig::default()
3048 .with_timeout(Duration::from_millis(300))
3049 .with_debounce(Duration::from_millis(100))
3050 .disable_sequences();
3051
3052 assert_eq!(config.esc_seq_timeout, Duration::from_millis(300));
3053 assert_eq!(config.esc_debounce, Duration::from_millis(100));
3054 assert!(config.disable_sequences);
3055 }
3056
3057 #[test]
3060 fn reset_clears_pending() {
3061 let mut detector = SequenceDetector::with_defaults();
3062 let t = now();
3063
3064 detector.feed(&esc_press(), t);
3065 assert!(detector.is_pending());
3066
3067 detector.reset();
3068 assert!(!detector.is_pending());
3069
3070 let output = detector.feed(&esc_press(), t + MS_100);
3072 assert_eq!(output, SequenceOutput::Pending);
3073 }
3074
3075 #[test]
3076 fn reset_discards_pending_esc() {
3077 let mut detector = SequenceDetector::with_defaults();
3078 let t = now();
3079
3080 detector.feed(&esc_press(), t);
3081 detector.reset();
3082
3083 assert!(detector.check_timeout(t + MS_300).is_none());
3085 }
3086
3087 #[test]
3090 fn rapid_triple_esc() {
3091 let mut detector = SequenceDetector::with_defaults();
3092 let t = now();
3093
3094 let out1 = detector.feed(&esc_press(), t);
3096 assert_eq!(out1, SequenceOutput::Pending);
3097
3098 let out2 = detector.feed(&esc_press(), t + MS_50);
3100 assert_eq!(out2, SequenceOutput::EscEsc);
3101
3102 let out3 = detector.feed(&esc_press(), t + MS_100);
3104 assert_eq!(out3, SequenceOutput::Pending);
3105 }
3106
3107 #[test]
3108 fn alternating_esc_and_key() {
3109 let mut detector = SequenceDetector::with_defaults();
3110 let t = now();
3111
3112 detector.feed(&esc_press(), t);
3114
3115 let out1 = detector.feed(&key_press(KeyCode::Char('a')), t + MS_50);
3117 assert_eq!(out1, SequenceOutput::Esc);
3118
3119 let out2 = detector.feed(&esc_press(), t + MS_100);
3121 assert_eq!(out2, SequenceOutput::Pending);
3122
3123 let out3 = detector.feed(&key_press(KeyCode::Char('b')), t + MS_200);
3125 assert_eq!(out3, SequenceOutput::Esc);
3126 }
3127
3128 #[test]
3129 fn enter_key_interrupts() {
3130 let mut detector = SequenceDetector::with_defaults();
3131 let t = now();
3132
3133 detector.feed(&esc_press(), t);
3134 let output = detector.feed(&key_press(KeyCode::Enter), t + MS_100);
3135
3136 assert_eq!(output, SequenceOutput::Esc);
3137 }
3138
3139 #[test]
3140 fn function_key_interrupts() {
3141 let mut detector = SequenceDetector::with_defaults();
3142 let t = now();
3143
3144 detector.feed(&esc_press(), t);
3145 let output = detector.feed(&key_press(KeyCode::F(1)), t + MS_100);
3146
3147 assert_eq!(output, SequenceOutput::Esc);
3148 }
3149
3150 #[test]
3151 fn arrow_key_interrupts() {
3152 let mut detector = SequenceDetector::with_defaults();
3153 let t = now();
3154
3155 detector.feed(&esc_press(), t);
3156 let output = detector.feed(&key_press(KeyCode::Up), t + MS_100);
3157
3158 assert_eq!(output, SequenceOutput::Esc);
3159 }
3160
3161 #[test]
3162 fn config_getter_and_setter() {
3163 let mut detector = SequenceDetector::with_defaults();
3164 assert_eq!(
3165 detector.config().esc_seq_timeout,
3166 Duration::from_millis(250)
3167 );
3168
3169 let new_config = SequenceConfig::default().with_timeout(Duration::from_millis(500));
3170 detector.set_config(new_config);
3171
3172 assert_eq!(
3173 detector.config().esc_seq_timeout,
3174 Duration::from_millis(500)
3175 );
3176 }
3177
3178 #[test]
3179 fn set_config_preserves_pending_state() {
3180 let mut detector = SequenceDetector::with_defaults();
3181 let t = now();
3182
3183 detector.feed(&esc_press(), t);
3184 assert!(detector.is_pending());
3185
3186 detector.set_config(SequenceConfig::default().with_timeout(Duration::from_millis(500)));
3188
3189 assert!(detector.is_pending());
3191
3192 let output = detector.feed(&esc_press(), t + MS_300);
3194 assert_eq!(output, SequenceOutput::EscEsc); }
3196
3197 #[test]
3198 fn debug_format() {
3199 let detector = SequenceDetector::with_defaults();
3200 let dbg = format!("{:?}", detector);
3201 assert!(dbg.contains("SequenceDetector"));
3202 }
3203
3204 #[test]
3205 fn config_debug_format() {
3206 let config = SequenceConfig::default();
3207 let dbg = format!("{:?}", config);
3208 assert!(dbg.contains("SequenceConfig"));
3209 }
3210
3211 #[test]
3212 fn output_debug_and_eq() {
3213 assert_eq!(SequenceOutput::Pending, SequenceOutput::Pending);
3214 assert_eq!(SequenceOutput::Esc, SequenceOutput::Esc);
3215 assert_eq!(SequenceOutput::EscEsc, SequenceOutput::EscEsc);
3216 assert_eq!(SequenceOutput::PassThrough, SequenceOutput::PassThrough);
3217 assert_ne!(SequenceOutput::Esc, SequenceOutput::EscEsc);
3218
3219 let dbg = format!("{:?}", SequenceOutput::EscEsc);
3220 assert!(dbg.contains("EscEsc"));
3221 }
3222
3223 #[test]
3226 fn no_stuck_state() {
3227 let mut detector = SequenceDetector::with_defaults();
3228 let t = now();
3229
3230 for i in 0..100 {
3232 let offset = Duration::from_millis(i * 10);
3233 if i % 3 == 0 {
3234 detector.feed(&esc_press(), t + offset);
3235 } else {
3236 detector.feed(&key_press(KeyCode::Char('x')), t + offset);
3237 }
3238 }
3239
3240 detector.check_timeout(t + Duration::from_secs(2));
3242
3243 assert!(!detector.is_pending());
3245 }
3246
3247 #[test]
3248 fn deterministic_output() {
3249 let config = SequenceConfig::default();
3251 let t = now();
3252
3253 let mut d1 = SequenceDetector::new(config.clone());
3254 let mut d2 = SequenceDetector::new(config);
3255
3256 let events = [
3257 (esc_press(), t),
3258 (esc_press(), t + MS_100),
3259 (key_press(KeyCode::Char('a')), t + MS_200),
3260 (esc_press(), t + MS_300),
3261 ];
3262
3263 for (event, time) in &events {
3264 let out1 = d1.feed(event, *time);
3265 let out2 = d2.feed(event, *time);
3266 assert_eq!(out1, out2);
3267 }
3268 }
3269
3270 mod action_mapper_tests {
3275 use super::*;
3276 use crate::event::Modifiers;
3277
3278 fn ctrl_c() -> KeyEvent {
3279 KeyEvent::new(KeyCode::Char('c')).with_modifiers(Modifiers::CTRL)
3280 }
3281
3282 fn ctrl_d() -> KeyEvent {
3283 KeyEvent::new(KeyCode::Char('d')).with_modifiers(Modifiers::CTRL)
3284 }
3285
3286 fn ctrl_q() -> KeyEvent {
3287 KeyEvent::new(KeyCode::Char('q')).with_modifiers(Modifiers::CTRL)
3288 }
3289
3290 fn idle_state() -> AppState {
3291 AppState::default()
3292 }
3293
3294 fn input_state() -> AppState {
3295 AppState::new().with_input(true)
3296 }
3297
3298 fn task_state() -> AppState {
3299 AppState::new().with_task(true)
3300 }
3301
3302 fn modal_state() -> AppState {
3303 AppState::new().with_modal(true)
3304 }
3305
3306 fn overlay_state() -> AppState {
3307 AppState::new().with_overlay(true)
3308 }
3309
3310 #[test]
3313 fn test_ctrl_c_clears_nonempty_input() {
3314 let mut mapper = ActionMapper::with_defaults();
3315 let t = now();
3316
3317 let action = mapper.map(&ctrl_c(), &input_state(), t);
3318 assert_eq!(action, Some(Action::ClearInput));
3319 }
3320
3321 #[test]
3322 fn test_ctrl_c_cancels_running_task() {
3323 let mut mapper = ActionMapper::with_defaults();
3324 let t = now();
3325
3326 let action = mapper.map(&ctrl_c(), &task_state(), t);
3327 assert_eq!(action, Some(Action::CancelTask));
3328 }
3329
3330 #[test]
3331 fn test_ctrl_c_quits_when_idle() {
3332 let mut mapper = ActionMapper::with_defaults();
3333 let t = now();
3334
3335 let action = mapper.map(&ctrl_c(), &idle_state(), t);
3336 assert_eq!(action, Some(Action::Quit));
3337 }
3338
3339 #[test]
3340 fn test_ctrl_c_dismisses_modal() {
3341 let mut mapper = ActionMapper::with_defaults();
3342 let t = now();
3343
3344 let action = mapper.map(&ctrl_c(), &modal_state(), t);
3345 assert_eq!(action, Some(Action::DismissModal));
3346 }
3347
3348 #[test]
3349 fn test_ctrl_c_modal_priority_over_input() {
3350 let mut mapper = ActionMapper::with_defaults();
3351 let t = now();
3352
3353 let state = AppState::new().with_modal(true).with_input(true);
3355 let action = mapper.map(&ctrl_c(), &state, t);
3356 assert_eq!(action, Some(Action::DismissModal));
3357 }
3358
3359 #[test]
3360 fn test_ctrl_c_input_priority_over_task() {
3361 let mut mapper = ActionMapper::with_defaults();
3362 let t = now();
3363
3364 let state = AppState::new().with_input(true).with_task(true);
3365 let action = mapper.map(&ctrl_c(), &state, t);
3366 assert_eq!(action, Some(Action::ClearInput));
3367 }
3368
3369 #[test]
3370 fn test_ctrl_c_idle_config_noop() {
3371 let config = ActionConfig::default().with_ctrl_c_idle(CtrlCIdleAction::Noop);
3372 let mut mapper = ActionMapper::new(config);
3373 let t = now();
3374
3375 let action = mapper.map(&ctrl_c(), &idle_state(), t);
3376 assert_eq!(action, None); }
3378
3379 #[test]
3380 fn test_ctrl_c_idle_config_bell() {
3381 let config = ActionConfig::default().with_ctrl_c_idle(CtrlCIdleAction::Bell);
3382 let mut mapper = ActionMapper::new(config);
3383 let t = now();
3384
3385 let action = mapper.map(&ctrl_c(), &idle_state(), t);
3386 assert_eq!(action, Some(Action::Bell));
3387 }
3388
3389 #[test]
3392 fn test_ctrl_d_soft_quit() {
3393 let mut mapper = ActionMapper::with_defaults();
3394 let t = now();
3395
3396 let action = mapper.map(&ctrl_d(), &idle_state(), t);
3397 assert_eq!(action, Some(Action::SoftQuit));
3398 }
3399
3400 #[test]
3401 fn test_ctrl_d_ignores_state() {
3402 let mut mapper = ActionMapper::with_defaults();
3403 let t = now();
3404
3405 let action = mapper.map(&ctrl_d(), &modal_state(), t);
3407 assert_eq!(action, Some(Action::SoftQuit));
3408
3409 let action = mapper.map(&ctrl_d(), &input_state(), t);
3410 assert_eq!(action, Some(Action::SoftQuit));
3411 }
3412
3413 #[test]
3414 fn test_ctrl_q_hard_quit() {
3415 let mut mapper = ActionMapper::with_defaults();
3416 let t = now();
3417
3418 let action = mapper.map(&ctrl_q(), &idle_state(), t);
3419 assert_eq!(action, Some(Action::HardQuit));
3420 }
3421
3422 #[test]
3423 fn test_ctrl_q_ignores_state() {
3424 let mut mapper = ActionMapper::with_defaults();
3425 let t = now();
3426
3427 let action = mapper.map(&ctrl_q(), &modal_state(), t);
3429 assert_eq!(action, Some(Action::HardQuit));
3430 }
3431
3432 #[test]
3435 fn test_esc_dismisses_modal() {
3436 let mut mapper = ActionMapper::with_defaults();
3437 let t = now();
3438
3439 let action1 = mapper.map(&esc_press(), &modal_state(), t);
3441 assert_eq!(action1, None);
3442
3443 let action2 = mapper.check_timeout(&modal_state(), t + MS_300);
3445 assert_eq!(action2, Some(Action::DismissModal));
3446 }
3447
3448 #[test]
3449 fn test_esc_clears_input_no_modal() {
3450 let mut mapper = ActionMapper::with_defaults();
3451 let t = now();
3452
3453 mapper.map(&esc_press(), &input_state(), t);
3454 let action = mapper.check_timeout(&input_state(), t + MS_300);
3455 assert_eq!(action, Some(Action::ClearInput));
3456 }
3457
3458 #[test]
3459 fn test_esc_cancels_task_empty_input() {
3460 let mut mapper = ActionMapper::with_defaults();
3461 let t = now();
3462
3463 mapper.map(&esc_press(), &task_state(), t);
3464 let action = mapper.check_timeout(&task_state(), t + MS_300);
3465 assert_eq!(action, Some(Action::CancelTask));
3466 }
3467
3468 #[test]
3469 fn test_esc_closes_overlay() {
3470 let mut mapper = ActionMapper::with_defaults();
3471 let t = now();
3472
3473 mapper.map(&esc_press(), &overlay_state(), t);
3474 let action = mapper.check_timeout(&overlay_state(), t + MS_300);
3475 assert_eq!(action, Some(Action::CloseOverlay));
3476 }
3477
3478 #[test]
3479 fn test_esc_modal_priority_over_overlay() {
3480 let mut mapper = ActionMapper::with_defaults();
3481 let t = now();
3482
3483 let state = AppState::new().with_modal(true).with_overlay(true);
3484 mapper.map(&esc_press(), &state, t);
3485 let action = mapper.check_timeout(&state, t + MS_300);
3486 assert_eq!(action, Some(Action::DismissModal));
3487 }
3488
3489 #[test]
3490 fn test_esc_passthrough_when_idle() {
3491 let mut mapper = ActionMapper::with_defaults();
3492 let t = now();
3493
3494 mapper.map(&esc_press(), &idle_state(), t);
3495 let action = mapper.check_timeout(&idle_state(), t + MS_300);
3496 assert_eq!(action, Some(Action::PassThrough));
3497 }
3498
3499 #[test]
3502 fn test_esc_esc_within_timeout() {
3503 let mut mapper = ActionMapper::with_defaults();
3504 let t = now();
3505
3506 mapper.map(&esc_press(), &idle_state(), t);
3507 let action = mapper.map(&esc_press(), &idle_state(), t + MS_100);
3508 assert_eq!(action, Some(Action::ToggleTreeView));
3509 }
3510
3511 #[test]
3512 fn test_esc_esc_ignores_state() {
3513 let mut mapper = ActionMapper::with_defaults();
3514 let t = now();
3515
3516 mapper.map(&esc_press(), &modal_state(), t);
3518 let action = mapper.map(&esc_press(), &modal_state(), t + MS_100);
3519 assert_eq!(action, Some(Action::ToggleTreeView));
3520 }
3521
3522 #[test]
3523 fn test_esc_esc_timeout_expired() {
3524 let mut mapper = ActionMapper::with_defaults();
3525 let t = now();
3526
3527 mapper.map(&esc_press(), &input_state(), t);
3528 let action = mapper.map(&esc_press(), &input_state(), t + MS_300);
3530
3531 assert_eq!(action, Some(Action::ClearInput));
3533 assert!(mapper.is_pending_esc());
3534 }
3535
3536 #[test]
3539 fn test_esc_then_other_key() {
3540 let mut mapper = ActionMapper::with_defaults();
3541 let t = now();
3542
3543 mapper.map(&esc_press(), &input_state(), t);
3544 let action = mapper.map(&key_press(KeyCode::Char('a')), &input_state(), t + MS_50);
3545
3546 assert_eq!(action, Some(Action::ClearInput));
3548 }
3549
3550 #[test]
3553 fn test_regular_key_passthrough() {
3554 let mut mapper = ActionMapper::with_defaults();
3555 let t = now();
3556
3557 let action = mapper.map(&key_press(KeyCode::Char('x')), &idle_state(), t);
3558 assert_eq!(action, Some(Action::PassThrough));
3559 }
3560
3561 #[test]
3562 fn test_release_event_passthrough() {
3563 let mut mapper = ActionMapper::with_defaults();
3564 let t = now();
3565
3566 let release = KeyEvent::new(KeyCode::Char('x')).with_kind(KeyEventKind::Release);
3567 let action = mapper.map(&release, &idle_state(), t);
3568 assert_eq!(action, Some(Action::PassThrough));
3569 }
3570
3571 #[test]
3574 fn test_app_state_builders() {
3575 let state = AppState::new()
3576 .with_input(true)
3577 .with_task(true)
3578 .with_modal(true)
3579 .with_overlay(true);
3580
3581 assert!(state.input_nonempty);
3582 assert!(state.task_running);
3583 assert!(state.modal_open);
3584 assert!(state.view_overlay);
3585 assert!(!state.is_idle());
3586 }
3587
3588 #[test]
3589 fn test_app_state_is_idle() {
3590 assert!(AppState::default().is_idle());
3591 assert!(!AppState::new().with_input(true).is_idle());
3592 assert!(!AppState::new().with_task(true).is_idle());
3593 assert!(!AppState::new().with_modal(true).is_idle());
3594 assert!(AppState::new().with_overlay(true).is_idle());
3596 }
3597
3598 #[test]
3601 fn test_action_consumes_event() {
3602 assert!(Action::ClearInput.consumes_event());
3603 assert!(Action::CancelTask.consumes_event());
3604 assert!(Action::Quit.consumes_event());
3605 assert!(!Action::PassThrough.consumes_event());
3606 }
3607
3608 #[test]
3609 fn test_action_is_quit() {
3610 assert!(Action::Quit.is_quit());
3611 assert!(Action::SoftQuit.is_quit());
3612 assert!(Action::HardQuit.is_quit());
3613 assert!(!Action::ClearInput.is_quit());
3614 assert!(!Action::PassThrough.is_quit());
3615 }
3616
3617 #[test]
3620 fn test_ctrl_c_idle_action_from_str() {
3621 assert_eq!(
3622 CtrlCIdleAction::from_str_opt("quit"),
3623 Some(CtrlCIdleAction::Quit)
3624 );
3625 assert_eq!(
3626 CtrlCIdleAction::from_str_opt("QUIT"),
3627 Some(CtrlCIdleAction::Quit)
3628 );
3629 assert_eq!(
3630 CtrlCIdleAction::from_str_opt("noop"),
3631 Some(CtrlCIdleAction::Noop)
3632 );
3633 assert_eq!(
3634 CtrlCIdleAction::from_str_opt("none"),
3635 Some(CtrlCIdleAction::Noop)
3636 );
3637 assert_eq!(
3638 CtrlCIdleAction::from_str_opt("ignore"),
3639 Some(CtrlCIdleAction::Noop)
3640 );
3641 assert_eq!(
3642 CtrlCIdleAction::from_str_opt("bell"),
3643 Some(CtrlCIdleAction::Bell)
3644 );
3645 assert_eq!(
3646 CtrlCIdleAction::from_str_opt("beep"),
3647 Some(CtrlCIdleAction::Bell)
3648 );
3649 assert_eq!(CtrlCIdleAction::from_str_opt("invalid"), None);
3650 }
3651
3652 #[test]
3653 fn test_ctrl_c_idle_action_to_action() {
3654 assert_eq!(CtrlCIdleAction::Quit.to_action(), Some(Action::Quit));
3655 assert_eq!(CtrlCIdleAction::Noop.to_action(), None);
3656 assert_eq!(CtrlCIdleAction::Bell.to_action(), Some(Action::Bell));
3657 }
3658
3659 #[test]
3660 fn test_action_config_builder() {
3661 let config = ActionConfig::default()
3662 .with_sequence_config(SequenceConfig::default().with_timeout(MS_100))
3663 .with_ctrl_c_idle(CtrlCIdleAction::Bell);
3664
3665 assert_eq!(config.sequence_config.esc_seq_timeout, MS_100);
3666 assert_eq!(config.ctrl_c_idle_action, CtrlCIdleAction::Bell);
3667 }
3668
3669 #[test]
3672 fn test_mapper_reset() {
3673 let mut mapper = ActionMapper::with_defaults();
3674 let t = now();
3675
3676 mapper.map(&esc_press(), &idle_state(), t);
3677 assert!(mapper.is_pending_esc());
3678
3679 mapper.reset();
3680 assert!(!mapper.is_pending_esc());
3681 }
3682
3683 #[test]
3686 fn test_deterministic_action_mapping() {
3687 let t = now();
3688
3689 let mut m1 = ActionMapper::with_defaults();
3690 let mut m2 = ActionMapper::with_defaults();
3691
3692 let events = [
3693 (ctrl_c(), input_state()),
3694 (ctrl_d(), modal_state()),
3695 (ctrl_q(), idle_state()),
3696 ];
3697
3698 for (event, state) in &events {
3699 let a1 = m1.map(event, state, t);
3700 let a2 = m2.map(event, state, t);
3701 assert_eq!(a1, a2);
3702 }
3703 }
3704
3705 #[test]
3706 fn test_uppercase_ctrl_keys() {
3707 let mut mapper = ActionMapper::with_defaults();
3708 let t = now();
3709
3710 let ctrl_c_upper = KeyEvent::new(KeyCode::Char('C')).with_modifiers(Modifiers::CTRL);
3712 let action = mapper.map(&ctrl_c_upper, &idle_state(), t);
3713 assert_eq!(action, Some(Action::Quit));
3714 }
3715
3716 #[test]
3719 fn test_sequence_config_validation_clamps_high_timeout() {
3720 let config = SequenceConfig::default()
3721 .with_timeout(Duration::from_millis(1000)) .validated();
3723
3724 assert_eq!(config.esc_seq_timeout.as_millis(), 400);
3726 }
3727
3728 #[test]
3729 fn test_sequence_config_validation_clamps_low_timeout() {
3730 let config = SequenceConfig::default()
3731 .with_timeout(Duration::from_millis(50)) .validated();
3733
3734 assert_eq!(config.esc_seq_timeout.as_millis(), 150);
3736 }
3737
3738 #[test]
3739 fn test_sequence_config_validation_clamps_high_debounce() {
3740 let config = SequenceConfig::default()
3741 .with_debounce(Duration::from_millis(200)) .validated();
3743
3744 assert_eq!(config.esc_debounce.as_millis(), 100);
3746 }
3747
3748 #[test]
3749 fn test_sequence_config_validation_debounce_not_exceeds_timeout() {
3750 let config = SequenceConfig::default()
3751 .with_timeout(Duration::from_millis(150))
3752 .with_debounce(Duration::from_millis(200)) .validated();
3754
3755 assert!(config.esc_debounce <= config.esc_seq_timeout);
3759 }
3760
3761 #[test]
3762 fn test_sequence_config_is_valid() {
3763 assert!(SequenceConfig::default().is_valid());
3764
3765 let invalid = SequenceConfig::default().with_timeout(Duration::from_millis(500));
3767 assert!(!invalid.is_valid());
3768
3769 assert!(invalid.validated().is_valid());
3771 }
3772
3773 #[test]
3774 fn test_sequence_config_constants() {
3775 assert_eq!(DEFAULT_ESC_SEQ_TIMEOUT_MS, 250);
3777 assert_eq!(MIN_ESC_SEQ_TIMEOUT_MS, 150);
3778 assert_eq!(MAX_ESC_SEQ_TIMEOUT_MS, 400);
3779 assert_eq!(DEFAULT_ESC_DEBOUNCE_MS, 50);
3780 assert_eq!(MIN_ESC_DEBOUNCE_MS, 0);
3781 assert_eq!(MAX_ESC_DEBOUNCE_MS, 100);
3782 }
3783
3784 #[test]
3785 fn test_action_config_validated() {
3786 let config = ActionConfig::default()
3787 .with_sequence_config(
3788 SequenceConfig::default().with_timeout(Duration::from_millis(1000)),
3789 )
3790 .validated();
3791
3792 assert_eq!(config.sequence_config.esc_seq_timeout.as_millis(), 400);
3794 }
3795 }
3796}