1use std::collections::HashMap;
7
8use crate::key::{KeyCode, KeyEvent, KeyModifiers};
9use rmut_session::Function;
10
11#[derive(Clone, Copy, PartialEq, Eq, Debug)]
12pub struct KeyPattern {
13 pub code: KeyCode,
14 pub mods: KeyModifiers,
15}
16
17impl KeyPattern {
18 fn plain(code: KeyCode) -> Self {
19 KeyPattern {
20 code,
21 mods: KeyModifiers::NONE,
22 }
23 }
24
25 fn ch(c: char) -> Self {
26 Self::plain(KeyCode::Char(c))
27 }
28
29 fn ctrl(c: char) -> Self {
30 KeyPattern {
31 code: KeyCode::Char(c),
32 mods: KeyModifiers::CONTROL,
33 }
34 }
35
36 fn alt(c: char) -> Self {
37 KeyPattern {
38 code: KeyCode::Char(c),
39 mods: KeyModifiers::ALT,
40 }
41 }
42
43 pub fn matches(&self, key: &KeyEvent) -> bool {
44 self.code == key.code
45 && key.modifiers & (KeyModifiers::CONTROL | KeyModifiers::ALT) == self.mods
46 }
47
48 pub fn display(&self) -> String {
49 let base = match self.code {
50 KeyCode::Char(' ') => "Space".to_string(),
51 KeyCode::Char(c) => c.to_string(),
52 KeyCode::Enter => "Enter".into(),
53 KeyCode::Esc => "Esc".into(),
54 KeyCode::Tab => "Tab".into(),
55 KeyCode::Backspace => "Backspace".into(),
56 KeyCode::Up => "Up".into(),
57 KeyCode::Down => "Down".into(),
58 KeyCode::PageUp => "PgUp".into(),
59 KeyCode::PageDown => "PgDn".into(),
60 KeyCode::Home => "Home".into(),
61 KeyCode::End => "End".into(),
62 other => format!("{other:?}"),
63 };
64 if self.mods.contains(KeyModifiers::CONTROL) {
65 format!("Ctrl+{base}")
66 } else if self.mods.contains(KeyModifiers::ALT) {
67 format!("Alt+{base}")
68 } else {
69 base
70 }
71 }
72}
73
74fn strip_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
75 (s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix))
76 .then(|| &s[prefix.len()..])
77}
78
79pub fn parse_key(input: &str) -> Option<KeyPattern> {
80 let mut mods = KeyModifiers::NONE;
81 let mut rest = input.trim();
82 loop {
83 if let Some(r) = strip_ci(rest, "ctrl+") {
84 mods |= KeyModifiers::CONTROL;
85 rest = r;
86 } else if let Some(r) = strip_ci(rest, "alt+") {
87 mods |= KeyModifiers::ALT;
88 rest = r;
89 } else {
90 break;
91 }
92 }
93 let code = match rest.to_lowercase().as_str() {
94 "enter" | "return" => KeyCode::Enter,
95 "esc" | "escape" => KeyCode::Esc,
96 "space" => KeyCode::Char(' '),
97 "tab" => KeyCode::Tab,
98 "backspace" => KeyCode::Backspace,
99 "up" => KeyCode::Up,
100 "down" => KeyCode::Down,
101 "left" => KeyCode::Left,
102 "right" => KeyCode::Right,
103 "pgup" | "pageup" => KeyCode::PageUp,
104 "pgdn" | "pagedown" => KeyCode::PageDown,
105 "home" => KeyCode::Home,
106 "end" => KeyCode::End,
107 _ => {
108 let mut chars = rest.chars();
109 let c = chars.next()?;
110 if chars.next().is_some() {
111 return None;
112 }
113 KeyCode::Char(c)
114 }
115 };
116 Some(KeyPattern { code, mods })
117}
118
119#[derive(Clone, Copy, PartialEq, Eq, Debug)]
120pub enum PagerAction {
121 Back,
122 Down,
123 Up,
124 PageDown,
125 PageUp,
126 HalfDown,
127 HalfUp,
128 Top,
129 Bottom,
130 ToggleQuoted,
131 SkipQuoted,
132 NextMsg,
133 PrevMsg,
134 NextUndeleted,
135 PrevUndeleted,
136 Delete,
137 Undelete,
138 Flag,
139 ToggleNew,
140 Tag,
141 Undo,
142 Redraw,
143 Suspend,
144 Headers,
145 Search,
146 SearchNext,
147 SearchPrev,
148 SearchToggle,
149 Attachments,
150 Compose,
151 Reply,
152 GroupReply,
153 ListReply,
154 Forward,
155 Print,
156 Save,
157 Copy,
158 Pipe,
159 Bounce,
160 Resend,
161 Edit,
162 CreateAlias,
163 EnterCommand,
164 Help,
165 ListAction,
166 ErrorHistory,
167 WhatKey,
168}
169
170impl PagerAction {
171 pub fn name(self) -> &'static str {
172 use PagerAction::*;
173 match self {
174 Back => "back",
175 Down => "down",
176 Up => "up",
177 PageDown => "page-down",
178 PageUp => "page-up",
179 HalfDown => "half-down",
180 HalfUp => "half-up",
181 Top => "top",
182 Bottom => "bottom",
183 ToggleQuoted => "toggle-quoted",
184 SkipQuoted => "skip-quoted",
185 NextMsg => "next",
186 PrevMsg => "previous",
187 NextUndeleted => "next-undeleted",
188 PrevUndeleted => "previous-undeleted",
189 Delete => "delete",
190 Undelete => "undelete",
191 Flag => "flag",
192 ToggleNew => "toggle-new",
193 Tag => "tag",
194 Undo => "undo",
195 Redraw => "refresh",
196 Suspend => "suspend",
197 Headers => "headers",
198 Search => "search",
199 SearchNext => "search-next",
200 SearchPrev => "search-prev",
201 SearchToggle => "search-toggle",
202 Attachments => "attachments",
203 Compose => "compose",
204 Reply => "reply",
205 GroupReply => "group-reply",
206 ListReply => "list-reply",
207 Forward => "forward",
208 Print => "print",
209 Save => "save",
210 Copy => "copy",
211 Pipe => "pipe",
212 Bounce => "bounce",
213 Resend => "resend",
214 Edit => "edit",
215 CreateAlias => "create-alias",
216 EnterCommand => "enter-command",
217 Help => "help",
218 ListAction => "list-action",
219 ErrorHistory => "error-history",
220 WhatKey => "what-key",
221 }
222 }
223
224 pub fn describe(self) -> &'static str {
225 use PagerAction::*;
226 match self {
227 Back => "back to the index",
228 Down => "scroll down one line",
229 Up => "scroll up one line",
230 PageDown => "page down",
231 PageUp => "page up",
232 HalfDown => "scroll down half a page",
233 HalfUp => "scroll up half a page",
234 Top => "jump to the top",
235 Bottom => "jump to the bottom",
236 ToggleQuoted => "show/hide quoted text",
237 SkipQuoted => "skip past the quoted text below",
238 NextMsg => "open next message",
239 PrevMsg => "open previous message",
240 NextUndeleted => "open next undeleted message",
241 PrevUndeleted => "open previous undeleted message",
242 Delete => "delete and advance",
243 Undelete => "unmark deletion",
244 Flag => "toggle flagged mark",
245 ToggleNew => "toggle read/unread (unbound here: N is the backwards search)",
246 Tag => "toggle the tag on this message",
247 Undo => "cancel a held send, or undo the last mark change",
248 Redraw => "repaint the screen",
249 Suspend => "suspend rmut (fg brings it back)",
250 Headers => "toggle full headers",
251 Search => "search the displayed text (unlike the index /, which matches messages)",
252 SearchNext => "next match of the pager search",
253 SearchPrev => "previous match of the pager search",
254 SearchToggle => "toggle the search highlighting",
255 Attachments => "list message parts",
256 Compose => "compose a new message",
257 Reply => "reply to sender",
258 GroupReply => "reply to all",
259 ListReply => "reply to the mailing list only",
260 Forward => "forward message",
261 Print => "pipe message to the print command",
262 Save => "save (copy + mark deleted) to a mailbox",
263 Copy => "copy to a mailbox (original stays)",
264 Pipe => "pipe raw message to a shell command",
265 Bounce => "bounce (resend) message to new recipients",
266 Resend => "edit the message as a new draft",
267 Edit => "edit the raw message and replace it",
268 CreateAlias => "add the sender to the alias file",
269 EnterCommand => "run a config command (set/bind/macro/color/...)",
270 Help => "this help",
271 ListAction => "act on the message's List-* headers (subscribe, help, ...)",
272 ErrorHistory => "show the recent errors",
273 WhatKey => "say what a key is (Ctrl+G ends it)",
274 }
275 }
276
277 fn all() -> &'static [PagerAction] {
278 use PagerAction::*;
279 &[
280 Back,
281 Down,
282 Up,
283 PageDown,
284 PageUp,
285 HalfDown,
286 HalfUp,
287 Top,
288 Bottom,
289 ToggleQuoted,
290 SkipQuoted,
291 NextMsg,
292 PrevMsg,
293 NextUndeleted,
294 PrevUndeleted,
295 Delete,
296 Undelete,
297 Flag,
298 ToggleNew,
299 Tag,
300 Undo,
301 Redraw,
302 Suspend,
303 Headers,
304 Search,
305 SearchNext,
306 SearchPrev,
307 SearchToggle,
308 Attachments,
309 Compose,
310 Reply,
311 GroupReply,
312 ListReply,
313 Forward,
314 Print,
315 Save,
316 Copy,
317 Pipe,
318 Bounce,
319 Resend,
320 Edit,
321 CreateAlias,
322 EnterCommand,
323 Help,
324 ListAction,
325 ErrorHistory,
326 WhatKey,
327 ]
328 }
329
330 pub fn from_name(name: &str) -> Option<PagerAction> {
331 let name = match name {
333 "mark-as-new" => "toggle-new",
334 other => other,
335 };
336 PagerAction::all()
337 .iter()
338 .copied()
339 .find(|a| a.name() == name)
340 }
341}
342
343pub fn parse_sequence(input: &str) -> Option<Vec<KeyEvent>> {
347 let mut out = Vec::new();
348 let mut chars = input.chars();
349 while let Some(c) = chars.next() {
350 if c == '<' {
351 let mut name = String::new();
352 loop {
353 match chars.next() {
354 Some('>') => break,
355 Some(c) => name.push(c),
356 None => return None,
357 }
358 }
359 let p = parse_key(&name)?;
360 out.push(KeyEvent::new(p.code, p.mods));
361 } else {
362 out.push(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
363 }
364 }
365 Some(out)
366}
367
368pub struct Keymap {
369 pub index: Vec<(KeyPattern, Function)>,
370 pub pager: Vec<(KeyPattern, PagerAction)>,
371 pub macros_index: Vec<(KeyPattern, Vec<KeyEvent>, String)>,
375 pub macros_pager: Vec<(KeyPattern, Vec<KeyEvent>, String)>,
376}
377
378fn index_defaults() -> Vec<(KeyPattern, Function)> {
379 use Function::*;
380 use KeyCode as K;
381 vec![
382 (KeyPattern::ch('q'), Quit),
383 (KeyPattern::ch('x'), Abort),
384 (KeyPattern::ch('j'), Down),
385 (KeyPattern::plain(K::Down), Down),
386 (KeyPattern::ch('k'), Up),
387 (KeyPattern::plain(K::Up), Up),
388 (KeyPattern::plain(K::PageDown), PageDown),
389 (KeyPattern::ctrl('f'), PageDown),
390 (KeyPattern::plain(K::PageUp), PageUp),
391 (KeyPattern::ctrl('b'), PageUp),
392 (KeyPattern::ch(' '), PageDown),
393 (KeyPattern::ch('='), First),
394 (KeyPattern::plain(K::Home), First),
395 (KeyPattern::ch('*'), Last),
396 (KeyPattern::plain(K::End), Last),
397 (KeyPattern::plain(K::Enter), View),
398 (KeyPattern::ch('d'), Delete),
399 (KeyPattern::ch('u'), Undelete),
400 (KeyPattern::ch('F'), Flag),
401 (KeyPattern::ch('N'), ToggleNew),
402 (KeyPattern::alt('a'), MarkAllRead),
403 (KeyPattern::ch('$'), Sync),
404 (KeyPattern::ch('m'), Compose),
405 (KeyPattern::ch('r'), Reply),
406 (KeyPattern::ch('g'), GroupReply),
407 (KeyPattern::ch('L'), ListReply),
408 (KeyPattern::ch('f'), Forward),
409 (KeyPattern::ch('o'), Sort),
410 (KeyPattern::ch('l'), Limit),
411 (KeyPattern::ch('/'), Search),
412 (KeyPattern::alt('/'), SearchReverse),
413 (KeyPattern::ch('n'), SearchNext),
414 (KeyPattern::plain(K::Tab), NextNew),
415 (
416 KeyPattern {
417 code: K::Tab,
418 mods: KeyModifiers::ALT,
419 },
420 PrevNew,
421 ),
422 (KeyPattern::ch('c'), ChangeMailbox),
423 (KeyPattern::alt('c'), ChangeMailboxReadOnly),
424 (KeyPattern::ch('y'), Folders),
425 (KeyPattern::ch('v'), Attachments),
426 (KeyPattern::alt('v'), FoldThread),
427 (KeyPattern::alt('V'), FoldAll),
428 (KeyPattern::ch('p'), Print),
429 (KeyPattern::ch('t'), Tag),
430 (KeyPattern::ch(';'), TagPrefix),
431 (KeyPattern::alt('d'), DeleteThread),
432 (KeyPattern::alt('u'), UndeleteThread),
433 (KeyPattern::alt('t'), TagThread),
434 (KeyPattern::ctrl('d'), DeleteSubthread),
435 (KeyPattern::ctrl('u'), UndeleteSubthread),
436 (KeyPattern::alt('n'), NextThread),
437 (KeyPattern::alt('p'), PrevThread),
438 (KeyPattern::ch('#'), BreakThread),
439 (KeyPattern::ch('&'), LinkThreads),
440 (KeyPattern::ctrl('r'), ReadThread),
441 (KeyPattern::alt('r'), ReadSubthread),
442 (KeyPattern::ch('P'), ParentMessage),
443 (KeyPattern::ch('Y'), EditLabel),
444 (KeyPattern::ch('V'), ShowVersion),
445 (KeyPattern::alt('l'), ShowLimit),
446 (KeyPattern::ch('@'), DisplayAddress),
447 (KeyPattern::ch('%'), ToggleWrite),
448 (KeyPattern::ch('H'), PageTop),
449 (KeyPattern::ch('M'), PageMiddle),
450 (KeyPattern::ch('z'), Undo),
451 (KeyPattern::ch('D'), DeletePattern),
452 (KeyPattern::ch('U'), UndeletePattern),
453 (KeyPattern::ch('T'), TagPattern),
454 (KeyPattern::ctrl('t'), UntagPattern),
455 (KeyPattern::ch('G'), FetchMail),
456 (KeyPattern::ch('s'), Save),
457 (KeyPattern::ch('C'), Copy),
458 (KeyPattern::alt('s'), DecodeSave),
459 (KeyPattern::alt('C'), DecodeCopy),
460 (KeyPattern::ch('|'), Pipe),
461 (KeyPattern::ch('b'), Bounce),
462 (KeyPattern::ch('e'), Edit),
463 (KeyPattern::alt('e'), Resend),
464 (KeyPattern::ch('B'), SidebarToggle),
465 (KeyPattern::ctrl('n'), SidebarNext),
466 (KeyPattern::ctrl('p'), SidebarPrev),
467 (KeyPattern::ctrl('o'), SidebarOpen),
468 (KeyPattern::ch('a'), CreateAlias),
469 (KeyPattern::ch('Q'), Query),
470 (KeyPattern::ch('X'), Notmuch),
471 (KeyPattern::ch(':'), EnterCommand),
472 (KeyPattern::ch('!'), Shell),
473 (KeyPattern::ctrl('l'), Redraw),
474 (KeyPattern::ctrl('z'), Suspend),
475 (KeyPattern::ch('?'), Help),
476 (KeyPattern::ch('~'), MarkMessage),
477 (KeyPattern::alt('L'), ListAction),
478 ]
479}
480
481fn pager_defaults() -> Vec<(KeyPattern, PagerAction)> {
482 use KeyCode as K;
483 use PagerAction::*;
484 vec![
485 (KeyPattern::ch('q'), Back),
486 (KeyPattern::ch('i'), Back),
487 (KeyPattern::plain(K::Esc), Back),
488 (KeyPattern::plain(K::Enter), Down),
491 (KeyPattern::plain(K::Backspace), Up),
492 (KeyPattern::ch('j'), NextUndeleted),
493 (KeyPattern::plain(K::Down), NextUndeleted),
494 (KeyPattern::plain(K::Right), NextUndeleted),
495 (KeyPattern::ch('k'), PrevUndeleted),
496 (KeyPattern::plain(K::Up), PrevUndeleted),
497 (KeyPattern::plain(K::Left), PrevUndeleted),
498 (KeyPattern::ch(' '), PageDown),
499 (KeyPattern::plain(K::PageDown), PageDown),
500 (KeyPattern::ch('-'), PageUp),
501 (KeyPattern::plain(K::PageUp), PageUp),
502 (KeyPattern::ctrl('d'), HalfDown),
503 (KeyPattern::ctrl('u'), HalfUp),
504 (KeyPattern::plain(K::Home), Top),
505 (KeyPattern::plain(K::End), Bottom),
506 (KeyPattern::ch('T'), ToggleQuoted),
507 (KeyPattern::ch('S'), SkipQuoted),
508 (KeyPattern::ch('J'), NextMsg),
509 (KeyPattern::ch('K'), PrevMsg),
510 (KeyPattern::ch('d'), Delete),
511 (KeyPattern::ch('u'), Undelete),
512 (KeyPattern::ch('F'), Flag),
513 (KeyPattern::ch('t'), Tag),
514 (KeyPattern::ch('z'), Undo),
517 (KeyPattern::ctrl('l'), Redraw),
518 (KeyPattern::ctrl('z'), Suspend),
519 (KeyPattern::ch('h'), Headers),
520 (KeyPattern::ch('/'), Search),
521 (KeyPattern::ch('n'), SearchNext),
522 (KeyPattern::ch('N'), SearchPrev),
523 (KeyPattern::ch('\\'), SearchToggle),
524 (KeyPattern::ch('v'), Attachments),
525 (KeyPattern::ch('m'), Compose),
526 (KeyPattern::ch('r'), Reply),
527 (KeyPattern::ch('g'), GroupReply),
528 (KeyPattern::ch('L'), ListReply),
529 (KeyPattern::ch('f'), Forward),
530 (KeyPattern::ch('p'), Print),
531 (KeyPattern::ch('s'), Save),
532 (KeyPattern::ch('C'), Copy),
533 (KeyPattern::ch('|'), Pipe),
534 (KeyPattern::ch('b'), Bounce),
535 (KeyPattern::ch('e'), Edit),
536 (KeyPattern::alt('e'), Resend),
537 (KeyPattern::ch('a'), CreateAlias),
538 (KeyPattern::ch(':'), EnterCommand),
539 (KeyPattern::ch('?'), Help),
540 (KeyPattern::alt('L'), ListAction),
541 ]
542}
543
544impl Keymap {
545 pub fn with_config(
549 index_over: &HashMap<String, String>,
550 pager_over: &HashMap<String, String>,
551 macros_index: &HashMap<String, String>,
552 macros_pager: &HashMap<String, String>,
553 ) -> (Keymap, Vec<String>) {
554 let mut warnings = Vec::new();
555 let mut index = index_defaults();
556 for (action_name, key_str) in index_over {
557 let (Some(action), Some(key)) = (Function::from_name(action_name), parse_key(key_str))
558 else {
559 warnings.push(format!("bad index binding {action_name} = {key_str:?}"));
560 continue;
561 };
562 index.retain(|(k, a)| *a != action && *k != key);
563 index.push((key, action));
564 }
565 let mut pager = pager_defaults();
566 for (action_name, key_str) in pager_over {
567 let (Some(action), Some(key)) =
568 (PagerAction::from_name(action_name), parse_key(key_str))
569 else {
570 warnings.push(format!("bad pager binding {action_name} = {key_str:?}"));
571 continue;
572 };
573 pager.retain(|(k, a)| *a != action && *k != key);
574 pager.push((key, action));
575 }
576 let mut macros = |table: &HashMap<String, String>, menu: &str| {
577 let mut out = Vec::new();
578 for (key_str, seq_str) in table {
579 let (Some(key), Some(seq)) = (parse_key(key_str), parse_sequence(seq_str)) else {
580 warnings.push(format!("bad {menu} macro {key_str} = {seq_str:?}"));
581 continue;
582 };
583 out.push((key, seq, seq_str.clone()));
584 }
585 out
586 };
587 let macros_index = macros(macros_index, "index");
588 let macros_pager = macros(macros_pager, "pager");
589 (
590 Keymap {
591 index,
592 pager,
593 macros_index,
594 macros_pager,
595 },
596 warnings,
597 )
598 }
599
600 pub fn lookup_index_macro(&self, key: &KeyEvent) -> Option<&[KeyEvent]> {
602 self.macros_index
603 .iter()
604 .find(|(p, _, _)| p.matches(key))
605 .map(|(_, seq, _)| seq.as_slice())
606 }
607
608 pub fn lookup_pager_macro(&self, key: &KeyEvent) -> Option<&[KeyEvent]> {
609 self.macros_pager
610 .iter()
611 .find(|(p, _, _)| p.matches(key))
612 .map(|(_, seq, _)| seq.as_slice())
613 }
614
615 pub fn lookup_index(&self, key: &KeyEvent) -> Option<Function> {
616 self.index
617 .iter()
618 .find(|(p, _)| p.matches(key))
619 .map(|&(_, a)| a)
620 }
621
622 pub fn lookup_pager(&self, key: &KeyEvent) -> Option<PagerAction> {
623 self.pager
624 .iter()
625 .find(|(p, _)| p.matches(key))
626 .map(|&(_, a)| a)
627 }
628
629 pub fn help_lines(&self) -> Vec<String> {
631 let mut lines = vec!["Index keys".to_string(), String::new()];
632 for &action in Function::all() {
633 let keys: Vec<String> = self
634 .index
635 .iter()
636 .filter(|&&(_, a)| a == action)
637 .map(|(k, _)| k.display())
638 .collect();
639 if !keys.is_empty() {
640 lines.push(format!(" {:<16} {}", keys.join(" "), action.describe()));
641 }
642 }
643 lines.extend([String::new(), "Pager keys".to_string(), String::new()]);
644 for &action in PagerAction::all() {
645 let keys: Vec<String> = self
646 .pager
647 .iter()
648 .filter(|&&(_, a)| a == action)
649 .map(|(k, _)| k.display())
650 .collect();
651 if !keys.is_empty() {
652 lines.push(format!(" {:<16} {}", keys.join(" "), action.describe()));
653 }
654 }
655 for (title, table) in [
656 ("Index macros", &self.macros_index),
657 ("Pager macros", &self.macros_pager),
658 ] {
659 if !table.is_empty() {
660 lines.extend([String::new(), title.to_string(), String::new()]);
661 for (key, _, raw) in table {
662 lines.push(format!(" {:<16} {raw}", key.display()));
663 }
664 }
665 }
666 lines.extend(
667 [
668 "",
669 "Patterns (limit/search)",
670 "",
671 " ~f x from ~s x subject ~b x body",
672 " ~t x to ~c x cc ~C x to or cc",
673 " ~e x sender ~d spec date word subject or from",
674 " ~N new ~U unread ~F flagged ~D deleted ~T tagged",
675 " ~p addressed to me",
676 "",
677 " x is a case-insensitive regex; \"quotes\" keep spaces.",
678 " ~d: 24/12/2026, 1/6/2026-30/6/2026, 24/12-, <1w, >2d, =3d",
679 " Terms AND; ! negates, | ORs, () groups:",
680 " !~D (~f jane | ~t jane) ~d <1m",
681 ]
682 .map(String::from),
683 );
684 lines
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn parse_key_forms() {
694 assert_eq!(parse_key("x"), Some(KeyPattern::ch('x')));
695 assert_eq!(parse_key("X"), Some(KeyPattern::ch('X')));
696 assert_eq!(parse_key("ctrl+f"), Some(KeyPattern::ctrl('f')));
697 assert_eq!(parse_key("Alt+v"), Some(KeyPattern::alt('v')));
698 assert_eq!(parse_key("space"), Some(KeyPattern::ch(' ')));
699 assert_eq!(
700 parse_key("pgdn"),
701 Some(KeyPattern::plain(KeyCode::PageDown))
702 );
703 assert_eq!(parse_key("enter"), Some(KeyPattern::plain(KeyCode::Enter)));
704 assert!(parse_key("bogus-key").is_none());
705 }
706
707 #[test]
708 fn remap_replaces_defaults_and_conflicts() {
709 let mut over = HashMap::new();
710 over.insert("sync".to_string(), "w".to_string());
711 over.insert("delete".to_string(), "ctrl+d".to_string());
712 let (map, warnings) =
713 Keymap::with_config(&over, &HashMap::new(), &HashMap::new(), &HashMap::new());
714 assert!(warnings.is_empty());
715 let ev = |p: KeyPattern| KeyEvent::new(p.code, p.mods);
716 assert_eq!(
717 map.lookup_index(&ev(KeyPattern::ch('w'))),
718 Some(Function::Sync)
719 );
720 assert_eq!(map.lookup_index(&ev(KeyPattern::ch('$'))), None);
721 assert_eq!(
722 map.lookup_index(&ev(KeyPattern::ctrl('d'))),
723 Some(Function::Delete)
724 );
725 assert_eq!(map.lookup_index(&ev(KeyPattern::ch('d'))), None);
726 }
727
728 #[test]
729 fn bad_bindings_warn_and_keep_defaults() {
730 let mut over = HashMap::new();
731 over.insert("frobnicate".to_string(), "z".to_string());
732 let (map, warnings) =
733 Keymap::with_config(&over, &HashMap::new(), &HashMap::new(), &HashMap::new());
734 assert_eq!(warnings.len(), 1);
735 let ev = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
736 assert_eq!(map.lookup_index(&ev), Some(Function::Quit));
737 }
738
739 #[test]
740 fn parse_sequence_forms() {
741 let seq = parse_sequence("l~f jane<enter>").unwrap();
742 assert_eq!(seq.len(), 9);
743 assert_eq!(seq[0].code, KeyCode::Char('l'));
744 assert_eq!(seq[2].code, KeyCode::Char('f'));
745 assert_eq!(seq[3].code, KeyCode::Char(' '));
746 assert_eq!(seq[8].code, KeyCode::Enter);
747 let seq = parse_sequence("<ctrl+x><Esc>").unwrap();
748 assert_eq!(seq[0].code, KeyCode::Char('x'));
749 assert!(seq[0].modifiers.contains(KeyModifiers::CONTROL));
750 assert_eq!(seq[1].code, KeyCode::Esc);
751 assert!(parse_sequence("<bogus>").is_none());
752 assert!(parse_sequence("<unclosed").is_none());
753 assert!(parse_sequence("").unwrap().is_empty());
754 }
755
756 #[test]
757 fn macros_parse_shadow_and_warn() {
758 let mut macros_index = HashMap::new();
759 macros_index.insert("d".to_string(), "l~f jane<enter>".to_string());
760 macros_index.insert("Z".to_string(), "<bogus>".to_string());
761 let (map, warnings) = Keymap::with_config(
762 &HashMap::new(),
763 &HashMap::new(),
764 ¯os_index,
765 &HashMap::new(),
766 );
767 assert_eq!(warnings.len(), 1);
768 assert!(warnings[0].contains("bad index macro Z"), "{warnings:?}");
769 let ev = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
770 assert_eq!(map.lookup_index_macro(&ev).unwrap().len(), 9);
773 assert!(
774 map.lookup_pager_macro(&ev).is_none(),
775 "index macro must not leak into the pager"
776 );
777 let help = map.help_lines().join("\n");
779 assert!(help.contains("Index macros"), "{help}");
780 assert!(help.contains("l~f jane<enter>"), "{help}");
781 }
782
783 #[test]
784 fn shift_in_event_does_not_block_match() {
785 let ev = KeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT);
787 let (map, _) = Keymap::with_config(
788 &HashMap::new(),
789 &HashMap::new(),
790 &HashMap::new(),
791 &HashMap::new(),
792 );
793 assert_eq!(map.lookup_index(&ev), Some(Function::Flag));
794 }
795}