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