use std::fmt::{self, Display};
use std::hash::Hash;
use std::str::FromStr;
use self::parse::{parse_key_str, parse_macro_str};
use crate::keybindings::InputKey;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MediaKeyCode};
pub(crate) mod parse;
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum MacroError {
#[error("Invalid macro string: {0:?}")]
InvalidMacro(String),
#[error("Empty macro string")]
EmptyMacro,
#[error("Ending suspected macro loop; macro run {0} times w/o keyboard input")]
LoopingMacro(usize),
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct TerminalKey {
code: KeyCode,
modifiers: KeyModifiers,
}
impl TerminalKey {
pub(crate) fn new(mut code: KeyCode, mut modifiers: KeyModifiers) -> Self {
if let KeyCode::Char(ref mut c) = code {
if modifiers.intersects(KeyModifiers::SHIFT | KeyModifiers::CONTROL) {
*c = c.to_ascii_uppercase();
} else if c.is_ascii_uppercase() {
modifiers.insert(KeyModifiers::SHIFT);
}
if modifiers == KeyModifiers::SHIFT && *c != ' ' {
modifiers -= KeyModifiers::SHIFT;
}
}
Self { code, modifiers }
}
pub(crate) fn get_char_mods(&self) -> Option<(char, KeyModifiers)> {
if let KeyCode::Char(c) = self.code {
return Some((c, self.modifiers));
}
None
}
pub fn get_literal_char(&self) -> Option<char> {
match self.code {
KeyCode::Char(c) => {
if (self.modifiers - KeyModifiers::SHIFT).is_empty() {
return Some(c);
}
if self.modifiers == KeyModifiers::CONTROL {
let cp = match c {
'A'..='Z' => c as u32 - b'A' as u32 + 0x01,
' ' | '@' => 0x0,
'4'..='7' => c as u32 - b'4' as u32 + 0x1C,
_ => {
panic!("unknown control key: {:?}", c)
},
};
return char::from_u32(cp);
}
return None;
},
KeyCode::Tab if self.modifiers.is_empty() => {
return Some('\u{09}');
},
KeyCode::Enter => {
return Some('\u{0D}');
},
KeyCode::Esc => {
return Some('\u{1B}');
},
KeyCode::Backspace => {
return Some('\u{7F}');
},
_ => {
return None;
},
}
}
}
impl FromStr for TerminalKey {
type Err = MacroError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
if input.is_empty() {
return Err(MacroError::EmptyMacro);
} else if let Ok((_, key)) = parse_key_str(input) {
return Ok(key);
} else {
return Err(MacroError::InvalidMacro(input.to_string()));
}
}
}
impl Display for TerminalKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let push_mods = |f: &mut fmt::Formatter, mods: KeyModifiers| -> fmt::Result {
if mods.contains(KeyModifiers::CONTROL) {
write!(f, "C-")?;
}
if mods.contains(KeyModifiers::ALT) {
write!(f, "A-")?;
}
if mods.contains(KeyModifiers::SHIFT) {
write!(f, "S-")?;
}
Ok(())
};
let push_named_mods = |f: &mut fmt::Formatter, name: &str, mods| -> fmt::Result {
write!(f, "<")
.and_then(|()| push_mods(f, mods))
.and_then(|()| write!(f, "{}>", name))
};
let push_named =
|f: &mut fmt::Formatter, name: &str| push_named_mods(f, name, self.modifiers);
match self.code {
KeyCode::Left => push_named(f, "Left"),
KeyCode::Right => push_named(f, "Right"),
KeyCode::Up => push_named(f, "Up"),
KeyCode::Down => push_named(f, "Down"),
KeyCode::Backspace => push_named(f, "BS"),
KeyCode::Enter => push_named(f, "Enter"),
KeyCode::Home => push_named(f, "Home"),
KeyCode::End => push_named(f, "End"),
KeyCode::PageUp => push_named(f, "PageUp"),
KeyCode::PageDown => push_named(f, "PageDown"),
KeyCode::Null => push_named(f, "Nul"),
KeyCode::Esc => push_named(f, "Esc"),
KeyCode::Delete => push_named(f, "Del"),
KeyCode::Insert => push_named(f, "Insert"),
KeyCode::CapsLock => push_named(f, "CapsLock"),
KeyCode::ScrollLock => push_named(f, "ScrollLock"),
KeyCode::NumLock => push_named(f, "NumLock"),
KeyCode::PrintScreen => push_named(f, "PrintScreen"),
KeyCode::Pause => push_named(f, "Pause"),
KeyCode::Menu => push_named(f, "Menu"),
KeyCode::Tab => push_named(f, "Tab"),
KeyCode::BackTab => push_named_mods(f, "Tab", self.modifiers | KeyModifiers::SHIFT),
KeyCode::F(n) => {
let n = n.to_string();
push_named(f, n.as_str())
},
KeyCode::Char(c) => {
if c == ' ' {
if self.modifiers.is_empty() {
write!(f, "{c}")
} else {
push_named(f, "Space")
}
} else if c == '<' {
push_named(f, "lt")
} else if (self.modifiers - KeyModifiers::SHIFT).is_empty() {
write!(f, "{c}")
} else {
let c =
if self.modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::SHIFT) {
c.to_uppercase().to_string()
} else {
c.to_string()
};
push_named(f, c.as_str())
}
},
KeyCode::Media(mc) => {
let name = match mc {
MediaKeyCode::PlayPause => "MediaPlayPause",
MediaKeyCode::Play => "MediaPlay",
MediaKeyCode::Pause => "MediaPause",
MediaKeyCode::Reverse => "MediaReverse",
MediaKeyCode::Stop => "MediaStop",
MediaKeyCode::FastForward => "MediaFastForward",
MediaKeyCode::Rewind => "MediaRewind",
MediaKeyCode::TrackNext => "MediaTrackNext",
MediaKeyCode::TrackPrevious => "MediaTrackPrevious",
MediaKeyCode::Record => "MediaRecord",
MediaKeyCode::LowerVolume => "MediaVolumeUp",
MediaKeyCode::RaiseVolume => "MediaVolumeDown",
MediaKeyCode::MuteVolume => "MediaVolumeMute",
};
push_named(f, name)
},
KeyCode::Modifier(_) | KeyCode::KeypadBegin => {
Ok(())
},
}
}
}
impl InputKey for TerminalKey {
type Error = MacroError;
fn decompose(&mut self) -> Option<Self> {
if let KeyCode::Char(_) = self.code {
if self.modifiers.contains(KeyModifiers::ALT) {
self.modifiers -= KeyModifiers::ALT;
return Some(Self::from(KeyCode::Esc));
}
}
return None;
}
fn from_macro_str(input: &str) -> Result<Vec<Self>, MacroError> {
if input.is_empty() {
return Err(MacroError::EmptyMacro);
} else if let Ok((_, keys)) = parse_macro_str(input) {
return Ok(keys);
} else {
return Err(MacroError::InvalidMacro(input.to_string()));
}
}
fn get_char(&self) -> Option<char> {
if let KeyCode::Char(c) = self.code {
if (self.modifiers - KeyModifiers::SHIFT).is_empty() {
return Some(c);
}
}
None
}
}
impl From<KeyCode> for TerminalKey {
fn from(code: KeyCode) -> Self {
TerminalKey::new(code, KeyModifiers::NONE)
}
}
impl From<KeyEvent> for TerminalKey {
fn from(ke: KeyEvent) -> Self {
TerminalKey::new(ke.code, ke.modifiers)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(s: &str) {
let key = TerminalKey::from_str(s).expect("can parse");
assert_eq!(s, key.to_string());
}
#[test]
fn test_roundtrips() {
roundtrip("s");
roundtrip("S");
roundtrip("5");
roundtrip("^");
roundtrip(":");
roundtrip(";");
roundtrip("<lt>");
roundtrip(" ");
roundtrip("<C-Space>");
roundtrip("<S-Space>");
roundtrip("<C-A-Del>");
roundtrip("<Left>");
roundtrip("<S-Left>");
roundtrip("<A-Left>");
roundtrip("<C-Left>");
roundtrip("<Enter>");
roundtrip("<S-Enter>");
roundtrip("<A-Enter>");
roundtrip("<C-Enter>");
roundtrip("<BS>");
roundtrip("<S-BS>");
roundtrip("<A-BS>");
roundtrip("<C-BS>");
roundtrip("<A-Tab>");
roundtrip("<A-S-Tab>");
roundtrip("<C-A-S-Tab>");
roundtrip("<C-A-Tab>");
roundtrip("<A-S-J>");
roundtrip("<A-j>");
roundtrip("<BS>");
roundtrip("<C-?>");
roundtrip("<C-\\>");
roundtrip("<C-4>");
roundtrip("<C-]>");
roundtrip("<C-5>");
roundtrip("<C-[>");
roundtrip("<Esc>");
}
}