use tastty::input::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum InputSegment {
Text(String),
Key(KeyEvent),
Bytes(Vec<u8>),
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ParseInputError {
#[error("unclosed input tag starting with '<'")]
UnclosedTag,
#[error("invalid input tag '<{tag}>'")]
InvalidTag {
tag: String,
},
}
pub fn parse_input(input: &str) -> Result<Vec<InputSegment>, ParseInputError> {
let mut segments = Vec::new();
let mut text = String::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '<' {
text.push(ch);
continue;
}
flush_text(&mut text, &mut segments);
let mut tag = String::new();
loop {
match chars.next() {
Some('>') => break,
Some(ch) => tag.push(ch),
None => return Err(ParseInputError::UnclosedTag),
}
}
segments.push(resolve_key_name(&tag)?);
}
flush_text(&mut text, &mut segments);
Ok(segments)
}
fn flush_text(text: &mut String, segments: &mut Vec<InputSegment>) {
if !text.is_empty() {
segments.push(InputSegment::Text(std::mem::take(text)));
}
}
fn resolve_key_name(tag: &str) -> Result<InputSegment, ParseInputError> {
let lower = tag.to_ascii_lowercase();
let mut modifiers = KeyModifiers::NONE;
let mut key_name = None;
for part in lower.split('-') {
match part {
"ctrl" | "control" => modifiers |= KeyModifiers::CONTROL,
"alt" => modifiers |= KeyModifiers::ALT,
"shift" => modifiers |= KeyModifiers::SHIFT,
"super" => modifiers |= KeyModifiers::SUPER,
"hyper" => modifiers |= KeyModifiers::HYPER,
"meta" => modifiers |= KeyModifiers::META,
_ => {
key_name = Some(part);
if lower.split('-').skip_while(|p| *p != part).count() > 1 {
return Err(invalid(tag));
}
}
}
}
let Some(name) = key_name else {
return Err(invalid(tag));
};
let code = match name {
"enter" | "cr" | "return" => KeyCode::Enter,
"tab" => KeyCode::Tab,
"backtab" => KeyCode::BackTab,
"space" => KeyCode::Char(' '),
"escape" | "esc" => KeyCode::Esc,
"backspace" | "bs" => KeyCode::Backspace,
"delete" | "del" => KeyCode::Delete,
"insert" | "ins" => KeyCode::Insert,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"right" => KeyCode::Right,
"left" => KeyCode::Left,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" | "pgup" => KeyCode::PageUp,
"pagedown" | "pgdn" => KeyCode::PageDown,
one if one.chars().count() == 1 => {
KeyCode::Char(one.chars().next().expect("count checked"))
}
f if f.starts_with('f') => {
let n = f[1..].parse::<u8>().map_err(|_err| invalid(tag))?;
if !(1..=24).contains(&n) {
return Err(invalid(tag));
}
KeyCode::F(n)
}
_ => return Err(invalid(tag)),
};
if code == KeyCode::BackTab && modifiers == KeyModifiers::SHIFT {
modifiers = KeyModifiers::NONE;
}
Ok(InputSegment::Key(KeyEvent::new(code, modifiers)))
}
fn invalid(tag: &str) -> ParseInputError {
ParseInputError::InvalidTag {
tag: tag.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(code: KeyCode, modifiers: KeyModifiers) -> InputSegment {
InputSegment::Key(KeyEvent::new(code, modifiers))
}
#[test]
fn parses_mixed_text_and_keys() {
assert_eq!(
parse_input("cd /tmp<Enter><Up>").unwrap(),
vec![
InputSegment::Text("cd /tmp".to_string()),
key(KeyCode::Enter, KeyModifiers::NONE),
key(KeyCode::Up, KeyModifiers::NONE),
]
);
}
#[test]
fn parses_modifier_keys() {
assert_eq!(
parse_input("<Ctrl-C><Alt-Shift-x>").unwrap(),
vec![
key(KeyCode::Char('c'), KeyModifiers::CONTROL),
key(KeyCode::Char('x'), KeyModifiers::ALT | KeyModifiers::SHIFT),
]
);
}
#[test]
fn rejects_invalid_tags() {
parse_input("<Bogus>").unwrap_err();
parse_input("abc<Enter").unwrap_err();
}
}