use std::collections::HashMap;
use crate::key::{KeyCode, KeyEvent, KeyModifiers};
use rmut_session::Function;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct KeyPattern {
pub code: KeyCode,
pub mods: KeyModifiers,
}
impl KeyPattern {
fn plain(code: KeyCode) -> Self {
KeyPattern {
code,
mods: KeyModifiers::NONE,
}
}
fn ch(c: char) -> Self {
Self::plain(KeyCode::Char(c))
}
fn ctrl(c: char) -> Self {
KeyPattern {
code: KeyCode::Char(c),
mods: KeyModifiers::CONTROL,
}
}
fn alt(c: char) -> Self {
KeyPattern {
code: KeyCode::Char(c),
mods: KeyModifiers::ALT,
}
}
pub fn matches(&self, key: &KeyEvent) -> bool {
self.code == key.code
&& key.modifiers & (KeyModifiers::CONTROL | KeyModifiers::ALT) == self.mods
}
pub fn display(&self) -> String {
let base = match self.code {
KeyCode::Char(' ') => "Space".to_string(),
KeyCode::Char(c) => c.to_string(),
KeyCode::Enter => "Enter".into(),
KeyCode::Esc => "Esc".into(),
KeyCode::Tab => "Tab".into(),
KeyCode::Backspace => "Backspace".into(),
KeyCode::Up => "Up".into(),
KeyCode::Down => "Down".into(),
KeyCode::PageUp => "PgUp".into(),
KeyCode::PageDown => "PgDn".into(),
KeyCode::Home => "Home".into(),
KeyCode::End => "End".into(),
other => format!("{other:?}"),
};
if self.mods.contains(KeyModifiers::CONTROL) {
format!("Ctrl+{base}")
} else if self.mods.contains(KeyModifiers::ALT) {
format!("Alt+{base}")
} else {
base
}
}
}
fn strip_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
(s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix))
.then(|| &s[prefix.len()..])
}
pub fn parse_key(input: &str) -> Option<KeyPattern> {
let mut mods = KeyModifiers::NONE;
let mut rest = input.trim();
loop {
if let Some(r) = strip_ci(rest, "ctrl+") {
mods |= KeyModifiers::CONTROL;
rest = r;
} else if let Some(r) = strip_ci(rest, "alt+") {
mods |= KeyModifiers::ALT;
rest = r;
} else {
break;
}
}
let code = match rest.to_lowercase().as_str() {
"enter" | "return" => KeyCode::Enter,
"esc" | "escape" => KeyCode::Esc,
"space" => KeyCode::Char(' '),
"tab" => KeyCode::Tab,
"backspace" => KeyCode::Backspace,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"pgup" | "pageup" => KeyCode::PageUp,
"pgdn" | "pagedown" => KeyCode::PageDown,
"home" => KeyCode::Home,
"end" => KeyCode::End,
_ => {
let mut chars = rest.chars();
let c = chars.next()?;
if chars.next().is_some() {
return None;
}
KeyCode::Char(c)
}
};
Some(KeyPattern { code, mods })
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PagerAction {
Back,
Down,
Up,
PageDown,
PageUp,
HalfDown,
HalfUp,
Top,
Bottom,
ToggleQuoted,
SkipQuoted,
NextMsg,
PrevMsg,
NextUndeleted,
PrevUndeleted,
Delete,
Undelete,
Flag,
ToggleNew,
Tag,
Undo,
Redraw,
Suspend,
Headers,
Search,
SearchNext,
SearchPrev,
SearchToggle,
Attachments,
Compose,
Reply,
GroupReply,
ListReply,
Forward,
Print,
Save,
Copy,
Pipe,
Bounce,
Resend,
Edit,
CreateAlias,
EnterCommand,
Help,
ListAction,
ErrorHistory,
WhatKey,
Urls,
}
impl PagerAction {
pub fn name(self) -> &'static str {
use PagerAction::*;
match self {
Back => "back",
Down => "down",
Up => "up",
PageDown => "page-down",
PageUp => "page-up",
HalfDown => "half-down",
HalfUp => "half-up",
Top => "top",
Bottom => "bottom",
ToggleQuoted => "toggle-quoted",
SkipQuoted => "skip-quoted",
NextMsg => "next",
PrevMsg => "previous",
NextUndeleted => "next-undeleted",
PrevUndeleted => "previous-undeleted",
Delete => "delete",
Undelete => "undelete",
Flag => "flag",
ToggleNew => "toggle-new",
Tag => "tag",
Undo => "undo",
Redraw => "refresh",
Suspend => "suspend",
Headers => "headers",
Search => "search",
SearchNext => "search-next",
SearchPrev => "search-prev",
SearchToggle => "search-toggle",
Attachments => "attachments",
Compose => "compose",
Reply => "reply",
GroupReply => "group-reply",
ListReply => "list-reply",
Forward => "forward",
Print => "print",
Save => "save",
Copy => "copy",
Pipe => "pipe",
Bounce => "bounce",
Resend => "resend",
Edit => "edit",
CreateAlias => "create-alias",
EnterCommand => "enter-command",
Help => "help",
ListAction => "list-action",
ErrorHistory => "error-history",
WhatKey => "what-key",
Urls => "urls",
}
}
pub fn describe(self) -> &'static str {
use PagerAction::*;
match self {
Back => "back to the index",
Down => "scroll down one line",
Up => "scroll up one line",
PageDown => "page down",
PageUp => "page up",
HalfDown => "scroll down half a page",
HalfUp => "scroll up half a page",
Top => "jump to the top",
Bottom => "jump to the bottom",
ToggleQuoted => "show/hide quoted text",
SkipQuoted => "skip past the quoted text below",
NextMsg => "open next message",
PrevMsg => "open previous message",
NextUndeleted => "open next undeleted message",
PrevUndeleted => "open previous undeleted message",
Delete => "delete and advance",
Undelete => "unmark deletion",
Flag => "toggle flagged mark",
ToggleNew => "toggle read/unread (unbound here: N is the backwards search)",
Tag => "toggle the tag on this message",
Undo => "cancel a held send, or undo the last mark change",
Redraw => "repaint the screen",
Suspend => "suspend rmut (fg brings it back)",
Headers => "toggle full headers",
Search => "search the displayed text (unlike the index /, which matches messages)",
SearchNext => "next match of the pager search",
SearchPrev => "previous match of the pager search",
SearchToggle => "toggle the search highlighting",
Attachments => "list message parts",
Compose => "compose a new message",
Reply => "reply to sender",
GroupReply => "reply to all",
ListReply => "reply to the mailing list only",
Forward => "forward message",
Print => "pipe message to the print command",
Save => "save (copy + mark deleted) to a mailbox",
Copy => "copy to a mailbox (original stays)",
Pipe => "pipe raw message to a shell command",
Bounce => "bounce (resend) message to new recipients",
Resend => "edit the message as a new draft",
Edit => "edit the raw message and replace it",
CreateAlias => "add the sender to the alias file",
EnterCommand => "run a config command (set/bind/macro/color/...)",
Help => "this help",
ListAction => "act on the message's List-* headers (subscribe, help, ...)",
ErrorHistory => "show the recent errors",
WhatKey => "say what a key is (Ctrl+G ends it)",
Urls => "list the message's links, to open or copy one",
}
}
fn all() -> &'static [PagerAction] {
use PagerAction::*;
&[
Back,
Down,
Up,
PageDown,
PageUp,
HalfDown,
HalfUp,
Top,
Bottom,
ToggleQuoted,
SkipQuoted,
NextMsg,
PrevMsg,
NextUndeleted,
PrevUndeleted,
Delete,
Undelete,
Flag,
ToggleNew,
Tag,
Undo,
Redraw,
Suspend,
Headers,
Search,
SearchNext,
SearchPrev,
SearchToggle,
Attachments,
Compose,
Reply,
GroupReply,
ListReply,
Forward,
Print,
Save,
Copy,
Pipe,
Bounce,
Resend,
Edit,
CreateAlias,
EnterCommand,
Help,
ListAction,
ErrorHistory,
WhatKey,
Urls,
]
}
pub fn from_name(name: &str) -> Option<PagerAction> {
let name = match name {
"mark-as-new" => "toggle-new",
other => other,
};
PagerAction::all()
.iter()
.copied()
.find(|a| a.name() == name)
}
}
pub fn parse_sequence(input: &str) -> Option<Vec<KeyEvent>> {
let mut out = Vec::new();
let mut chars = input.chars();
while let Some(c) = chars.next() {
if c == '<' {
let mut name = String::new();
loop {
match chars.next() {
Some('>') => break,
Some(c) => name.push(c),
None => return None,
}
}
let p = parse_key(&name)?;
out.push(KeyEvent::new(p.code, p.mods));
} else {
out.push(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
}
}
Some(out)
}
pub struct Keymap {
pub index: Vec<(KeyPattern, Function)>,
pub pager: Vec<(KeyPattern, PagerAction)>,
pub macros_index: Vec<(KeyPattern, Vec<KeyEvent>, String)>,
pub macros_pager: Vec<(KeyPattern, Vec<KeyEvent>, String)>,
}
fn index_defaults() -> Vec<(KeyPattern, Function)> {
use Function::*;
use KeyCode as K;
vec![
(KeyPattern::ch('q'), Quit),
(KeyPattern::ch('x'), Abort),
(KeyPattern::ch('j'), Down),
(KeyPattern::plain(K::Down), Down),
(KeyPattern::ch('k'), Up),
(KeyPattern::plain(K::Up), Up),
(KeyPattern::plain(K::PageDown), PageDown),
(KeyPattern::ctrl('f'), PageDown),
(KeyPattern::plain(K::PageUp), PageUp),
(KeyPattern::ctrl('b'), PageUp),
(KeyPattern::ch(' '), PageDown),
(KeyPattern::ch('='), First),
(KeyPattern::plain(K::Home), First),
(KeyPattern::ch('*'), Last),
(KeyPattern::plain(K::End), Last),
(KeyPattern::plain(K::Enter), View),
(KeyPattern::ch('d'), Delete),
(KeyPattern::ch('u'), Undelete),
(KeyPattern::ch('F'), Flag),
(KeyPattern::ch('N'), ToggleNew),
(KeyPattern::alt('a'), MarkAllRead),
(KeyPattern::ch('$'), Sync),
(KeyPattern::ch('m'), Compose),
(KeyPattern::ch('r'), Reply),
(KeyPattern::ch('g'), GroupReply),
(KeyPattern::ch('L'), ListReply),
(KeyPattern::ch('f'), Forward),
(KeyPattern::ch('o'), Sort),
(KeyPattern::ch('l'), Limit),
(KeyPattern::ch('/'), Search),
(KeyPattern::alt('/'), SearchReverse),
(KeyPattern::ch('n'), SearchNext),
(KeyPattern::plain(K::Tab), NextNew),
(
KeyPattern {
code: K::Tab,
mods: KeyModifiers::ALT,
},
PrevNew,
),
(KeyPattern::ch('c'), ChangeMailbox),
(KeyPattern::alt('c'), ChangeMailboxReadOnly),
(KeyPattern::ch('y'), Folders),
(KeyPattern::ch('v'), Attachments),
(KeyPattern::alt('v'), FoldThread),
(KeyPattern::alt('V'), FoldAll),
(KeyPattern::ch('p'), Print),
(KeyPattern::ch('t'), Tag),
(KeyPattern::ch(';'), TagPrefix),
(KeyPattern::alt('d'), DeleteThread),
(KeyPattern::alt('u'), UndeleteThread),
(KeyPattern::alt('t'), TagThread),
(KeyPattern::ctrl('d'), DeleteSubthread),
(KeyPattern::ctrl('u'), UndeleteSubthread),
(KeyPattern::alt('n'), NextThread),
(KeyPattern::alt('p'), PrevThread),
(KeyPattern::ch('#'), BreakThread),
(KeyPattern::ch('&'), LinkThreads),
(KeyPattern::ctrl('r'), ReadThread),
(KeyPattern::alt('r'), ReadSubthread),
(KeyPattern::ch('P'), ParentMessage),
(KeyPattern::ch('Y'), EditLabel),
(KeyPattern::ch('V'), ShowVersion),
(KeyPattern::alt('l'), ShowLimit),
(KeyPattern::ch('@'), DisplayAddress),
(KeyPattern::ch('%'), ToggleWrite),
(KeyPattern::ch('H'), PageTop),
(KeyPattern::ch('M'), PageMiddle),
(KeyPattern::ch('z'), Undo),
(KeyPattern::ch('D'), DeletePattern),
(KeyPattern::ch('U'), UndeletePattern),
(KeyPattern::ch('T'), TagPattern),
(KeyPattern::ctrl('t'), UntagPattern),
(KeyPattern::ch('G'), FetchMail),
(KeyPattern::ch('s'), Save),
(KeyPattern::ch('C'), Copy),
(KeyPattern::alt('s'), DecodeSave),
(KeyPattern::alt('C'), DecodeCopy),
(KeyPattern::ch('|'), Pipe),
(KeyPattern::ch('b'), Bounce),
(KeyPattern::ch('e'), Edit),
(KeyPattern::alt('e'), Resend),
(KeyPattern::ch('B'), SidebarToggle),
(KeyPattern::ctrl('n'), SidebarNext),
(KeyPattern::ctrl('p'), SidebarPrev),
(KeyPattern::ctrl('o'), SidebarOpen),
(KeyPattern::ch('a'), CreateAlias),
(KeyPattern::ch('Q'), Query),
(KeyPattern::ch('X'), Notmuch),
(KeyPattern::ch(':'), EnterCommand),
(KeyPattern::ch('!'), Shell),
(KeyPattern::ctrl('l'), Redraw),
(KeyPattern::ctrl('z'), Suspend),
(KeyPattern::ch('?'), Help),
(KeyPattern::ch('~'), MarkMessage),
(KeyPattern::alt('L'), ListAction),
]
}
pub fn resolve_function(menu: rmut_core::command::Menu, name: &str) -> Option<String> {
if menu == rmut_core::command::Menu::Index {
if Function::from_name(name).is_some() {
return Some(name.to_string());
}
let mapped = rmut_core::muttrc::index_function(name)?;
Function::from_name(mapped).map(|_| mapped.to_string())
} else {
if PagerAction::from_name(name).is_some() {
return Some(name.to_string());
}
let mapped = rmut_core::muttrc::pager_function(name)?;
PagerAction::from_name(mapped).map(|_| mapped.to_string())
}
}
fn pager_defaults() -> Vec<(KeyPattern, PagerAction)> {
use KeyCode as K;
use PagerAction::*;
vec![
(KeyPattern::ch('q'), Back),
(KeyPattern::ch('i'), Back),
(KeyPattern::plain(K::Esc), Back),
(KeyPattern::plain(K::Enter), Down),
(KeyPattern::plain(K::Backspace), Up),
(KeyPattern::ch('j'), NextUndeleted),
(KeyPattern::plain(K::Down), NextUndeleted),
(KeyPattern::plain(K::Right), NextUndeleted),
(KeyPattern::ch('k'), PrevUndeleted),
(KeyPattern::plain(K::Up), PrevUndeleted),
(KeyPattern::plain(K::Left), PrevUndeleted),
(KeyPattern::ch(' '), PageDown),
(KeyPattern::plain(K::PageDown), PageDown),
(KeyPattern::ch('-'), PageUp),
(KeyPattern::plain(K::PageUp), PageUp),
(KeyPattern::ctrl('d'), HalfDown),
(KeyPattern::ctrl('u'), HalfUp),
(KeyPattern::plain(K::Home), Top),
(KeyPattern::plain(K::End), Bottom),
(KeyPattern::ch('T'), ToggleQuoted),
(KeyPattern::ch('S'), SkipQuoted),
(KeyPattern::ch('J'), NextMsg),
(KeyPattern::ch('K'), PrevMsg),
(KeyPattern::ch('d'), Delete),
(KeyPattern::ch('u'), Undelete),
(KeyPattern::ch('F'), Flag),
(KeyPattern::ch('t'), Tag),
(KeyPattern::ch('z'), Undo),
(KeyPattern::ctrl('l'), Redraw),
(KeyPattern::ctrl('z'), Suspend),
(KeyPattern::ch('h'), Headers),
(KeyPattern::ch('/'), Search),
(KeyPattern::ch('n'), SearchNext),
(KeyPattern::ch('N'), SearchPrev),
(KeyPattern::ch('\\'), SearchToggle),
(KeyPattern::ch('v'), Attachments),
(KeyPattern::ch('m'), Compose),
(KeyPattern::ch('r'), Reply),
(KeyPattern::ch('g'), GroupReply),
(KeyPattern::ch('L'), ListReply),
(KeyPattern::ch('f'), Forward),
(KeyPattern::ch('p'), Print),
(KeyPattern::ch('s'), Save),
(KeyPattern::ch('C'), Copy),
(KeyPattern::ch('|'), Pipe),
(KeyPattern::ch('b'), Bounce),
(KeyPattern::ch('e'), Edit),
(KeyPattern::alt('e'), Resend),
(KeyPattern::ch('a'), CreateAlias),
(KeyPattern::ch(':'), EnterCommand),
(KeyPattern::ch('?'), Help),
(KeyPattern::alt('L'), ListAction),
(KeyPattern::ctrl('b'), Urls),
]
}
impl Keymap {
pub fn with_config(
index_over: &HashMap<String, String>,
pager_over: &HashMap<String, String>,
macros_index: &HashMap<String, String>,
macros_pager: &HashMap<String, String>,
) -> (Keymap, Vec<String>) {
let mut warnings = Vec::new();
let mut index = index_defaults();
for (action_name, key_str) in index_over {
let (Some(action), Some(key)) = (Function::from_name(action_name), parse_key(key_str))
else {
warnings.push(format!("bad index binding {action_name} = {key_str:?}"));
continue;
};
index.retain(|(k, a)| *a != action && *k != key);
index.push((key, action));
}
let mut pager = pager_defaults();
for (action_name, key_str) in pager_over {
let (Some(action), Some(key)) =
(PagerAction::from_name(action_name), parse_key(key_str))
else {
warnings.push(format!("bad pager binding {action_name} = {key_str:?}"));
continue;
};
pager.retain(|(k, a)| *a != action && *k != key);
pager.push((key, action));
}
let mut macros = |table: &HashMap<String, String>, menu: &str| {
let mut out = Vec::new();
for (key_str, seq_str) in table {
let (Some(key), Some(seq)) = (parse_key(key_str), parse_sequence(seq_str)) else {
warnings.push(format!("bad {menu} macro {key_str} = {seq_str:?}"));
continue;
};
out.push((key, seq, seq_str.clone()));
}
out
};
let macros_index = macros(macros_index, "index");
let macros_pager = macros(macros_pager, "pager");
(
Keymap {
index,
pager,
macros_index,
macros_pager,
},
warnings,
)
}
pub fn lookup_index_macro(&self, key: &KeyEvent) -> Option<&[KeyEvent]> {
self.macros_index
.iter()
.find(|(p, _, _)| p.matches(key))
.map(|(_, seq, _)| seq.as_slice())
}
pub fn lookup_pager_macro(&self, key: &KeyEvent) -> Option<&[KeyEvent]> {
self.macros_pager
.iter()
.find(|(p, _, _)| p.matches(key))
.map(|(_, seq, _)| seq.as_slice())
}
pub fn lookup_index(&self, key: &KeyEvent) -> Option<Function> {
self.index
.iter()
.find(|(p, _)| p.matches(key))
.map(|&(_, a)| a)
}
pub fn lookup_pager(&self, key: &KeyEvent) -> Option<PagerAction> {
self.pager
.iter()
.find(|(p, _)| p.matches(key))
.map(|&(_, a)| a)
}
pub fn help_lines(&self) -> Vec<String> {
let mut lines = vec!["Index keys".to_string(), String::new()];
for &action in Function::all() {
let keys: Vec<String> = self
.index
.iter()
.filter(|&&(_, a)| a == action)
.map(|(k, _)| k.display())
.collect();
if !keys.is_empty() {
lines.push(format!(" {:<16} {}", keys.join(" "), action.describe()));
}
}
lines.extend([String::new(), "Pager keys".to_string(), String::new()]);
for &action in PagerAction::all() {
let keys: Vec<String> = self
.pager
.iter()
.filter(|&&(_, a)| a == action)
.map(|(k, _)| k.display())
.collect();
if !keys.is_empty() {
lines.push(format!(" {:<16} {}", keys.join(" "), action.describe()));
}
}
for (title, table) in [
("Index macros", &self.macros_index),
("Pager macros", &self.macros_pager),
] {
if !table.is_empty() {
lines.extend([String::new(), title.to_string(), String::new()]);
for (key, _, raw) in table {
lines.push(format!(" {:<16} {raw}", key.display()));
}
}
}
lines.extend(
[
"",
"Patterns (limit/search)",
"",
" ~f x from ~s x subject ~b x body",
" ~t x to ~c x cc ~C x to or cc",
" ~e x sender ~d spec date word subject or from",
" ~N new ~U unread ~F flagged ~D deleted ~T tagged",
" ~p addressed to me",
"",
" x is a case-insensitive regex; \"quotes\" keep spaces.",
" ~d: 24/12/2026, 1/6/2026-30/6/2026, 24/12-, <1w, >2d, =3d",
" Terms AND; ! negates, | ORs, () groups:",
" !~D (~f jane | ~t jane) ~d <1m",
]
.map(String::from),
);
lines
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_key_forms() {
assert_eq!(parse_key("x"), Some(KeyPattern::ch('x')));
assert_eq!(parse_key("X"), Some(KeyPattern::ch('X')));
assert_eq!(parse_key("ctrl+f"), Some(KeyPattern::ctrl('f')));
assert_eq!(parse_key("Alt+v"), Some(KeyPattern::alt('v')));
assert_eq!(parse_key("space"), Some(KeyPattern::ch(' ')));
assert_eq!(
parse_key("pgdn"),
Some(KeyPattern::plain(KeyCode::PageDown))
);
assert_eq!(parse_key("enter"), Some(KeyPattern::plain(KeyCode::Enter)));
assert!(parse_key("bogus-key").is_none());
}
#[test]
fn remap_replaces_defaults_and_conflicts() {
let mut over = HashMap::new();
over.insert("sync".to_string(), "w".to_string());
over.insert("delete".to_string(), "ctrl+d".to_string());
let (map, warnings) =
Keymap::with_config(&over, &HashMap::new(), &HashMap::new(), &HashMap::new());
assert!(warnings.is_empty());
let ev = |p: KeyPattern| KeyEvent::new(p.code, p.mods);
assert_eq!(
map.lookup_index(&ev(KeyPattern::ch('w'))),
Some(Function::Sync)
);
assert_eq!(map.lookup_index(&ev(KeyPattern::ch('$'))), None);
assert_eq!(
map.lookup_index(&ev(KeyPattern::ctrl('d'))),
Some(Function::Delete)
);
assert_eq!(map.lookup_index(&ev(KeyPattern::ch('d'))), None);
}
#[test]
fn bad_bindings_warn_and_keep_defaults() {
let mut over = HashMap::new();
over.insert("frobnicate".to_string(), "z".to_string());
let (map, warnings) =
Keymap::with_config(&over, &HashMap::new(), &HashMap::new(), &HashMap::new());
assert_eq!(warnings.len(), 1);
let ev = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
assert_eq!(map.lookup_index(&ev), Some(Function::Quit));
}
#[test]
fn parse_sequence_forms() {
let seq = parse_sequence("l~f jane<enter>").unwrap();
assert_eq!(seq.len(), 9);
assert_eq!(seq[0].code, KeyCode::Char('l'));
assert_eq!(seq[2].code, KeyCode::Char('f'));
assert_eq!(seq[3].code, KeyCode::Char(' '));
assert_eq!(seq[8].code, KeyCode::Enter);
let seq = parse_sequence("<ctrl+x><Esc>").unwrap();
assert_eq!(seq[0].code, KeyCode::Char('x'));
assert!(seq[0].modifiers.contains(KeyModifiers::CONTROL));
assert_eq!(seq[1].code, KeyCode::Esc);
assert!(parse_sequence("<bogus>").is_none());
assert!(parse_sequence("<unclosed").is_none());
assert!(parse_sequence("").unwrap().is_empty());
}
#[test]
fn macros_parse_shadow_and_warn() {
let mut macros_index = HashMap::new();
macros_index.insert("d".to_string(), "l~f jane<enter>".to_string());
macros_index.insert("Z".to_string(), "<bogus>".to_string());
let (map, warnings) = Keymap::with_config(
&HashMap::new(),
&HashMap::new(),
¯os_index,
&HashMap::new(),
);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("bad index macro Z"), "{warnings:?}");
let ev = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
assert_eq!(map.lookup_index_macro(&ev).unwrap().len(), 9);
assert!(
map.lookup_pager_macro(&ev).is_none(),
"index macro must not leak into the pager"
);
let help = map.help_lines().join("\n");
assert!(help.contains("Index macros"), "{help}");
assert!(help.contains("l~f jane<enter>"), "{help}");
}
#[test]
fn shift_in_event_does_not_block_match() {
let ev = KeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT);
let (map, _) = Keymap::with_config(
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(map.lookup_index(&ev), Some(Function::Flag));
}
}