From 3fbb9127a60ad0ecbc884bf5834568f27d853de8 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 26 Apr 2023 15:00:41 +0200 Subject: [PATCH] Model and (de-)serialize key bindings --- Cargo.lock | 44 +++++++++ Cargo.toml | 6 ++ cove-config/Cargo.toml | 3 +- cove-input/Cargo.toml | 4 + cove-input/src/keys.rs | 198 +++++++++++++++++++++++++++++++++++++++++ cove-input/src/lib.rs | 2 + cove/Cargo.toml | 5 +- 7 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 cove-input/src/keys.rs diff --git a/Cargo.lock b/Cargo.lock index ffd8e6e..ecdc77d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,6 +283,12 @@ dependencies = [ [[package]] name = "cove-input" version = "0.6.1" +dependencies = [ + "crossterm", + "serde", + "serde_either", + "thiserror", +] [[package]] name = "cove-macro" @@ -705,6 +711,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.15.0" @@ -736,6 +751,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "ordered-float" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +dependencies = [ + "num-traits", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1029,6 +1053,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.159" @@ -1040,6 +1074,16 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "serde_either" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "689643f4e7826ffcd227d2cc166bfdf5869750191ffe9fd593531e6ba351f2fb" +dependencies = [ + "serde", + "serde-value", +] + [[package]] name = "serde_json" version = "1.0.95" diff --git a/Cargo.toml b/Cargo.toml index 2873003..33e3c2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,11 @@ members = ["cove", "cove-*"] version = "0.6.1" edition = "2021" +[workspace.dependencies] +crossterm = "0.26.1" +serde = { version = "1.0.159", features = ["derive"] } +serde_either = "0.2.1" +thiserror = "1.0.40" + [profile.dev.package."*"] opt-level = 3 diff --git a/cove-config/Cargo.toml b/cove-config/Cargo.toml index 11fcc5b..65c8d55 100644 --- a/cove-config/Cargo.toml +++ b/cove-config/Cargo.toml @@ -6,5 +6,6 @@ edition = { workspace = true } [dependencies] cove-macro = { path = "../cove-macro" } -serde = { version = "1.0.159", features = ["derive"] } +serde = { workspace = true } + toml = "0.7.3" diff --git a/cove-input/Cargo.toml b/cove-input/Cargo.toml index 346ce18..2c3c243 100644 --- a/cove-input/Cargo.toml +++ b/cove-input/Cargo.toml @@ -4,3 +4,7 @@ version = { workspace = true } edition = { workspace = true } [dependencies] +crossterm = { workspace = true } +serde = { workspace = true } +serde_either = { workspace = true } +thiserror = { workspace = true } diff --git a/cove-input/src/keys.rs b/cove-input/src/keys.rs new file mode 100644 index 0000000..ddedcd9 --- /dev/null +++ b/cove-input/src/keys.rs @@ -0,0 +1,198 @@ +use std::fmt; +use std::num::ParseIntError; +use std::str::FromStr; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use serde::{de::Error, Deserialize, Deserializer}; +use serde::{Serialize, Serializer}; +use serde_either::SingleOrVec; + +#[derive(Debug, thiserror::Error)] +pub enum ParseKeysError { + #[error("no key code specified")] + NoKeyCode, + #[error("unknown key code: {0:?}")] + UnknownKeyCode(String), + #[error("invalid function key number: {0}")] + InvalidFNumber(#[from] ParseIntError), + #[error("unknown modifier: {0:?}")] + UnknownModifier(String), + #[error("modifier {0} conflicts with previous modifier")] + ConflictingModifier(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KeyPress { + pub code: KeyCode, + pub shift: bool, + pub ctrl: bool, + pub alt: bool, + pub any: bool, +} + +impl KeyPress { + fn parse_key_code(code: &str) -> Result { + let code = match code { + "backspace" => KeyCode::Backspace, + "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, + "tab" => KeyCode::Tab, + "backtab" => KeyCode::BackTab, + "delete" => KeyCode::Delete, + "insert" => KeyCode::Insert, + "esc" => KeyCode::Esc, + c if c.starts_with('F') => KeyCode::F(c.strip_prefix('F').unwrap().parse()?), + c if c.chars().count() == 1 => KeyCode::Char(c.chars().next().unwrap()), + "" => return Err(ParseKeysError::NoKeyCode), + c => return Err(ParseKeysError::UnknownKeyCode(c.to_string())), + }; + Ok(Self { + code, + shift: false, + ctrl: false, + alt: false, + any: false, + }) + } + + fn display_key_code(code: KeyCode) -> String { + match code { + KeyCode::Backspace => "backspace".to_string(), + KeyCode::Enter => "enter".to_string(), + KeyCode::Left => "left".to_string(), + KeyCode::Right => "right".to_string(), + KeyCode::Up => "up".to_string(), + KeyCode::Down => "down".to_string(), + KeyCode::Home => "home".to_string(), + KeyCode::End => "end".to_string(), + KeyCode::PageUp => "pageup".to_string(), + KeyCode::PageDown => "pagedown".to_string(), + KeyCode::Tab => "tab".to_string(), + KeyCode::BackTab => "backtab".to_string(), + KeyCode::Delete => "delete".to_string(), + KeyCode::Insert => "insert".to_string(), + KeyCode::Esc => "esc".to_string(), + KeyCode::F(n) => format!("F{n}"), + KeyCode::Char(c) => c.to_string(), + _ => "unknown".to_string(), + } + } + + fn parse_modifier(&mut self, modifier: &str) -> Result<(), ParseKeysError> { + match modifier { + m if self.any => return Err(ParseKeysError::ConflictingModifier(m.to_string())), + "shift" if !self.shift => self.shift = true, + "ctrl" if !self.ctrl => self.ctrl = true, + "alt" if !self.alt => self.alt = true, + "any" if !self.shift && !self.ctrl && !self.alt => self.any = true, + m @ ("shift" | "ctrl" | "alt" | "any") => { + return Err(ParseKeysError::ConflictingModifier(m.to_string())) + } + m => return Err(ParseKeysError::UnknownModifier(m.to_string())), + } + Ok(()) + } + + pub fn matches(&self, event: KeyEvent) -> bool { + if event.code != self.code { + return false; + } + + if self.any { + return true; + } + + let shift = event.modifiers.contains(KeyModifiers::SHIFT); + let ctrl = event.modifiers.contains(KeyModifiers::CONTROL); + let alt = event.modifiers.contains(KeyModifiers::ALT); + self.shift == shift && self.ctrl == ctrl && self.alt == alt + } +} + +impl FromStr for KeyPress { + type Err = ParseKeysError; + + fn from_str(s: &str) -> Result { + let mut parts = s.split('+'); + let code = parts.next_back().ok_or(ParseKeysError::NoKeyCode)?; + + let mut keys = KeyPress::parse_key_code(code)?; + for modifier in parts { + keys.parse_modifier(modifier)?; + } + + Ok(keys) + } +} + +impl fmt::Display for KeyPress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let code = Self::display_key_code(self.code); + + let mut segments = vec![]; + if self.shift { + segments.push("Shift"); + } + if self.ctrl { + segments.push("Ctrl"); + } + if self.alt { + segments.push("Alt"); + } + if self.any { + segments.push("Any"); + } + segments.push(&code); + + segments.join("+").fmt(f) + } +} + +impl Serialize for KeyPress { + fn serialize(&self, serializer: S) -> Result { + format!("{self}").serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for KeyPress { + fn deserialize>(deserializer: D) -> Result { + String::deserialize(deserializer)? + .parse() + .map_err(|e| D::Error::custom(format!("{e}"))) + } +} + +#[derive(Debug, Clone)] +pub struct KeyBinding(Vec); + +impl KeyBinding { + pub fn matches(&self, event: KeyEvent) -> bool { + self.0.iter().any(|kp| kp.matches(event)) + } +} + +impl Serialize for KeyBinding { + fn serialize(&self, serializer: S) -> Result { + if self.0.len() == 1 { + self.0[0].serialize(serializer) + } else { + self.0.serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for KeyBinding { + fn deserialize>(deserializer: D) -> Result { + Ok(match SingleOrVec::::deserialize(deserializer)? { + SingleOrVec::Single(key) => Self(vec![key]), + SingleOrVec::Vec(keys) => Self(keys), + }) + } +} diff --git a/cove-input/src/lib.rs b/cove-input/src/lib.rs index 8b13789..1da1a6f 100644 --- a/cove-input/src/lib.rs +++ b/cove-input/src/lib.rs @@ -1 +1,3 @@ +mod keys; +pub use keys::*; diff --git a/cove/Cargo.toml b/cove/Cargo.toml index 123b7c9..a15d8af 100644 --- a/cove/Cargo.toml +++ b/cove/Cargo.toml @@ -6,11 +6,13 @@ edition = { workspace = true } [dependencies] cove-config = { path = "../cove-config" } +crossterm = { workspace = true } +thiserror = { workspace = true } + anyhow = "1.0.70" async-trait = "0.1.68" clap = { version = "4.2.1", features = ["derive", "deprecated"] } cookie = "0.17.0" -crossterm = "0.26.1" directories = "5.0.0" edit = "0.1.4" linkify = "0.9.0" @@ -20,7 +22,6 @@ open = "4.0.1" parking_lot = "0.12.1" rusqlite = { version = "0.29.0", features = ["bundled", "time"] } serde_json = "1.0.95" -thiserror = "1.0.40" tokio = { version = "1.27.0", features = ["full"] } unicode-segmentation = "1.10.1" unicode-width = "0.1.10"