use std::fmt;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MediaKeyCode};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[allow(clippy::struct_excessive_bools)]
pub struct SingleKey {
pub code: KeyCodeValue,
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub super_key: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyCodeValue {
Char(char),
Enter,
Esc,
Tab,
Backspace,
Delete,
Insert,
Up,
Down,
Left,
Right,
Home,
End,
PageUp,
PageDown,
Space,
F(u8),
Media(MediaKeyCode),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum KeyInput {
Single(SingleKey),
Sequence(Vec<SingleKey>),
}
impl SingleKey {
pub fn from_event(event: &KeyEvent) -> Option<Self> {
let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
let alt = event.modifiers.contains(KeyModifiers::ALT);
let shift = event.modifiers.contains(KeyModifiers::SHIFT);
let super_key = event.modifiers.contains(KeyModifiers::SUPER);
let code = match event.code {
KeyCode::Char(' ') => KeyCodeValue::Space,
KeyCode::Char(c) => {
if shift && !ctrl && !alt && !super_key && c.is_ascii_uppercase() {
return Some(SingleKey {
code: KeyCodeValue::Char(c),
ctrl: false,
alt: false,
shift: false,
super_key: false,
});
}
KeyCodeValue::Char(c)
}
KeyCode::Enter => KeyCodeValue::Enter,
KeyCode::Esc => KeyCodeValue::Esc,
KeyCode::Tab => KeyCodeValue::Tab,
KeyCode::BackTab => {
return Some(SingleKey {
code: KeyCodeValue::Tab,
ctrl,
alt,
shift: true,
super_key,
});
}
KeyCode::Backspace => KeyCodeValue::Backspace,
KeyCode::Delete => KeyCodeValue::Delete,
KeyCode::Insert => KeyCodeValue::Insert,
KeyCode::Up => KeyCodeValue::Up,
KeyCode::Down => KeyCodeValue::Down,
KeyCode::Left => KeyCodeValue::Left,
KeyCode::Right => KeyCodeValue::Right,
KeyCode::Home => KeyCodeValue::Home,
KeyCode::End => KeyCodeValue::End,
KeyCode::PageUp => KeyCodeValue::PageUp,
KeyCode::PageDown => KeyCodeValue::PageDown,
KeyCode::F(n) => KeyCodeValue::F(n),
KeyCode::Media(m) => KeyCodeValue::Media(m),
_ => return None,
};
Some(SingleKey {
code,
ctrl,
alt,
shift: if matches!(code, KeyCodeValue::Char(_)) {
false
} else {
shift
},
super_key,
})
}
pub fn parse(s: &str) -> Result<Self, String> {
let s = s.trim();
let parts: Vec<&str> = s.split('-').collect();
let mut ctrl = false;
let mut alt = false;
let mut shift = false;
let mut super_key = false;
for &part in &parts[..parts.len() - 1] {
match part.to_lowercase().as_str() {
"ctrl" => ctrl = true,
"alt" => alt = true,
"shift" => shift = true,
"super" | "cmd" | "win" => super_key = true,
_ => return Err(format!("unknown modifier: {part}")),
}
}
let key_part = parts[parts.len() - 1];
let code = match key_part.to_lowercase().as_str() {
"enter" | "return" => KeyCodeValue::Enter,
"esc" | "escape" => KeyCodeValue::Esc,
"tab" => KeyCodeValue::Tab,
"backspace" => KeyCodeValue::Backspace,
"delete" | "del" => KeyCodeValue::Delete,
"insert" | "ins" => KeyCodeValue::Insert,
"up" => KeyCodeValue::Up,
"down" => KeyCodeValue::Down,
"left" => KeyCodeValue::Left,
"right" => KeyCodeValue::Right,
"home" => KeyCodeValue::Home,
"end" => KeyCodeValue::End,
"pageup" => KeyCodeValue::PageUp,
"pagedown" => KeyCodeValue::PageDown,
"space" => KeyCodeValue::Space,
s if s.starts_with('f') && s.len() > 1 => {
if let Ok(n) = s[1..].parse::<u8>() {
if (1..=24).contains(&n) {
KeyCodeValue::F(n)
} else {
return Err(format!("function key out of range: {key_part}"));
}
} else {
return Err(format!("unknown key: {key_part}"));
}
}
"[" => KeyCodeValue::Char('['),
"]" => KeyCodeValue::Char(']'),
"?" => KeyCodeValue::Char('?'),
"/" => KeyCodeValue::Char('/'),
"$" => KeyCodeValue::Char('$'),
"play" => KeyCodeValue::Media(MediaKeyCode::Play),
"pause" => KeyCodeValue::Media(MediaKeyCode::Pause),
"playpause" => KeyCodeValue::Media(MediaKeyCode::PlayPause),
"stop" => KeyCodeValue::Media(MediaKeyCode::Stop),
"fastforward" => KeyCodeValue::Media(MediaKeyCode::FastForward),
"rewind" => KeyCodeValue::Media(MediaKeyCode::Rewind),
"tracknext" => KeyCodeValue::Media(MediaKeyCode::TrackNext),
"trackprevious" => KeyCodeValue::Media(MediaKeyCode::TrackPrevious),
"record" => KeyCodeValue::Media(MediaKeyCode::Record),
"lowervolume" => KeyCodeValue::Media(MediaKeyCode::LowerVolume),
"raisevolume" => KeyCodeValue::Media(MediaKeyCode::RaiseVolume),
"mutevolume" | "mute" => KeyCodeValue::Media(MediaKeyCode::MuteVolume),
_ => {
let chars: Vec<char> = key_part.chars().collect();
if chars.len() == 1 {
let c = chars[0];
if c.is_ascii_uppercase() && !ctrl && !alt && !super_key {
return Ok(SingleKey {
code: KeyCodeValue::Char(c),
ctrl: false,
alt: false,
shift: false,
super_key: false,
});
}
KeyCodeValue::Char(c)
} else {
return Err(format!("unknown key: {key_part}"));
}
}
};
Ok(SingleKey {
code,
ctrl,
alt,
shift,
super_key,
})
}
}
impl fmt::Display for SingleKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.super_key {
write!(f, "super-")?;
}
if self.ctrl {
write!(f, "ctrl-")?;
}
if self.alt {
write!(f, "alt-")?;
}
if self.shift {
write!(f, "shift-")?;
}
match &self.code {
KeyCodeValue::Char(c) => write!(f, "{c}"),
KeyCodeValue::Enter => write!(f, "enter"),
KeyCodeValue::Esc => write!(f, "esc"),
KeyCodeValue::Tab => write!(f, "tab"),
KeyCodeValue::Backspace => write!(f, "backspace"),
KeyCodeValue::Delete => write!(f, "delete"),
KeyCodeValue::Insert => write!(f, "insert"),
KeyCodeValue::Up => write!(f, "up"),
KeyCodeValue::Down => write!(f, "down"),
KeyCodeValue::Left => write!(f, "left"),
KeyCodeValue::Right => write!(f, "right"),
KeyCodeValue::Home => write!(f, "home"),
KeyCodeValue::End => write!(f, "end"),
KeyCodeValue::PageUp => write!(f, "pageup"),
KeyCodeValue::PageDown => write!(f, "pagedown"),
KeyCodeValue::Space => write!(f, "space"),
KeyCodeValue::F(n) => write!(f, "f{n}"),
KeyCodeValue::Media(m) => match m {
MediaKeyCode::Play => write!(f, "play"),
MediaKeyCode::Pause => write!(f, "media-pause"),
MediaKeyCode::PlayPause => write!(f, "playpause"),
MediaKeyCode::Stop => write!(f, "stop"),
MediaKeyCode::FastForward => write!(f, "fastforward"),
MediaKeyCode::Rewind => write!(f, "rewind"),
MediaKeyCode::TrackNext => write!(f, "tracknext"),
MediaKeyCode::TrackPrevious => write!(f, "trackprevious"),
MediaKeyCode::Record => write!(f, "record"),
MediaKeyCode::LowerVolume => write!(f, "lowervolume"),
MediaKeyCode::RaiseVolume => write!(f, "raisevolume"),
MediaKeyCode::MuteVolume => write!(f, "mutevolume"),
MediaKeyCode::Reverse => write!(f, "reverse"),
},
}
}
}
impl KeyInput {
pub fn parse(s: &str) -> Result<Self, String> {
let s = s.trim();
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() > 1 {
let keys: Result<Vec<SingleKey>, String> =
parts.iter().map(|p| SingleKey::parse(p)).collect();
Ok(KeyInput::Sequence(keys?))
} else {
Ok(KeyInput::Single(SingleKey::parse(s)?))
}
}
}
impl fmt::Display for KeyInput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyInput::Single(k) => write!(f, "{k}"),
KeyInput::Sequence(keys) => {
for (i, k) in keys.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{k}")?;
}
Ok(())
}
}
}
}
impl Serialize for KeyInput {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for KeyInput {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
KeyInput::parse(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use rstest::rstest;
use super::*;
#[rstest]
#[case::char_a("a", KeyCodeValue::Char('a'), false, false, false, false)]
#[case::enter("enter", KeyCodeValue::Enter, false, false, false, false)]
#[case::esc("esc", KeyCodeValue::Esc, false, false, false, false)]
#[case::tab("tab", KeyCodeValue::Tab, false, false, false, false)]
#[case::space("space", KeyCodeValue::Space, false, false, false, false)]
#[case::ctrl_c("ctrl-c", KeyCodeValue::Char('c'), true, false, false, false)]
#[case::alt_f("alt-f", KeyCodeValue::Char('f'), false, true, false, false)]
#[case::ctrl_alt_x("ctrl-alt-x", KeyCodeValue::Char('x'), true, true, false, false)]
#[case::upper_g("G", KeyCodeValue::Char('G'), false, false, false, false)]
#[case::ctrl_lbracket("ctrl-[", KeyCodeValue::Char('['), true, false, false, false)]
#[case::question("?", KeyCodeValue::Char('?'), false, false, false, false)]
#[case::slash("/", KeyCodeValue::Char('/'), false, false, false, false)]
#[case::super_a("super-a", KeyCodeValue::Char('a'), false, false, false, true)]
#[case::super_ctrl_c("super-ctrl-c", KeyCodeValue::Char('c'), true, false, false, true)]
#[case::super_g("super-G", KeyCodeValue::Char('G'), false, false, false, true)]
#[case::f1("f1", KeyCodeValue::F(1), false, false, false, false)]
#[case::f12_upper("F12", KeyCodeValue::F(12), false, false, false, false)]
#[case::ctrl_f5("ctrl-f5", KeyCodeValue::F(5), true, false, false, false)]
#[case::f24("f24", KeyCodeValue::F(24), false, false, false, false)]
#[case::insert("insert", KeyCodeValue::Insert, false, false, false, false)]
#[case::ins("ins", KeyCodeValue::Insert, false, false, false, false)]
#[case::ctrl_insert("ctrl-insert", KeyCodeValue::Insert, true, false, false, false)]
fn parse_single_key(
#[case] input: &str,
#[case] code: KeyCodeValue,
#[case] ctrl: bool,
#[case] alt: bool,
#[case] shift: bool,
#[case] super_key: bool,
) {
let k = SingleKey::parse(input).unwrap();
assert_eq!(k.code, code);
assert_eq!(k.ctrl, ctrl);
assert_eq!(k.alt, alt);
assert_eq!(k.shift, shift);
assert_eq!(k.super_key, super_key);
}
#[rstest]
fn super_aliases_parse_equal(#[values("super-a", "cmd-a", "win-a")] input: &str) {
assert_eq!(SingleKey::parse(input).unwrap(), SingleKey::parse("super-a").unwrap());
}
#[rstest]
#[case("ctrl-alt-shift-xxx")]
#[case("foobar-a")]
#[case("f0")]
#[case("f25")]
fn parse_errors(#[case] input: &str) {
assert!(SingleKey::parse(input).is_err());
}
#[rstest]
#[case::ctrl_c(
KeyCode::Char('c'),
KeyModifiers::CONTROL,
KeyCodeValue::Char('c'),
true,
false,
false,
false
)]
#[case::enter(
KeyCode::Enter,
KeyModifiers::NONE,
KeyCodeValue::Enter,
false,
false,
false,
false
)]
#[case::upper_g(
KeyCode::Char('G'),
KeyModifiers::SHIFT,
KeyCodeValue::Char('G'),
false,
false,
false,
false
)]
#[case::super_a(
KeyCode::Char('a'),
KeyModifiers::SUPER,
KeyCodeValue::Char('a'),
false,
false,
false,
true
)]
#[case::f1(KeyCode::F(1), KeyModifiers::NONE, KeyCodeValue::F(1), false, false, false, false)]
#[case::f12_ctrl(
KeyCode::F(12),
KeyModifiers::CONTROL,
KeyCodeValue::F(12),
true,
false,
false,
false
)]
#[case::insert(
KeyCode::Insert,
KeyModifiers::NONE,
KeyCodeValue::Insert,
false,
false,
false,
false
)]
#[case::backtab(
KeyCode::BackTab,
KeyModifiers::NONE,
KeyCodeValue::Tab,
false,
false,
true,
false
)]
#[case::backtab_ctrl(
KeyCode::BackTab,
KeyModifiers::CONTROL,
KeyCodeValue::Tab,
true,
false,
true,
false
)]
fn from_event_cases(
#[case] code: KeyCode,
#[case] mods: KeyModifiers,
#[case] expected: KeyCodeValue,
#[case] ctrl: bool,
#[case] alt: bool,
#[case] shift: bool,
#[case] super_key: bool,
) {
let event = KeyEvent::new(code, mods);
let k = SingleKey::from_event(&event).unwrap();
assert_eq!(k.code, expected);
assert_eq!(k.ctrl, ctrl);
assert_eq!(k.alt, alt);
assert_eq!(k.shift, shift);
assert_eq!(k.super_key, super_key);
}
#[rstest]
#[case(KeyCode::Char('c'), KeyModifiers::CONTROL, "ctrl-c")]
#[case(KeyCode::Char('G'), KeyModifiers::SHIFT, "G")]
#[case(KeyCode::Char('a'), KeyModifiers::SUPER, "super-a")]
#[case(KeyCode::F(12), KeyModifiers::NONE, "f12")]
#[case(KeyCode::BackTab, KeyModifiers::NONE, "shift-tab")]
fn from_event_matches_parsed(
#[case] code: KeyCode,
#[case] mods: KeyModifiers,
#[case] parsed: &str,
) {
assert_eq!(
SingleKey::from_event(&KeyEvent::new(code, mods)).unwrap(),
SingleKey::parse(parsed).unwrap()
);
}
#[rstest]
fn display_round_trip(
#[values(
"ctrl-c", "alt-f", "enter", "G", "tab", "pageup", "f1", "f12", "ctrl-f5", "alt-f10",
"insert", "super-a", "g g"
)]
s: &str,
) {
let k = KeyInput::parse(s).unwrap();
assert_eq!(k, KeyInput::parse(&k.to_string()).unwrap(), "round-trip failed for {s}");
}
#[rstest]
#[case("g g", "g g")]
#[case("super-a", "super-a")]
#[case("super-ctrl-x", "super-ctrl-x")]
#[case("f1", "f1")]
#[case("ctrl-f12", "ctrl-f12")]
#[case("insert", "insert")]
fn display_exact(#[case] input: &str, #[case] expected: &str) {
assert_eq!(KeyInput::parse(input).unwrap().to_string(), expected);
}
#[rstest]
fn parse_multi_key_sequence() {
let ki = KeyInput::parse("g g").unwrap();
match ki {
KeyInput::Sequence(keys) => {
assert_eq!(keys.len(), 2);
assert_eq!(keys[0].code, KeyCodeValue::Char('g'));
assert_eq!(keys[1].code, KeyCodeValue::Char('g'));
}
KeyInput::Single(_) => panic!("expected sequence"),
}
}
}