add substitute, foreach, sprintf. improve errors a bit

This commit is contained in:
emilis 2024-03-01 19:36:29 +00:00
parent 4036ed42f0
commit 22925fc738
8 changed files with 222 additions and 30 deletions

View File

@ -3,7 +3,7 @@ use std::{
cmp::Ordering, cmp::Ordering,
convert::Infallible, convert::Infallible,
env, env,
fmt::Display, fmt::{Debug, Display},
fs::{self}, fs::{self},
io, io,
ops::Deref, ops::Deref,
@ -33,6 +33,7 @@ use crate::{
window::Window, window::Window,
Align, Client, Direction, Index, Operator, Separator, StringParseError, ToggleBool, Align, Client, Direction, Index, Operator, Separator, StringParseError, ToggleBool,
}, },
logerr::UnwrapLog,
rule, window_types, rule, window_types,
}; };
@ -52,8 +53,8 @@ pub enum ConfigError {
CommandError(#[from] CommandError), CommandError(#[from] CommandError),
#[error("non-utf8 string error: {0}")] #[error("non-utf8 string error: {0}")]
Utf8StringError(#[from] FromUtf8Error), Utf8StringError(#[from] FromUtf8Error),
#[error("failed parsing keybind: {0}")] #[error("failed parsing keybind [{0}]: [{1}]")]
KeyParseError(#[from] KeyParseError), KeyParseError(String, KeyParseError),
#[error("failed parsing value from string: {0}")] #[error("failed parsing value from string: {0}")]
StringParseError(#[from] StringParseError), StringParseError(#[from] StringParseError),
} }
@ -387,9 +388,12 @@ impl Config {
name: &str, name: &str,
f: F, f: F,
default: T, default: T,
) -> T { ) -> T
where
T: Debug,
{
match Client::new().get_attr(name.to_string()) { 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, Err(_) => default,
} }
} }
@ -401,11 +405,11 @@ impl Config {
|f| -> Result<_, Infallible> { Ok(f.to_string()) }, |f| -> Result<_, Infallible> { Ok(f.to_string()) },
default.font, default.font,
), ),
mod_key: setting(Self::MOD_KEY, Key::from_str, default.mod_key),
font_bold: client font_bold: client
.get_attr(ThemeAttr::TitleFont(String::new()).attr_path()) .get_attr(ThemeAttr::TitleFont(String::new()).attr_path())
.map(|a| a.to_string()) .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( services: setting(
Self::SERVICES, Self::SERVICES,
|v| serde_json::de::from_str(v), |v| serde_json::de::from_str(v),
@ -425,14 +429,14 @@ impl Config {
&client &client
.get_attr(attr_path.clone()) .get_attr(attr_path.clone())
.map(|t| t.to_string()) .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() .collect()
})(), })(),
}, },
keybinds: Self::active_keybinds(true).unwrap_or(default.keybinds), keybinds: Self::active_keybinds(true).unwrap_or_log(default.keybinds),
tags: (|| -> Result<Vec<Tag>, _> { tags: (|| -> Result<Vec<Tag>, _> {
Result::<_, ConfigError>::Ok({ Result::<_, ConfigError>::Ok({
let mut tags = client let mut tags = client
@ -451,7 +455,7 @@ impl Config {
tags tags
}) })
})() })()
.unwrap_or(default.tags), .unwrap_or_log(default.tags),
rules: (|| -> Result<Vec<Rule>, ConfigError> { rules: (|| -> Result<Vec<Rule>, ConfigError> {
Ok( Ok(
String::from_utf8(client.execute(HlwmCommand::ListRules)?.stdout)? String::from_utf8(client.execute(HlwmCommand::ListRules)?.stdout)?
@ -462,7 +466,7 @@ impl Config {
.collect::<Result<_, _>>()?, .collect::<Result<_, _>>()?,
) )
})() })()
.unwrap_or(default.rules), .unwrap_or_log(default.rules),
settings: (|| -> Result<Vec<_>, CommandError> { settings: (|| -> Result<Vec<_>, CommandError> {
default default
.settings .settings
@ -471,7 +475,7 @@ impl Config {
.map(|s| Ok(client.get_setting(s.into())?)) .map(|s| Ok(client.get_setting(s.into())?))
.collect::<Result<Vec<_>, CommandError>>() .collect::<Result<Vec<_>, CommandError>>()
})() })()
.unwrap_or(default.settings), .unwrap_or_log(default.settings),
..default ..default
} }
} }
@ -501,7 +505,10 @@ impl Config {
None => false, 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::<Result<Vec<_>, ConfigError>>() .collect::<Result<Vec<_>, ConfigError>>()
} }
} }
@ -602,7 +609,7 @@ impl Inclusive<10> {
impl<const MAX: u8> Display for Inclusive<MAX> { impl<const MAX: u8> Display for Inclusive<MAX> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f) write!(f, "{}", self.0.to_string())
} }
} }

View File

@ -13,7 +13,7 @@ use super::{
StringParseError, StringParseError,
}; };
#[derive(Debug, Error)] #[derive(Debug, Clone, Error)]
pub enum AttributeError { pub enum AttributeError {
#[error("error parsing integer value: {0:?}")] #[error("error parsing integer value: {0:?}")]
ParseIntError(#[from] ParseIntError), ParseIntError(#[from] ParseIntError),
@ -60,7 +60,7 @@ impl AttributeOption {
"color" => Ok(Self::Color(None)), "color" => Ok(Self::Color(None)),
"windowid" => Ok(Self::WindowID(None)), "windowid" => Ok(Self::WindowID(None)),
"rectangle" => Ok(Self::Rectangle(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())), _ => Err(AttributeError::UnknownType(type_string.to_string())),
} }
} }
@ -179,7 +179,9 @@ impl Attribute {
}, },
"color" => Ok(Attribute::Color(Color::from_str(value_string)?)), "color" => Ok(Attribute::Color(Color::from_str(value_string)?)),
"int" => Ok(Attribute::Int(value_string.parse()?)), "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()?)), "uint" => Ok(Attribute::Uint(value_string.parse()?)),
"rectangle" => { "rectangle" => {
let parts = value_string.split('x').collect::<Vec<_>>(); let parts = value_string.split('x').collect::<Vec<_>>();

View File

@ -95,7 +95,14 @@ pub enum HlwmCommand {
Get(SettingName), Get(SettingName),
/// Emits a custom `hook` to all idling herbstclients /// Emits a custom `hook` to all idling herbstclients
EmitHook(Hook), 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<HlwmCommand>,
},
Keybind(Keybind), Keybind(Keybind),
Keyunbind(KeyUnbind), Keyunbind(KeyUnbind),
Mousebind(Mousebind), Mousebind(Mousebind),
@ -178,6 +185,20 @@ pub enum HlwmCommand {
monitor: Monitor, monitor: Monitor,
pad: Pad, 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<String>,
identifier: String,
object: String,
},
#[strum(serialize = "sprintf")]
Sprintf(Vec<String>),
} }
impl FromStr for Box<HlwmCommand> { impl FromStr for Box<HlwmCommand> {
@ -310,7 +331,6 @@ impl HlwmCommand {
| HlwmCommand::ListRules | HlwmCommand::ListRules
| HlwmCommand::False | HlwmCommand::False
| HlwmCommand::True | HlwmCommand::True
| HlwmCommand::Substitute
| HlwmCommand::Mouseunbind | HlwmCommand::Mouseunbind
| HlwmCommand::ListMonitors | HlwmCommand::ListMonitors
| HlwmCommand::ListKeybinds => Ok::<_, CommandParseError>(command.clone()), | HlwmCommand::ListKeybinds => Ok::<_, CommandParseError>(command.clone()),
@ -504,6 +524,55 @@ impl HlwmCommand {
HlwmCommand::Pad { monitor: _, pad: _ } => { HlwmCommand::Pad { monitor: _, pad: _ } => {
parse!(monitor: FromStr, pad: FromStrAll => 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<String>, Vec<String>) =
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::<Vec<_>>().join("\t"));
let mut unique = false;
let mut recursive = false;
let mut filter_name: Option<String> = 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<String>] => Sprintf),
}?; }?;
assert_eq!(command.to_string(), parsed_command.to_string()); 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 { pub enum CommandError {
#[error("IO error")] #[error("IO error")]
IoError(#[from] io::Error), IoError(String),
#[error("exited with status code {0}")] #[error("exited with status code {0}")]
StatusCode(i32, Option<String>), StatusCode(i32, Option<String>),
#[error("killed by signal ({signal}); core dumped: {core_dumped}")] #[error("killed by signal ({signal}); core dumped: {core_dumped}")]
@ -550,6 +619,12 @@ pub enum CommandError {
Empty, Empty,
} }
impl From<io::Error> for CommandError {
fn from(value: io::Error) -> Self {
Self::IoError(value.to_string())
}
}
impl ToCommandString for HlwmCommand { impl ToCommandString for HlwmCommand {
fn to_command_string(&self) -> String { fn to_command_string(&self) -> String {
let cmd_string = match self { let cmd_string = match self {
@ -561,7 +636,6 @@ impl ToCommandString for HlwmCommand {
| HlwmCommand::Reload | HlwmCommand::Reload
| HlwmCommand::Version | HlwmCommand::Version
| HlwmCommand::ListRules | HlwmCommand::ListRules
| HlwmCommand::Substitute
| HlwmCommand::Mouseunbind | HlwmCommand::Mouseunbind
| HlwmCommand::True | HlwmCommand::True
| HlwmCommand::False | HlwmCommand::False
@ -724,6 +798,41 @@ impl ToCommandString for HlwmCommand {
parts.join("\t") parts.join("\t")
} }
HlwmCommand::Pad { monitor, pad } => format!("{self}\t{monitor}\t{pad}"), 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::<Vec<_>>()
.join("\t"),
}; };
if let Some(s) = cmd_string.strip_suffix('\t') { if let Some(s) = cmd_string.strip_suffix('\t') {
return s.to_string(); return s.to_string();
@ -767,7 +876,6 @@ mod test {
| HlwmCommand::Unlock | HlwmCommand::Unlock
| HlwmCommand::True | HlwmCommand::True
| HlwmCommand::False | HlwmCommand::False
| HlwmCommand::Substitute
| HlwmCommand::Mouseunbind => (cmd.clone(), cmd.to_string()), | HlwmCommand::Mouseunbind => (cmd.clone(), cmd.to_string()),
HlwmCommand::Echo(_) => ( HlwmCommand::Echo(_) => (
HlwmCommand::Echo(vec!["Hello world!".into()]), HlwmCommand::Echo(vec!["Hello world!".into()]),
@ -1004,6 +1112,43 @@ mod test {
}, },
"pad\t1\t2\t3\t4\t5".into(), "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::<Vec<_>>(); .collect::<Vec<_>>();
for (command, expected_string) in commands { for (command, expected_string) in commands {

View File

@ -250,7 +250,7 @@ impl ToCommandString for MousebindAction {
} }
} }
#[derive(Debug, Error)] #[derive(Debug, Clone, Error)]
pub enum KeyParseError { pub enum KeyParseError {
#[error("value too short (expected >= 2 parts, got {0} parts)")] #[error("value too short (expected >= 2 parts, got {0} parts)")]
TooShort(usize), TooShort(usize),

View File

@ -101,10 +101,14 @@ impl Client {
} }
pub fn get_setting(&self, setting: SettingName) -> Result<Setting, CommandError> { pub fn get_setting(&self, setting: SettingName) -> Result<Setting, CommandError> {
Ok(Setting::from_str(&String::from_utf8( Ok(Setting::from_str(&format!(
self.execute(HlwmCommand::Get(setting))?.stdout, "{setting}\t{}",
)?) String::from_utf8(self.execute(HlwmCommand::Get(setting))?.stdout,)?
.map_err(|_| StringParseError::UnknownValue)?) ))
.map_err(|err| {
error!("failed getting setting [{setting}]: {err}");
StringParseError::UnknownValue
})?)
} }
pub fn query(&self, command: HlwmCommand) -> Result<Vec<String>, CommandError> { pub fn query(&self, command: HlwmCommand) -> Result<Vec<String>, CommandError> {
@ -136,7 +140,7 @@ pub trait ToCommandString {
fn to_command_string(&self) -> String; fn to_command_string(&self) -> String;
} }
#[derive(Debug, Error)] #[derive(Debug, Clone, Error)]
pub enum StringParseError { pub enum StringParseError {
#[error("unknown value")] #[error("unknown value")]
UnknownValue, UnknownValue,

View File

@ -1,5 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{gen_parse, hlwm::command::CommandParseError}; use crate::{gen_parse, hlwm::command::CommandParseError};
@ -246,6 +247,7 @@ impl FromStr for Setting {
type Err = CommandParseError; type Err = CommandParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
debug!("beginning to parse [{s}] as setting");
let ((command, arg), _) = split::on_first_match(s, &['\t', ' ']) let ((command, arg), _) = split::on_first_match(s, &['\t', ' '])
.ok_or(CommandParseError::InvalidArgumentCount(0, "setting".into()))?; .ok_or(CommandParseError::InvalidArgumentCount(0, "setting".into()))?;

31
src/logerr.rs Normal file
View File

@ -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<T, E> UnwrapLog for Result<T, E>
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::<Self::Target>()
);
debug!("^ defaulting to {default:#?}");
default
}
}
}
}

View File

@ -8,6 +8,7 @@ use log::{error, info};
pub mod cmd; pub mod cmd;
mod config; mod config;
mod hlwm; mod hlwm;
pub mod logerr;
mod panel; mod panel;
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]