From 22925fc738786589547d38375494eff1b38f8f56 Mon Sep 17 00:00:00 2001 From: emilis Date: Fri, 1 Mar 2024 19:36:29 +0000 Subject: [PATCH] add substitute, foreach, sprintf. improve errors a bit --- src/config.rs | 37 ++++++---- src/hlwm/attribute.rs | 8 ++- src/hlwm/command.rs | 157 ++++++++++++++++++++++++++++++++++++++++-- src/hlwm/key.rs | 2 +- src/hlwm/mod.rs | 14 ++-- src/hlwm/setting.rs | 2 + src/logerr.rs | 31 +++++++++ src/main.rs | 1 + 8 files changed, 222 insertions(+), 30 deletions(-) create mode 100644 src/logerr.rs diff --git a/src/config.rs b/src/config.rs index 8162f22..5e9976e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::{ cmp::Ordering, convert::Infallible, env, - fmt::Display, + fmt::{Debug, Display}, fs::{self}, io, ops::Deref, @@ -33,6 +33,7 @@ use crate::{ window::Window, Align, Client, Direction, Index, Operator, Separator, StringParseError, ToggleBool, }, + logerr::UnwrapLog, rule, window_types, }; @@ -52,8 +53,8 @@ pub enum ConfigError { CommandError(#[from] CommandError), #[error("non-utf8 string error: {0}")] Utf8StringError(#[from] FromUtf8Error), - #[error("failed parsing keybind: {0}")] - KeyParseError(#[from] KeyParseError), + #[error("failed parsing keybind [{0}]: [{1}]")] + KeyParseError(String, KeyParseError), #[error("failed parsing value from string: {0}")] StringParseError(#[from] StringParseError), } @@ -387,9 +388,12 @@ impl Config { name: &str, f: F, default: T, - ) -> T { + ) -> T + where + T: Debug, + { match Client::new().get_attr(name.to_string()) { - Ok(setting) => f(&setting.to_string()).unwrap_or(default), + Ok(setting) => f(&setting.to_string()).unwrap_or_log(default), Err(_) => default, } } @@ -401,11 +405,11 @@ impl Config { |f| -> Result<_, Infallible> { Ok(f.to_string()) }, default.font, ), - mod_key: setting(Self::MOD_KEY, Key::from_str, default.mod_key), font_bold: client .get_attr(ThemeAttr::TitleFont(String::new()).attr_path()) .map(|a| a.to_string()) - .unwrap_or(default.font_bold), + .unwrap_or_log(default.font_bold), + mod_key: setting(Self::MOD_KEY, Key::from_str, default.mod_key), services: setting( Self::SERVICES, |v| serde_json::de::from_str(v), @@ -425,14 +429,14 @@ impl Config { &client .get_attr(attr_path.clone()) .map(|t| t.to_string()) - .unwrap_or(default.to_string()), + .unwrap_or_log(default.to_string()), ) - .unwrap_or(default.clone()) + .unwrap_or_log(default.clone()) }) .collect() })(), }, - keybinds: Self::active_keybinds(true).unwrap_or(default.keybinds), + keybinds: Self::active_keybinds(true).unwrap_or_log(default.keybinds), tags: (|| -> Result, _> { Result::<_, ConfigError>::Ok({ let mut tags = client @@ -451,7 +455,7 @@ impl Config { tags }) })() - .unwrap_or(default.tags), + .unwrap_or_log(default.tags), rules: (|| -> Result, ConfigError> { Ok( String::from_utf8(client.execute(HlwmCommand::ListRules)?.stdout)? @@ -462,7 +466,7 @@ impl Config { .collect::>()?, ) })() - .unwrap_or(default.rules), + .unwrap_or_log(default.rules), settings: (|| -> Result, CommandError> { default .settings @@ -471,7 +475,7 @@ impl Config { .map(|s| Ok(client.get_setting(s.into())?)) .collect::, CommandError>>() })() - .unwrap_or(default.settings), + .unwrap_or_log(default.settings), ..default } } @@ -501,7 +505,10 @@ impl Config { None => false, } }) - .map(|row: &str| Keybind::from_str(row).map_err(|err| err.into())) + .map(|row: &str| { + Keybind::from_str(row) + .map_err(|err| ConfigError::KeyParseError(row.to_string(), err)) + }) .collect::, ConfigError>>() } } @@ -602,7 +609,7 @@ impl Inclusive<10> { impl Display for Inclusive { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) + write!(f, "{}", self.0.to_string()) } } diff --git a/src/hlwm/attribute.rs b/src/hlwm/attribute.rs index e2a2e11..dc83425 100644 --- a/src/hlwm/attribute.rs +++ b/src/hlwm/attribute.rs @@ -13,7 +13,7 @@ use super::{ StringParseError, }; -#[derive(Debug, Error)] +#[derive(Debug, Clone, Error)] pub enum AttributeError { #[error("error parsing integer value: {0:?}")] ParseIntError(#[from] ParseIntError), @@ -60,7 +60,7 @@ impl AttributeOption { "color" => Ok(Self::Color(None)), "windowid" => Ok(Self::WindowID(None)), "rectangle" => Ok(Self::Rectangle(None)), - "string" | "names" | "regex" => Ok(Self::String(None)), + "string" | "names" | "regex" | "font" => Ok(Self::String(None)), _ => Err(AttributeError::UnknownType(type_string.to_string())), } } @@ -179,7 +179,9 @@ impl Attribute { }, "color" => Ok(Attribute::Color(Color::from_str(value_string)?)), "int" => Ok(Attribute::Int(value_string.parse()?)), - "string" | "names" | "regex" => Ok(Attribute::String(value_string.to_string())), + "string" | "names" | "regex" | "font" => { + Ok(Attribute::String(value_string.to_string())) + } "uint" => Ok(Attribute::Uint(value_string.parse()?)), "rectangle" => { let parts = value_string.split('x').collect::>(); diff --git a/src/hlwm/command.rs b/src/hlwm/command.rs index 5b40b0d..0a8faf8 100644 --- a/src/hlwm/command.rs +++ b/src/hlwm/command.rs @@ -95,7 +95,14 @@ pub enum HlwmCommand { Get(SettingName), /// Emits a custom `hook` to all idling herbstclients EmitHook(Hook), - Substitute, + /// Replaces all exact occurrences of `identifier` in `command` and its `args` by the value of the `attribute`. + /// Note that the `command` also is replaced by the attribute value if it equals `identifier`. + /// The replaced command with its arguments then is executed + Substitute { + identifier: String, + attribute_path: String, + command: Box, + }, Keybind(Keybind), Keyunbind(KeyUnbind), Mousebind(Mousebind), @@ -178,6 +185,20 @@ pub enum HlwmCommand { monitor: Monitor, pad: Pad, }, + /// For each child of the given `object` the `command` is called with its `args`, where the `identifier` is replaced by the path of the child + #[strum(serialize = "foreach")] + ForEach { + /// do not print duplicates (some objects can be reached via multiple paths, such as `clients.focus`) + unique: bool, + /// print `object` and all its children of arbitrary depth in breadth-first search order. This implicitly activates `unique` + recursive: bool, + /// consider children whose name match the specified REGEX + filter_name: Option, + identifier: String, + object: String, + }, + #[strum(serialize = "sprintf")] + Sprintf(Vec), } impl FromStr for Box { @@ -310,7 +331,6 @@ impl HlwmCommand { | HlwmCommand::ListRules | HlwmCommand::False | HlwmCommand::True - | HlwmCommand::Substitute | HlwmCommand::Mouseunbind | HlwmCommand::ListMonitors | HlwmCommand::ListKeybinds => Ok::<_, CommandParseError>(command.clone()), @@ -504,6 +524,55 @@ impl HlwmCommand { HlwmCommand::Pad { monitor: _, pad: _ } => { parse!(monitor: FromStr, pad: FromStrAll => Pad) } + HlwmCommand::Substitute { + identifier: _, + attribute_path: _, + command: _, + } => { + parse!(identifier: String, attribute_path: String, command: FromStrAll => Substitute) + } + HlwmCommand::ForEach { + unique: _, + recursive: _, + filter_name: _, + identifier: _, + object: _, + } => { + 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, + other => { + if let Some(name) = other.strip_prefix("--filter-name=") { + filter_name = Some(name.to_string()); + } + } + } + } + + Ok(HlwmCommand::ForEach { + unique, + recursive, + filter_name, + identifier, + object, + }) + } + HlwmCommand::Sprintf(_) => parse!([Vec] => Sprintf), }?; assert_eq!(command.to_string(), parsed_command.to_string()); @@ -528,10 +597,10 @@ impl HlwmCommand { } } -#[derive(Debug, Error)] +#[derive(Debug, Clone, Error)] pub enum CommandError { #[error("IO error")] - IoError(#[from] io::Error), + IoError(String), #[error("exited with status code {0}")] StatusCode(i32, Option), #[error("killed by signal ({signal}); core dumped: {core_dumped}")] @@ -550,6 +619,12 @@ pub enum CommandError { Empty, } +impl From for CommandError { + fn from(value: io::Error) -> Self { + Self::IoError(value.to_string()) + } +} + impl ToCommandString for HlwmCommand { fn to_command_string(&self) -> String { let cmd_string = match self { @@ -561,7 +636,6 @@ impl ToCommandString for HlwmCommand { | HlwmCommand::Reload | HlwmCommand::Version | HlwmCommand::ListRules - | HlwmCommand::Substitute | HlwmCommand::Mouseunbind | HlwmCommand::True | HlwmCommand::False @@ -724,6 +798,41 @@ impl ToCommandString for HlwmCommand { parts.join("\t") } HlwmCommand::Pad { monitor, pad } => format!("{self}\t{monitor}\t{pad}"), + HlwmCommand::Substitute { + identifier, + attribute_path, + command, + } => format!( + "{self}\t{identifier}\t{attribute_path}\t{}", + command.to_command_string() + ), + HlwmCommand::ForEach { + unique, + recursive, + filter_name, + identifier, + object, + } => { + let mut parts = Vec::with_capacity(6); + parts.push(self.to_string()); + parts.push(identifier.to_string()); + parts.push(object.to_string()); + if let Some(filter_name) = filter_name { + parts.push(format!("--filter-name={filter_name}")); + } + if *unique { + parts.push("--unique".into()); + } + if *recursive { + parts.push("--recursive".into()); + } + parts.join("\t") + } + HlwmCommand::Sprintf(args) => [self.to_string()] + .into_iter() + .chain(args.into_iter().cloned()) + .collect::>() + .join("\t"), }; if let Some(s) = cmd_string.strip_suffix('\t') { return s.to_string(); @@ -767,7 +876,6 @@ mod test { | HlwmCommand::Unlock | HlwmCommand::True | HlwmCommand::False - | HlwmCommand::Substitute | HlwmCommand::Mouseunbind => (cmd.clone(), cmd.to_string()), HlwmCommand::Echo(_) => ( HlwmCommand::Echo(vec!["Hello world!".into()]), @@ -1004,6 +1112,43 @@ mod test { }, "pad\t1\t2\t3\t4\t5".into(), ), + HlwmCommand::Substitute { + identifier: _, + attribute_path: _, + command: _, + } => ( + HlwmCommand::Substitute { + identifier: "MYTITLE".into(), + attribute_path: "clients.focus.title".into(), + command: Box::new(HlwmCommand::Echo(vec!["MYTITLE".to_string()])), + }, + "substitute\tMYTITLE\tclients.focus.title\techo\tMYTITLE".into(), + ), + HlwmCommand::ForEach { + unique: _, + recursive: _, + filter_name: _, + identifier: _, + object: _, + } => ( + HlwmCommand::ForEach { + unique: true, + recursive: true, + filter_name: Some(".+".into()), + identifier: "CLIENT".into(), + object: "clients.".into(), + }, + "foreach\tCLIENT\tclients.\t--filter-name=.+\t--unique\t--recursive".into(), + ), + HlwmCommand::Sprintf(_) => ( + HlwmCommand::Sprintf( + ["X", "tag=%s", "tags.focus.name", "rule", "once", "X"] + .into_iter() + .map(|s| s.to_string()) + .collect(), + ), + "sprintf\tX\ttag=%s\ttags.focus.name\trule\tonce\tX".into(), + ), }) .collect::>(); for (command, expected_string) in commands { diff --git a/src/hlwm/key.rs b/src/hlwm/key.rs index 0fd79d0..057d45c 100644 --- a/src/hlwm/key.rs +++ b/src/hlwm/key.rs @@ -250,7 +250,7 @@ impl ToCommandString for MousebindAction { } } -#[derive(Debug, Error)] +#[derive(Debug, Clone, Error)] pub enum KeyParseError { #[error("value too short (expected >= 2 parts, got {0} parts)")] TooShort(usize), diff --git a/src/hlwm/mod.rs b/src/hlwm/mod.rs index b6337f3..077b35e 100644 --- a/src/hlwm/mod.rs +++ b/src/hlwm/mod.rs @@ -101,10 +101,14 @@ impl Client { } pub fn get_setting(&self, setting: SettingName) -> Result { - Ok(Setting::from_str(&String::from_utf8( - self.execute(HlwmCommand::Get(setting))?.stdout, - )?) - .map_err(|_| StringParseError::UnknownValue)?) + 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 + })?) } pub fn query(&self, command: HlwmCommand) -> Result, CommandError> { @@ -136,7 +140,7 @@ pub trait ToCommandString { fn to_command_string(&self) -> String; } -#[derive(Debug, Error)] +#[derive(Debug, Clone, Error)] pub enum StringParseError { #[error("unknown value")] UnknownValue, diff --git a/src/hlwm/setting.rs b/src/hlwm/setting.rs index bab5d7f..e86503d 100644 --- a/src/hlwm/setting.rs +++ b/src/hlwm/setting.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use log::debug; use serde::{Deserialize, Serialize}; use crate::{gen_parse, hlwm::command::CommandParseError}; @@ -246,6 +247,7 @@ impl FromStr for Setting { type Err = CommandParseError; 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()))?; diff --git a/src/logerr.rs b/src/logerr.rs new file mode 100644 index 0000000..2d785b1 --- /dev/null +++ b/src/logerr.rs @@ -0,0 +1,31 @@ +use std::{error::Error, fmt::Debug}; + +use log::{debug, error}; + +pub trait UnwrapLog { + type Target; + + fn unwrap_or_log(self, default: Self::Target) -> Self::Target; +} + +impl UnwrapLog for Result +where + T: Debug, + E: Error, +{ + type Target = T; + + fn unwrap_or_log(self, default: Self::Target) -> Self::Target { + match self { + Ok(val) => val, + Err(err) => { + error!( + "[{}] unwrap_or_log got error: {err}", + std::any::type_name::() + ); + debug!("^ defaulting to {default:#?}"); + default + } + } + } +} diff --git a/src/main.rs b/src/main.rs index ad3a6a8..83f9053 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use log::{error, info}; pub mod cmd; mod config; mod hlwm; +pub mod logerr; mod panel; #[derive(Parser, Debug, Clone)]