#[must_use]
pub fn parse_test_keys(spec: &str) -> Option<Vec<String>> {
if spec.is_empty() {
return None;
}
let token_mode = spec.contains(',')
|| (!spec.is_empty() && spec.chars().all(|c| c.is_ascii_uppercase() || c == '-'));
let keys = if token_mode {
let mut keys = Vec::new();
for tok in split_comma_ws(spec) {
let up = tok.to_uppercase();
let mapped: Option<&str> = match up.as_str() {
"UP" => Some("\x1b[A"),
"DOWN" => Some("\x1b[B"),
"LEFT" => Some("\x1b[D"),
"RIGHT" => Some("\x1b[C"),
"ENTER" => Some("\r"),
"ESC" => Some("\x1b"),
"BACKSPACE" => Some("\x7f"),
"CTRL-A" | "CTRLA" => Some("\x01"),
"CTRL-B" | "CTRLB" => Some("\x02"),
"CTRL-D" | "CTRLD" => Some("\x04"),
"CTRL-E" | "CTRLE" => Some("\x05"),
"CTRL-F" | "CTRLF" => Some("\x06"),
"CTRL-G" | "CTRLG" => Some("\x07"),
"CTRL-H" | "CTRLH" => Some("\x08"),
"CTRL-K" | "CTRLK" => Some("\x0b"),
"CTRL-N" | "CTRLN" => Some("\x0e"),
"CTRL-P" | "CTRLP" => Some("\x10"),
"CTRL-R" | "CTRLR" => Some("\x12"),
"CTRL-T" | "CTRLT" => Some("\x14"),
"CTRL-W" | "CTRLW" => Some("\x17"),
_ => None,
};
if let Some(k) = mapped {
keys.push(k.to_string());
} else if let Some(text) = strip_type_prefix(tok) {
keys.extend(text.chars().map(|c| c.to_string()));
} else if tok.chars().count() == 1 {
keys.push(tok.to_string());
}
}
keys
} else {
let chars: Vec<char> = spec.chars().collect();
let mut keys = Vec::new();
let mut i = 0;
while i < chars.len() {
if chars[i] == '\x1b' && i + 2 < chars.len() && chars[i + 1] == '[' {
keys.push(chars[i..i + 3].iter().collect());
i += 3;
} else {
keys.push(chars[i].to_string());
i += 1;
}
}
keys
};
Some(keys)
}
fn split_comma_ws(s: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut rest = s;
while let Some(idx) = rest.find(',') {
out.push(&rest[..idx]);
rest = rest[idx + 1..].trim_start();
}
out.push(rest);
out
}
fn strip_type_prefix(tok: &str) -> Option<&str> {
let bytes = tok.as_bytes();
if bytes.len() >= 5 && tok[..5].eq_ignore_ascii_case("TYPE=") {
Some(&tok[5..])
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_mode_basics() {
assert_eq!(
parse_test_keys("DOWN, ENTER").unwrap(),
vec!["\x1b[B".to_string(), "\r".to_string()]
);
assert_eq!(parse_test_keys("ESC").unwrap(), vec!["\x1b".to_string()]);
}
#[test]
fn unknown_tokens_silently_dropped_vim_nav_quirk() {
assert_eq!(parse_test_keys("CTRL-J").unwrap(), Vec::<String>::new());
assert_eq!(parse_test_keys("CTRL-K").unwrap(), vec!["\x0b".to_string()]);
}
#[test]
fn type_injection_and_single_chars() {
assert_eq!(
parse_test_keys("TYPE=ab, ENTER").unwrap(),
vec!["a".to_string(), "b".to_string(), "\r".to_string()]
);
assert_eq!(parse_test_keys("x, ENTER").unwrap()[0], "x");
}
#[test]
fn raw_mode_keeps_escape_sequences_whole() {
let keys = parse_test_keys("a\x1b[Ab").unwrap();
assert_eq!(
keys,
vec!["a".to_string(), "\x1b[A".to_string(), "b".to_string()]
);
let keys = parse_test_keys("\x1b[A").unwrap();
assert_eq!(keys, vec!["\x1b[A".to_string()]);
let keys = parse_test_keys("\x1b[").unwrap();
assert_eq!(keys, vec!["\x1b".to_string(), "[".to_string()]);
}
#[test]
fn empty_spec_is_none() {
assert_eq!(parse_test_keys(""), None);
}
}