From d8194cf78b0c85a3bf580eed337fe890ebe1d869 Mon Sep 17 00:00:00 2001 From: emilis Date: Wed, 6 Mar 2024 19:49:15 +0000 Subject: [PATCH] refactor command parsing and switch back to stable rustc --- Cargo.lock | 8 +- Cargo.toml | 2 +- rust-toolchain.toml | 2 + src/config.rs | 126 ++++----- src/environ.rs | 38 +-- src/hlwm/and_or_command.rs | 117 ++++++++ src/hlwm/attribute.rs | 217 +++++++++------ src/hlwm/color/mod.rs | 16 +- src/hlwm/color/x11.rs | 6 +- src/hlwm/command.rs | 537 +++++++++++++++++++------------------ src/hlwm/hlwmbool.rs | 16 +- src/hlwm/hook.rs | 98 +++---- src/hlwm/key.rs | 171 +++++------- src/hlwm/macros.rs | 153 ----------- src/hlwm/mod.rs | 138 +++++----- src/hlwm/pad.rs | 21 +- src/hlwm/parser/either.rs | 4 + src/hlwm/parser/mod.rs | 234 ++++++++++++++++ src/hlwm/parser/traits.rs | 67 +++++ src/hlwm/rule.rs | 90 ++++--- src/hlwm/setting.rs | 172 ++++++------ src/hlwm/tag.rs | 14 +- src/hlwm/theme.rs | 77 ++---- src/hlwm/window.rs | 6 +- src/main.rs | 1 - src/split.rs | 31 +++ 26 files changed, 1336 insertions(+), 1026 deletions(-) create mode 100644 rust-toolchain.toml create mode 100644 src/hlwm/and_or_command.rs delete mode 100644 src/hlwm/macros.rs create mode 100644 src/hlwm/parser/either.rs create mode 100644 src/hlwm/parser/mod.rs create mode 100644 src/hlwm/parser/traits.rs diff --git a/Cargo.lock b/Cargo.lock index d0a356e..da9eb2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1010,18 +1010,18 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 16b85f0..71f7ef6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ clap = { version = "4.4.18", features = ["derive", "cargo"] } paste = "1.0.14" serde = { version = "1.0.195", features = ["derive"] } serde_json = { version = "1.0.113" } -strum = { version = "0.25.0", features = ["derive"] } +strum = { version = "0.26", features = ["derive"] } thiserror = "1.0.57" toml = "0.8.8" which = "6.0.0" diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/src/config.rs b/src/config.rs index e69ee8a..d8a44dd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,22 +17,23 @@ use strum::IntoEnumIterator; use thiserror::Error; use crate::{ - environ::{self, ActiveKeybinds, EnvironError}, + environ::{self, ActiveKeybinds}, hlwm::{ self, - attribute::Attribute, + attribute::{Attribute, AttributeType}, color::Color, command::{CommandError, HlwmCommand}, hook::Hook, key::{Key, KeyUnbind, Keybind, MouseButton, Mousebind, MousebindAction}, + parser::{ArgParser, FromCommandArgs, FromStrings, ParseError}, rule::{Condition, Consequence, FloatPlacement, Rule, Unrule}, setting::{FrameLayout, Setting, ShowFrameDecoration, SmartFrameSurroundings}, theme::ThemeAttr, window::Window, - Align, Client, Direction, Index, Operator, Separator, StringParseError, ToggleBool, + Align, Client, Direction, Index, Operator, Separator, ToggleBool, }, logerr::UnwrapLog, - rule, window_types, + rule, split, window_types, }; #[derive(Debug, Error)] @@ -51,14 +52,13 @@ pub enum ConfigError { CommandError(#[from] CommandError), #[error("non-utf8 string error: {0}")] Utf8StringError(#[from] FromUtf8Error), - #[error("failed parsing value from string: {0}")] - StringParseError(#[from] StringParseError), - #[error("getting from environment error: [{0}]")] - EnvironError(#[from] EnvironError), + #[error("failed parsing value: {0}")] + ParseError(#[from] ParseError), } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Config { + pub use_panel: bool, pub font: String, pub font_bold: String, /// Pango font for use where pango fonts are @@ -98,35 +98,36 @@ impl Display for SetAttribute { } } -impl FromStr for SetAttribute { - type Err = StringParseError; - - fn from_str(s: &str) -> Result { - let mut parts = s.split(')'); - let type_string = parts.next().map(|p| p.strip_prefix('(')).flatten().ok_or( - StringParseError::RequiredArgMissing("attribute type".into()), - )?; - let mut parts = parts - .next() - .ok_or(StringParseError::RequiredArgMissing( - "attribute path/value".into(), - ))? - .split('=') - .map(|v| v.trim()); - let path = parts - .next() - .ok_or(StringParseError::RequiredArgMissing( - "attribute path".into(), - ))? - .to_string(); - let value_string = parts.collect::>().join("="); - if value_string.is_empty() { - return Err(StringParseError::RequiredArgMissing( - "attribute value".into(), - )); +impl FromStrings for SetAttribute { + fn from_strings>(s: I) -> Result { + let s = s.collect::>(); + let original_str = s.join(" "); + let mut parser = ArgParser::from_strings(s.into_iter()); + let x = parser.must_string("set_attribute(type/name)")?; + let (attr_ty, remainder) = split::parens(&x).ok_or(ParseError::InvalidValue { + value: original_str, + expected: "does not have a valid type string (type)", + })?; + if remainder.contains('=') { + let mut parser = ArgParser::from_strings(remainder.split('=').map(|c| c.to_string())); + return Ok(Self { + path: parser.must_string("set_attribute(path)")?, + value: Attribute::new( + AttributeType::from_str(&attr_ty)?, + &parser.must_string("set_attribute(value)")?, + )?, + }); } - let value = Attribute::new(type_string, &value_string)?; - Ok(Self { path, value }) + if parser.must_string("set_attribute(equals)")? != "=" { + return Err(ParseError::ValueMissing); + } + Ok(Self { + path: remainder, + value: Attribute::new( + AttributeType::from_str(&attr_ty)?, + &parser.must_string("set_attribute(value)")?, + )?, + }) } } @@ -151,13 +152,16 @@ impl<'de> Deserialize<'de> for SetAttribute { } } let str_val: String = Deserialize::deserialize(deserializer)?; - Ok(Self::from_str(&str_val).map_err(|_| { - serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &ExpectedKey) - })?) + Ok( + Self::from_strings(split::tab_or_space(&str_val).into_iter()).map_err(|_| { + serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &ExpectedKey) + })?, + ) } } impl Config { + const USE_PANEL: &'static str = "settings.my_hlctl_panel_enabled"; const FONT: &'static str = "theme.my_font"; const FONT_BOLD: &'static str = "theme.my_font_bold"; const FONT_PANGO: &'static str = "theme.my_font_pango"; @@ -216,6 +220,7 @@ impl Config { fn attrs_set(&self) -> Result, ConfigError> { info!("loading attr settings command set"); Ok([ + (Self::USE_PANEL, Some(self.use_panel.to_string())), (Self::FONT, Some(self.font.clone())), (Self::FONT_BOLD, Some(self.font_bold.clone())), (Self::FONT_PANGO, Some(self.font_pango.clone())), @@ -291,8 +296,7 @@ impl Config { HlwmCommand::Spawn { executable: s.name, args: s.arguments, - } - .to_try(), + }, ] }) .flatten() @@ -389,6 +393,9 @@ impl Config { let default = Config::default(); let mod_key = setting(Self::MOD_KEY, Key::from_str, default.mod_key); Config { + use_panel: client + .get_from_str_attr(Self::USE_PANEL.to_string()) + .unwrap_or_log(default.use_panel), font: client .get_str_attr(Self::FONT.to_string()) .unwrap_or_log(default.font), @@ -416,12 +423,13 @@ impl Config { .into_iter() .find(|f| (*f).eq(&attr)) .unwrap(); - ThemeAttr::from_raw_parts( + ThemeAttr::from_command_args( &attr_path, - &client + [client .get_attr(attr_path.clone()) .map(|t| t.to_string()) - .unwrap_or_log(default.to_string()), + .unwrap_or_log(default.to_string())] + .into_iter(), ) .unwrap_or_log(default.clone()) }) @@ -432,14 +440,13 @@ impl Config { .unwrap_or_log(default.keybinds), tags: Self::active_tags(mod_key).unwrap_or_log(default.tags), rules: (|| -> Result, ConfigError> { - Ok( - String::from_utf8(client.execute(HlwmCommand::ListRules)?.stdout)? - .split('\n') - .map(|l| l.trim()) - .filter(|l| !l.is_empty()) - .map(|line| Rule::from_str(line)) - .collect::>()?, - ) + Ok(HlwmCommand::ListRules + .execute_str()? + .split('\n') + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .map(|line| Rule::from_strings(split::tab_or_space(line).into_iter())) + .collect::>()?) })() .unwrap_or_log(default.rules), settings: (|| -> Result, CommandError> { @@ -582,6 +589,7 @@ impl Default for Config { let text_color = Color::from_hex("#898989").expect("default text color"); Self { + use_panel: true, mod_key, font: String::from("-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*"), font_bold: font_bold.clone(), @@ -719,16 +727,10 @@ impl Default for Config { HlwmCommand::JumpTo(Window::Urgent), ), ], - services: vec![ - Service { - name: String::from("fcitx5"), - arguments: vec![], - }, - Service { - name: String::from("hlctl"), - arguments: vec![String::from("panel")], - }, - ], + services: vec![Service { + name: String::from("fcitx5"), + arguments: vec![], + }], tags: [ Tag::standard(".1", Key::Char('1')), Tag::standard(".2", Key::Char('2')), diff --git a/src/environ.rs b/src/environ.rs index 182f3ee..a7415b5 100644 --- a/src/environ.rs +++ b/src/environ.rs @@ -1,43 +1,27 @@ -use std::{str::FromStr, string::FromUtf8Error}; - use log::debug; -use thiserror::Error; use crate::{ hlwm::{ command::{CommandError, HlwmCommand}, - key::{KeyParseError, Keybind}, - Client, + key::Keybind, + parser::FromStrings, }, split, }; -#[derive(Clone, Debug, Error)] -pub enum EnvironError { - #[error("keybind parsing error: [{0}]")] - KeyParseError(#[from] KeyParseError), - #[error("command execution error: [{0}]")] - CommandError(#[from] CommandError), -} - -impl From for EnvironError { - fn from(value: FromUtf8Error) -> Self { - CommandError::UtfError(value).into() - } -} - pub enum ActiveKeybinds { All, OmitNamedTagBinds, OnlyNamedTagBinds, } -pub fn active_keybinds(ty: ActiveKeybinds) -> Result, EnvironError> { +pub fn active_keybinds(ty: ActiveKeybinds) -> Result, CommandError> { let (use_tag, move_tag) = ( HlwmCommand::UseTag(String::new()).to_string(), HlwmCommand::MoveTag(String::new()).to_string(), ); - String::from_utf8(Client::new().execute(HlwmCommand::ListKeybinds)?.stdout)? + HlwmCommand::ListKeybinds + .execute_str()? .split("\n") .map(|l| l.trim()) .filter(|l| !l.is_empty()) @@ -58,10 +42,12 @@ pub fn active_keybinds(ty: ActiveKeybinds) -> Result, EnvironError> } }) .map(|row: &str| { - Ok(Keybind::from_str(row).map_err(|err| { - debug!("row: [{row}], error: [{err}]"); - err - })?) + Ok( + Keybind::from_strings(split::tab_or_space(row).into_iter()).map_err(|err| { + debug!("row: [{row}], error: [{err}]"); + err + })?, + ) }) - .collect::, EnvironError>>() + .collect::, CommandError>>() } diff --git a/src/hlwm/and_or_command.rs b/src/hlwm/and_or_command.rs new file mode 100644 index 0000000..f083dd9 --- /dev/null +++ b/src/hlwm/and_or_command.rs @@ -0,0 +1,117 @@ +use crate::hlwm::parser::ParseError; + +use super::{ + command::HlwmCommand, + parser::{ArgParser, FromCommandArgs, FromStringsHint}, + Separator, +}; + +pub struct AndOrCommands(Vec); + +impl AndOrCommands { + pub fn commands(self) -> Vec { + self.0 + } +} + +impl FromStringsHint for AndOrCommands { + fn from_strings_hint>( + s: I, + hint: Separator, + ) -> Result { + Ok(Self( + split_by_separator(s, hint) + .into_iter() + .map(|cmd_args| -> Result { + let mut parser = ArgParser::from_strings(cmd_args.into_iter()); + HlwmCommand::from_command_args( + &parser.must_string("hlwmcommand(command)")?, + parser.collect::>().into_iter(), + ) + }) + .collect::, ParseError>>()?, + )) + } +} + +fn split_by_separator>(s: I, separator: Separator) -> Vec> { + let separator = separator.to_string(); + let mut commands = Vec::new(); + let mut working = Vec::new(); + for itm in s { + if itm.len() == 1 && itm == separator { + if !working.is_empty() { + commands.push(working); + working = Vec::new(); + } + continue; + } + working.push(itm); + } + if !working.is_empty() { + commands.push(working); + } + commands +} + +#[cfg(test)] +mod test { + use crate::hlwm::Separator; + + use super::split_by_separator; + use pretty_assertions::assert_eq; + + #[test] + fn test_split_by_separator() { + let cases = [( + [ + "or", + ",", + "and", + ".", + "compare", + "tags.focus.curframe_wcount", + "=", + "2", + ".", + "cycle_layout", + "+1", + "vertical", + "horizontal", + "max", + "vertical", + "grid", + ",", + "cycle_layout", + "+1", + ] + .into_iter(), + Separator::Comma, + vec![ + vec!["or"], + vec![ + "and", + ".", + "compare", + "tags.focus.curframe_wcount", + "=", + "2", + ".", + "cycle_layout", + "+1", + "vertical", + "horizontal", + "max", + "vertical", + "grid", + ], + vec!["cycle_layout", "+1"], + ], + )]; + for (input, separator, expected) in cases { + let input = input.map(|i| i.to_string()); + let actual = split_by_separator(input, separator); + assert_eq!(expected, actual); + } + } +} diff --git a/src/hlwm/attribute.rs b/src/hlwm/attribute.rs index dc83425..336f412 100644 --- a/src/hlwm/attribute.rs +++ b/src/hlwm/attribute.rs @@ -1,26 +1,23 @@ -use std::{num::ParseIntError, str::FromStr}; +use std::str::FromStr; use serde::{Deserialize, Serialize}; -use strum::IntoEnumIterator; use thiserror::Error; -use crate::hlwm::hex::ParseHex; +use crate::hlwm::{command::HlwmCommand, hex::ParseHex}; use super::{ - color::{self, Color, X11Color}, + color::{Color, X11Color}, + command::CommandError, hex::HexError, + hlwmbool, octal::{OctalError, ParseOctal}, - StringParseError, + parser::{FromStringsHint, ParseError, ToOption}, }; #[derive(Debug, Clone, Error)] pub enum AttributeError { - #[error("error parsing integer value: {0:?}")] - ParseIntError(#[from] ParseIntError), - #[error("error parsing bool value: [{0}]")] - ParseBoolError(String), - #[error("error parsing color value: {0}")] - ParseColorError(#[from] color::ParseError), + #[error("error parsing value: {0}")] + ParseError(#[from] ParseError), #[error("unknown attribute type [{0}]")] UnknownType(String), #[error("not a valid rectangle: [{0}]")] @@ -31,6 +28,15 @@ pub enum AttributeError { OctalError(#[from] OctalError), } +impl From for ParseError { + fn from(value: AttributeError) -> Self { + match value { + AttributeError::UnknownType(t) => ParseError::InvalidCommand(t), + _ => ParseError::PrimitiveError(value.to_string()), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum AttributeOption { Bool(Option), @@ -42,6 +48,13 @@ pub enum AttributeOption { WindowID(Option), } +impl FromStringsHint<&str> for AttributeOption { + fn from_strings_hint>(s: I, hint: &str) -> Result { + let s = s.collect::>().to_option().map(|t| t.join(" ")); + Ok(Self::new(AttributeType::from_str(hint)?, s.as_ref())?) + } +} + impl Default for AttributeOption { fn default() -> Self { Self::Int(None) @@ -49,19 +62,21 @@ impl Default for AttributeOption { } impl AttributeOption { - pub fn new(type_string: &str, value_string: &Option) -> Result { + pub fn new( + attr_type: AttributeType, + value_string: Option<&String>, + ) -> Result { if let Some(val) = value_string { - return Ok(Attribute::new(type_string, val)?.into()); + return Ok(Attribute::new(attr_type, val)?.into()); } - match type_string { - "int" => Ok(Self::Int(None)), - "bool" => Ok(Self::Bool(None)), - "uint" => Ok(Self::Uint(None)), - "color" => Ok(Self::Color(None)), - "windowid" => Ok(Self::WindowID(None)), - "rectangle" => Ok(Self::Rectangle(None)), - "string" | "names" | "regex" | "font" => Ok(Self::String(None)), - _ => Err(AttributeError::UnknownType(type_string.to_string())), + match attr_type { + AttributeType::Int => Ok(Self::Int(None)), + AttributeType::Bool => Ok(Self::Bool(None)), + AttributeType::Uint => Ok(Self::Uint(None)), + AttributeType::Color => Ok(Self::Color(None)), + AttributeType::WindowID => Ok(Self::WindowID(None)), + AttributeType::Rectangle => Ok(Self::Rectangle(None)), + AttributeType::String => Ok(Self::String(None)), } } @@ -169,75 +184,57 @@ impl From<(u32, u32)> for Attribute { } } +impl FromStringsHint<&str> for Attribute { + fn from_strings_hint>(s: I, hint: &str) -> Result { + let value = s.collect::>().join(" "); + Ok(Self::new( + AttributeType::get_type_or_guess(hint, &value), + &value, + )?) + } +} + impl Attribute { - pub fn new(type_string: &str, value_string: &str) -> Result { - match type_string { - "bool" => match value_string { - "on" | "true" => Ok(Attribute::Bool(true)), - "off" | "false" => Ok(Attribute::Bool(false)), - _ => Err(AttributeError::ParseBoolError(type_string.to_string())), - }, - "color" => Ok(Attribute::Color(Color::from_str(value_string)?)), - "int" => Ok(Attribute::Int(value_string.parse()?)), - "string" | "names" | "regex" | "font" => { - Ok(Attribute::String(value_string.to_string())) - } - "uint" => Ok(Attribute::Uint(value_string.parse()?)), - "rectangle" => { + pub fn new(attr_type: AttributeType, value_string: &str) -> Result { + match attr_type { + AttributeType::Bool => Ok(Self::Bool(hlwmbool::from_hlwm_string(value_string)?)), + AttributeType::Color => Ok(Attribute::Color(Color::from_str(value_string)?)), + AttributeType::Int => Ok(Attribute::Int( + value_string.parse().map_err(|err| ParseError::from(err))?, + )), + AttributeType::String => Ok(Attribute::String(value_string.to_string())), + AttributeType::Uint => Ok(Attribute::Uint( + value_string.parse().map_err(|err| ParseError::from(err))?, + )), + AttributeType::Rectangle => { let parts = value_string.split('x').collect::>(); if parts.len() != 2 { return Err(AttributeError::NotRectangle(value_string.to_string())); } Ok(Attribute::Rectangle { - x: parts.get(0).unwrap().parse()?, - y: parts.get(1).unwrap().parse()?, + x: parts + .get(0) + .unwrap() + .parse() + .map_err(|err| ParseError::from(err))?, + y: parts + .get(1) + .unwrap() + .parse() + .map_err(|err| ParseError::from(err))?, }) } - "windowid" => { + AttributeType::WindowID => { if let Some(hex) = value_string.strip_prefix("0x") { Ok(Attribute::WindowID(hex.parse_hex()?)) } else if let Some(octal) = value_string.strip_prefix("0") { Ok(Attribute::WindowID(octal.parse_octal()?)) } else { - Ok(Attribute::WindowID(value_string.parse()?)) + Ok(Attribute::WindowID( + value_string.parse().map_err(|err| ParseError::from(err))?, + )) } } - _ => Err(AttributeError::UnknownType(type_string.to_string())), - } - } - - fn guess_from_value(value: &str) -> Option { - match value { - "on" | "true" => Some(Self::Bool(true)), - "off" | "false" => Some(Self::Bool(false)), - _ => { - // Match for all colors first - if let Some(color) = X11Color::iter() - .into_iter() - .find(|c| c.to_string() == value) - { - return Some(Attribute::Color(Color::X11(color))); - } - - // Match for primitive types or color string - let mut chars = value.chars(); - match chars.next().unwrap() { - '+' => Self::guess_from_value(&chars.collect::()), - '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '-' => { - i32::from_str(value).ok().map(|i| Attribute::Int(i)) - } - '#' => Color::from_hex(value).ok().map(|c| Attribute::Color(c)), - _ => None, - } - } - } - } - - pub fn guess_type(value: &str) -> Self { - if value.is_empty() { - Self::String(String::new()) - } else { - Self::guess_from_value(value).unwrap_or(Attribute::String(value.to_string())) } } @@ -281,11 +278,71 @@ impl Default for AttributeType { } impl FromStr for AttributeType { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { - Self::iter() - .find(|t| t.to_string() == s) - .ok_or(StringParseError::UnknownValue) + match s { + "bool" => Ok(Self::Bool), + "color" => Ok(Self::Color), + "int" => Ok(Self::Int), + "string" | "names" | "regex" | "font" => Ok(Self::String), + "uint" => Ok(Self::Uint), + "rectangle" => Ok(Self::Rectangle), + "windowid" => Ok(Self::WindowID), + _ => Err(ParseError::InvalidCommand(s.to_string())), + } + } +} + +impl AttributeType { + /// Gets the type for the given `path` from herbstluftwm + pub fn get_type(path: &str) -> Result { + Ok(Self::from_str( + &String::from_utf8(HlwmCommand::AttrType(path.to_string()).execute()?.stdout)? + .split('\n') + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .next() + .ok_or(CommandError::Empty)?, + )?) + } + + /// Tries to get the type for the given `path`, but defaults to [AttributeType::String] + /// if that fails in any way + pub fn get_type_or_string(path: &str) -> AttributeType { + Self::get_type(path).unwrap_or(AttributeType::String) + } + + /// Tries to get the type for the given `path`, and if that fails + /// tries to guess the type using [Self::guess] + pub fn get_type_or_guess(path: &str, value: &str) -> Self { + Self::get_type(path).unwrap_or(Self::guess(value)) + } + + pub fn guess(value: &str) -> Self { + if value.is_empty() { + return Self::String; + } + match value { + "on" | "true" | "off" | "false" => Self::Bool, + _ => { + // Match for all colors first + if X11Color::iter().into_iter().any(|c| c.to_string() == value) { + return Self::Color; + } + // Is it a valid color string? + if Color::from_hex(value).is_ok() { + return Self::Color; + } + + // Match for primitive types or color string + let mut chars = value.chars(); + match chars.next().unwrap() { + '+' => Self::guess(&chars.collect::()), + '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '-' => Self::Int, + _ => Self::String, + } + } + } } } diff --git a/src/hlwm/color/mod.rs b/src/hlwm/color/mod.rs index 30fbb1a..c2ca0b7 100644 --- a/src/hlwm/color/mod.rs +++ b/src/hlwm/color/mod.rs @@ -1,9 +1,8 @@ use std::str::FromStr; use serde::{de::Unexpected, Deserialize, Serialize}; -use thiserror::Error; -use super::hex::{HexError, ParseHex}; +use super::{hex::ParseHex, parser::ParseError}; mod x11; pub use x11::X11Color; @@ -51,14 +50,6 @@ impl Default for Color { } } -#[derive(Debug, Clone, Error)] -pub enum ParseError { - #[error("length must be either 6 characters or 7 with '#' prefix")] - InvalidLength, - #[error("invalid hex value: [{0}]")] - HexError(#[from] HexError), -} - impl FromStr for Color { type Err = ParseError; @@ -76,7 +67,10 @@ impl Color { pub fn from_hex(hex: &str) -> Result { let expected_len = if hex.starts_with('#') { 7 } else { 6 }; if hex.len() != expected_len { - return Err(ParseError::InvalidLength); + return Err(ParseError::InvalidValue { + value: hex.to_string(), + expected: "a 6 digit hex value", + }); } let hex = hex.strip_prefix('#').unwrap_or(hex); diff --git a/src/hlwm/color/x11.rs b/src/hlwm/color/x11.rs index 3891a6c..767c840 100644 --- a/src/hlwm/color/x11.rs +++ b/src/hlwm/color/x11.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; -use crate::hlwm::StringParseError; +use crate::hlwm::parser::ParseError; macro_rules! color_impl { () => {}; @@ -50,13 +50,13 @@ macro_rules! color_impl { } impl FromStr for X11Color { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { X11Color::ALL .into_iter() .find(|c| c.color_names().into_iter().any(|n| n == s)) - .ok_or(StringParseError::UnknownValue) + .ok_or(ParseError::InvalidCommand(s.to_string())) } } diff --git a/src/hlwm/command.rs b/src/hlwm/command.rs index 9ccd958..b79fdf7 100644 --- a/src/hlwm/command.rs +++ b/src/hlwm/command.rs @@ -1,22 +1,35 @@ -use std::{io, process::ExitStatus, str::FromStr, string::FromUtf8Error}; +use std::{ + borrow::BorrowMut, + io, + process::{self, ExitStatus}, + str::FromStr, + string::FromUtf8Error, +}; use strum::IntoEnumIterator; use serde::{de::Expected, Deserialize, Serialize}; use thiserror::Error; -use crate::{gen_parse, hlwm::Client, split}; +use crate::{ + hlwm::{ + parser::{either::Either, ParseError}, + Client, + }, + split, +}; use super::{ - attribute::{Attribute, AttributeError, AttributeOption}, + and_or_command::AndOrCommands, + attribute::{Attribute, AttributeError, AttributeOption, AttributeType}, hlwmbool::ToggleBool, hook::Hook, key::{KeyUnbind, Keybind, Mousebind}, pad::Pad, + parser::{self, ArgParser, Flip, FromCommandArgs, FromStrings, ToOption}, rule::{Rule, Unrule}, setting::{FrameLayout, Setting, SettingName}, window::Window, - Align, Direction, Index, Monitor, Operator, Separator, StringParseError, TagSelect, - ToCommandString, + Align, Direction, Index, Monitor, Operator, Separator, TagSelect, ToCommandString, }; #[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq)] @@ -196,6 +209,7 @@ pub enum HlwmCommand { filter_name: Option, identifier: String, object: String, + command: Box, }, #[strum(serialize = "sprintf")] Sprintf(Vec), @@ -205,7 +219,7 @@ pub enum HlwmCommand { } impl FromStr for Box { - type Err = CommandParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { Ok(Box::new(HlwmCommand::from_str(s)?)) @@ -227,42 +241,33 @@ impl<'de> Deserialize<'de> for HlwmCommand { D: serde::Deserializer<'de>, { pub enum Expect { - NotEmpty, + ParseError(ParseError), } impl Expected for Expect { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - Expect::NotEmpty => write!(f, "value not being empty"), + Expect::ParseError(err) => f.write_str(&err.to_string()), } } } let str_val: String = Deserialize::deserialize(deserializer)?; - - let parts = split::tab_or_space(&str_val); - if parts.is_empty() { - return Err(serde::de::Error::invalid_length(0, &Expect::NotEmpty)); - } - let mut parts = parts.into_iter(); - let command = parts.next().unwrap(); - let args = parts.collect(); - - Ok(Self::from_raw_parts(&command, args).map_err(|err| { - serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &err) - })?) + ArgParser::from_strings(split::tab_or_space(&str_val).into_iter()) + .collect_command("hlwm_command") + .map_err(|err| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(&str_val), + &Expect::ParseError(err), + ) + }) } } impl FromStr for HlwmCommand { - type Err = CommandParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { - let mut parts = s.split("\t"); - let command = parts - .next() - .ok_or(CommandParseError::UnknownCommand(s.to_string()))?; - let args = parts.map(String::from).collect(); - HlwmCommand::from_raw_parts(command, args) + ArgParser::from_strings(split::tab_or_space(s).into_iter()).collect_command("hlwm_command") } } @@ -274,12 +279,6 @@ impl Default for HlwmCommand { #[derive(Debug, Error)] pub enum CommandParseError { - #[error("unknown command [{0}]")] - UnknownCommand(String), - #[error("bad argument for command [{command}]")] - BadArgument { command: String }, - #[error("missing required argument")] - MissingArgument, #[error("invalid argument count [{0}] at [{1}]")] InvalidArgumentCount(usize, String), #[error("error parsing attribute: [{0}]")] @@ -288,8 +287,8 @@ pub enum CommandParseError { CommandError(#[from] CommandError), #[error("string utf8 error")] StringUtf8Error(#[from] FromUtf8Error), - #[error("parsing string value error: [{0}]")] - StringParseError(#[from] StringParseError), + #[error("parsing error: [{0}]")] + StringParseError(#[from] ParseError), } impl serde::de::Expected for CommandParseError { @@ -298,14 +297,6 @@ impl serde::de::Expected for CommandParseError { } } -fn trim_quotes(itm: String) -> String { - if itm.starts_with('"') && itm.ends_with('"') { - itm.trim_matches('"').to_string() - } else { - itm - } -} - impl HlwmCommand { pub fn silent(self) -> HlwmCommand { HlwmCommand::Silent(Box::new(self)) @@ -315,14 +306,33 @@ impl HlwmCommand { HlwmCommand::Try(Box::new(self)) } - pub fn from_raw_parts(command: &str, args: Vec) -> Result { - let command = HlwmCommand::iter() - .find(|cmd| cmd.to_string() == command) - .ok_or(CommandParseError::UnknownCommand(command.to_string()))?; + pub fn execute(self) -> Result { + Client::new().execute(self) + } - gen_parse!(command, args); + pub fn execute_str(self) -> Result { + Ok(String::from_utf8(self.execute()?.stdout)?) + } +} - let parsed_command = match command { +impl FromCommandArgs for HlwmCommand { + fn from_command_args, I: Iterator>( + cmd_name: &str, + args: I, + ) -> Result { + let mut command = HlwmCommand::iter() + .find(|cmd| cmd.to_string() == cmd_name) + .ok_or(parser::ParseError::InvalidCommand(cmd_name.to_string()))?; + // Since HlwmCommand will often be parsed by its constituent commands (such as keybind) + // just passing in `args.map(|s| s.into())` results in the typechecker overflowing its + // recursion limit. So, to get around this, HlwmCommand will collect whatever type I + // is into a `Vec`, which makes this loop not infinite. + // + // There really should be a better error for this. I'm lucky I figured it out fast. + let mut parser = + ArgParser::from_strings(args.map(|s| s.into()).collect::>().into_iter()); + + match command.borrow_mut() { HlwmCommand::Quit | HlwmCommand::Lock | HlwmCommand::Cycle @@ -336,252 +346,199 @@ impl HlwmCommand { | HlwmCommand::True | HlwmCommand::Mouseunbind | HlwmCommand::ListMonitors - | HlwmCommand::ListKeybinds => Ok::<_, CommandParseError>(command.clone()), - HlwmCommand::Echo(_) => Ok(Self::Echo(args)), - HlwmCommand::Close { window: _ } => parse!(window: [Option] => Close), - HlwmCommand::Spawn { - executable: _, - args: _, - } => { - parse!(executable: String, args: [Vec] => Spawn).map(|spawn| match spawn { - HlwmCommand::Spawn { executable, args } => HlwmCommand::Spawn { - executable: trim_quotes(executable), - args: args.into_iter().map(trim_quotes).collect(), - }, - _ => unreachable!(), - }) + | HlwmCommand::ListKeybinds => (), + HlwmCommand::Echo(arg) => *arg = vec![parser.collect::>().join(" ")], + HlwmCommand::Close { window } => { + *window = parser.optional_next_from_str("close(window)")? } - HlwmCommand::GetAttr(_) => parse!(String => GetAttr), - HlwmCommand::SetAttr { - path: _, - new_value: _, - } => { - let mut args = args.into_iter(); - let path = args.next().ok_or(CommandParseError::BadArgument { - command: command.to_string(), - })?; - Ok(HlwmCommand::SetAttr { - path: path.clone(), - new_value: { - Attribute::new( - &String::from_utf8( - Client::new() - .execute(HlwmCommand::AttrType(path.clone()))? - .stdout, - )? - .split('\n') - .next() - .ok_or(CommandParseError::CommandError(CommandError::Empty))?, - &args.collect::>().join(" "), - )? - }, - }) + HlwmCommand::Spawn { executable, args } => { + *executable = parser.must_string("spawn(executable)")?; + *args = parser.collect(); } - HlwmCommand::Attr { - path: _, - new_value: _, - } => { - let mut args = args.into_iter(); - Ok(HlwmCommand::Attr { - path: args.next().ok_or(CommandParseError::BadArgument { - command: command.to_string(), - })?, - new_value: { - let args = args.collect::>(); - if args.is_empty() { - None - } else { - Some(Attribute::guess_type(&args.join("\t"))) - } - }, - }) + HlwmCommand::GetAttr(attr) => *attr = parser.must_string("get_attr")?, + HlwmCommand::SetAttr { path, new_value } => { + *path = parser.must_string("set_attr(path)")?; + *new_value = + parser.collect_from_strings_hint(path.as_str(), "set_attr(new_value)")?; } - HlwmCommand::NewAttr { path: _, attr: _ } => { - let mut args = args.into_iter(); - let attr_type = args.next().ok_or(CommandParseError::BadArgument { - command: command.to_string(), - })?; - let path = args.next().ok_or(CommandParseError::BadArgument { - command: command.to_string(), - })?; - Ok(HlwmCommand::NewAttr { - path, - attr: { - let attr = args.collect::>(); - let attr = if attr.len() == 0 { - None - } else { - Some(attr.join("\t")) - }; - AttributeOption::new(&attr_type, &attr)? - }, - }) - } - HlwmCommand::AttrType(_) => parse!(String => AttrType), - HlwmCommand::RemoveAttr(_) => parse!(String => RemoveAttr), - HlwmCommand::Set(_) => parse!(FromStrAll => Set), - HlwmCommand::EmitHook(_) => parse!(FromStr => EmitHook), - HlwmCommand::Keybind(_) => parse!(FromStrAll => Keybind), - HlwmCommand::Keyunbind(_) => parse!(FromStr => Keyunbind), - HlwmCommand::Mousebind(_) => parse!(FromStrAll => Mousebind), - HlwmCommand::JumpTo(_) => parse!(FromStr => JumpTo), - HlwmCommand::AddTag(_) => parse!(String => AddTag), - HlwmCommand::MergeTag { tag: _, target: _ } => { - parse!(tag: String, target: [Option] => MergeTag) - } - HlwmCommand::Focus(_) => parse!(FromStr => Focus), - HlwmCommand::Shift(_) => parse!(FromStr => Shift), - HlwmCommand::Split(_) => parse!(FromStrAll => Split), - HlwmCommand::Fullscreen(_) => parse!(FromStr => Fullscreen), - HlwmCommand::CycleLayout { - delta: _, - layouts: _, - } => { - if args.is_empty() { - Ok(HlwmCommand::CycleLayout { - delta: None, - layouts: vec![], + HlwmCommand::Attr { path, new_value } => { + *path = parser.must_string("attr(path)")?; + *new_value = parser + .collect::>() + .to_option() + .map(|t| { + let value = t.join(" "); + Attribute::new(AttributeType::get_type_or_guess(&path, &value), &value) }) - } else { - let first = args.first().ok_or(CommandParseError::BadArgument { - command: command.to_string(), - })?; - match FrameLayout::from_str(first) { - Ok(_) => { - // only frame layouts - Ok(HlwmCommand::CycleLayout { - delta: None, - layouts: args - .into_iter() - .map(|i| FrameLayout::from_str(&i)) - .collect::>() - .map_err(|_| CommandParseError::BadArgument { - command: command.to_string(), - })?, - }) + .flip()?; + } + HlwmCommand::NewAttr { path, attr } => { + let attr_type: AttributeType = parser.next_from_str("new_attr(attr_type)")?; + *path = parser.must_string("new_attr(path)")?; + let value = parser.collect::>().to_option().map(|v| v.join(" ")); + *attr = AttributeOption::new(attr_type, value.as_ref())?; + } + HlwmCommand::AttrType(path) | HlwmCommand::RemoveAttr(path) => { + *path = parser.must_string(cmd_name)? + } + HlwmCommand::Set(setting) => *setting = parser.collect_command(cmd_name)?, + HlwmCommand::EmitHook(hook) => *hook = parser.collect_command(cmd_name)?, + HlwmCommand::Keybind(keybind) => *keybind = parser.collect_from_strings(cmd_name)?, + HlwmCommand::Keyunbind(keyunbind) => *keyunbind = parser.next_from_str(cmd_name)?, + HlwmCommand::Mousebind(mouseunbind) => { + *mouseunbind = parser.collect_from_strings(cmd_name)? + } + HlwmCommand::JumpTo(win) => *win = parser.next_from_str(cmd_name)?, + HlwmCommand::AddTag(tag) => *tag = parser.must_string(cmd_name)?, + HlwmCommand::MergeTag { tag, target } => { + *tag = parser.must_string("merge_tag(tag)")?; + *target = parser.optional_next_from_str("merge_tag(target)")?; + } + HlwmCommand::Focus(dir) | HlwmCommand::Shift(dir) => { + *dir = parser.next_from_str(cmd_name)? + } + HlwmCommand::Split(align) => *align = parser.collect_from_strings(cmd_name)?, + HlwmCommand::Fullscreen(set) => *set = parser.next_from_str(cmd_name)?, + HlwmCommand::CycleLayout { delta, layouts } => { + let cycle_res = parser.try_first( + |s| Index::::from_str(s), + |s| Ok(FrameLayout::from_str(s)?), + cmd_name, + ); + match cycle_res { + Ok(res) => match res { + Either::Left(idx) => { + *delta = Some(idx); + *layouts = parser.collect_from_str("cycle_layout(layouts)")?; } - Err(_) => { - // Has index - let mut args = args.into_iter(); - Ok(HlwmCommand::CycleLayout { - delta: Some(args.next().unwrap().parse().map_err(|_| { - CommandParseError::BadArgument { - command: command.to_string(), - } - })?), - layouts: args - .map(|i| FrameLayout::from_str(&i)) - .collect::>() - .map_err(|_| CommandParseError::BadArgument { - command: command.to_string(), - })?, - }) + Either::Right(layout) => { + *delta = None; + *layouts = [layout] + .into_iter() + .chain(parser.collect_from_str("cycle_layout(layouts)")?) + .collect(); + } + }, + Err(err) => { + if let ParseError::Empty = err { + *delta = None; + *layouts = vec![]; + } else { + return Err(err); } } } } HlwmCommand::Resize { - direction: _, - fraction_delta: _, - } => parse!(direction: FromStr, fraction_delta: [Option] => Resize), + direction, + fraction_delta, + } => { + *direction = parser.next_from_str("resize(direction)")?; + *fraction_delta = parser.optional_next_from_str("resize(fraction_delta)")?; + } HlwmCommand::Or { - separator: _, - commands: _, - } => parse!(And_Or => Or), - HlwmCommand::And { - separator: _, - commands: _, - } => parse!(And_Or => And), + separator, + commands, + } + | HlwmCommand::And { + separator, + commands, + } => { + *separator = parser.next_from_str(&format!("{cmd_name}(separator)"))?; + let commands_wrap: AndOrCommands = parser + .collect_from_strings_hint(*separator, &format!("{cmd_name}(commands)"))?; + *commands = commands_wrap.commands(); + } HlwmCommand::Compare { - attribute: _, - operator: _, - value: _, - } => parse!(attribute: String, operator: FromStr, value: String => Compare), - HlwmCommand::TagStatus { monitor: _ } => { - parse!(monitor: [Option] => TagStatus) - } - HlwmCommand::Rule(_) => parse!(FromStrAll => Rule), - HlwmCommand::Get(_) => parse!(FromStr => Get), - HlwmCommand::MoveIndex { - index: _, - skip_visible: _, + attribute, + operator, + value, } => { - parse!(skip_visible: [Flag("--skip-visible")], index: FromStr => MoveIndex) + *attribute = parser.must_string("compare(attribute)")?; + *operator = parser.next_from_str("compare(operator)")?; + *value = parser.must_string("compare(value)")?; } + HlwmCommand::TagStatus { monitor } => { + *monitor = parser.optional_next_from_str(cmd_name)?; + } + HlwmCommand::Rule(rule) => { + *rule = parser.collect_from_strings(cmd_name)?; + } + HlwmCommand::Get(set) => *set = parser.next_from_str(cmd_name)?, HlwmCommand::UseIndex { - index: _, - skip_visible: _, - } => { - parse!(skip_visible: [Flag("--skip-visible")], index: FromStr => UseIndex) + index, + skip_visible, + } + | HlwmCommand::MoveIndex { + index, + skip_visible, + } => { + let (args, skip) = parser.collect_strings_with_flag("--skip-visible"); + *skip_visible = skip; + *index = ArgParser::from_strings(args.into_iter()).next_from_str(cmd_name)?; + } + HlwmCommand::MoveTag(tag) | HlwmCommand::UseTag(tag) => { + *tag = parser.must_string(cmd_name)? + } + HlwmCommand::Try(hlcmd) | HlwmCommand::Silent(hlcmd) => { + *hlcmd = Box::new(parser.collect_command(cmd_name)?); } - HlwmCommand::UseTag(_) => parse!(String => UseTag), - HlwmCommand::MoveTag(_) => parse!(String => MoveTag), - HlwmCommand::Try(_) => parse!(FromStrAll => Try), - HlwmCommand::Silent(_) => parse!(FromStrAll => Silent), HlwmCommand::MonitorRect { - monitor: _, - without_pad: _, + monitor, + without_pad, } => { - parse!(without_pad: [Flag("-p")], monitor: [Option] => MonitorRect) + let (args, pad) = parser.collect_strings_with_flag("-p"); + *without_pad = pad; + *monitor = args.first().map(|s| u32::from_str(s)).flip()?; } - HlwmCommand::Pad { monitor: _, pad: _ } => { - parse!(monitor: FromStr, pad: FromStrAll => Pad) + HlwmCommand::Pad { monitor, pad } => { + *monitor = parser.next_from_str("pad(monitor)")?; + *pad = parser.collect_from_strings("pad(pad)")?; } HlwmCommand::Substitute { - identifier: _, - attribute_path: _, - command: _, + identifier, + attribute_path, + command, } => { - parse!(identifier: String, attribute_path: String, command: FromStrAll => Substitute) + *identifier = parser.must_string("substitute(identifier)")?; + *attribute_path = parser.must_string("substitute(attribute_path)")?; + *command = Box::new(parser.collect_command("substitute(command)")?); } HlwmCommand::ForEach { - unique: _, - recursive: _, - filter_name: _, - identifier: _, - object: _, + unique, + recursive, + filter_name, + identifier, + object, + command, } => { - let (params, args): (Vec, Vec) = - args.into_iter().partition(|a| a.starts_with("--")); - - if args.len() < 2 { - return Err(CommandParseError::BadArgument { - command: command.to_string(), - }); - } - let mut args = args.into_iter(); - let (identifier, object) = - (args.next().unwrap(), args.collect::>().join("\t")); - let mut unique = false; - let mut recursive = false; - let mut filter_name: Option = None; - for param in params { - match param.as_str() { - "--unique" => unique = true, - "--recursive" => recursive = true, + // Note: ForEach is still likely bugged due to this method of parsing the parts, as foreach may be nested + let (normal, flags): (Vec<_>, Vec<_>) = parser.inner().partition(|c| { + !c.starts_with("--unique") + && !c.starts_with("--filter-name") + && !c.starts_with("--recursive") + }); + let mut normal = ArgParser::from_strings(normal.into_iter()); + for flag in flags { + match flag.as_str() { + "--unique" => *unique = true, + "--recursive" => *recursive = true, other => { if let Some(name) = other.strip_prefix("--filter-name=") { - filter_name = Some(name.to_string()); + *filter_name = Some(name.to_string()); } } } } - - Ok(HlwmCommand::ForEach { - unique, - recursive, - filter_name, - identifier, - object, - }) + *identifier = normal.must_string("for_each(identifier)")?; + *object = normal.must_string("for_each(object)")?; + *command = Box::new(normal.collect_command("for_each(command)")?); } - HlwmCommand::Sprintf(_) => parse!([Vec] => Sprintf), - HlwmCommand::Unrule(_) => parse!(FromStr => Unrule), - }?; + HlwmCommand::Sprintf(s) => *s = parser.collect(), + HlwmCommand::Unrule(unrule) => *unrule = parser.next_from_str(cmd_name)?, + }; - assert_eq!(command.to_string(), parsed_command.to_string()); - - Ok(parsed_command) + Ok(command) } } @@ -611,10 +568,12 @@ pub enum CommandError { UtfError(#[from] FromUtf8Error), #[error("attribute error: {0:?}")] AttributeError(#[from] AttributeError), - #[error("string parse error: {0}")] - StringParseError(#[from] StringParseError), + #[error("parse error: {0}")] + ParseError(#[from] ParseError), #[error("unexpected empty result")] Empty, + #[error("invalid value")] + Invalid, } impl From for CommandError { @@ -803,8 +762,9 @@ impl ToCommandString for HlwmCommand { filter_name, identifier, object, + command, } => { - let mut parts = Vec::with_capacity(6); + let mut parts = Vec::with_capacity(7); parts.push(self.to_string()); parts.push(identifier.to_string()); parts.push(object.to_string()); @@ -817,6 +777,7 @@ impl ToCommandString for HlwmCommand { if *recursive { parts.push("--recursive".into()); } + parts.push(command.to_command_string()); parts.join("\t") } HlwmCommand::Sprintf(args) => [self.to_string()] @@ -833,6 +794,40 @@ impl ToCommandString for HlwmCommand { } } +#[derive(Default)] +struct ForEach { + unique: bool, + recursive: bool, + filter_name: Option, + identifier: String, + object: String, + command: HlwmCommand, +} + +impl FromStrings for ForEach { + fn from_strings>(s: I) -> Result { + let mut for_each = Self::default(); + let (normal, flags): (Vec<_>, Vec<_>) = s.partition(|c| !c.starts_with("--")); + let mut normal = ArgParser::from_strings(normal.into_iter()); + for flag in flags { + match flag.as_str() { + "--unique" => for_each.unique = true, + "--recursive" => for_each.recursive = true, + other => { + if let Some(filter_name) = other.strip_prefix("--filter-name=") { + for_each.filter_name = Some(filter_name.to_string()); + } + } + } + } + for_each.identifier = normal.must_string("for_each(identifier)")?; + for_each.object = normal.must_string("for_each(object)")?; + for_each.command = normal.collect_command("for_each(command)")?; + + Ok(for_each) + } +} + #[cfg(test)] mod test { @@ -843,7 +838,7 @@ mod test { command::{FrameLayout, HlwmCommand, Index, Operator, Separator}, hlwmbool::ToggleBool, hook::Hook, - key::{Key, KeyUnbind, MouseButton, Mousebind, MousebindAction}, + key::{Key, KeyUnbind, Keybind, MouseButton, Mousebind, MousebindAction}, pad::Pad, rule::{Condition, Consequence, Rule, RuleOperator}, setting::{Setting, SettingName}, @@ -937,7 +932,10 @@ mod test { "emit_hook\treload".into(), ), HlwmCommand::Keybind(_) => ( - HlwmCommand::Keybind("Mod4+1\treload".parse().unwrap()), + HlwmCommand::Keybind(Keybind::new( + [Key::Mod4Super, Key::Char('1')], + HlwmCommand::Reload, + )), "keybind\tMod4+1\treload".into(), ), HlwmCommand::Keyunbind(_) => ( @@ -1122,6 +1120,7 @@ mod test { filter_name: _, identifier: _, object: _, + command: _, } => ( HlwmCommand::ForEach { unique: true, @@ -1129,8 +1128,10 @@ mod test { filter_name: Some(".+".into()), identifier: "CLIENT".into(), object: "clients.".into(), + command: Box::new(HlwmCommand::Cycle), }, - "foreach\tCLIENT\tclients.\t--filter-name=.+\t--unique\t--recursive".into(), + "foreach\tCLIENT\tclients.\t--filter-name=.+\t--unique\t--recursive\tcycle" + .into(), ), HlwmCommand::Sprintf(_) => ( HlwmCommand::Sprintf( diff --git a/src/hlwm/hlwmbool.rs b/src/hlwm/hlwmbool.rs index b61f4ed..b6c98ae 100644 --- a/src/hlwm/hlwmbool.rs +++ b/src/hlwm/hlwmbool.rs @@ -2,14 +2,17 @@ use std::{fmt::Display, str::FromStr}; use serde::{Deserialize, Serialize}; -use super::StringParseError; +use super::parser::ParseError; #[inline(always)] -pub fn from_hlwm_string(s: &str) -> Result { +pub fn from_hlwm_string(s: &str) -> Result { match s { "on" | "true" => Ok(true), "off" | "false" => Ok(false), - _ => Err(StringParseError::BoolError(s.to_string())), + _ => Err(ParseError::PrimitiveError(format!( + "value [{s}] is not a {}", + std::any::type_name::() + ))), } } @@ -26,7 +29,7 @@ impl From for ToggleBool { } impl FromStr for ToggleBool { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { if let Ok(b) = from_hlwm_string(s) { @@ -34,7 +37,10 @@ impl FromStr for ToggleBool { } else if s == "toggle" { Ok(Self::Toggle) } else { - Err(StringParseError::UnknownValue) + Err(ParseError::PrimitiveError(format!( + "value [{s}] is not a {}", + std::any::type_name::() + ))) } } } diff --git a/src/hlwm/hook.rs b/src/hlwm/hook.rs index 454677e..186598e 100644 --- a/src/hlwm/hook.rs +++ b/src/hlwm/hook.rs @@ -1,11 +1,13 @@ -use std::str::FromStr; +use std::{borrow::BorrowMut, str::FromStr}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; -use crate::gen_parse; - -use super::{command::CommandParseError, window::Window, Monitor, TagSelect, ToCommandString}; +use super::{ + parser::{ArgParser, FromCommandArgs, ParseError}, + window::Window, + Monitor, TagSelect, ToCommandString, +}; #[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumIter, PartialEq)] #[strum(serialize_all = "snake_case")] @@ -59,55 +61,55 @@ pub enum Hook { Reload, } -impl Hook { - fn from_raw_parts(command: &str, args: Vec) -> Result { - let command = Self::iter() - .find(|cmd| cmd.to_string() == command) - .ok_or(CommandParseError::UnknownCommand(command.to_string()))?; - gen_parse!(command, args); +impl FromCommandArgs for Hook { + fn from_command_args, I: IntoIterator>( + cmd: &str, + args: I, + ) -> Result { + let mut command = Self::iter() + .find(|c: &Hook| c.to_string() == cmd) + .ok_or(ParseError::InvalidCommand(cmd.to_string()))?; - match command { - Hook::AttributeChanged { - path: _, - old: _, - new: _, - } => parse!(path: String, old: String, new: String => AttributeChanged), - Hook::Fullscreen { on: _, window: _ } => { - parse!(on: FromStr, window: FromStr => Fullscreen) + let x = args.into_iter().map(|i| i.into()).collect::>(); + let mut parser = ArgParser::from_strings(x.into_iter()); + + match command.borrow_mut() { + Hook::QuitPanel | Hook::Reload | Hook::TagFlags => (), + Hook::AttributeChanged { path, old, new } => { + *path = parser.must_string("attribute_changed(path)")?; + *old = parser.must_string("attribute_changed(old)")?; + *new = parser.must_string("attribute_changed(new)")?; } - Hook::TagChanged { tag: _, monitor: _ } => { - parse!(tag: String, monitor: FromStr => TagChanged) + Hook::Fullscreen { on, window } => { + *on = parser.next_from_str("fullscreen(on)")?; + *window = parser.next_from_str("fullscreen(window)")?; + } + Hook::TagChanged { tag, monitor } => { + *tag = parser.must_string("tag_changed(tag)")?; + *monitor = parser.next_from_str("tag_changed(monitor)")?; + } + Hook::FocusChanged { window, title } | Hook::WindowTitleChanged { window, title } => { + *window = parser.next_from_str(&format!("{cmd}(window)"))?; + *title = parser.must_string(&format!("{cmd}(title)"))?; + } + Hook::TagAdded(tag) => { + *tag = TagSelect::from_str(&parser.must_string(cmd)?).expect("infallible") + } + Hook::TagRenamed { old, new } => { + *old = parser.must_string("tag_renamed(old)")?; + *new = parser.must_string("tag_renamed(new)")?; + } + Hook::Urgent { on, window } => { + *on = parser.next_from_str("urgent(on)")?; + *window = parser.next_from_str("urgent(window)")?; + } + Hook::Rule { hook, window } => { + *hook = parser.must_string("rule(hook)")?; + *window = parser.next_from_str("rule(window)")?; } - Hook::FocusChanged { - window: _, - title: _, - } => parse!(window: FromStr, title: String => FocusChanged), - Hook::WindowTitleChanged { - window: _, - title: _, - } => parse!(window: FromStr, title: String => WindowTitleChanged), - Hook::TagFlags => Ok(Hook::TagFlags), - Hook::TagAdded(_) => parse!(FromStr => TagAdded), - Hook::TagRenamed { old: _, new: _ } => parse!(old: String, new: String => TagRenamed), - Hook::Urgent { on: _, window: _ } => parse!(on: Bool, window: FromStr => Urgent), - Hook::Rule { hook: _, window: _ } => parse!(hook: String, window: FromStr => Rule), - Hook::QuitPanel => Ok(Hook::QuitPanel), - Hook::Reload => Ok(Hook::Reload), } - } -} -impl FromStr for Hook { - type Err = CommandParseError; - - fn from_str(s: &str) -> Result { - let mut split = s.split("\t"); - Hook::from_raw_parts( - split - .next() - .ok_or(CommandParseError::UnknownCommand(format!("hook {s}")))?, - split.map(String::from).collect(), - ) + Ok(command) } } diff --git a/src/hlwm/key.rs b/src/hlwm/key.rs index 60b92c7..bf7e852 100644 --- a/src/hlwm/key.rs +++ b/src/hlwm/key.rs @@ -1,14 +1,14 @@ -use std::{fmt::Display, str::FromStr}; +use std::{borrow::BorrowMut, fmt::Display, str::FromStr}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; -use thiserror::Error; use crate::split; use super::{ - command::{CommandParseError, HlwmCommand}, - StringParseError, ToCommandString, + command::HlwmCommand, + parser::{ArgParser, FromCommandArgs, FromStrings, ParseError}, + ToCommandString, }; #[derive(Debug, Clone, Copy, PartialEq, strum::EnumIter)] @@ -62,6 +62,12 @@ impl Key { _ => false, } } + + pub fn parse_keybind_keys(s: &str) -> Result, ParseError> { + s.split(['-', '+']) + .map(Key::from_str) + .collect::, _>>() + } } impl From for Key { @@ -104,7 +110,7 @@ impl<'de> Deserialize<'de> for Key { } impl FromStr for Key { - type Err = KeyParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { match s { @@ -121,7 +127,10 @@ impl FromStr for Key { if s.len() == 1 { Ok(Self::Char(s.chars().next().unwrap())) } else { - Err(KeyParseError::ExpectedCharKey(s.to_string())) + Err(ParseError::InvalidValue { + value: s.to_string(), + expected: "a valid key", + }) } } } @@ -144,7 +153,7 @@ impl Default for MouseButton { } impl FromStr for MouseButton { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { match s { @@ -153,7 +162,7 @@ impl FromStr for MouseButton { "Button3" => Ok(Self::Button3), "Button4" => Ok(Self::Button4), "Button5" => Ok(Self::Button5), - _ => Err(StringParseError::UnknownValue), + _ => Err(ParseError::InvalidCommand(s.to_string())), } } } @@ -242,27 +251,48 @@ impl<'de> Deserialize<'de> for MousebindAction { } } -impl FromStr for MousebindAction { - type Err = StringParseError; +impl FromCommandArgs for MousebindAction { + fn from_command_args, I: Iterator>( + command: &str, + args: I, + ) -> Result { + let mut action = Self::iter() + .find(|i| i.to_string() == command) + .ok_or(ParseError::InvalidCommand(command.to_string()))?; - fn from_str(s: &str) -> Result { - let mut parts = split::tab_or_space(s).into_iter(); - let first = parts.next().ok_or(StringParseError::UnknownValue)?; - let act = Self::iter() - .find(|i| i.to_string() == first) - .ok_or(StringParseError::UnknownValue)?; - - match act { - MousebindAction::Move | MousebindAction::Resize | MousebindAction::Zoom => Ok(act), - MousebindAction::Call(_) => { - let command = parts.next().ok_or(StringParseError::UnknownValue)?; - let args = parts.collect(); - Ok(MousebindAction::Call(Box::new( - HlwmCommand::from_raw_parts(&command, args) - .map_err(|err| StringParseError::CommandParseError(err.to_string()))?, - ))) + match action.borrow_mut() { + MousebindAction::Move | MousebindAction::Resize | MousebindAction::Zoom => (), + MousebindAction::Call(arg) => { + *arg = Box::new( + ArgParser::from_strings(args.map(|a| a.into())) + .collect_command("mousebind_args(command)")?, + ) } } + + Ok(action) + } +} + +impl FromStr for MousebindAction { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let mut parser = ArgParser::from_strings(split::tab_or_space(s).into_iter()); + let action_str = parser.must_string("mousebind_action(action)")?; + + let mut action = Self::iter() + .find(|i| i.to_string() == action_str) + .ok_or(ParseError::InvalidCommand(s.to_string()))?; + + match action.borrow_mut() { + MousebindAction::Move | MousebindAction::Resize | MousebindAction::Zoom => (), + MousebindAction::Call(command) => { + *command = Box::new(parser.collect_command("mousebind_action(command)")?); + } + } + + Ok(action) } } @@ -283,32 +313,6 @@ impl ToCommandString for MousebindAction { } } -#[derive(Debug, Clone, Error)] -pub enum KeyParseError { - #[error("value too short (expected >= 2 parts, got {0} parts)")] - TooShort(usize), - #[error("no keys in keybind")] - NoKeys, - #[error("command parse error: {0}")] - CommandParseError(String), - #[error("expected char key, got: [{0}]")] - ExpectedCharKey(String), - #[error("string parse error: [{0}]")] - StringParseError(String), -} - -impl From for KeyParseError { - fn from(value: StringParseError) -> Self { - KeyParseError::StringParseError(value.to_string()) - } -} - -impl From for KeyParseError { - fn from(value: CommandParseError) -> Self { - KeyParseError::CommandParseError(value.to_string()) - } -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Keybind { pub keys: Vec, @@ -323,29 +327,13 @@ impl Default for Keybind { } } } +impl FromStrings for Keybind { + fn from_strings>(s: I) -> Result { + let mut parser = ArgParser::from_strings(s); -impl FromStr for Keybind { - type Err = KeyParseError; - - fn from_str(s: &str) -> Result { - let parts = s.split('\t').collect::>(); - if parts.len() < 2 { - return Err(KeyParseError::TooShort(s.len())); - } - let mut parts = parts.into_iter(); - - let keys = parts - .next() - .unwrap() - .split("+") - .map(|key| Key::from_str(key)) - .collect::, _>>()?; - - let command = parts.next().unwrap(); - let args: Vec = parts.map(String::from).collect(); Ok(Self { - keys, - command: Box::new(HlwmCommand::from_raw_parts(command, args)?), + keys: Key::parse_keybind_keys(&parser.must_string("keybind(keys)")?)?, + command: Box::new(parser.collect_command("keybind(command)")?), }) } } @@ -383,29 +371,12 @@ pub struct Mousebind { pub action: MousebindAction, } -impl FromStr for Mousebind { - type Err = StringParseError; +impl FromStrings for Mousebind { + fn from_strings>(s: I) -> Result { + let mut parser = ArgParser::from_strings(s); + let keys = Key::parse_keybind_keys(&parser.must_string("mousebind(keys)")?)?; - fn from_str(s: &str) -> Result { - let mut parts = s.split("\t"); - let keys = parts - .next() - .ok_or(StringParseError::UnknownValue)? - .split("-") - .map(|key| key.parse()) - .collect::, _>>()?; - let action = parts.next().ok_or(StringParseError::UnknownValue)?; - let mut action = MousebindAction::iter() - .into_iter() - .find(|act| act.to_string() == action) - .ok_or(StringParseError::UnknownValue)?; - if let MousebindAction::Call(_) = action { - let command = parts.next().ok_or(StringParseError::UnknownValue)?; - action = MousebindAction::Call(Box::new( - HlwmCommand::from_raw_parts(command, parts.map(String::from).collect()) - .map_err(|_| StringParseError::UnknownValue)?, - )); - } + let action: MousebindAction = parser.collect_command("mousebind(action)")?; Ok(Self { keys, action }) } @@ -449,16 +420,12 @@ pub enum KeyUnbind { } impl FromStr for KeyUnbind { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { match s { - "--all" => Ok(Self::All), - _ => Ok(KeyUnbind::Keybind( - s.split("-") - .map(|key| key.parse()) - .collect::>()?, - )), + "--all" | "-F" => Ok(Self::All), + _ => Ok(Self::Keybind(Key::parse_keybind_keys(s)?)), } } } diff --git a/src/hlwm/macros.rs b/src/hlwm/macros.rs deleted file mode 100644 index 439ad1e..0000000 --- a/src/hlwm/macros.rs +++ /dev/null @@ -1,153 +0,0 @@ -#[macro_export] -macro_rules! gen_parse { - ($command:ident, $args:ident) => { - macro_rules! parse { - (And_Or => $$val:tt) => { - { - let mut args = $args.into_iter(); - let separator: Separator = args - .next() - .ok_or(CommandParseError::MissingArgument)? - .parse()?; - let sep_str = separator.to_string(); - let args = args.collect::>(); - let commands = args - .split(|itm| itm.eq(&sep_str)) - .map(|itm| match itm.len() { - 0 => Err(CommandParseError::MissingArgument), - 1 => HlwmCommand::from_str(itm.first().unwrap()), - _ => { - let mut args = itm.into_iter(); - HlwmCommand::from_raw_parts( - &args.next().unwrap(), - args.map(|i| i.to_owned()).collect(), - ) - } - }) - .collect::, _>>()?; - Ok(Self::$$val{ - separator, - commands, - })} - }; - ($$arg_type:tt => $$val:tt) => { - { - #[allow(unused_mut)] - let mut args_iter = $args.into_iter(); - Ok(Self::$$val(parse!(Argument args_iter: $$arg_type))) - } - }; - ($$($$arg:tt: $$arg_type:tt),+ => $val:tt) => { - { - #[allow(unused_mut)] - let mut args_iter = $args.into_iter(); - Ok(Self::$val{ - $$( - $$arg: parse!(Argument args_iter: $$arg_type) - ),+ - }) - } - }; - (Argument $$args:ident: [Flag($pat:literal)]) => { - { - let mut args = $$args.clone().into_iter(); - let mut flag = false; - if args.any(|i| i == $pat) { - $$args = $$args.filter(|a| a != $pat).collect::>().into_iter(); - flag = true; - } - flag - } - }; - (Argument $$args:ident: String) => { - $$args - .next() - .ok_or(CommandParseError::BadArgument { - command: $command.to_string(), - })? - }; - (Argument $$args:ident: FromStrAll) => { - $$args - .collect::>() - .join("\t") - .parse() - .map_err(|_| CommandParseError::BadArgument { - command: $command.to_string(), - })? - }; - (Argument $$args:ident: FromStr) => { - parse!(Argument $$args: String) - .parse() - .map_err(|_| CommandParseError::BadArgument { - command: $command.to_string(), - })? - }; - (Argument $$args:ident: [Option]) => { - { - let args: Vec<_> = $$args.collect(); - if args.len() == 0 { - None - } else { - let mut args = args.into_iter(); - Some(parse!(Argument args: String) - .parse() - .map_err(|_| CommandParseError::BadArgument { - command: $command.to_string(), - })?) - } - } - }; - (Argument $$args:ident: [Vec]) => { - $$args.collect::>() - }; - (Argument $$args:ident: Bool) => { - match parse!(Argument $$args: String).as_str() { - "on" | "true" => true, - "off" | "false" => false, - _ => return Err(CommandParseError::BadArgument { - command: $command.to_string(), - }), - } - }; - (Argument $$args:ident: [Option]) => { - { - let args = $$args.collect::>(); - if args.len() == 0 { - None - } else { - Some(args.join("\t")) - } - } - }; - (Argument $$args:ident: [Option>]) => { - { - let args = $$args.collect::>(); - if args.len() == 0 { - None - } else { - Some(args) - } - } - }; - (Argument $$args:ident: [Option]) => { - { - let args = $$args.map(|item| item.parse().map_err(|_| CommandParseError::BadArgument { - command: $command.to_string(), - })).collect::, _>>()?; - if args.len() == 0 { - None - } else { - Some(args) - } - } - }; - (Argument $$args:ident: [Vec]) => { - { - $$args.map(|item| item.parse().map_err(|_| CommandParseError::BadArgument { - command: $command.to_string(), - })).collect::, _>>()? - } - }; - } - }; -} diff --git a/src/hlwm/mod.rs b/src/hlwm/mod.rs index cfe176b..397f494 100644 --- a/src/hlwm/mod.rs +++ b/src/hlwm/mod.rs @@ -1,26 +1,25 @@ use std::{ convert::Infallible, fmt::Display, - num::{ParseFloatError, ParseIntError}, process::{self, Stdio}, str::FromStr, }; -use log::{debug, error}; +use log::{error, trace}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; -use thiserror::Error; use crate::cmd; use self::{ - attribute::{Attribute, AttributeError}, + attribute::{Attribute, AttributeType}, command::{CommandError, HlwmCommand}, - key::KeyParseError, + parser::{ArgParser, FromStrings, ParseError}, setting::{Setting, SettingName}, tag::TagStatus, }; +mod and_or_command; pub mod attribute; pub mod color; pub mod command; @@ -30,13 +29,12 @@ pub mod hook; pub mod key; mod octal; pub mod pad; +pub mod parser; pub mod rule; pub mod setting; pub mod tag; pub mod theme; pub mod window; -#[macro_use] -mod macros; pub use hlwmbool::ToggleBool; @@ -58,7 +56,7 @@ impl Client { /// Run the command and wait for it to finish. pub fn execute(&self, command: HlwmCommand) -> Result { let args = command.args(); - debug!("running command: [{}]", (&args).join(" "),); + trace!("running command: [{}]", (&args).join(" "),); let output = Self::herbstclient() .arg("--no-newline") .args(args) @@ -90,29 +88,37 @@ impl Client { .ok_or(CommandError::Empty) } + pub fn get_from_str_attr, T: FromStr>( + &self, + attr: String, + ) -> Result { + Ok(self + .get_str_attr(attr)? + .parse() + .map_err(|err: E| err.into())?) + } + pub fn get_attr(&self, attr: String) -> Result { - let attr_type = self - .query(HlwmCommand::AttrType(attr.clone()))? - .first() - .cloned() - .ok_or(CommandError::Empty)?; + let attr_type = AttributeType::get_type(&attr)?; let attr_val = self .query(HlwmCommand::GetAttr(attr))? .first() .cloned() .ok_or(CommandError::Empty)?; - Ok(Attribute::new(&attr_type, &attr_val)?) + Ok(Attribute::new(attr_type, &attr_val)?) } pub fn get_setting(&self, setting: SettingName) -> Result { - Ok(Setting::from_str(&format!( - "{setting}\t{}", - String::from_utf8(self.execute(HlwmCommand::Get(setting))?.stdout,)? - )) - .map_err(|err| { - error!("failed getting setting [{setting}]: {err}"); - StringParseError::UnknownValue - })?) + let setting_value = String::from_utf8(self.execute(HlwmCommand::Get(setting))?.stdout)?; + Ok( + Setting::from_str(&format!("{setting}\t{}", setting_value)).map_err(|err| { + error!("failed getting setting [{setting}]: {err}"); + ParseError::InvalidValue { + value: setting_value, + expected: "setting value (get_setting)", + } + })?, + ) } pub fn query(&self, command: HlwmCommand) -> Result, CommandError> { @@ -145,28 +151,6 @@ pub trait ToCommandString { fn to_command_string(&self) -> String; } -#[derive(Debug, Clone, Error)] -pub enum StringParseError { - #[error("unknown value")] - UnknownValue, - #[error("failed parsing float: [{0}]")] - FloatError(#[from] ParseFloatError), - #[error("invalid bool value: [{0}]")] - BoolError(String), - #[error("failed parsing int: [{0}]")] - IntError(#[from] ParseIntError), - #[error("command parse error")] - CommandParseError(String), - #[error("invalid length for part [{1}]: [{0}]")] - InvalidLength(usize, &'static str), - #[error("required arguments missing: [{0}]")] - RequiredArgMissing(String), - #[error("attribute error: [{0}]")] - AttributeError(#[from] AttributeError), - #[error("key parse error: [{0}]")] - KeyParseError(#[from] KeyParseError), -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum TagSelect { Index(i32), @@ -213,22 +197,31 @@ impl FromStr for Index where N: PartialEq + FromStr, { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { let mut chars = s.chars(); - let prefix = chars.next().ok_or(StringParseError::UnknownValue)?; + let prefix = chars.next().ok_or(ParseError::Empty)?; match prefix { - '+' => Ok(Self::Relative( - N::from_str(&chars.collect::()) - .map_err(|_| StringParseError::UnknownValue)?, - )), - '-' => Ok(Self::Relative( - N::from_str(s).map_err(|_| StringParseError::UnknownValue)?, - )), - _ => Ok(Self::Absolute( - N::from_str(s).map_err(|_| StringParseError::UnknownValue)?, - )), + '+' => Ok(Self::Relative({ + let s = chars.collect::(); + N::from_str(&s).map_err(|_| ParseError::InvalidValue { + value: s, + expected: std::any::type_name::(), + })? + })), + '-' => Ok(Self::Relative(N::from_str(s).map_err(|_| { + ParseError::InvalidValue { + value: s.to_string(), + expected: std::any::type_name::(), + } + })?)), + _ => Ok(Self::Absolute(N::from_str(s).map_err(|_| { + ParseError::InvalidValue { + value: s.to_string(), + expected: std::any::type_name::(), + } + })?)), } } } @@ -260,20 +253,20 @@ where } } -#[derive(Debug, Clone, Serialize, Deserialize, strum::EnumIter, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::EnumIter, PartialEq)] pub enum Separator { Comma, Period, } impl FromStr for Separator { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { Self::iter() .into_iter() .find(|i| i.to_string() == s) - .ok_or(StringParseError::UnknownValue) + .ok_or(ParseError::InvalidCommand(s.to_string())) } } @@ -309,13 +302,13 @@ pub enum Operator { } impl FromStr for Operator { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { Self::iter() .into_iter() .find(|i| i.to_string() == s) - .ok_or(StringParseError::UnknownValue) + .ok_or(ParseError::InvalidCommand(s.to_string())) } } @@ -348,13 +341,13 @@ pub enum Direction { } impl FromStr for Direction { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { Self::iter() .into_iter() .find(|dir| dir.to_string() == s) - .ok_or(StringParseError::UnknownValue) + .ok_or(ParseError::InvalidCommand(s.to_string())) } } @@ -375,25 +368,20 @@ pub enum Align { Auto, } -impl FromStr for Align { - type Err = StringParseError; +impl FromStrings for Align { + fn from_strings>(s: I) -> Result { + let mut args = ArgParser::from_strings(s); + let alignment = args.must_string("align(align)")?; + let fraction = args.optional_next_from_str("align(fraction)")?; - fn from_str(s: &str) -> Result { - let mut parts = s.split("\t"); - let align = parts.next().ok_or(StringParseError::UnknownValue)?; - let fraction = match parts.next().map(|f| f64::from_str(f)) { - Some(val) => Some(val?), - None => None, - }; - - match align { + match alignment.as_str() { "bottom" | "vertical" | "vert" | "v" => Ok(Self::Bottom(fraction)), "left" => Ok(Self::Left(fraction)), "right" | "horizontal" | "horiz" | "h" => Ok(Self::Right(fraction)), "top" => Ok(Self::Top(fraction)), "explode" => Ok(Self::Explode), "auto" => Ok(Self::Auto), - _ => Err(StringParseError::UnknownValue), + _ => Err(ParseError::InvalidCommand(alignment)), } } } diff --git a/src/hlwm/pad.rs b/src/hlwm/pad.rs index 7a3279e..f886f10 100644 --- a/src/hlwm/pad.rs +++ b/src/hlwm/pad.rs @@ -2,7 +2,7 @@ use std::{fmt::Display, str::FromStr}; use crate::split; -use super::StringParseError; +use super::parser::{FromStrings, ParseError}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Pad { @@ -13,13 +13,11 @@ pub enum Pad { UpRightDownLeft(u32, u32, u32, u32), } -impl FromStr for Pad { - type Err = StringParseError; - - fn from_str(s: &str) -> Result { - let parts = split::tab_or_space(s); +impl FromStrings for Pad { + fn from_strings>(s: I) -> Result { + let parts = s.collect::>(); match parts.len() { - 0 => Err(StringParseError::InvalidLength(0, "pad")), + 0 => Err(ParseError::Empty), 1 => Ok(Pad::Up(parts[0].parse()?)), 2 => Ok(Pad::UpRight(parts[0].parse()?, parts[1].parse()?)), 3 => Ok(Pad::UpRightDown( @@ -37,6 +35,15 @@ impl FromStr for Pad { } } +impl FromStr for Pad { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let parts = split::tab_or_space(s); + Self::from_strings(parts.into_iter()) + } +} + impl Display for Pad { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/src/hlwm/parser/either.rs b/src/hlwm/parser/either.rs new file mode 100644 index 0000000..d430cc1 --- /dev/null +++ b/src/hlwm/parser/either.rs @@ -0,0 +1,4 @@ +pub enum Either { + Left(L), + Right(R), +} diff --git a/src/hlwm/parser/mod.rs b/src/hlwm/parser/mod.rs new file mode 100644 index 0000000..63c59c7 --- /dev/null +++ b/src/hlwm/parser/mod.rs @@ -0,0 +1,234 @@ +use std::{ + convert::Infallible, + num::{ParseFloatError, ParseIntError}, + str::{FromStr, ParseBoolError}, + string::FromUtf8Error, +}; + +use log::error; +use thiserror::Error; + +use self::either::Either; + +use super::hex::HexError; + +pub mod either; +mod traits; +pub use traits::*; + +#[derive(Debug, Clone, Error, strum::EnumIs)] +pub enum ParseError { + #[error("no more items left in parser")] + Empty, + #[error("missing expected value")] + ValueMissing, + #[error("invalid command: [{0}]")] + InvalidCommand(String), + #[error("invalid value [{value}], expected [{expected}]")] + InvalidValue { + value: String, + expected: &'static str, + }, + #[error("hex decoding error: [{0}]")] + HexError(#[from] HexError), + #[error("primitive type parsing error: [{0}]")] + PrimitiveError(String), +} + +impl From for ParseError { + fn from(_: Infallible) -> Self { + // nonsense + unreachable!() + } +} + +macro_rules! from_primitive { + ($($err:ty,)*) => { + $( + impl From<$err> for ParseError { + fn from(value: $err) -> Self { + ParseError::PrimitiveError(value.to_string()) + } + } + )* + }; +} +from_primitive!( + ParseBoolError, + ParseIntError, + FromUtf8Error, + ParseFloatError, +); + +impl From for ParseError { + fn from(value: strum::ParseError) -> Self { + ParseError::InvalidCommand(value.to_string()) + } +} + +pub struct ArgParser +where + I: Iterator, +{ + inner: I, +} + +impl Clone for ArgParser +where + I: Iterator + Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl ArgParser +where + I: Iterator, +{ + pub fn from_strings(src: I) -> Self { + Self { inner: src } + } + + #[inline(always)] + pub fn next_string(&mut self) -> Option { + self.inner.next() + } + + #[inline(always)] + pub fn inner(self) -> I { + self.inner + } + + /// Gets the next string, then tries the `try_this` function on it. + /// If that function returns [Err], it runs the `then_this` function + /// and returns that result. + pub fn try_first( + &mut self, + try_this: F1, + then_this: F2, + on_error: &str, + ) -> Result, ParseError> + where + F1: FnOnce(&String) -> Result, + F2: FnOnce(&String) -> Result, + E: Into, + { + let next = self.must_string(on_error)?; + if let Ok(t1) = try_this(&next) { + return Ok(Either::Left(t1)); + } + match then_this(&next) { + Ok(t2) => Ok(Either::Right(t2)), + Err(err) => { + let err = err.into(); + error!("{on_error}: {err}"); + Err(err) + } + } + } + + /// Get the next string, or [ParseError::Empty] + #[inline(always)] + pub fn must_string(&mut self, on_error: &str) -> Result { + self.inner + .next() + .ok_or(ParseError::Empty) + .inspect_err(|err| error!("{on_error}: {err}")) + } + + pub fn next_from_str, T: FromStr>( + &mut self, + on_error: &str, + ) -> Result { + Ok(self.must_string(on_error)?.parse().map_err(|err: E| { + let err = err.into(); + error!("{on_error}: {err}"); + err + })?) + } + + /// Consumes the remainder of the iterator and uses the [FromCommandArgs] + /// trait to parse it into the desired type + pub fn collect_command(mut self, on_error: &str) -> Result { + C::from_command_args(&self.must_string(on_error)?, self.inner) + } + + /// Consumes the remainder of the iterator and uses the [FromStrings] + /// trait to parse it into the desired type + pub fn collect_from_strings(self, on_error: &str) -> Result { + T::from_strings(self.inner).inspect_err(|err| { + error!("{on_error}: {err}"); + }) + } + + /// Consumes the remainder of the iterator and uses the [FromStringsHint] + /// trait to parse it into the desired type. + /// + /// Takes a `hint` that may aid in the parsing of that string + pub fn collect_from_strings_hint>( + self, + hint: H, + on_error: &str, + ) -> Result { + T::from_strings_hint(self.inner, hint).inspect_err(|err| { + error!("{on_error}: {err}"); + }) + } + + #[inline(always)] + pub fn collect>(self) -> B { + self.inner.collect() + } + + /// Get the next [String] from the iterator and tries to parse it with [FromStr] + /// if there's any remaining items. Otherwise just returns [None] + pub fn optional_next_from_str, T: FromStr>( + &mut self, + on_error: &str, + ) -> Result, ParseError> { + match self.next_string() { + Some(next) => Ok(Some(next.parse().map_err(|err: E| { + let err = err.into(); + error!("{on_error}: {err}"); + err + })?)), + None => Ok(None), + } + } + + /// Consumes the iterator and takes every remaining element, parses them with + /// [FromStr] and returns them in a [Vec] + pub fn collect_from_str, T: FromStr>( + self, + on_error: &str, + ) -> Result, ParseError> { + self.inner + .map(|item| T::from_str(&item)) + .collect::, _>>() + .map_err(|err| { + let err = err.into(); + error!("{on_error}: {err}"); + err + }) + } + + /// Consumes the iterator and takes every remaining element, checks if any match + /// `flag`, and returns the strings in the iterator that don't match `flag`, + /// as well as a boolean on whether `flag` was found. + pub fn collect_strings_with_flag(self, flag: &str) -> (Vec, bool) { + let mut out = Vec::new(); + let mut found = false; + for item in self.inner { + if item == flag { + found = true; + continue; + } + out.push(item); + } + + (out, found) + } +} diff --git a/src/hlwm/parser/traits.rs b/src/hlwm/parser/traits.rs new file mode 100644 index 0000000..0805002 --- /dev/null +++ b/src/hlwm/parser/traits.rs @@ -0,0 +1,67 @@ +use super::ParseError; + +pub trait FromStrings: Sized { + fn from_strings>(s: I) -> Result; +} + +/// Same as [FromStrings] but can pass a hint to the function which can help +/// in some determinations (e.g. passing in the path of the attribute for +/// parsing a [crate::hlwm::Attribute] so it can use the path to determine the +/// type of the attribute) +pub trait FromStringsHint: Sized { + fn from_strings_hint>(s: I, hint: Hint) -> Result; +} + +pub trait FromCommandArgs: Sized { + fn from_command_args, I: Iterator>( + command: &str, + args: I, + ) -> Result; +} + +pub trait ToOption: Sized { + fn to_option(self) -> Option; +} + +impl ToOption for Vec { + fn to_option(self) -> Option { + match self.is_empty() { + false => Some(self), + true => None, + } + } +} + +impl ToOption for &str { + fn to_option(self) -> Option { + match self.is_empty() { + false => Some(self), + true => None, + } + } +} + +impl ToOption for String { + fn to_option(self) -> Option { + match self.is_empty() { + false => Some(self), + true => None, + } + } +} + +pub trait Flip { + type Target; + fn flip(self) -> Self::Target; +} + +impl Flip for Option> { + type Target = Result, E>; + + fn flip(self) -> Self::Target { + match self { + Some(r) => r.map(Some), + None => Self::Target::Ok(None), + } + } +} diff --git a/src/hlwm/rule.rs b/src/hlwm/rule.rs index 1d79b70..98cd61d 100644 --- a/src/hlwm/rule.rs +++ b/src/hlwm/rule.rs @@ -10,7 +10,12 @@ use strum::IntoEnumIterator; use crate::split; -use super::{hlwmbool, hook::Hook, StringParseError, ToCommandString}; +use super::{ + hlwmbool, + hook::Hook, + parser::{FromCommandArgs, FromStrings, ParseError}, + ToCommandString, +}; #[derive(Debug, Clone, PartialEq)] pub struct Rule { @@ -78,23 +83,17 @@ impl Rule { } } -impl FromStr for Rule { - type Err = StringParseError; - - fn from_str(s: &str) -> Result { - let parts = split::tab_or_space(s); - if parts.is_empty() { - return Err(StringParseError::InvalidLength(0, "parse rule")); - } +impl FromStrings for Rule { + fn from_strings>(s: I) -> Result { let mut condition: Option = None; let mut consequences = vec![]; let mut label: Option = None; let mut flag: Option = None; - let mut args = parts - .into_iter() - .map(|part| part.strip_prefix("--").unwrap_or(part.as_str()).to_string()); + let mut args = s.map(|part| part.strip_prefix("--").unwrap_or(part.as_str()).to_string()); + let mut original = Vec::new(); // For InvalidValue error while let Some(arg) = args.next() { + original.push(arg.clone()); if label.is_none() { if let Some((name, value)) = arg.split_once('=') { if name.trim() == "label" { @@ -122,9 +121,10 @@ impl FromStr for Rule { } if consequences.is_empty() { - return Err(StringParseError::RequiredArgMissing( - "condition and/or consequences".into(), - )); + return Err(ParseError::InvalidValue { + value: original.join(" "), + expected: "condition and/or consequences", + }); } Ok(Self { @@ -158,8 +158,9 @@ impl<'de> Deserialize<'de> for Rule { } let str_val: String = Deserialize::deserialize(deserializer)?; + let strings = split::tab_or_space(&str_val); - Ok(Self::from_str(&str_val).map_err(|_| { + Ok(Self::from_strings(strings.into_iter()).map_err(|_| { serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &Expect) })?) } @@ -187,12 +188,12 @@ impl RuleOperator { } impl TryFrom for RuleOperator { - type Error = StringParseError; + type Error = ParseError; fn try_from(value: char) -> Result { Self::iter() .find(|i| i.char() == value) - .ok_or(StringParseError::UnknownValue) + .ok_or(ParseError::InvalidCommand(value.to_string())) } } @@ -209,13 +210,10 @@ impl Display for RuleOperator { } impl FromStr for RuleOperator { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { - s.chars() - .next() - .ok_or(StringParseError::InvalidLength(s.len(), "rule operator"))? - .try_into() + s.chars().next().ok_or(ParseError::Empty)?.try_into() } } @@ -294,7 +292,7 @@ impl ToCommandString for Condition { } impl FromStr for Condition { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { // Handle the case for fixedsize first so that we can treat the rest @@ -307,11 +305,11 @@ impl FromStr for Condition { let ((name, match_val), match_char) = match split::on_first_match(s, &RuleOperator::match_set()) { Some(parts) => parts, - None => return Err(StringParseError::InvalidLength(1, "property")), + None => return Err(ParseError::Empty), }; let mut prop = Self::iter() .find(|i| i.to_string() == name) - .ok_or(StringParseError::UnknownValue)?; + .ok_or(ParseError::InvalidCommand(s.to_string()))?; match prop.borrow_mut() { Condition::Instance { operator, value } @@ -327,7 +325,12 @@ impl FromStr for Condition { } // Should be handled at the top of the function. If it's here that's not a valid // use of fixedsize. - Condition::FixedSize => return Err(StringParseError::UnknownValue), + Condition::FixedSize => { + return Err(ParseError::InvalidValue { + value: s.to_string(), + expected: "[BUG] Should be handled at the top of the function", + }) + } }; Ok(prop) @@ -433,7 +436,7 @@ impl ToCommandString for Consequence { } impl FromStr for Consequence { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { let parts = split::tab_or_space(s); @@ -442,7 +445,7 @@ impl FromStr for Consequence { let (name, value_str) = if name.contains('=') { let parts = name.split('=').collect::>(); if parts.len() != 2 { - return Err(StringParseError::UnknownValue); + return Err(ParseError::InvalidCommand(s.to_string())); } let mut parts = parts.into_iter(); ( @@ -453,10 +456,10 @@ impl FromStr for Consequence { match parts.next() { Some(op) => { if op != "=" { - return Err(StringParseError::UnknownValue); + return Err(ParseError::InvalidCommand(s.to_string())); } } - None => return Err(StringParseError::UnknownValue), + None => return Err(ParseError::InvalidCommand(s.to_string())), }; let value = parts.collect::>().join("\t"); (name, value) @@ -464,7 +467,7 @@ impl FromStr for Consequence { let mut cons = Self::iter() .find(|i| i.to_string() == name) - .ok_or(StringParseError::UnknownValue)?; + .ok_or(ParseError::InvalidCommand(s.to_string()))?; match cons.borrow_mut() { Consequence::Focus(value) | Consequence::SwitchTag(value) @@ -483,15 +486,13 @@ impl FromStr for Consequence { | Consequence::KeysInactive(value) => *value = value_str, Consequence::Index(value) => *value = i32::from_str(&value_str)?, Consequence::Hook(value) => { - *value = Hook::from_str(&value_str).map_err(|_| StringParseError::UnknownValue)? + *value = Hook::from_command_args(&name, [value_str].into_iter())?; } Consequence::FloatPlacement(value) => *value = FloatPlacement::from_str(&value_str)?, Consequence::FloatingGeometry { x, y } => { let mut values = value_str.split('='); - *x = u32::from_str(values.next().ok_or(StringParseError::UnknownValue)?) - .map_err(|_| StringParseError::UnknownValue)?; - *y = u32::from_str(values.next().ok_or(StringParseError::UnknownValue)?) - .map_err(|_| StringParseError::UnknownValue)?; + *x = u32::from_str(values.next().ok_or(ParseError::ValueMissing)?)?; + *y = u32::from_str(values.next().ok_or(ParseError::ValueMissing)?)?; } }; @@ -513,12 +514,12 @@ pub enum FloatPlacement { } impl FromStr for FloatPlacement { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { Self::iter() .find(|i| i.to_string() == s) - .ok_or(StringParseError::UnknownValue) + .ok_or(ParseError::InvalidCommand(s.to_string())) } } @@ -553,7 +554,7 @@ impl Display for Flag { } impl FromStr for Flag { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { match s { @@ -561,7 +562,7 @@ impl FromStr for Flag { "once" => Ok(Self::Once), "printlabel" => Ok(Self::PrintLabel), "prepend" => Ok(Self::Prepend), - _ => Err(StringParseError::UnknownValue), + _ => Err(ParseError::InvalidCommand(s.to_string())), } } } @@ -649,7 +650,10 @@ mod test { use serde::{Deserialize, Serialize}; - use crate::hlwm::rule::FloatPlacement; + use crate::{ + hlwm::{parser::FromStrings, rule::FloatPlacement}, + split, + }; use pretty_assertions::assert_eq; use super::{Condition, Consequence, Flag, Rule, RuleOperator}; @@ -697,7 +701,7 @@ label=5 fixedsize=0 floating=true"#; let parsed = INPUT .split('\n') - .map(|l| Rule::from_str(l)) + .map(|l| Rule::from_strings(split::tab_or_space(l).into_iter())) .collect::, _>>() .expect("parsing error"); diff --git a/src/hlwm/setting.rs b/src/hlwm/setting.rs index e588682..07b7977 100644 --- a/src/hlwm/setting.rs +++ b/src/hlwm/setting.rs @@ -1,12 +1,20 @@ -use std::str::FromStr; +use std::{borrow::BorrowMut, str::FromStr}; -use log::debug; -use serde::{Deserialize, Serialize}; +use log::trace; +use serde::{de::Expected, Deserialize, Serialize}; -use crate::{gen_parse, hlwm::command::CommandParseError, split}; +use crate::{ + hlwm::{command::CommandParseError, parser::ArgParser}, + split, +}; use strum::IntoEnumIterator; -use super::{color::Color, hlwmbool::ToggleBool, StringParseError, ToCommandString}; +use super::{ + color::Color, + hlwmbool::ToggleBool, + parser::{FromCommandArgs, ParseError}, + ToCommandString, +}; #[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq, strum::EnumDiscriminants)] #[strum_discriminants( @@ -201,12 +209,12 @@ impl Default for SettingName { } impl FromStr for SettingName { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { Self::iter() .find(|i| i.to_string() == s) - .ok_or(StringParseError::UnknownValue) + .ok_or(ParseError::InvalidCommand(s.to_string())) } } @@ -224,6 +232,18 @@ impl<'de> Deserialize<'de> for Setting { where D: serde::Deserializer<'de>, { + pub enum Expect { + Command(String), + } + impl Expected for Expect { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Expect::Command(err) => { + write!(f, "a valid [command][space/tab][value] string: {err}") + } + } + } + } let str_val: String = Deserialize::deserialize(deserializer)?; let ((command, arg), _) = split::on_first_match(&str_val, &['=']) .ok_or(CommandParseError::InvalidArgumentCount(0, "setting".into())) @@ -231,90 +251,92 @@ impl<'de> Deserialize<'de> for Setting { serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &err) })?; Ok( - Self::from_raw_parts(&command.trim(), &arg.trim()).map_err(|err| { - serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &err) + Self::from_command_args(&command.trim(), [arg.trim()].into_iter()).map_err(|err| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(&str_val), + &Expect::Command(err.to_string()), + ) })?, ) } } impl FromStr for Setting { - type Err = CommandParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { - debug!("beginning to parse [{s}] as setting"); - let ((command, arg), _) = split::on_first_match(s, &['\t', ' ']) - .ok_or(CommandParseError::InvalidArgumentCount(0, "setting".into()))?; + trace!("[from_str] beginning to parse [{s}] as setting"); + let ((command, arg), _) = + split::on_first_match(s, &['\t', ' ']).ok_or(ParseError::InvalidValue { + value: s.to_string(), + expected: "[setting] [value]", + })?; - Self::from_raw_parts(&command.trim(), &arg.trim()) + Self::from_command_args(&command.trim(), [arg].into_iter()) } } -impl Setting { - pub fn from_raw_parts(command: &str, arg: &str) -> Result { - let command = Self::iter() - .find(|cmd| cmd.to_string() == command) - .ok_or(CommandParseError::UnknownCommand(command.to_string()))?; +impl FromCommandArgs for Setting { + fn from_command_args, I: Iterator>( + cmd: &str, + args: I, + ) -> Result { + let mut command = Self::iter() + .find(|s| s.to_string() == cmd) + .ok_or(ParseError::InvalidCommand(cmd.to_string()))?; - let args = [arg.to_string()]; - gen_parse!(command, args); + let mut parser = ArgParser::from_strings(args.map(|i| i.into())); - match command { - Setting::Verbose(_) => parse!(FromStr => Verbose), - Setting::TabbedMax(_) => parse!(FromStr => TabbedMax), - Setting::GaplessGrid(_) => parse!(FromStr => GaplessGrid), - Setting::RaiseOnClick(_) => parse!(FromStr => RaiseOnClick), - Setting::RaiseOnFocus(_) => parse!(FromStr => RaiseOnFocus), - Setting::AutoDetectPanels(_) => parse!(FromStr => AutoDetectPanels), - Setting::FocusFollowsMouse(_) => parse!(FromStr => FocusFollowsMouse), - Setting::HideCoveredWindows(_) => parse!(FromStr => HideCoveredWindows), - Setting::AutoDetectMonitors(_) => parse!(FromStr => AutoDetectMonitors), - Setting::FrameBgTransparent(_) => parse!(FromStr => FrameBgTransparent), - Setting::SwapMonitorsToGetTag(_) => parse!(FromStr => SwapMonitorsToGetTag), - Setting::UpdateDraggedClients(_) => parse!(FromStr => UpdateDraggedClients), - Setting::FocusStealingPrevention(_) => parse!(FromStr => FocusStealingPrevention), - Setting::RaiseOnFocusTemporarily(_) => parse!(FromStr => RaiseOnFocusTemporarily), - Setting::SmartWindowSurroundings(_) => parse!(FromStr => SmartWindowSurroundings), - Setting::DefaultDirectionExternalOnly(_) => { - parse!(FromStr => DefaultDirectionExternalOnly) + match command.borrow_mut() { + Setting::Verbose(arg) + | Setting::TabbedMax(arg) + | Setting::GaplessGrid(arg) + | Setting::RaiseOnClick(arg) + | Setting::RaiseOnFocus(arg) + | Setting::AutoDetectPanels(arg) + | Setting::FocusFollowsMouse(arg) + | Setting::HideCoveredWindows(arg) + | Setting::AutoDetectMonitors(arg) + | Setting::FrameBgTransparent(arg) + | Setting::SwapMonitorsToGetTag(arg) + | Setting::UpdateDraggedClients(arg) + | Setting::FocusStealingPrevention(arg) + | Setting::RaiseOnFocusTemporarily(arg) + | Setting::SmartWindowSurroundings(arg) + | Setting::DefaultDirectionExternalOnly(arg) + | Setting::FocusCrossesMonitorBoundaries(arg) => *arg = parser.next_from_str(cmd)?, + Setting::FrameBgActiveColor(arg) + | Setting::FrameBgNormalColor(arg) + | Setting::FrameBorderInnerColor(arg) + | Setting::FrameBorderActiveColor(arg) + | Setting::FrameBorderNormalColor(arg) + | Setting::WindowBorderInnerColor(arg) + | Setting::WindowBorderActiveColor(arg) + | Setting::WindowBorderNormalColor(arg) + | Setting::WindowBorderUrgentColor(arg) => *arg = parser.next_from_str(cmd)?, + Setting::DefaultFrameLayout(arg) => *arg = parser.next_from_str(cmd)?, + Setting::ShowFrameDecorations(arg) => *arg = parser.next_from_str(cmd)?, + Setting::SmartFrameSurroundings(arg) => *arg = parser.next_from_str(cmd)?, + Setting::MonitorsLocked(arg) + | Setting::FrameActiveOpacity(arg) + | Setting::FrameNormalOpacity(arg) => *arg = parser.next_from_str(cmd)?, + Setting::SnapGap(arg) + | Setting::FrameGap(arg) + | Setting::WindowGap(arg) + | Setting::SnapDistance(arg) + | Setting::FramePadding(arg) + | Setting::MouseRecenterGap(arg) + | Setting::FrameBorderWidth(arg) + | Setting::WindowBorderWidth(arg) + | Setting::FrameTransparentWidth(arg) + | Setting::FrameBorderInnerWidth(arg) + | Setting::WindowBorderInnerWidth(arg) + | Setting::PseudotileCenterThreshold(arg) => *arg = parser.next_from_str(cmd)?, + Setting::Wmname(arg) | Setting::Ellipsis(arg) | Setting::TreeStyle(arg) => { + *arg = parser.must_string(cmd)? } - Setting::FocusCrossesMonitorBoundaries(_) => { - parse!(FromStr => FocusCrossesMonitorBoundaries) - } - - Setting::FrameBgActiveColor(_) => parse!(FromStr => FrameBgActiveColor), - Setting::FrameBgNormalColor(_) => parse!(FromStr => FrameBgNormalColor), - Setting::DefaultFrameLayout(_) => parse!(FromStr => DefaultFrameLayout), - Setting::ShowFrameDecorations(_) => parse!(FromStr => ShowFrameDecorations), - Setting::FrameBorderInnerColor(_) => parse!(FromStr => FrameBorderInnerColor), - Setting::FrameBorderActiveColor(_) => parse!(FromStr => FrameBorderActiveColor), - Setting::FrameBorderNormalColor(_) => parse!(FromStr => FrameBorderNormalColor), - Setting::SmartFrameSurroundings(_) => parse!(FromStr => SmartFrameSurroundings), - Setting::WindowBorderInnerColor(_) => parse!(FromStr => WindowBorderInnerColor), - Setting::WindowBorderActiveColor(_) => parse!(FromStr => WindowBorderActiveColor), - Setting::WindowBorderNormalColor(_) => parse!(FromStr => WindowBorderNormalColor), - Setting::WindowBorderUrgentColor(_) => parse!(FromStr => WindowBorderUrgentColor), - - Setting::SnapGap(_) => parse!(FromStr => SnapGap), - Setting::FrameGap(_) => parse!(FromStr => FrameGap), - Setting::WindowGap(_) => parse!(FromStr => WindowGap), - Setting::SnapDistance(_) => parse!(FromStr => SnapDistance), - Setting::FramePadding(_) => parse!(FromStr => FramePadding), - Setting::MonitorsLocked(_) => parse!(FromStr => MonitorsLocked), - Setting::MouseRecenterGap(_) => parse!(FromStr => MouseRecenterGap), - Setting::FrameBorderWidth(_) => parse!(FromStr => FrameBorderWidth), - Setting::WindowBorderWidth(_) => parse!(FromStr => WindowBorderWidth), - Setting::FrameActiveOpacity(_) => parse!(FromStr => FrameActiveOpacity), - Setting::FrameNormalOpacity(_) => parse!(FromStr => FrameNormalOpacity), - Setting::FrameTransparentWidth(_) => parse!(FromStr => FrameTransparentWidth), - Setting::FrameBorderInnerWidth(_) => parse!(FromStr => FrameBorderInnerWidth), - Setting::WindowBorderInnerWidth(_) => parse!(FromStr => WindowBorderInnerWidth), - Setting::PseudotileCenterThreshold(_) => parse!(FromStr => PseudotileCenterThreshold), - - Setting::Wmname(_) => parse!(String => Wmname), - Setting::Ellipsis(_) => parse!(String => Ellipsis), - Setting::TreeStyle(_) => parse!(String => TreeStyle), } + Ok(command) } } diff --git a/src/hlwm/tag.rs b/src/hlwm/tag.rs index 84c82b3..4beacdf 100644 --- a/src/hlwm/tag.rs +++ b/src/hlwm/tag.rs @@ -6,7 +6,7 @@ use std::{ use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; -use super::StringParseError; +use super::parser::ParseError; pub struct TagStatus { name: String, @@ -25,13 +25,13 @@ impl TagStatus { } impl FromStr for TagStatus { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { let mut parts = s.chars(); let state = parts .next() - .ok_or(StringParseError::UnknownValue)? + .ok_or(ParseError::InvalidCommand(s.to_string()))? .try_into()?; let name = parts.collect(); @@ -56,24 +56,24 @@ pub enum TagState { } impl TryFrom for TagState { - type Error = StringParseError; + type Error = ParseError; fn try_from(value: char) -> Result { Self::iter() .into_iter() .find(|i| char::from(i) == value) - .ok_or(StringParseError::UnknownValue) + .ok_or(ParseError::InvalidCommand(value.to_string())) } } impl FromStr for TagState { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { Self::iter() .into_iter() .find(|i| i.to_string() == s) - .ok_or(StringParseError::UnknownValue) + .ok_or(ParseError::InvalidCommand(s.to_string())) } } diff --git a/src/hlwm/theme.rs b/src/hlwm/theme.rs index 5e3101c..5fdc318 100644 --- a/src/hlwm/theme.rs +++ b/src/hlwm/theme.rs @@ -1,36 +1,23 @@ -use std::{borrow::BorrowMut, num::ParseIntError}; +use std::borrow::BorrowMut; use serde::{de::Expected, Deserialize, Serialize}; use strum::IntoEnumIterator; -use thiserror::Error; use crate::split; use super::{ attribute::Attribute, - color::{self, Color}, - StringParseError, + color::Color, + parser::{ArgParser, FromCommandArgs, ParseError}, }; -#[derive(Debug, Error)] -pub enum ThemeAttrParseError { - #[error("path not found")] - PathNotFound, - #[error("integer value parse error: [{0}]")] - ParseIntError(#[from] ParseIntError), - #[error("color value parse error: [{0}]")] - ColorParseError(#[from] color::ParseError), - #[error("string value parse error: [{0}]")] - StringParseError(#[from] StringParseError), -} - macro_rules! theme_attr { - ($($name:tt($value:tt),)*) => { + ($($name:tt($ty:tt),)*) => { #[derive(Debug, Clone, strum::Display, strum::EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ThemeAttr { $( - $name($value), + $name($ty), )* } @@ -49,31 +36,35 @@ macro_rules! theme_attr { fn from(value: ThemeAttr) -> Self { match value { $( - ThemeAttr::$name(val) => theme_attr!(Attr $value)(val), + ThemeAttr::$name(val) => theme_attr!(Attr $ty)(val), )* } } } - impl ThemeAttr { - pub fn from_raw_parts(path: &str, value: &str) -> Result { - match ThemeAttr::iter() - .find(|attr| attr.attr_path() == path) - .map(|attr| -> Result { + impl FromCommandArgs for ThemeAttr { + fn from_command_args, I: Iterator>( + command: &str, + args: I, + ) -> Result { + match ThemeAttr::iter() + .find(|attr| attr.attr_path() == command) + .map(|attr| -> Result { let mut attr = attr.clone(); + let mut parser = ArgParser::from_strings(args.map(|a| a.into())); match attr.borrow_mut() { $( - ThemeAttr::$name(val) => *val = theme_attr!(Parse value: $value), + ThemeAttr::$name(val) => *val = parser.next_from_str(concat!("theme_attr(", stringify!($name), ")"))?, )+ }; Ok(attr) }) .map(|res| if let Err(err) = res {panic!("::: {err}")} else {res}) - .ok_or(ThemeAttrParseError::PathNotFound) - { - Ok(res) => res, - Err(err) => return Err(err), + .ok_or(ParseError::InvalidCommand(command.to_string())) + { + Ok(res) => res, + Err(err) => return Err(err), + } } - } } }; (Attr u32) => { @@ -91,24 +82,6 @@ macro_rules! theme_attr { (Attr i32) => { Attribute::Int }; - (Default u32) => { - 0u32 - }; - (Default i32) => { - 0i32 - }; - (Default Color) => { - Color::BLACK - }; - (Default String) => { - String::new() - }; - (Parse $v:tt: String) => { - $v.to_string() - }; - (Parse $v:tt: $v_ty:tt) => { - $v.parse::<$v_ty>()? - } } theme_attr!( @@ -178,13 +151,13 @@ impl<'de> Deserialize<'de> for ThemeAttr { { pub enum Expect { NoEquals, - ThemeAttrParseError(ThemeAttrParseError), + ParseError(ParseError), } impl Expected for Expect { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Expect::NoEquals => write!(f, "value having an equals sign"), - Expect::ThemeAttrParseError(err) => write!(f, "parsing error: {err}"), + Expect::ParseError(err) => write!(f, "parsing error: {err}"), } } } @@ -198,10 +171,10 @@ impl<'de> Deserialize<'de> for ThemeAttr { ))?; Ok( - Self::from_raw_parts(&first.trim(), &second.trim()).map_err(|err| { + Self::from_command_args(&first.trim(), [second.trim()].into_iter()).map_err(|err| { serde::de::Error::invalid_value( serde::de::Unexpected::Str(&str_val), - &Expect::ThemeAttrParseError(err), + &Expect::ParseError(err), ) })?, ) diff --git a/src/hlwm/window.rs b/src/hlwm/window.rs index aa18e64..e3ae551 100644 --- a/src/hlwm/window.rs +++ b/src/hlwm/window.rs @@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use super::{ + parser::ParseError, rule::{Condition, RuleOperator}, - StringParseError, }; #[derive(Debug, Clone, Serialize, Deserialize, strum::EnumIter, PartialEq)] @@ -21,7 +21,7 @@ pub enum Window { } impl FromStr for Window { - type Err = StringParseError; + type Err = ParseError; fn from_str(s: &str) -> Result { if let Ok(val) = i32::from_str(s) { @@ -29,7 +29,7 @@ impl FromStr for Window { } else { Window::iter() .find(|w| w.to_string() == s) - .ok_or(StringParseError::UnknownValue) + .ok_or(ParseError::ValueMissing) } } } diff --git a/src/main.rs b/src/main.rs index fc5fbb1..36b1e0a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -#![feature(macro_metavar_expr)] use std::path::{Path, PathBuf}; use clap::{Parser, Subcommand}; diff --git a/src/split.rs b/src/split.rs index 2f5087e..a92928b 100644 --- a/src/split.rs +++ b/src/split.rs @@ -38,6 +38,37 @@ impl<'a> From<&'a String> for SplitArg<'a> { } } +/// Assuming the input string is in format `({type_name}){path}`, +/// it will return `Some(("{type_name}", "{path}"))` +pub fn parens(s: &str) -> Option<(String, String)> { + let mut chars = s.chars(); + match chars.next() { + Some(c) => { + if c != '(' { + return None; + } + } + None => return None, + } + + let mut working = Vec::with_capacity(s.len()); + let type_name = loop { + if let Some(next) = chars.next() { + if next == ')' { + break working.into_iter().collect::(); + } + if !next.is_alphanumeric() { + return None; + } + working.push(next); + } else { + return None; + } + }; + + Some((type_name, chars.collect())) +} + pub fn tab_or_space<'a, S>(s: S) -> Vec where S: Into>,