use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use taria::key::{Key, KeyPress, Modifiers};
pub fn to_crossterm_key(key: &str) -> Option<KeyEvent> {
key.parse::<KeyPress>().ok().and_then(to_crossterm)
}
pub fn to_crossterm(press: KeyPress) -> Option<KeyEvent> {
let code = match press.key {
Key::Char(c) => KeyCode::Char(c),
Key::Enter => KeyCode::Enter,
Key::Esc => KeyCode::Esc,
Key::Tab => KeyCode::Tab,
Key::BackTab => KeyCode::BackTab,
Key::Backspace => KeyCode::Backspace,
Key::Delete => KeyCode::Delete,
Key::Up => KeyCode::Up,
Key::Down => KeyCode::Down,
Key::Left => KeyCode::Left,
Key::Right => KeyCode::Right,
Key::Home => KeyCode::Home,
Key::End => KeyCode::End,
Key::PageUp => KeyCode::PageUp,
Key::PageDown => KeyCode::PageDown,
Key::F(n) => KeyCode::F(n),
_ => return None,
};
Some(KeyEvent::new(code, to_crossterm_modifiers(press.modifiers)))
}
fn to_crossterm_modifiers(modifiers: Modifiers) -> KeyModifiers {
let mut out = KeyModifiers::NONE;
if modifiers.ctrl {
out |= KeyModifiers::CONTROL;
}
if modifiers.alt {
out |= KeyModifiers::ALT;
}
if modifiers.shift {
out |= KeyModifiers::SHIFT;
}
out
}
pub fn text_to_keys(text: &str) -> Vec<KeyEvent> {
text.chars()
.filter(|c| *c != '\r')
.map(|c| {
let code = match c {
'\n' => KeyCode::Enter,
'\t' => KeyCode::Tab,
other => KeyCode::Char(other),
};
KeyEvent::new(code, KeyModifiers::NONE)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
const CTRL: Modifiers = Modifiers::new(true, false, false);
const SHIFT: Modifiers = Modifiers::new(false, false, true);
const CTRL_ALT_SHIFT: Modifiers = Modifiers::new(true, true, true);
fn event(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent::new(code, modifiers)
}
#[test]
fn every_key_lowers_to_its_crossterm_counterpart() {
let cases = [
(Key::Char('a'), KeyCode::Char('a')),
(Key::Char('Q'), KeyCode::Char('Q')),
(Key::Char(' '), KeyCode::Char(' ')),
(Key::Enter, KeyCode::Enter),
(Key::Esc, KeyCode::Esc),
(Key::Tab, KeyCode::Tab),
(Key::BackTab, KeyCode::BackTab),
(Key::Backspace, KeyCode::Backspace),
(Key::Delete, KeyCode::Delete),
(Key::Up, KeyCode::Up),
(Key::Down, KeyCode::Down),
(Key::Left, KeyCode::Left),
(Key::Right, KeyCode::Right),
(Key::Home, KeyCode::Home),
(Key::End, KeyCode::End),
(Key::PageUp, KeyCode::PageUp),
(Key::PageDown, KeyCode::PageDown),
(Key::F(1), KeyCode::F(1)),
(Key::F(12), KeyCode::F(12)),
];
for (key, code) in cases {
assert_eq!(
to_crossterm(KeyPress::new(key, Modifiers::NONE)),
Some(event(code, KeyModifiers::NONE)),
"key: {key:?}"
);
}
}
#[test]
fn modifiers_lower_to_the_matching_bits() {
let cases = [
(Modifiers::NONE, KeyModifiers::NONE),
(CTRL, KeyModifiers::CONTROL),
(SHIFT, KeyModifiers::SHIFT),
(
CTRL_ALT_SHIFT,
KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT,
),
];
for (modifiers, expected) in cases {
assert_eq!(
to_crossterm(KeyPress::new(Key::Char('x'), modifiers)),
Some(event(KeyCode::Char('x'), expected)),
"modifiers: {modifiers:?}"
);
}
}
#[test]
fn backtab_keeps_its_shift() {
assert_eq!(
to_crossterm(KeyPress::new(Key::BackTab, SHIFT)),
Some(event(KeyCode::BackTab, KeyModifiers::SHIFT))
);
}
#[test]
fn strings_parse_and_lower() {
let cases = [
("ctrl+c", event(KeyCode::Char('c'), KeyModifiers::CONTROL)),
("shift+tab", event(KeyCode::BackTab, KeyModifiers::SHIFT)),
("space", event(KeyCode::Char(' '), KeyModifiers::NONE)),
(" ", event(KeyCode::Char(' '), KeyModifiers::NONE)),
("Q", event(KeyCode::Char('Q'), KeyModifiers::NONE)),
];
for (input, expected) in cases {
assert_eq!(to_crossterm_key(input), Some(expected), "input: {input:?}");
}
assert_eq!(to_crossterm_key("meta+x"), None);
}
#[test]
fn text_becomes_one_event_per_character() {
assert_eq!(
text_to_keys("hi!"),
[
event(KeyCode::Char('h'), KeyModifiers::NONE),
event(KeyCode::Char('i'), KeyModifiers::NONE),
event(KeyCode::Char('!'), KeyModifiers::NONE),
]
);
}
#[test]
fn empty_text_produces_no_events() {
assert!(text_to_keys("").is_empty());
}
#[test]
fn newlines_and_tabs_become_their_keys() {
assert_eq!(
text_to_keys("a\n\tb"),
[
event(KeyCode::Char('a'), KeyModifiers::NONE),
event(KeyCode::Enter, KeyModifiers::NONE),
event(KeyCode::Tab, KeyModifiers::NONE),
event(KeyCode::Char('b'), KeyModifiers::NONE),
]
);
}
#[test]
fn crlf_text_matches_the_same_text_with_lf() {
assert_eq!(text_to_keys("one\r\ntwo"), text_to_keys("one\ntwo"));
assert!(text_to_keys("\r").is_empty());
}
#[test]
fn uppercase_text_carries_no_shift() {
assert_eq!(
text_to_keys("Q"),
[event(KeyCode::Char('Q'), KeyModifiers::NONE)]
);
assert_eq!(text_to_keys("Q").first().copied(), to_crossterm_key("Q"));
}
}