diff --git a/Cargo.lock b/Cargo.lock index d746749..66106a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +dependencies = [ + "memchr", +] + [[package]] name = "anstyle" version = "1.0.0" @@ -70,7 +79,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", ] [[package]] @@ -104,6 +113,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "derive-new" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -203,9 +223,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "parking_lot" @@ -230,20 +250,46 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "parse-display" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6509d08722b53e8dafe97f2027b22ccbe3a5db83cb352931e9716b0aa44bc5c" +dependencies = [ + "once_cell", + "parse-display-derive", + "regex", +] + +[[package]] +name = "parse-display-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68517892c8daf78da08c0db777fcc17e07f2f63ef70041718f8a7630ad84f341" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.28", +] + [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -270,6 +316,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + [[package]] name = "rustversion" version = "1.0.12" @@ -299,7 +374,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", ] [[package]] @@ -347,6 +422,29 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "structmeta" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.28", +] + +[[package]] +name = "structmeta-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "strum" version = "0.24.1" @@ -379,9 +477,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -462,9 +560,11 @@ dependencies = [ "anyhow", "clap", "crossterm", + "derive-new", "derive_more", "indoc", "itertools", + "parse-display", "ratatui", "serde", "strum", diff --git a/Cargo.toml b/Cargo.toml index 9577628..1a36837 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ derive_more = { version = "0.99.17", default-features = false, features = ["from tabwriter = "1.2.1" strum = "0.24.1" strum_macros = "0.24.3" +parse-display = "0.8.2" +derive-new = "0.5.9" # generated by 'cargo dist init' [profile.dist] diff --git a/src/config/keybindings/key.rs b/src/config/keybindings/key.rs index 87d7b3b..7b5e33d 100644 --- a/src/config/keybindings/key.rs +++ b/src/config/keybindings/key.rs @@ -1,117 +1,202 @@ -use anyhow::{bail, Result}; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use anyhow::{bail, Error, Result}; +use crossterm::event::{KeyCode as CKeyCode, KeyEvent as CKeyEvent, KeyModifiers as CKeyModifiers}; use derive_more::From; -use std::str::FromStr; +use parse_display::{Display, FromStr}; +use std::{fmt, str}; -#[cfg_attr(test, derive(Debug))] -#[derive(Hash, Eq, PartialEq, From, Clone)] -pub struct Key(KeyEvent); +/// The specific combinations of modifiers and key codes that we allow/handle. +#[derive(Hash, Eq, PartialEq, From, Clone, Debug)] +pub struct KeyEvent { + modifier: KeyModifier, + code: KeyCode, +} + +#[derive(Hash, Eq, PartialEq, From, Clone, Debug, Display, FromStr)] +#[display(style = "lowercase")] +enum KeyModifier { + Alt, + Ctrl, + #[from_str(ignore)] + None, +} + +#[derive(Hash, Eq, PartialEq, From, Clone, Debug, Display, FromStr)] +#[display(style = "lowercase")] +enum KeyCode { + Esc, + Enter, + Left, + Right, + Up, + Down, + Home, + End, + PageUp, + PageDown, + BackTab, + Backspace, + Delete, + Insert, + Tab, + Space, -impl FromStr for Key { - type Err = anyhow::Error; + #[display("{0}")] + Char(char), + + // Parse only values 1 to 12 + #[from_str(regex = "f(?<0>[1-9]|1[0-2])")] + #[display("f{0}")] + F(u8), +} + +impl str::FromStr for KeyEvent { + type Err = Error; fn from_str(s: &str) -> Result { - let event = match s.split_once('+') { - Some((s1, s2)) => { - let mut event = parse_code(s2)?; - event.modifiers.insert(parse_modifier(s1)?); - event - } - None => parse_code(s)?, + let (code, modifier) = match s.split_once('+') { + Some((modifier, code)) => (code.parse()?, modifier.parse()?), + None => (s.parse()?, KeyModifier::None), }; - Ok(Key(event)) + Ok(Self { modifier, code }) + } +} + +impl fmt::Display for KeyEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.modifier == KeyModifier::None { + write!(f, "{}", self.code)?; + } else { + write!(f, "{}+{}", self.modifier, self.code)?; + } + Ok(()) + } +} + +impl TryFrom for KeyEvent { + type Error = Error; + fn try_from(key: CKeyEvent) -> std::result::Result { + let code = key.code.try_into()?; + let modifier = key.modifiers.try_into()?; + Ok(Self { modifier, code }) } } -fn parse_modifier(s: &str) -> Result { - Ok(match s { - "alt" => KeyModifiers::ALT, - "ctrl" => KeyModifiers::CONTROL, - invalid => bail!("Invalid key modifier provided in keybinding: {}", invalid), - }) +impl TryFrom for KeyModifier { + type Error = Error; + fn try_from(value: CKeyModifiers) -> std::result::Result { + Ok(match value { + CKeyModifiers::ALT => Self::Alt, + CKeyModifiers::CONTROL => Self::Ctrl, + CKeyModifiers::NONE => Self::None, + // TODO: shouldn't use debug output for display output + _ => bail!("Invalid modifier key: {:?}", value), + }) + } } -fn parse_code(s: &str) -> Result { - let code = match s { - "esc" => KeyCode::Esc, - "enter" => KeyCode::Enter, - "left" => KeyCode::Left, - "right" => KeyCode::Right, - "up" => KeyCode::Up, - "down" => KeyCode::Down, - "home" => KeyCode::Home, - "end" => KeyCode::End, - "pageup" => KeyCode::PageUp, - "pagedown" => KeyCode::PageDown, - "backtab" => KeyCode::BackTab, - "backspace" => KeyCode::Backspace, - "del" => KeyCode::Delete, - "delete" => KeyCode::Delete, - "insert" => KeyCode::Insert, - "ins" => KeyCode::Insert, - "f1" => KeyCode::F(1), - "f2" => KeyCode::F(2), - "f3" => KeyCode::F(3), - "f4" => KeyCode::F(4), - "f5" => KeyCode::F(5), - "f6" => KeyCode::F(6), - "f7" => KeyCode::F(7), - "f8" => KeyCode::F(8), - "f9" => KeyCode::F(9), - "f10" => KeyCode::F(10), - "f11" => KeyCode::F(11), - "f12" => KeyCode::F(12), - "space" => KeyCode::Char(' '), - "tab" => KeyCode::Tab, - c if c.len() == 1 => KeyCode::Char(c.chars().next().unwrap()), - invalid => bail!("Invalid key code provided in keybinding: {}", invalid), - }; - Ok(KeyEvent::from(code)) +impl TryFrom for KeyCode { + type Error = Error; + fn try_from(value: CKeyCode) -> std::result::Result { + Ok(match value { + CKeyCode::Esc => KeyCode::Esc, + CKeyCode::Enter => KeyCode::Enter, + CKeyCode::Left => KeyCode::Left, + CKeyCode::Right => KeyCode::Right, + CKeyCode::Up => KeyCode::Up, + CKeyCode::Down => KeyCode::Down, + CKeyCode::Home => KeyCode::Home, + CKeyCode::End => KeyCode::End, + CKeyCode::PageUp => KeyCode::PageUp, + CKeyCode::PageDown => KeyCode::PageDown, + CKeyCode::BackTab => KeyCode::BackTab, + CKeyCode::Backspace => KeyCode::Backspace, + CKeyCode::Delete => KeyCode::Delete, + CKeyCode::Insert => KeyCode::Insert, + CKeyCode::F(c) => KeyCode::F(c), + CKeyCode::Tab => KeyCode::Tab, + CKeyCode::Char(' ') => KeyCode::Space, + CKeyCode::Char(c) => KeyCode::Char(c), + // TODO: shouldn't use debug output for display output + _ => bail!("Invalid key code: {:?}", value), + }) + } } #[cfg(test)] mod tests { use super::*; + fn assert_eq_parse_display(input_str: &str, expected: T) + where + T: str::FromStr + fmt::Display + PartialEq + fmt::Debug, + ::Err: fmt::Debug, + { + // Test FromStr + assert_eq!( + expected, + // TODO: non-ideal unwrap here + input_str.parse().unwrap(), + "Expected the input string '{}' to be parsed into {:?}", + input_str, + expected + ); + + // Test Display + assert_eq!( + input_str, + expected.to_string(), + "Expected the expected {:?} to be displayed as string '{}'", + expected, + input_str + ); + } + #[test] - fn test_parse_lowercase_key() -> Result<()> { - assert_eq!("k".parse::()?, Key(KeyCode::Char('k').into())); + fn test_valid_function_keys() -> Result<()> { + assert_eq_parse_display("f1", KeyCode::F(1)); + assert_eq_parse_display("f12", KeyCode::F(12)); Ok(()) } #[test] - fn test_parse_uppercase_key() -> Result<()> { - assert_eq!("G".parse::()?, Key(KeyCode::Char('G').into())); - Ok(()) + #[should_panic] + fn test_invalid_function_keys() { + let _: KeyCode = "f0".parse().unwrap(); + let _: KeyCode = "f13".parse().unwrap(); } #[test] - fn test_parse_ctrl_modifier() -> Result<()> { - assert_eq!( - "ctrl+c".parse::()?, - Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)) + fn test_valid_modifiers() { + assert_eq_parse_display( + "c", + KeyEvent { + modifier: KeyModifier::None, + code: KeyCode::Char('c'), + }, ); - // TODO: passes test, but doesn't practically work in all terminals - assert_eq!( - "ctrl+S".parse::()?, - Key(KeyEvent::new(KeyCode::Char('S'), KeyModifiers::CONTROL)) + + assert_eq_parse_display( + "alt+P", + KeyEvent { + modifier: KeyModifier::Alt, + code: KeyCode::Char('P'), + }, ); - Ok(()) - } - #[test] - fn test_parse_alt_modifier() -> Result<()> { - assert_eq!( - "alt+z".parse::()?, - Key(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::ALT)) + assert_eq_parse_display( + "ctrl+c", + KeyEvent { + modifier: KeyModifier::Ctrl, + code: KeyCode::Char('c'), + }, ); - Ok(()) } #[test] - fn test_parse_invalid_modifiers() { - assert!("shift+a".parse::().is_err()); - assert!("super+a".parse::().is_err()); - assert!("alt+shift+a".parse::().is_err()); - assert!("alt+ctrl+a".parse::().is_err()); + #[should_panic] + fn test_invalid_modifiers() { + let _: KeyModifier = "none".parse().unwrap(); + let _: KeyModifier = "shift".parse().unwrap(); + let _: KeyModifier = "super".parse().unwrap(); + let _: KeyModifier = "alt+ctrl".parse().unwrap(); } } diff --git a/src/config/keybindings/mod.rs b/src/config/keybindings/mod.rs index 6fcb477..ad0cb1b 100644 --- a/src/config/keybindings/mod.rs +++ b/src/config/keybindings/mod.rs @@ -1,7 +1,7 @@ mod key; mod operations; -pub use key::Key; +pub use key::KeyEvent; pub use operations::{Operation, Operations}; use anyhow::{bail, Result}; @@ -9,10 +9,10 @@ use serde::Deserialize; use std::collections::HashMap; #[derive(Clone)] -pub struct Keybindings(HashMap); +pub struct Keybindings(HashMap); impl Keybindings { - pub fn get_operations(&self, key: &Key) -> Option<&Operations> { + pub fn get_operations(&self, key: &KeyEvent) -> Option<&Operations> { self.0.get(key) } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 6b5a31a..7aaba8f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,7 +1,7 @@ mod keybindings; mod style; -pub use keybindings::{Key, Keybindings, Operations}; +pub use keybindings::{KeyEvent, Keybindings, Operations}; pub use style::Styles; use crate::command::Command; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7ca9b4f..2ff1ffc 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,9 +5,9 @@ pub use state::State; use crate::command::Command; use crate::config::Config; -use crate::config::{Key, Keybindings}; +use crate::config::{KeyEvent as CKeyEvent, Keybindings}; use anyhow::Result; -use crossterm::event::{self, Event::Key as CKey}; +use crossterm::event::{self, Event::Key}; use std::{ sync::mpsc::{self, Receiver, Sender, TryRecvError}, thread, @@ -16,7 +16,7 @@ use std::{ use terminal_manager::{Terminal, TerminalManager}; pub enum Event { - KeyPressed(Key), + KeyPressed(CKeyEvent), CommandOutput(Result), } @@ -110,10 +110,11 @@ fn poll_execute_command( fn poll_key_events(tx: Sender, keybindings: Keybindings) { thread::spawn(move || loop { - if let CKey(key_event) = event::read().unwrap() { - let key = key_event.into(); - if keybindings.get_operations(&key).is_some() { - tx.send(Event::KeyPressed(key)).unwrap(); + if let Key(key_event) = event::read().unwrap() { + if let Ok(key) = key_event.try_into() { + if keybindings.get_operations(&key).is_some() { + tx.send(Event::KeyPressed(key)).unwrap(); + } } } });