add substitute, foreach, sprintf. improve errors a bit
This commit is contained in:
parent
4036ed42f0
commit
22925fc738
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<_>>();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()))?;
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ use log::{error, info};
|
|||
pub mod cmd;
|
||||
mod config;
|
||||
mod hlwm;
|
||||
pub mod logerr;
|
||||
mod panel;
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
|
|
Loading…
Reference in New Issue