From 3e6a944df8e6104b24b997679921ca1dfb5a9dfa Mon Sep 17 00:00:00 2001 From: emilis Date: Sun, 3 Mar 2024 14:47:59 +0000 Subject: [PATCH] add initial parse phase for add subcommand (keybind), improve key parsing --- Cargo.lock | 97 +++++++++++++++++++++++ Cargo.toml | 1 + src/add/mod.rs | 81 +++++++++++++++++++ src/config.rs | 3 + src/hlwm/command.rs | 10 ++- src/hlwm/key.rs | 185 ++++++++++++++++++++++++++++++++------------ src/logerr.rs | 42 ++++++++++ src/main.rs | 17 +++- 8 files changed, 384 insertions(+), 52 deletions(-) create mode 100644 src/add/mod.rs diff --git a/Cargo.lock b/Cargo.lock index da9eb2c..a9e3d45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,6 +257,15 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "clipboard-win" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9a0700e0127ba15d1d52dd742097f821cd9c65939303a44d970465040a297" +dependencies = [ + "error-code", +] + [[package]] name = "cnx" version = "0.3.0" @@ -309,6 +318,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "env_logger" version = "0.10.2" @@ -338,6 +353,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "futures" version = "0.3.30" @@ -553,6 +585,7 @@ dependencies = [ "paste", "pretty_assertions", "pretty_env_logger", + "rustyline", "serde", "serde_json", "strum", @@ -685,6 +718,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -877,6 +930,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "regex" version = "1.10.3" @@ -931,6 +994,28 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rustyline" +version = "13.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + [[package]] name = "ryu" version = "1.0.17" @@ -1189,6 +1274,18 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 71f7ef6..9cc88e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ which = "6.0.0" log = "0.4" pretty_env_logger = "0.5" cnx = { git = "https://github.com/mjkillough/cnx.git", rev = "7845d99baa296901171c083db61588a62f9a8b34" } +rustyline = "13.0.0" [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/src/add/mod.rs b/src/add/mod.rs new file mode 100644 index 0000000..3af67a0 --- /dev/null +++ b/src/add/mod.rs @@ -0,0 +1,81 @@ +use clap::Subcommand; +use rustyline::{error::ReadlineError, DefaultEditor}; +use thiserror::Error; + +use crate::hlwm::{ + command::{CommandParseError, HlwmCommand}, + key::Key, + parser::{FromCommandArgs, ParseError}, +}; + +#[derive(Subcommand, Debug, Clone)] +pub enum Add { + /// Add a new keybind to the existing hlctl config + #[clap(arg_required_else_help = true)] + Keybind { + #[arg(short, long)] + interactive: bool, + }, +} + +#[derive(Debug, Clone, Error)] +pub enum Error { + #[error("readline error: [{0}]")] + ReadlineError(String), + #[error("parse error: [{0}]")] + KeyParseError(#[from] ParseError), + #[error("could not parse command: [{0}]")] + CommandParseError(#[from] CommandParseError), +} + +impl From for Error { + fn from(value: ReadlineError) -> Self { + Self::ReadlineError(value.to_string()) + } +} + +impl Add { + pub fn run(self) -> Result<(), Error> { + match self { + Add::Keybind { interactive } => { + if interactive { + keybind_interactive() + } else { + todo!() + } + } + } + } +} + +fn keybind_interactive() -> Result<(), Error> { + let mut read = DefaultEditor::new()?; + let keys = read + .readline("Keys (Separated by spaces, tabs, +, or -): ")? + .trim() + .split([' ', '\t', '+', '-']) + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .map(|k| Key::from_str_case_insensitive(k)) + .collect::, _>>()?; + + let command = { + let read = read.readline("Command (Arguments separated by tabs or spaces): ")?; + let mut args = read + .trim() + .split([' ', '\t']) + .map(|a| a.trim()) + .filter(|a| !a.is_empty()) + .map(|a| a.to_string()); + match args.next() { + Some(cmd) => HlwmCommand::from_command_args(&cmd, args)?, + None => { + println!("No command provided"); + std::process::exit(1); + } + } + }; + + println!("{keys:?} -> {command:?}"); + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index d8a44dd..c301a7c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -962,6 +962,9 @@ mod test { value: Attribute::Color(Color::X11(X11Color::NavajoWhite3)), }); let cfg_serialized = cfg.serialize().unwrap(); + println!("--> Config <--"); + println!("{cfg_serialized}"); + println!("--> Config <--"); let parsed_cfg = Config::deserialize(&cfg_serialized).unwrap(); assert_eq!(cfg, parsed_cfg,) diff --git a/src/hlwm/command.rs b/src/hlwm/command.rs index b79fdf7..1384d2b 100644 --- a/src/hlwm/command.rs +++ b/src/hlwm/command.rs @@ -277,7 +277,7 @@ impl Default for HlwmCommand { } } -#[derive(Debug, Error)] +#[derive(Debug, Clone, Error)] pub enum CommandParseError { #[error("invalid argument count [{0}] at [{1}]")] InvalidArgumentCount(usize, String), @@ -286,11 +286,17 @@ pub enum CommandParseError { #[error("command execution error: [{0}]")] CommandError(#[from] CommandError), #[error("string utf8 error")] - StringUtf8Error(#[from] FromUtf8Error), + StringUtf8Error(String), #[error("parsing error: [{0}]")] StringParseError(#[from] ParseError), } +impl From for CommandParseError { + fn from(value: FromUtf8Error) -> Self { + Self::StringUtf8Error(value.to_string()) + } +} + impl serde::de::Expected for CommandParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "expected valid command string. Got error: {self}") diff --git a/src/hlwm/key.rs b/src/hlwm/key.rs index bf7e852..439ad15 100644 --- a/src/hlwm/key.rs +++ b/src/hlwm/key.rs @@ -3,7 +3,7 @@ use std::{borrow::BorrowMut, fmt::Display, str::FromStr}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; -use crate::split; +use crate::{logerr::UnwrapLog, split}; use super::{ command::HlwmCommand, @@ -43,6 +43,21 @@ pub enum Key { Mouse(MouseButton), } +fn title_case(s: &str) -> String { + let mut chars = s.chars(); + let mut out_chars = Vec::with_capacity(s.len()); + + if let Some(first) = chars.next() { + out_chars.push(first.to_ascii_uppercase()); + } + + while let Some(c) = chars.next() { + out_chars.push(c.to_ascii_lowercase()); + } + + out_chars.into_iter().collect() +} + impl Key { pub fn is_standard(&self) -> bool { match self { @@ -68,6 +83,57 @@ impl Key { .map(Key::from_str) .collect::, _>>() } + pub fn from_str_case_insensitive(s: &str) -> Result { + Key::from_str(&title_case(s)) + } + + /// Returns the name for this key as it should appear in the config + pub fn config_name(&self) -> String { + self.aliases() + .first() + .expect_log("all keys should have at least one alias") + .to_string() + } + + /// Returns a list of aliases for the key + pub fn aliases(&self) -> Vec { + match self { + Key::Mod1Alt => vec!["Alt".into(), "Mod1".into()], + Key::Mod4Super => vec!["Super".into(), "Mod4".into()], + Key::Return => vec!["Return".into(), "Enter".into()], + Key::Shift => vec!["Shift".into()], + Key::Tab => vec!["Tab".into()], + Key::Left => vec!["Left".into()], + Key::Right => vec!["Right".into()], + Key::Up => vec!["Up".into()], + Key::Down => vec!["Down".into()], + Key::Space => vec!["Space".into()], + Key::Control => vec!["Ctrl".into(), "Ctl".into(), "Control".into()], + Key::Backtick => vec!["Backtick".into(), "Grave".into()], + Key::F1 => vec!["F1".into()], + Key::F2 => vec!["F2".into()], + Key::F3 => vec!["F3".into()], + Key::F4 => vec!["F4".into()], + Key::F5 => vec!["F5".into()], + Key::F6 => vec!["F6".into()], + Key::F7 => vec!["F7".into()], + Key::F8 => vec!["F8".into()], + Key::F9 => vec!["F9".into()], + Key::F10 => vec!["F10".into()], + Key::F11 => vec!["F11".into()], + Key::F12 => vec!["F12".into()], + Key::Home => vec!["Home".into()], + Key::Delete => vec!["Delete".into(), "Del".into()], + Key::Char(c) => vec![c.to_ascii_lowercase().to_string()], + Key::Mouse(m) => vec![m.to_string()], + } + } +} + +impl Display for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.aliases().first().unwrap()) + } } impl From for Key { @@ -87,7 +153,7 @@ impl Serialize for Key { where S: serde::Serializer, { - serializer.serialize_str(&self.to_string()) + serializer.serialize_str(&self.config_name()) } } @@ -103,7 +169,7 @@ impl<'de> Deserialize<'de> for Key { } } let str_val: String = Deserialize::deserialize(deserializer)?; - Ok(Self::from_str(&str_val).map_err(|_| { + Ok(Self::from_str_case_insensitive(&str_val).map_err(|_| { serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &ExpectedKey) })?) } @@ -118,14 +184,16 @@ impl FromStr for Key { Ok(Self::Mouse(MouseButton::from_str(s)?)) } _ => { - if let Some(key) = Self::iter().into_iter().find(|key| key.to_string() == s) { - match key { - Key::Char(_) | Key::Mouse(_) => (), - _ => return Ok(key), - } + if let Some(key) = Self::iter().into_iter().find(|key| match key { + Key::Char(_) | Key::Mouse(_) => false, + _ => key.aliases().contains(&s.to_string()), + }) { + return Ok(key); } if s.len() == 1 { - Ok(Self::Char(s.chars().next().unwrap())) + Ok(Self::Char( + s.chars().next().unwrap_log().to_ascii_lowercase(), + )) } else { Err(ParseError::InvalidValue { value: s.to_string(), @@ -179,42 +247,6 @@ impl Display for MouseButton { } } -impl Display for Key { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let data = match self { - Key::Return => "Return".to_string(), - Key::Shift => "Shift".to_string(), - Key::Tab => "Tab".to_string(), - Key::Left => "Left".to_string(), - Key::Right => "Right".to_string(), - Key::Up => "Up".to_string(), - Key::Down => "Down".to_string(), - Key::Space => "space".to_string(), - Key::Control => "Control".to_string(), - Key::Backtick => "grave".to_string(), - Key::Char(c) => c.to_string(), - Key::Mouse(m) => m.to_string(), - Key::F1 => "F1".to_string(), - Key::F2 => "F2".to_string(), - Key::F3 => "F3".to_string(), - Key::F4 => "F4".to_string(), - Key::F5 => "F5".to_string(), - Key::F6 => "F6".to_string(), - Key::F7 => "F7".to_string(), - Key::F8 => "F8".to_string(), - Key::F9 => "F9".to_string(), - Key::F10 => "F10".to_string(), - Key::F11 => "F11".to_string(), - Key::F12 => "F12".to_string(), - Key::Home => "Home".to_string(), - Key::Delete => "Delete".to_string(), - Key::Mod1Alt => "Mod1".to_string(), - Key::Mod4Super => "Mod4".to_string(), - }; - f.write_str(&data) - } -} - #[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq)] #[strum(serialize_all = "snake_case")] pub enum MousebindAction { @@ -456,9 +488,10 @@ mod test { use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; - use crate::hlwm::command::HlwmCommand; + use crate::{hlwm::command::HlwmCommand, logerr::UnwrapLog}; use super::{Key, MousebindAction}; + use pretty_assertions::assert_eq; #[derive(Debug, Serialize, Deserialize)] pub struct KeyWrapper { @@ -468,8 +501,8 @@ mod test { #[test] fn key_serialize_deserialize() { for (original_key, wrapper) in Key::iter().map(|key| (key, KeyWrapper { key })) { - let wrapper_str = toml::to_string_pretty(&wrapper).unwrap(); - let parsed: KeyWrapper = toml::from_str(&wrapper_str).unwrap(); + let wrapper_str = toml::to_string_pretty(&wrapper).expect_log("serialize"); + let parsed: KeyWrapper = toml::from_str(&wrapper_str).expect_log("deserialize"); assert_eq!(original_key, parsed.key); } } @@ -494,4 +527,60 @@ mod test { assert_eq!(original_key, parsed.action); } } + + fn case_random>(s: S) -> Vec { + let s = s.into(); + let counts: usize = s.chars().map(|c| c.is_ascii_alphabetic() as usize).sum(); + if counts < 2 { + return vec![s.to_string()]; + } + let mut variants: Vec = Vec::with_capacity(counts + 2); + let mut working = Vec::new(); + + for idx in 0..counts { + let mut c_idx = 0; + for c in s.chars() { + if !c.is_ascii_alphabetic() { + working.push(c); + continue; + } + if c_idx == idx { + working.push(c.to_ascii_uppercase()); + } else { + working.push(c.to_ascii_lowercase()); + } + c_idx += 1; + } + + variants.push(working.into_iter().collect()); + working = Vec::new(); + } + variants.push(s.to_ascii_lowercase()); + variants.push(s.to_ascii_uppercase()); + + variants + } + + #[test] + fn key_from_case_insensitive() { + let test_cases = Key::iter().map(|key| { + let strings = key + .aliases() + .into_iter() + .map(|a| case_random(a)) + .flatten() + .collect::>(); + (strings, key) + }); + + for (strings, key) in test_cases { + for case in strings { + println!("comparing str: [{case}] with key: [{key}]"); + let parsed = Key::from_str_case_insensitive(&case).expect(&format!( + "key [{key}] parse from insensitive string [{case}] failed" + )); + assert_eq!(key, parsed); + } + } + } } diff --git a/src/logerr.rs b/src/logerr.rs index 19d848b..77aa02c 100644 --- a/src/logerr.rs +++ b/src/logerr.rs @@ -6,6 +6,8 @@ pub trait UnwrapLog { type Target; fn unwrap_or_log(self, default: Self::Target) -> Self::Target; + fn unwrap_log(self) -> Self::Target; + fn expect_log(self, expect: &str) -> Self::Target; } impl UnwrapLog for Result @@ -28,6 +30,26 @@ where } } } + + fn unwrap_log(self) -> Self::Target { + match self { + Ok(target) => target, + Err(err) => { + error!("called `Result::unwrap_log()` on an `Err` value: {err}"); + panic!("called `Result::unwrap_log()` on an `Err` value: {err}"); + } + } + } + + fn expect_log(self, expect: &str) -> Self::Target { + match self { + Ok(target) => target, + Err(err) => { + error!("[{expect}] called `Result::expect_log()` on an `Err` value: {err}"); + panic!("[{expect}] called `Result::expect_log()` on an `Err` value: {err}"); + } + } + } } impl UnwrapLog for Option @@ -49,4 +71,24 @@ where } } } + + fn unwrap_log(self) -> Self::Target { + match self { + Some(target) => target, + None => { + error!("called `Option::unwrap_log()` on a `None` value"); + panic!("called `Option::unwrap_log()` on a `None` value"); + } + } + } + + fn expect_log(self, expect: &str) -> Self::Target { + match self { + Some(target) => target, + None => { + error!("[{expect}] called `Option::expect_log()` on an `None` value"); + panic!("[{expect}] called `Option::expect_log()` on an `None` value"); + } + } + } } diff --git a/src/main.rs b/src/main.rs index 36b1e0a..af96656 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ use std::path::{Path, PathBuf}; +use add::Add; use clap::{Parser, Subcommand}; use config::Config; -use log::{error, info}; +use log::{error, info, LevelFilter}; +mod add; pub mod cmd; mod config; pub mod environ; @@ -40,10 +42,16 @@ enum HlctlCommand { }, /// Start the top panel Panel, + /// Add an element to the existing hlctl config + #[command(subcommand)] + Add(Add), } fn main() { - pretty_env_logger::init(); + pretty_env_logger::formatted_builder() + .filter_module("rustyline", LevelFilter::Error) + .parse_default_env() + .init(); let args = Args::parse(); match args.command { @@ -55,6 +63,11 @@ fn main() { error!("panel: {err}"); } } + HlctlCommand::Add(add) => { + if let Err(err) = add.run() { + error!("add: {err}"); + } + } } }