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,
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<Vec<Tag>, _> {
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<Vec<Rule>, ConfigError> {
Ok(
String::from_utf8(client.execute(HlwmCommand::ListRules)?.stdout)?
@ -462,7 +466,7 @@ impl Config {
.collect::<Result<_, _>>()?,
)
})()
.unwrap_or(default.rules),
.unwrap_or_log(default.rules),
settings: (|| -> Result<Vec<_>, CommandError> {
default
.settings
@ -471,7 +475,7 @@ impl Config {
.map(|s| Ok(client.get_setting(s.into())?))
.collect::<Result<Vec<_>, 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::<Result<Vec<_>, ConfigError>>()
}
}
@ -602,7 +609,7 @@ impl Inclusive<10> {
impl<const MAX: u8> Display for Inclusive<MAX> {
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,
};
#[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::<Vec<_>>();

View File

@ -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<HlwmCommand>,
},
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<String>,
identifier: String,
object: String,
},
#[strum(serialize = "sprintf")]
Sprintf(Vec<String>),
}
impl FromStr for Box<HlwmCommand> {
@ -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<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());
@ -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<String>),
#[error("killed by signal ({signal}); core dumped: {core_dumped}")]
@ -550,6 +619,12 @@ pub enum CommandError {
Empty,
}
impl From<io::Error> 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::<Vec<_>>()
.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::<Vec<_>>();
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 {
#[error("value too short (expected >= 2 parts, got {0} parts)")]
TooShort(usize),

View File

@ -101,10 +101,14 @@ impl Client {
}
pub fn get_setting(&self, setting: SettingName) -> Result<Setting, CommandError> {
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<Vec<String>, 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,

View File

@ -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<Self, Self::Err> {
debug!("beginning to parse [{s}] as setting");
let ((command, arg), _) = split::on_first_match(s, &['\t', ' '])
.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;
mod config;
mod hlwm;
pub mod logerr;
mod panel;
#[derive(Parser, Debug, Clone)]