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 Urls,
169}
170
171impl PagerAction {
172 pub fn name(self) -> &'static str {
173 use PagerAction::*;
174 match self {
175 Back => "back",
176 Down => "down",
177 Up => "up",
178 PageDown => "page-down",
179 PageUp => "page-up",
180 HalfDown => "half-down",
181 HalfUp => "half-up",
182 Top => "top",
183 Bottom => "bottom",
184 ToggleQuoted => "toggle-quoted",
185 SkipQuoted => "skip-quoted",
186 NextMsg => "next",
187 PrevMsg => "previous",
188 NextUndeleted => "next-undeleted",
189 PrevUndeleted => "previous-undeleted",
190 Delete => "delete",
191 Undelete => "undelete",
192 Flag => "flag",
193 ToggleNew => "toggle-new",
194 Tag => "tag",
195 Undo => "undo",
196 Redraw => "refresh",
197 Suspend => "suspend",
198 Headers => "headers",
199 Search => "search",
200 SearchNext => "search-next",
201 SearchPrev => "search-prev",
202 SearchToggle => "search-toggle",
203 Attachments => "attachments",
204 Compose => "compose",
205 Reply => "reply",
206 GroupReply => "group-reply",
207 ListReply => "list-reply",
208 Forward => "forward",
209 Print => "print",
210 Save => "save",
211 Copy => "copy",
212 Pipe => "pipe",
213 Bounce => "bounce",
214 Resend => "resend",
215 Edit => "edit",
216 CreateAlias => "create-alias",
217 EnterCommand => "enter-command",
218 Help => "help",
219 ListAction => "list-action",
220 ErrorHistory => "error-history",
221 WhatKey => "what-key",
222 Urls => "urls",
223 }
224 }
225
226 pub fn describe(self) -> &'static str {
227 use PagerAction::*;
228 match self {
229 Back => "back to the index",
230 Down => "scroll down one line",
231 Up => "scroll up one line",
232 PageDown => "page down",
233 PageUp => "page up",
234 HalfDown => "scroll down half a page",
235 HalfUp => "scroll up half a page",
236 Top => "jump to the top",
237 Bottom => "jump to the bottom",
238 ToggleQuoted => "show/hide quoted text",
239 SkipQuoted => "skip past the quoted text below",
240 NextMsg => "open next message",
241 PrevMsg => "open previous message",
242 NextUndeleted => "open next undeleted message",
243 PrevUndeleted => "open previous undeleted message",
244 Delete => "delete and advance",
245 Undelete => "unmark deletion",
246 Flag => "toggle flagged mark",
247 ToggleNew => "toggle read/unread (unbound here: N is the backwards search)",
248 Tag => "toggle the tag on this message",
249 Undo => "cancel a held send, or undo the last mark change",
250 Redraw => "repaint the screen",
251 Suspend => "suspend rmut (fg brings it back)",
252 Headers => "toggle full headers",
253 Search => "search the displayed text (unlike the index /, which matches messages)",
254 SearchNext => "next match of the pager search",
255 SearchPrev => "previous match of the pager search",
256 SearchToggle => "toggle the search highlighting",
257 Attachments => "list message parts",
258 Compose => "compose a new message",
259 Reply => "reply to sender",
260 GroupReply => "reply to all",
261 ListReply => "reply to the mailing list only",
262 Forward => "forward message",
263 Print => "pipe message to the print command",
264 Save => "save (copy + mark deleted) to a mailbox",
265 Copy => "copy to a mailbox (original stays)",
266 Pipe => "pipe raw message to a shell command",
267 Bounce => "bounce (resend) message to new recipients",
268 Resend => "edit the message as a new draft",
269 Edit => "edit the raw message and replace it",
270 CreateAlias => "add the sender to the alias file",
271 EnterCommand => "run a config command (set/bind/macro/color/...)",
272 Help => "this help",
273 ListAction => "act on the message's List-* headers (subscribe, help, ...)",
274 ErrorHistory => "show the recent errors",
275 WhatKey => "say what a key is (Ctrl+G ends it)",
276 Urls => "list the message's links, to open or copy one",
277 }
278 }
279
280 fn all() -> &'static [PagerAction] {
281 use PagerAction::*;
282 &[
283 Back,
284 Down,
285 Up,
286 PageDown,
287 PageUp,
288 HalfDown,
289 HalfUp,
290 Top,
291 Bottom,
292 ToggleQuoted,
293 SkipQuoted,
294 NextMsg,
295 PrevMsg,
296 NextUndeleted,
297 PrevUndeleted,
298 Delete,
299 Undelete,
300 Flag,
301 ToggleNew,
302 Tag,
303 Undo,
304 Redraw,
305 Suspend,
306 Headers,
307 Search,
308 SearchNext,
309 SearchPrev,
310 SearchToggle,
311 Attachments,
312 Compose,
313 Reply,
314 GroupReply,
315 ListReply,
316 Forward,
317 Print,
318 Save,
319 Copy,
320 Pipe,
321 Bounce,
322 Resend,
323 Edit,
324 CreateAlias,
325 EnterCommand,
326 Help,
327 ListAction,
328 ErrorHistory,
329 WhatKey,
330 Urls,
331 ]
332 }
333
334 pub fn from_name(name: &str) -> Option<PagerAction> {
335 let name = match name {
337 "mark-as-new" => "toggle-new",
338 other => other,
339 };
340 PagerAction::all()
341 .iter()
342 .copied()
343 .find(|a| a.name() == name)
344 }
345}
346
347pub fn parse_sequence(input: &str) -> Option<Vec<KeyEvent>> {
351 let mut out = Vec::new();
352 let mut chars = input.chars();
353 while let Some(c) = chars.next() {
354 if c == '<' {
355 let mut name = String::new();
356 loop {
357 match chars.next() {
358 Some('>') => break,
359 Some(c) => name.push(c),
360 None => return None,
361 }
362 }
363 let p = parse_key(&name)?;
364 out.push(KeyEvent::new(p.code, p.mods));
365 } else {
366 out.push(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
367 }
368 }
369 Some(out)
370}
371
372pub struct Keymap {
373 pub index: Vec<(KeyPattern, Function)>,
374 pub pager: Vec<(KeyPattern, PagerAction)>,
375 pub macros_index: Vec<(KeyPattern, Vec<KeyEvent>, String)>,
379 pub macros_pager: Vec<(KeyPattern, Vec<KeyEvent>, String)>,
380}
381
382fn index_defaults() -> Vec<(KeyPattern, Function)> {
383 use Function::*;
384 use KeyCode as K;
385 vec![
386 (KeyPattern::ch('q'), Quit),
387 (KeyPattern::ch('x'), Abort),
388 (KeyPattern::ch('j'), Down),
389 (KeyPattern::plain(K::Down), Down),
390 (KeyPattern::ch('k'), Up),
391 (KeyPattern::plain(K::Up), Up),
392 (KeyPattern::plain(K::PageDown), PageDown),
393 (KeyPattern::ctrl('f'), PageDown),
394 (KeyPattern::plain(K::PageUp), PageUp),
395 (KeyPattern::ctrl('b'), PageUp),
396 (KeyPattern::ch(' '), PageDown),
397 (KeyPattern::ch('='), First),
398 (KeyPattern::plain(K::Home), First),
399 (KeyPattern::ch('*'), Last),
400 (KeyPattern::plain(K::End), Last),
401 (KeyPattern::plain(K::Enter), View),
402 (KeyPattern::ch('d'), Delete),
403 (KeyPattern::ch('u'), Undelete),
404 (KeyPattern::ch('F'), Flag),
405 (KeyPattern::ch('N'), ToggleNew),
406 (KeyPattern::alt('a'), MarkAllRead),
407 (KeyPattern::ch('$'), Sync),
408 (KeyPattern::ch('m'), Compose),
409 (KeyPattern::ch('r'), Reply),
410 (KeyPattern::ch('g'), GroupReply),
411 (KeyPattern::ch('L'), ListReply),
412 (KeyPattern::ch('f'), Forward),
413 (KeyPattern::ch('o'), Sort),
414 (KeyPattern::ch('l'), Limit),
415 (KeyPattern::ch('/'), Search),
416 (KeyPattern::alt('/'), SearchReverse),
417 (KeyPattern::ch('n'), SearchNext),
418 (KeyPattern::plain(K::Tab), NextNew),
419 (
420 KeyPattern {
421 code: K::Tab,
422 mods: KeyModifiers::ALT,
423 },
424 PrevNew,
425 ),
426 (KeyPattern::ch('c'), ChangeMailbox),
427 (KeyPattern::alt('c'), ChangeMailboxReadOnly),
428 (KeyPattern::ch('y'), Folders),
429 (KeyPattern::ch('v'), Attachments),
430 (KeyPattern::alt('v'), FoldThread),
431 (KeyPattern::alt('V'), FoldAll),
432 (KeyPattern::ch('p'), Print),
433 (KeyPattern::ch('t'), Tag),
434 (KeyPattern::ch(';'), TagPrefix),
435 (KeyPattern::alt('d'), DeleteThread),
436 (KeyPattern::alt('u'), UndeleteThread),
437 (KeyPattern::alt('t'), TagThread),
438 (KeyPattern::ctrl('d'), DeleteSubthread),
439 (KeyPattern::ctrl('u'), UndeleteSubthread),
440 (KeyPattern::alt('n'), NextThread),
441 (KeyPattern::alt('p'), PrevThread),
442 (KeyPattern::ch('#'), BreakThread),
443 (KeyPattern::ch('&'), LinkThreads),
444 (KeyPattern::ctrl('r'), ReadThread),
445 (KeyPattern::alt('r'), ReadSubthread),
446 (KeyPattern::ch('P'), ParentMessage),
447 (KeyPattern::ch('Y'), EditLabel),
448 (KeyPattern::ch('V'), ShowVersion),
449 (KeyPattern::alt('l'), ShowLimit),
450 (KeyPattern::ch('@'), DisplayAddress),
451 (KeyPattern::ch('%'), ToggleWrite),
452 (KeyPattern::ch('H'), PageTop),
453 (KeyPattern::ch('M'), PageMiddle),
454 (KeyPattern::ch('z'), Undo),
455 (KeyPattern::ch('D'), DeletePattern),
456 (KeyPattern::ch('U'), UndeletePattern),
457 (KeyPattern::ch('T'), TagPattern),
458 (KeyPattern::ctrl('t'), UntagPattern),
459 (KeyPattern::ch('G'), FetchMail),
460 (KeyPattern::ch('s'), Save),
461 (KeyPattern::ch('C'), Copy),
462 (KeyPattern::alt('s'), DecodeSave),
463 (KeyPattern::alt('C'), DecodeCopy),
464 (KeyPattern::ch('|'), Pipe),
465 (KeyPattern::ch('b'), Bounce),
466 (KeyPattern::ch('e'), Edit),
467 (KeyPattern::alt('e'), Resend),
468 (KeyPattern::ch('B'), SidebarToggle),
469 (KeyPattern::ctrl('n'), SidebarNext),
470 (KeyPattern::ctrl('p'), SidebarPrev),
471 (KeyPattern::ctrl('o'), SidebarOpen),
472 (KeyPattern::ch('a'), CreateAlias),
473 (KeyPattern::ch('Q'), Query),
474 (KeyPattern::ch('X'), Notmuch),
475 (KeyPattern::ch(':'), EnterCommand),
476 (KeyPattern::ch('!'), Shell),
477 (KeyPattern::ctrl('l'), Redraw),
478 (KeyPattern::ctrl('z'), Suspend),
479 (KeyPattern::ch('?'), Help),
480 (KeyPattern::ch('~'), MarkMessage),
481 (KeyPattern::alt('L'), ListAction),
482 ]
483}
484
485pub fn resolve_function(menu: rmut_core::command::Menu, name: &str) -> Option<String> {
490 if menu == rmut_core::command::Menu::Index {
491 if Function::from_name(name).is_some() {
492 return Some(name.to_string());
493 }
494 let mapped = rmut_core::muttrc::index_function(name)?;
495 Function::from_name(mapped).map(|_| mapped.to_string())
496 } else {
497 if PagerAction::from_name(name).is_some() {
498 return Some(name.to_string());
499 }
500 let mapped = rmut_core::muttrc::pager_function(name)?;
501 PagerAction::from_name(mapped).map(|_| mapped.to_string())
502 }
503}
504
505fn pager_defaults() -> Vec<(KeyPattern, PagerAction)> {
506 use KeyCode as K;
507 use PagerAction::*;
508 vec![
509 (KeyPattern::ch('q'), Back),
510 (KeyPattern::ch('i'), Back),
511 (KeyPattern::plain(K::Esc), Back),
512 (KeyPattern::plain(K::Enter), Down),
515 (KeyPattern::plain(K::Backspace), Up),
516 (KeyPattern::ch('j'), NextUndeleted),
517 (KeyPattern::plain(K::Down), NextUndeleted),
518 (KeyPattern::plain(K::Right), NextUndeleted),
519 (KeyPattern::ch('k'), PrevUndeleted),
520 (KeyPattern::plain(K::Up), PrevUndeleted),
521 (KeyPattern::plain(K::Left), PrevUndeleted),
522 (KeyPattern::ch(' '), PageDown),
523 (KeyPattern::plain(K::PageDown), PageDown),
524 (KeyPattern::ch('-'), PageUp),
525 (KeyPattern::plain(K::PageUp), PageUp),
526 (KeyPattern::ctrl('d'), HalfDown),
527 (KeyPattern::ctrl('u'), HalfUp),
528 (KeyPattern::plain(K::Home), Top),
529 (KeyPattern::plain(K::End), Bottom),
530 (KeyPattern::ch('T'), ToggleQuoted),
531 (KeyPattern::ch('S'), SkipQuoted),
532 (KeyPattern::ch('J'), NextMsg),
533 (KeyPattern::ch('K'), PrevMsg),
534 (KeyPattern::ch('d'), Delete),
535 (KeyPattern::ch('u'), Undelete),
536 (KeyPattern::ch('F'), Flag),
537 (KeyPattern::ch('t'), Tag),
538 (KeyPattern::ch('z'), Undo),
541 (KeyPattern::ctrl('l'), Redraw),
542 (KeyPattern::ctrl('z'), Suspend),
543 (KeyPattern::ch('h'), Headers),
544 (KeyPattern::ch('/'), Search),
545 (KeyPattern::ch('n'), SearchNext),
546 (KeyPattern::ch('N'), SearchPrev),
547 (KeyPattern::ch('\\'), SearchToggle),
548 (KeyPattern::ch('v'), Attachments),
549 (KeyPattern::ch('m'), Compose),
550 (KeyPattern::ch('r'), Reply),
551 (KeyPattern::ch('g'), GroupReply),
552 (KeyPattern::ch('L'), ListReply),
553 (KeyPattern::ch('f'), Forward),
554 (KeyPattern::ch('p'), Print),
555 (KeyPattern::ch('s'), Save),
556 (KeyPattern::ch('C'), Copy),
557 (KeyPattern::ch('|'), Pipe),
558 (KeyPattern::ch('b'), Bounce),
559 (KeyPattern::ch('e'), Edit),
560 (KeyPattern::alt('e'), Resend),
561 (KeyPattern::ch('a'), CreateAlias),
562 (KeyPattern::ch(':'), EnterCommand),
563 (KeyPattern::ch('?'), Help),
564 (KeyPattern::alt('L'), ListAction),
565 (KeyPattern::ctrl('b'), Urls),
568 ]
569}
570
571impl Keymap {
572 pub fn with_config(
576 index_over: &HashMap<String, String>,
577 pager_over: &HashMap<String, String>,
578 macros_index: &HashMap<String, String>,
579 macros_pager: &HashMap<String, String>,
580 ) -> (Keymap, Vec<String>) {
581 let mut warnings = Vec::new();
582 let mut index = index_defaults();
583 for (action_name, key_str) in index_over {
584 let (Some(action), Some(key)) = (Function::from_name(action_name), parse_key(key_str))
585 else {
586 warnings.push(format!("bad index binding {action_name} = {key_str:?}"));
587 continue;
588 };
589 index.retain(|(k, a)| *a != action && *k != key);
590 index.push((key, action));
591 }
592 let mut pager = pager_defaults();
593 for (action_name, key_str) in pager_over {
594 let (Some(action), Some(key)) =
595 (PagerAction::from_name(action_name), parse_key(key_str))
596 else {
597 warnings.push(format!("bad pager binding {action_name} = {key_str:?}"));
598 continue;
599 };
600 pager.retain(|(k, a)| *a != action && *k != key);
601 pager.push((key, action));
602 }
603 let mut macros = |table: &HashMap<String, String>, menu: &str| {
604 let mut out = Vec::new();
605 for (key_str, seq_str) in table {
606 let (Some(key), Some(seq)) = (parse_key(key_str), parse_sequence(seq_str)) else {
607 warnings.push(format!("bad {menu} macro {key_str} = {seq_str:?}"));
608 continue;
609 };
610 out.push((key, seq, seq_str.clone()));
611 }
612 out
613 };
614 let macros_index = macros(macros_index, "index");
615 let macros_pager = macros(macros_pager, "pager");
616 (
617 Keymap {
618 index,
619 pager,
620 macros_index,
621 macros_pager,
622 },
623 warnings,
624 )
625 }
626
627 pub fn lookup_index_macro(&self, key: &KeyEvent) -> Option<&[KeyEvent]> {
629 self.macros_index
630 .iter()
631 .find(|(p, _, _)| p.matches(key))
632 .map(|(_, seq, _)| seq.as_slice())
633 }
634
635 pub fn lookup_pager_macro(&self, key: &KeyEvent) -> Option<&[KeyEvent]> {
636 self.macros_pager
637 .iter()
638 .find(|(p, _, _)| p.matches(key))
639 .map(|(_, seq, _)| seq.as_slice())
640 }
641
642 pub fn lookup_index(&self, key: &KeyEvent) -> Option<Function> {
643 self.index
644 .iter()
645 .find(|(p, _)| p.matches(key))
646 .map(|&(_, a)| a)
647 }
648
649 pub fn lookup_pager(&self, key: &KeyEvent) -> Option<PagerAction> {
650 self.pager
651 .iter()
652 .find(|(p, _)| p.matches(key))
653 .map(|&(_, a)| a)
654 }
655
656 pub fn help_lines(&self) -> Vec<String> {
658 let mut lines = vec!["Index keys".to_string(), String::new()];
659 for &action in Function::all() {
660 let keys: Vec<String> = self
661 .index
662 .iter()
663 .filter(|&&(_, a)| a == action)
664 .map(|(k, _)| k.display())
665 .collect();
666 if !keys.is_empty() {
667 lines.push(format!(" {:<16} {}", keys.join(" "), action.describe()));
668 }
669 }
670 lines.extend([String::new(), "Pager keys".to_string(), String::new()]);
671 for &action in PagerAction::all() {
672 let keys: Vec<String> = self
673 .pager
674 .iter()
675 .filter(|&&(_, a)| a == action)
676 .map(|(k, _)| k.display())
677 .collect();
678 if !keys.is_empty() {
679 lines.push(format!(" {:<16} {}", keys.join(" "), action.describe()));
680 }
681 }
682 for (title, table) in [
683 ("Index macros", &self.macros_index),
684 ("Pager macros", &self.macros_pager),
685 ] {
686 if !table.is_empty() {
687 lines.extend([String::new(), title.to_string(), String::new()]);
688 for (key, _, raw) in table {
689 lines.push(format!(" {:<16} {raw}", key.display()));
690 }
691 }
692 }
693 lines.extend(
694 [
695 "",
696 "Patterns (limit/search)",
697 "",
698 " ~f x from ~s x subject ~b x body",
699 " ~t x to ~c x cc ~C x to or cc",
700 " ~e x sender ~d spec date word subject or from",
701 " ~N new ~U unread ~F flagged ~D deleted ~T tagged",
702 " ~p addressed to me",
703 "",
704 " x is a case-insensitive regex; \"quotes\" keep spaces.",
705 " ~d: 24/12/2026, 1/6/2026-30/6/2026, 24/12-, <1w, >2d, =3d",
706 " Terms AND; ! negates, | ORs, () groups:",
707 " !~D (~f jane | ~t jane) ~d <1m",
708 ]
709 .map(String::from),
710 );
711 lines
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718
719 #[test]
720 fn parse_key_forms() {
721 assert_eq!(parse_key("x"), Some(KeyPattern::ch('x')));
722 assert_eq!(parse_key("X"), Some(KeyPattern::ch('X')));
723 assert_eq!(parse_key("ctrl+f"), Some(KeyPattern::ctrl('f')));
724 assert_eq!(parse_key("Alt+v"), Some(KeyPattern::alt('v')));
725 assert_eq!(parse_key("space"), Some(KeyPattern::ch(' ')));
726 assert_eq!(
727 parse_key("pgdn"),
728 Some(KeyPattern::plain(KeyCode::PageDown))
729 );
730 assert_eq!(parse_key("enter"), Some(KeyPattern::plain(KeyCode::Enter)));
731 assert!(parse_key("bogus-key").is_none());
732 }
733
734 #[test]
735 fn remap_replaces_defaults_and_conflicts() {
736 let mut over = HashMap::new();
737 over.insert("sync".to_string(), "w".to_string());
738 over.insert("delete".to_string(), "ctrl+d".to_string());
739 let (map, warnings) =
740 Keymap::with_config(&over, &HashMap::new(), &HashMap::new(), &HashMap::new());
741 assert!(warnings.is_empty());
742 let ev = |p: KeyPattern| KeyEvent::new(p.code, p.mods);
743 assert_eq!(
744 map.lookup_index(&ev(KeyPattern::ch('w'))),
745 Some(Function::Sync)
746 );
747 assert_eq!(map.lookup_index(&ev(KeyPattern::ch('$'))), None);
748 assert_eq!(
749 map.lookup_index(&ev(KeyPattern::ctrl('d'))),
750 Some(Function::Delete)
751 );
752 assert_eq!(map.lookup_index(&ev(KeyPattern::ch('d'))), None);
753 }
754
755 #[test]
756 fn bad_bindings_warn_and_keep_defaults() {
757 let mut over = HashMap::new();
758 over.insert("frobnicate".to_string(), "z".to_string());
759 let (map, warnings) =
760 Keymap::with_config(&over, &HashMap::new(), &HashMap::new(), &HashMap::new());
761 assert_eq!(warnings.len(), 1);
762 let ev = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
763 assert_eq!(map.lookup_index(&ev), Some(Function::Quit));
764 }
765
766 #[test]
767 fn parse_sequence_forms() {
768 let seq = parse_sequence("l~f jane<enter>").unwrap();
769 assert_eq!(seq.len(), 9);
770 assert_eq!(seq[0].code, KeyCode::Char('l'));
771 assert_eq!(seq[2].code, KeyCode::Char('f'));
772 assert_eq!(seq[3].code, KeyCode::Char(' '));
773 assert_eq!(seq[8].code, KeyCode::Enter);
774 let seq = parse_sequence("<ctrl+x><Esc>").unwrap();
775 assert_eq!(seq[0].code, KeyCode::Char('x'));
776 assert!(seq[0].modifiers.contains(KeyModifiers::CONTROL));
777 assert_eq!(seq[1].code, KeyCode::Esc);
778 assert!(parse_sequence("<bogus>").is_none());
779 assert!(parse_sequence("<unclosed").is_none());
780 assert!(parse_sequence("").unwrap().is_empty());
781 }
782
783 #[test]
784 fn macros_parse_shadow_and_warn() {
785 let mut macros_index = HashMap::new();
786 macros_index.insert("d".to_string(), "l~f jane<enter>".to_string());
787 macros_index.insert("Z".to_string(), "<bogus>".to_string());
788 let (map, warnings) = Keymap::with_config(
789 &HashMap::new(),
790 &HashMap::new(),
791 ¯os_index,
792 &HashMap::new(),
793 );
794 assert_eq!(warnings.len(), 1);
795 assert!(warnings[0].contains("bad index macro Z"), "{warnings:?}");
796 let ev = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
797 assert_eq!(map.lookup_index_macro(&ev).unwrap().len(), 9);
800 assert!(
801 map.lookup_pager_macro(&ev).is_none(),
802 "index macro must not leak into the pager"
803 );
804 let help = map.help_lines().join("\n");
806 assert!(help.contains("Index macros"), "{help}");
807 assert!(help.contains("l~f jane<enter>"), "{help}");
808 }
809
810 #[test]
811 fn shift_in_event_does_not_block_match() {
812 let ev = KeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT);
814 let (map, _) = Keymap::with_config(
815 &HashMap::new(),
816 &HashMap::new(),
817 &HashMap::new(),
818 &HashMap::new(),
819 );
820 assert_eq!(map.lookup_index(&ev), Some(Function::Flag));
821 }
822}