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::ch('$'), Sync),
403 (KeyPattern::ch('m'), Compose),
404 (KeyPattern::ch('r'), Reply),
405 (KeyPattern::ch('g'), GroupReply),
406 (KeyPattern::ch('L'), ListReply),
407 (KeyPattern::ch('f'), Forward),
408 (KeyPattern::ch('o'), Sort),
409 (KeyPattern::ch('l'), Limit),
410 (KeyPattern::ch('/'), Search),
411 (KeyPattern::alt('/'), SearchReverse),
412 (KeyPattern::ch('n'), SearchNext),
413 (KeyPattern::plain(K::Tab), NextNew),
414 (
415 KeyPattern {
416 code: K::Tab,
417 mods: KeyModifiers::ALT,
418 },
419 PrevNew,
420 ),
421 (KeyPattern::ch('c'), ChangeMailbox),
422 (KeyPattern::alt('c'), ChangeMailboxReadOnly),
423 (KeyPattern::ch('y'), Folders),
424 (KeyPattern::ch('v'), Attachments),
425 (KeyPattern::alt('v'), FoldThread),
426 (KeyPattern::alt('V'), FoldAll),
427 (KeyPattern::ch('p'), Print),
428 (KeyPattern::ch('t'), Tag),
429 (KeyPattern::ch(';'), TagPrefix),
430 (KeyPattern::alt('d'), DeleteThread),
431 (KeyPattern::alt('u'), UndeleteThread),
432 (KeyPattern::alt('t'), TagThread),
433 (KeyPattern::ctrl('d'), DeleteSubthread),
434 (KeyPattern::ctrl('u'), UndeleteSubthread),
435 (KeyPattern::alt('n'), NextThread),
436 (KeyPattern::alt('p'), PrevThread),
437 (KeyPattern::ch('#'), BreakThread),
438 (KeyPattern::ch('&'), LinkThreads),
439 (KeyPattern::ctrl('r'), ReadThread),
440 (KeyPattern::alt('r'), ReadSubthread),
441 (KeyPattern::ch('P'), ParentMessage),
442 (KeyPattern::ch('Y'), EditLabel),
443 (KeyPattern::ch('V'), ShowVersion),
444 (KeyPattern::alt('l'), ShowLimit),
445 (KeyPattern::ch('@'), DisplayAddress),
446 (KeyPattern::ch('%'), ToggleWrite),
447 (KeyPattern::ch('H'), PageTop),
448 (KeyPattern::ch('M'), PageMiddle),
449 (KeyPattern::ch('z'), Undo),
450 (KeyPattern::ch('D'), DeletePattern),
451 (KeyPattern::ch('U'), UndeletePattern),
452 (KeyPattern::ch('T'), TagPattern),
453 (KeyPattern::ctrl('t'), UntagPattern),
454 (KeyPattern::ch('G'), FetchMail),
455 (KeyPattern::ch('s'), Save),
456 (KeyPattern::ch('C'), Copy),
457 (KeyPattern::alt('s'), DecodeSave),
458 (KeyPattern::alt('C'), DecodeCopy),
459 (KeyPattern::ch('|'), Pipe),
460 (KeyPattern::ch('b'), Bounce),
461 (KeyPattern::ch('e'), Edit),
462 (KeyPattern::alt('e'), Resend),
463 (KeyPattern::ch('B'), SidebarToggle),
464 (KeyPattern::ctrl('n'), SidebarNext),
465 (KeyPattern::ctrl('p'), SidebarPrev),
466 (KeyPattern::ctrl('o'), SidebarOpen),
467 (KeyPattern::ch('a'), CreateAlias),
468 (KeyPattern::ch('Q'), Query),
469 (KeyPattern::ch('X'), Notmuch),
470 (KeyPattern::ch(':'), EnterCommand),
471 (KeyPattern::ch('!'), Shell),
472 (KeyPattern::ctrl('l'), Redraw),
473 (KeyPattern::ctrl('z'), Suspend),
474 (KeyPattern::ch('?'), Help),
475 (KeyPattern::ch('~'), MarkMessage),
476 (KeyPattern::alt('L'), ListAction),
477 ]
478}
479
480fn pager_defaults() -> Vec<(KeyPattern, PagerAction)> {
481 use KeyCode as K;
482 use PagerAction::*;
483 vec![
484 (KeyPattern::ch('q'), Back),
485 (KeyPattern::ch('i'), Back),
486 (KeyPattern::plain(K::Esc), Back),
487 (KeyPattern::plain(K::Enter), Down),
490 (KeyPattern::plain(K::Backspace), Up),
491 (KeyPattern::ch('j'), NextUndeleted),
492 (KeyPattern::plain(K::Down), NextUndeleted),
493 (KeyPattern::plain(K::Right), NextUndeleted),
494 (KeyPattern::ch('k'), PrevUndeleted),
495 (KeyPattern::plain(K::Up), PrevUndeleted),
496 (KeyPattern::plain(K::Left), PrevUndeleted),
497 (KeyPattern::ch(' '), PageDown),
498 (KeyPattern::plain(K::PageDown), PageDown),
499 (KeyPattern::ch('-'), PageUp),
500 (KeyPattern::plain(K::PageUp), PageUp),
501 (KeyPattern::ctrl('d'), HalfDown),
502 (KeyPattern::ctrl('u'), HalfUp),
503 (KeyPattern::plain(K::Home), Top),
504 (KeyPattern::plain(K::End), Bottom),
505 (KeyPattern::ch('T'), ToggleQuoted),
506 (KeyPattern::ch('S'), SkipQuoted),
507 (KeyPattern::ch('J'), NextMsg),
508 (KeyPattern::ch('K'), PrevMsg),
509 (KeyPattern::ch('d'), Delete),
510 (KeyPattern::ch('u'), Undelete),
511 (KeyPattern::ch('F'), Flag),
512 (KeyPattern::ch('t'), Tag),
513 (KeyPattern::ch('z'), Undo),
516 (KeyPattern::ctrl('l'), Redraw),
517 (KeyPattern::ctrl('z'), Suspend),
518 (KeyPattern::ch('h'), Headers),
519 (KeyPattern::ch('/'), Search),
520 (KeyPattern::ch('n'), SearchNext),
521 (KeyPattern::ch('N'), SearchPrev),
522 (KeyPattern::ch('\\'), SearchToggle),
523 (KeyPattern::ch('v'), Attachments),
524 (KeyPattern::ch('m'), Compose),
525 (KeyPattern::ch('r'), Reply),
526 (KeyPattern::ch('g'), GroupReply),
527 (KeyPattern::ch('L'), ListReply),
528 (KeyPattern::ch('f'), Forward),
529 (KeyPattern::ch('p'), Print),
530 (KeyPattern::ch('s'), Save),
531 (KeyPattern::ch('C'), Copy),
532 (KeyPattern::ch('|'), Pipe),
533 (KeyPattern::ch('b'), Bounce),
534 (KeyPattern::ch('e'), Edit),
535 (KeyPattern::alt('e'), Resend),
536 (KeyPattern::ch('a'), CreateAlias),
537 (KeyPattern::ch(':'), EnterCommand),
538 (KeyPattern::ch('?'), Help),
539 (KeyPattern::alt('L'), ListAction),
540 ]
541}
542
543impl Keymap {
544 pub fn with_config(
548 index_over: &HashMap<String, String>,
549 pager_over: &HashMap<String, String>,
550 macros_index: &HashMap<String, String>,
551 macros_pager: &HashMap<String, String>,
552 ) -> (Keymap, Vec<String>) {
553 let mut warnings = Vec::new();
554 let mut index = index_defaults();
555 for (action_name, key_str) in index_over {
556 let (Some(action), Some(key)) = (Function::from_name(action_name), parse_key(key_str))
557 else {
558 warnings.push(format!("bad index binding {action_name} = {key_str:?}"));
559 continue;
560 };
561 index.retain(|(k, a)| *a != action && *k != key);
562 index.push((key, action));
563 }
564 let mut pager = pager_defaults();
565 for (action_name, key_str) in pager_over {
566 let (Some(action), Some(key)) =
567 (PagerAction::from_name(action_name), parse_key(key_str))
568 else {
569 warnings.push(format!("bad pager binding {action_name} = {key_str:?}"));
570 continue;
571 };
572 pager.retain(|(k, a)| *a != action && *k != key);
573 pager.push((key, action));
574 }
575 let mut macros = |table: &HashMap<String, String>, menu: &str| {
576 let mut out = Vec::new();
577 for (key_str, seq_str) in table {
578 let (Some(key), Some(seq)) = (parse_key(key_str), parse_sequence(seq_str)) else {
579 warnings.push(format!("bad {menu} macro {key_str} = {seq_str:?}"));
580 continue;
581 };
582 out.push((key, seq, seq_str.clone()));
583 }
584 out
585 };
586 let macros_index = macros(macros_index, "index");
587 let macros_pager = macros(macros_pager, "pager");
588 (
589 Keymap {
590 index,
591 pager,
592 macros_index,
593 macros_pager,
594 },
595 warnings,
596 )
597 }
598
599 pub fn lookup_index_macro(&self, key: &KeyEvent) -> Option<&[KeyEvent]> {
601 self.macros_index
602 .iter()
603 .find(|(p, _, _)| p.matches(key))
604 .map(|(_, seq, _)| seq.as_slice())
605 }
606
607 pub fn lookup_pager_macro(&self, key: &KeyEvent) -> Option<&[KeyEvent]> {
608 self.macros_pager
609 .iter()
610 .find(|(p, _, _)| p.matches(key))
611 .map(|(_, seq, _)| seq.as_slice())
612 }
613
614 pub fn lookup_index(&self, key: &KeyEvent) -> Option<Function> {
615 self.index
616 .iter()
617 .find(|(p, _)| p.matches(key))
618 .map(|&(_, a)| a)
619 }
620
621 pub fn lookup_pager(&self, key: &KeyEvent) -> Option<PagerAction> {
622 self.pager
623 .iter()
624 .find(|(p, _)| p.matches(key))
625 .map(|&(_, a)| a)
626 }
627
628 pub fn help_lines(&self) -> Vec<String> {
630 let mut lines = vec!["Index keys".to_string(), String::new()];
631 for &action in Function::all() {
632 let keys: Vec<String> = self
633 .index
634 .iter()
635 .filter(|&&(_, a)| a == action)
636 .map(|(k, _)| k.display())
637 .collect();
638 if !keys.is_empty() {
639 lines.push(format!(" {:<16} {}", keys.join(" "), action.describe()));
640 }
641 }
642 lines.extend([String::new(), "Pager keys".to_string(), String::new()]);
643 for &action in PagerAction::all() {
644 let keys: Vec<String> = self
645 .pager
646 .iter()
647 .filter(|&&(_, a)| a == action)
648 .map(|(k, _)| k.display())
649 .collect();
650 if !keys.is_empty() {
651 lines.push(format!(" {:<16} {}", keys.join(" "), action.describe()));
652 }
653 }
654 for (title, table) in [
655 ("Index macros", &self.macros_index),
656 ("Pager macros", &self.macros_pager),
657 ] {
658 if !table.is_empty() {
659 lines.extend([String::new(), title.to_string(), String::new()]);
660 for (key, _, raw) in table {
661 lines.push(format!(" {:<16} {raw}", key.display()));
662 }
663 }
664 }
665 lines.extend(
666 [
667 "",
668 "Patterns (limit/search)",
669 "",
670 " ~f x from ~s x subject ~b x body",
671 " ~t x to ~c x cc ~C x to or cc",
672 " ~e x sender ~d spec date word subject or from",
673 " ~N new ~U unread ~F flagged ~D deleted ~T tagged",
674 " ~p addressed to me",
675 "",
676 " x is a case-insensitive regex; \"quotes\" keep spaces.",
677 " ~d: 24/12/2026, 1/6/2026-30/6/2026, 24/12-, <1w, >2d, =3d",
678 " Terms AND; ! negates, | ORs, () groups:",
679 " !~D (~f jane | ~t jane) ~d <1m",
680 ]
681 .map(String::from),
682 );
683 lines
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690
691 #[test]
692 fn parse_key_forms() {
693 assert_eq!(parse_key("x"), Some(KeyPattern::ch('x')));
694 assert_eq!(parse_key("X"), Some(KeyPattern::ch('X')));
695 assert_eq!(parse_key("ctrl+f"), Some(KeyPattern::ctrl('f')));
696 assert_eq!(parse_key("Alt+v"), Some(KeyPattern::alt('v')));
697 assert_eq!(parse_key("space"), Some(KeyPattern::ch(' ')));
698 assert_eq!(
699 parse_key("pgdn"),
700 Some(KeyPattern::plain(KeyCode::PageDown))
701 );
702 assert_eq!(parse_key("enter"), Some(KeyPattern::plain(KeyCode::Enter)));
703 assert!(parse_key("bogus-key").is_none());
704 }
705
706 #[test]
707 fn remap_replaces_defaults_and_conflicts() {
708 let mut over = HashMap::new();
709 over.insert("sync".to_string(), "w".to_string());
710 over.insert("delete".to_string(), "ctrl+d".to_string());
711 let (map, warnings) =
712 Keymap::with_config(&over, &HashMap::new(), &HashMap::new(), &HashMap::new());
713 assert!(warnings.is_empty());
714 let ev = |p: KeyPattern| KeyEvent::new(p.code, p.mods);
715 assert_eq!(
716 map.lookup_index(&ev(KeyPattern::ch('w'))),
717 Some(Function::Sync)
718 );
719 assert_eq!(map.lookup_index(&ev(KeyPattern::ch('$'))), None);
720 assert_eq!(
721 map.lookup_index(&ev(KeyPattern::ctrl('d'))),
722 Some(Function::Delete)
723 );
724 assert_eq!(map.lookup_index(&ev(KeyPattern::ch('d'))), None);
725 }
726
727 #[test]
728 fn bad_bindings_warn_and_keep_defaults() {
729 let mut over = HashMap::new();
730 over.insert("frobnicate".to_string(), "z".to_string());
731 let (map, warnings) =
732 Keymap::with_config(&over, &HashMap::new(), &HashMap::new(), &HashMap::new());
733 assert_eq!(warnings.len(), 1);
734 let ev = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
735 assert_eq!(map.lookup_index(&ev), Some(Function::Quit));
736 }
737
738 #[test]
739 fn parse_sequence_forms() {
740 let seq = parse_sequence("l~f jane<enter>").unwrap();
741 assert_eq!(seq.len(), 9);
742 assert_eq!(seq[0].code, KeyCode::Char('l'));
743 assert_eq!(seq[2].code, KeyCode::Char('f'));
744 assert_eq!(seq[3].code, KeyCode::Char(' '));
745 assert_eq!(seq[8].code, KeyCode::Enter);
746 let seq = parse_sequence("<ctrl+x><Esc>").unwrap();
747 assert_eq!(seq[0].code, KeyCode::Char('x'));
748 assert!(seq[0].modifiers.contains(KeyModifiers::CONTROL));
749 assert_eq!(seq[1].code, KeyCode::Esc);
750 assert!(parse_sequence("<bogus>").is_none());
751 assert!(parse_sequence("<unclosed").is_none());
752 assert!(parse_sequence("").unwrap().is_empty());
753 }
754
755 #[test]
756 fn macros_parse_shadow_and_warn() {
757 let mut macros_index = HashMap::new();
758 macros_index.insert("d".to_string(), "l~f jane<enter>".to_string());
759 macros_index.insert("Z".to_string(), "<bogus>".to_string());
760 let (map, warnings) = Keymap::with_config(
761 &HashMap::new(),
762 &HashMap::new(),
763 ¯os_index,
764 &HashMap::new(),
765 );
766 assert_eq!(warnings.len(), 1);
767 assert!(warnings[0].contains("bad index macro Z"), "{warnings:?}");
768 let ev = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
769 assert_eq!(map.lookup_index_macro(&ev).unwrap().len(), 9);
772 assert!(
773 map.lookup_pager_macro(&ev).is_none(),
774 "index macro must not leak into the pager"
775 );
776 let help = map.help_lines().join("\n");
778 assert!(help.contains("Index macros"), "{help}");
779 assert!(help.contains("l~f jane<enter>"), "{help}");
780 }
781
782 #[test]
783 fn shift_in_event_does_not_block_match() {
784 let ev = KeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT);
786 let (map, _) = Keymap::with_config(
787 &HashMap::new(),
788 &HashMap::new(),
789 &HashMap::new(),
790 &HashMap::new(),
791 );
792 assert_eq!(map.lookup_index(&ev), Some(Function::Flag));
793 }
794}