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
481pub fn resolve_function(menu: rmut_core::command::Menu, name: &str) -> Option<String> {
486 if menu == rmut_core::command::Menu::Index {
487 if Function::from_name(name).is_some() {
488 return Some(name.to_string());
489 }
490 let mapped = rmut_core::muttrc::index_function(name)?;
491 Function::from_name(mapped).map(|_| mapped.to_string())
492 } else {
493 if PagerAction::from_name(name).is_some() {
494 return Some(name.to_string());
495 }
496 let mapped = rmut_core::muttrc::pager_function(name)?;
497 PagerAction::from_name(mapped).map(|_| mapped.to_string())
498 }
499}
500
501fn pager_defaults() -> Vec<(KeyPattern, PagerAction)> {
502 use KeyCode as K;
503 use PagerAction::*;
504 vec![
505 (KeyPattern::ch('q'), Back),
506 (KeyPattern::ch('i'), Back),
507 (KeyPattern::plain(K::Esc), Back),
508 (KeyPattern::plain(K::Enter), Down),
511 (KeyPattern::plain(K::Backspace), Up),
512 (KeyPattern::ch('j'), NextUndeleted),
513 (KeyPattern::plain(K::Down), NextUndeleted),
514 (KeyPattern::plain(K::Right), NextUndeleted),
515 (KeyPattern::ch('k'), PrevUndeleted),
516 (KeyPattern::plain(K::Up), PrevUndeleted),
517 (KeyPattern::plain(K::Left), PrevUndeleted),
518 (KeyPattern::ch(' '), PageDown),
519 (KeyPattern::plain(K::PageDown), PageDown),
520 (KeyPattern::ch('-'), PageUp),
521 (KeyPattern::plain(K::PageUp), PageUp),
522 (KeyPattern::ctrl('d'), HalfDown),
523 (KeyPattern::ctrl('u'), HalfUp),
524 (KeyPattern::plain(K::Home), Top),
525 (KeyPattern::plain(K::End), Bottom),
526 (KeyPattern::ch('T'), ToggleQuoted),
527 (KeyPattern::ch('S'), SkipQuoted),
528 (KeyPattern::ch('J'), NextMsg),
529 (KeyPattern::ch('K'), PrevMsg),
530 (KeyPattern::ch('d'), Delete),
531 (KeyPattern::ch('u'), Undelete),
532 (KeyPattern::ch('F'), Flag),
533 (KeyPattern::ch('t'), Tag),
534 (KeyPattern::ch('z'), Undo),
537 (KeyPattern::ctrl('l'), Redraw),
538 (KeyPattern::ctrl('z'), Suspend),
539 (KeyPattern::ch('h'), Headers),
540 (KeyPattern::ch('/'), Search),
541 (KeyPattern::ch('n'), SearchNext),
542 (KeyPattern::ch('N'), SearchPrev),
543 (KeyPattern::ch('\\'), SearchToggle),
544 (KeyPattern::ch('v'), Attachments),
545 (KeyPattern::ch('m'), Compose),
546 (KeyPattern::ch('r'), Reply),
547 (KeyPattern::ch('g'), GroupReply),
548 (KeyPattern::ch('L'), ListReply),
549 (KeyPattern::ch('f'), Forward),
550 (KeyPattern::ch('p'), Print),
551 (KeyPattern::ch('s'), Save),
552 (KeyPattern::ch('C'), Copy),
553 (KeyPattern::ch('|'), Pipe),
554 (KeyPattern::ch('b'), Bounce),
555 (KeyPattern::ch('e'), Edit),
556 (KeyPattern::alt('e'), Resend),
557 (KeyPattern::ch('a'), CreateAlias),
558 (KeyPattern::ch(':'), EnterCommand),
559 (KeyPattern::ch('?'), Help),
560 (KeyPattern::alt('L'), ListAction),
561 ]
562}
563
564impl Keymap {
565 pub fn with_config(
569 index_over: &HashMap<String, String>,
570 pager_over: &HashMap<String, String>,
571 macros_index: &HashMap<String, String>,
572 macros_pager: &HashMap<String, String>,
573 ) -> (Keymap, Vec<String>) {
574 let mut warnings = Vec::new();
575 let mut index = index_defaults();
576 for (action_name, key_str) in index_over {
577 let (Some(action), Some(key)) = (Function::from_name(action_name), parse_key(key_str))
578 else {
579 warnings.push(format!("bad index binding {action_name} = {key_str:?}"));
580 continue;
581 };
582 index.retain(|(k, a)| *a != action && *k != key);
583 index.push((key, action));
584 }
585 let mut pager = pager_defaults();
586 for (action_name, key_str) in pager_over {
587 let (Some(action), Some(key)) =
588 (PagerAction::from_name(action_name), parse_key(key_str))
589 else {
590 warnings.push(format!("bad pager binding {action_name} = {key_str:?}"));
591 continue;
592 };
593 pager.retain(|(k, a)| *a != action && *k != key);
594 pager.push((key, action));
595 }
596 let mut macros = |table: &HashMap<String, String>, menu: &str| {
597 let mut out = Vec::new();
598 for (key_str, seq_str) in table {
599 let (Some(key), Some(seq)) = (parse_key(key_str), parse_sequence(seq_str)) else {
600 warnings.push(format!("bad {menu} macro {key_str} = {seq_str:?}"));
601 continue;
602 };
603 out.push((key, seq, seq_str.clone()));
604 }
605 out
606 };
607 let macros_index = macros(macros_index, "index");
608 let macros_pager = macros(macros_pager, "pager");
609 (
610 Keymap {
611 index,
612 pager,
613 macros_index,
614 macros_pager,
615 },
616 warnings,
617 )
618 }
619
620 pub fn lookup_index_macro(&self, key: &KeyEvent) -> Option<&[KeyEvent]> {
622 self.macros_index
623 .iter()
624 .find(|(p, _, _)| p.matches(key))
625 .map(|(_, seq, _)| seq.as_slice())
626 }
627
628 pub fn lookup_pager_macro(&self, key: &KeyEvent) -> Option<&[KeyEvent]> {
629 self.macros_pager
630 .iter()
631 .find(|(p, _, _)| p.matches(key))
632 .map(|(_, seq, _)| seq.as_slice())
633 }
634
635 pub fn lookup_index(&self, key: &KeyEvent) -> Option<Function> {
636 self.index
637 .iter()
638 .find(|(p, _)| p.matches(key))
639 .map(|&(_, a)| a)
640 }
641
642 pub fn lookup_pager(&self, key: &KeyEvent) -> Option<PagerAction> {
643 self.pager
644 .iter()
645 .find(|(p, _)| p.matches(key))
646 .map(|&(_, a)| a)
647 }
648
649 pub fn help_lines(&self) -> Vec<String> {
651 let mut lines = vec!["Index keys".to_string(), String::new()];
652 for &action in Function::all() {
653 let keys: Vec<String> = self
654 .index
655 .iter()
656 .filter(|&&(_, a)| a == action)
657 .map(|(k, _)| k.display())
658 .collect();
659 if !keys.is_empty() {
660 lines.push(format!(" {:<16} {}", keys.join(" "), action.describe()));
661 }
662 }
663 lines.extend([String::new(), "Pager keys".to_string(), String::new()]);
664 for &action in PagerAction::all() {
665 let keys: Vec<String> = self
666 .pager
667 .iter()
668 .filter(|&&(_, a)| a == action)
669 .map(|(k, _)| k.display())
670 .collect();
671 if !keys.is_empty() {
672 lines.push(format!(" {:<16} {}", keys.join(" "), action.describe()));
673 }
674 }
675 for (title, table) in [
676 ("Index macros", &self.macros_index),
677 ("Pager macros", &self.macros_pager),
678 ] {
679 if !table.is_empty() {
680 lines.extend([String::new(), title.to_string(), String::new()]);
681 for (key, _, raw) in table {
682 lines.push(format!(" {:<16} {raw}", key.display()));
683 }
684 }
685 }
686 lines.extend(
687 [
688 "",
689 "Patterns (limit/search)",
690 "",
691 " ~f x from ~s x subject ~b x body",
692 " ~t x to ~c x cc ~C x to or cc",
693 " ~e x sender ~d spec date word subject or from",
694 " ~N new ~U unread ~F flagged ~D deleted ~T tagged",
695 " ~p addressed to me",
696 "",
697 " x is a case-insensitive regex; \"quotes\" keep spaces.",
698 " ~d: 24/12/2026, 1/6/2026-30/6/2026, 24/12-, <1w, >2d, =3d",
699 " Terms AND; ! negates, | ORs, () groups:",
700 " !~D (~f jane | ~t jane) ~d <1m",
701 ]
702 .map(String::from),
703 );
704 lines
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711
712 #[test]
713 fn parse_key_forms() {
714 assert_eq!(parse_key("x"), Some(KeyPattern::ch('x')));
715 assert_eq!(parse_key("X"), Some(KeyPattern::ch('X')));
716 assert_eq!(parse_key("ctrl+f"), Some(KeyPattern::ctrl('f')));
717 assert_eq!(parse_key("Alt+v"), Some(KeyPattern::alt('v')));
718 assert_eq!(parse_key("space"), Some(KeyPattern::ch(' ')));
719 assert_eq!(
720 parse_key("pgdn"),
721 Some(KeyPattern::plain(KeyCode::PageDown))
722 );
723 assert_eq!(parse_key("enter"), Some(KeyPattern::plain(KeyCode::Enter)));
724 assert!(parse_key("bogus-key").is_none());
725 }
726
727 #[test]
728 fn remap_replaces_defaults_and_conflicts() {
729 let mut over = HashMap::new();
730 over.insert("sync".to_string(), "w".to_string());
731 over.insert("delete".to_string(), "ctrl+d".to_string());
732 let (map, warnings) =
733 Keymap::with_config(&over, &HashMap::new(), &HashMap::new(), &HashMap::new());
734 assert!(warnings.is_empty());
735 let ev = |p: KeyPattern| KeyEvent::new(p.code, p.mods);
736 assert_eq!(
737 map.lookup_index(&ev(KeyPattern::ch('w'))),
738 Some(Function::Sync)
739 );
740 assert_eq!(map.lookup_index(&ev(KeyPattern::ch('$'))), None);
741 assert_eq!(
742 map.lookup_index(&ev(KeyPattern::ctrl('d'))),
743 Some(Function::Delete)
744 );
745 assert_eq!(map.lookup_index(&ev(KeyPattern::ch('d'))), None);
746 }
747
748 #[test]
749 fn bad_bindings_warn_and_keep_defaults() {
750 let mut over = HashMap::new();
751 over.insert("frobnicate".to_string(), "z".to_string());
752 let (map, warnings) =
753 Keymap::with_config(&over, &HashMap::new(), &HashMap::new(), &HashMap::new());
754 assert_eq!(warnings.len(), 1);
755 let ev = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
756 assert_eq!(map.lookup_index(&ev), Some(Function::Quit));
757 }
758
759 #[test]
760 fn parse_sequence_forms() {
761 let seq = parse_sequence("l~f jane<enter>").unwrap();
762 assert_eq!(seq.len(), 9);
763 assert_eq!(seq[0].code, KeyCode::Char('l'));
764 assert_eq!(seq[2].code, KeyCode::Char('f'));
765 assert_eq!(seq[3].code, KeyCode::Char(' '));
766 assert_eq!(seq[8].code, KeyCode::Enter);
767 let seq = parse_sequence("<ctrl+x><Esc>").unwrap();
768 assert_eq!(seq[0].code, KeyCode::Char('x'));
769 assert!(seq[0].modifiers.contains(KeyModifiers::CONTROL));
770 assert_eq!(seq[1].code, KeyCode::Esc);
771 assert!(parse_sequence("<bogus>").is_none());
772 assert!(parse_sequence("<unclosed").is_none());
773 assert!(parse_sequence("").unwrap().is_empty());
774 }
775
776 #[test]
777 fn macros_parse_shadow_and_warn() {
778 let mut macros_index = HashMap::new();
779 macros_index.insert("d".to_string(), "l~f jane<enter>".to_string());
780 macros_index.insert("Z".to_string(), "<bogus>".to_string());
781 let (map, warnings) = Keymap::with_config(
782 &HashMap::new(),
783 &HashMap::new(),
784 ¯os_index,
785 &HashMap::new(),
786 );
787 assert_eq!(warnings.len(), 1);
788 assert!(warnings[0].contains("bad index macro Z"), "{warnings:?}");
789 let ev = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
790 assert_eq!(map.lookup_index_macro(&ev).unwrap().len(), 9);
793 assert!(
794 map.lookup_pager_macro(&ev).is_none(),
795 "index macro must not leak into the pager"
796 );
797 let help = map.help_lines().join("\n");
799 assert!(help.contains("Index macros"), "{help}");
800 assert!(help.contains("l~f jane<enter>"), "{help}");
801 }
802
803 #[test]
804 fn shift_in_event_does_not_block_match() {
805 let ev = KeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT);
807 let (map, _) = Keymap::with_config(
808 &HashMap::new(),
809 &HashMap::new(),
810 &HashMap::new(),
811 &HashMap::new(),
812 );
813 assert_eq!(map.lookup_index(&ev), Some(Function::Flag));
814 }
815}