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