good progress - state stuff is still broken, and I'm confused

This commit is contained in:
elkowar 2021-07-21 21:47:02 +02:00
parent 3efcafed84
commit c752cc928e
No known key found for this signature in database
GPG key ID: E321AD71B1D1F27F
23 changed files with 262 additions and 1366 deletions

View file

@ -1,9 +1,7 @@
use crate::{
config,
config::{window_definition::WindowName, AnchorPoint},
config::{self, EwwConfig},
display_backend, eww_state,
script_var_handler::*,
dynval::{Coords, NumWithUnit, DynVal, VarName},
EwwPaths,
};
use anyhow::*;
@ -11,8 +9,13 @@ use debug_stub_derive::*;
use gdk::WindowExt;
use gtk::{ContainerExt, CssProviderExt, GtkWindowExt, StyleContextExt, WidgetExt};
use itertools::Itertools;
use simplexpr::dynval::DynVal;
use std::collections::HashMap;
use tokio::sync::mpsc::UnboundedSender;
use yuck::{
config::window_geometry::{AnchorPoint, WindowGeometry},
value::{Coords, VarName},
};
/// Response that the app may send as a response to a event.
/// This is used in `DaemonCommand`s that contain a response sender.
@ -43,11 +46,11 @@ pub enum DaemonCommand {
UpdateConfig(config::EwwConfig),
UpdateCss(String),
OpenMany {
windows: Vec<WindowName>,
windows: Vec<String>,
sender: DaemonResponseSender,
},
OpenWindow {
window_name: WindowName,
window_name: String,
pos: Option<Coords>,
size: Option<Coords>,
anchor: Option<AnchorPoint>,
@ -55,7 +58,7 @@ pub enum DaemonCommand {
sender: DaemonResponseSender,
},
CloseWindow {
window_name: WindowName,
window_name: String,
sender: DaemonResponseSender,
},
KillServer,
@ -70,7 +73,7 @@ pub enum DaemonCommand {
#[derive(Debug, Clone)]
pub struct EwwWindow {
pub name: WindowName,
pub name: String,
pub definition: config::EwwWindowDefinition,
pub gtk_window: gtk::Window,
}
@ -85,7 +88,7 @@ impl EwwWindow {
pub struct App {
pub eww_state: eww_state::EwwState,
pub eww_config: config::EwwConfig,
pub open_windows: HashMap<WindowName, EwwWindow>,
pub open_windows: HashMap<String, EwwWindow>,
pub css_provider: gtk::CssProvider,
#[debug_stub = "ScriptVarHandler(...)"]
@ -109,27 +112,30 @@ impl App {
}
}
DaemonCommand::ReloadConfigAndCss(sender) => {
let mut errors = Vec::new();
let config_result = config::RawEwwConfig::read_from_file(&self.paths.get_eww_xml_path())
.and_then(config::EwwConfig::generate);
match config_result {
Ok(new_config) => self.handle_command(DaemonCommand::UpdateConfig(new_config)),
Err(e) => errors.push(e),
}
// TODO implement this
//let mut errors = Vec::new();
todo!()
let css_result = crate::util::parse_scss_from_file(&self.paths.get_eww_scss_path());
match css_result {
Ok(new_css) => self.handle_command(DaemonCommand::UpdateCss(new_css)),
Err(e) => errors.push(e),
}
//let config_result =
//EwwConfig::read_from_file(&self.paths.get_eww_xml_path()).and_then(config::EwwConfig::generate);
//match config_result {
//Ok(new_config) => self.handle_command(DaemonCommand::UpdateConfig(new_config)),
//Err(e) => errors.push(e),
//}
let errors = errors.into_iter().map(|e| format!("{:?}", e)).join("\n");
if errors.is_empty() {
sender.send(DaemonResponse::Success(String::new()))?;
} else {
sender.send(DaemonResponse::Failure(errors))?;
}
//let css_result = crate::util::parse_scss_from_file(&self.paths.get_eww_scss_path());
//match css_result {
//Ok(new_css) => self.handle_command(DaemonCommand::UpdateCss(new_css)),
//Err(e) => errors.push(e),
//}
//let errors = errors.into_iter().map(|e| format!("{:?}", e)).join("\n");
//if errors.is_empty() {
//sender.send(DaemonResponse::Success(String::new()))?;
//} else {
//sender.send(DaemonResponse::Failure(errors))?;
//}
}
DaemonCommand::UpdateConfig(config) => {
self.load_config(config)?;
@ -203,7 +209,7 @@ impl App {
self.eww_state.update_variable(fieldname, value)
}
fn close_window(&mut self, window_name: &WindowName) -> Result<()> {
fn close_window(&mut self, window_name: &String) -> Result<()> {
for unused_var in self.variables_only_used_in(window_name) {
log::info!("stopping for {}", &unused_var);
self.script_var_handler.stop_for_variable(unused_var.clone());
@ -220,11 +226,11 @@ impl App {
fn open_window(
&mut self,
window_name: &WindowName,
window_name: &String,
pos: Option<Coords>,
size: Option<Coords>,
monitor: Option<i32>,
anchor: Option<config::AnchorPoint>,
anchor: Option<AnchorPoint>,
) -> Result<()> {
// remove and close existing window with the same name
let _ = self.close_window(window_name);
@ -238,7 +244,7 @@ impl App {
root_widget.get_style_context().add_class(&window_name.to_string());
let monitor_geometry =
get_monitor_geometry(monitor.or(window_def.screen_number).unwrap_or_else(get_default_monitor_index));
get_monitor_geometry(monitor.or(window_def.monitor_number).unwrap_or_else(get_default_monitor_index));
let eww_window = initialize_window(monitor_geometry, root_widget, window_def)?;
self.open_windows.insert(window_name.clone(), eww_window);
@ -281,7 +287,7 @@ impl App {
}
/// Get all variables mapped to a list of windows they are being used in.
pub fn currently_used_variables<'a>(&'a self) -> HashMap<&'a VarName, Vec<&'a WindowName>> {
pub fn currently_used_variables<'a>(&'a self) -> HashMap<&'a VarName, Vec<&'a String>> {
let mut vars: HashMap<&'a VarName, Vec<_>> = HashMap::new();
for window_name in self.open_windows.keys() {
for var in self.eww_state.vars_referenced_in(window_name) {
@ -292,7 +298,7 @@ impl App {
}
/// Get all variables that are only used in the given window.
pub fn variables_only_used_in<'a>(&'a self, window: &'a WindowName) -> impl Iterator<Item = &'a VarName> {
pub fn variables_only_used_in<'a>(&'a self, window: &'a String) -> impl Iterator<Item = &'a VarName> {
self.currently_used_variables()
.into_iter()
.filter(move |(_, wins)| wins.len() == 1 && wins.contains(&window))
@ -306,14 +312,14 @@ fn initialize_window(
window_def: config::EwwWindowDefinition,
) -> Result<EwwWindow> {
let window = display_backend::initialize_window(&window_def, monitor_geometry)
.with_context(|| format!("monitor {} is unavailable", window_def.screen_number.unwrap()))?;
.with_context(|| format!("monitor {} is unavailable", window_def.monitor_number.unwrap()))?;
window.set_title(&format!("Eww - {}", window_def.name));
window.set_position(gtk::WindowPosition::None);
window.set_gravity(gdk::Gravity::Center);
if let Some(geometry) = window_def.geometry {
let actual_window_rect = geometry.get_window_rectangle(monitor_geometry);
let actual_window_rect = get_window_rectangle(geometry, monitor_geometry);
window.set_size_request(actual_window_rect.width, actual_window_rect.height);
window.set_default_size(actual_window_rect.width, actual_window_rect.height);
}
@ -345,13 +351,13 @@ fn initialize_window(
/// Apply the provided window-positioning rules to the window.
fn apply_window_position(
mut window_geometry: config::EwwWindowGeometry,
mut window_geometry: WindowGeometry,
monitor_geometry: gdk::Rectangle,
window: &gtk::Window,
) -> Result<()> {
let gdk_window = window.get_window().context("Failed to get gdk window from gtk window")?;
window_geometry.size = Coords::from_pixels(window.get_size());
let actual_window_rect = window_geometry.get_window_rectangle(monitor_geometry);
let actual_window_rect = get_window_rectangle(window_geometry, monitor_geometry);
gdk_window.move_(actual_window_rect.x, actual_window_rect.y);
Ok(())
}
@ -382,3 +388,11 @@ fn respond_with_error<T>(sender: DaemonResponseSender, result: Result<T>) -> Res
}
.context("sending response from main thread")
}
pub fn get_window_rectangle(geometry: WindowGeometry, screen_rect: gdk::Rectangle) -> gdk::Rectangle {
let (offset_x, offset_y) = geometry.offset.relative_to(screen_rect.width, screen_rect.height);
let (width, height) = geometry.size.relative_to(screen_rect.width, screen_rect.height);
let x = screen_rect.x + offset_x + geometry.anchor_point.x.alignment_to_coordinate(width, screen_rect.width);
let y = screen_rect.y + offset_y + geometry.anchor_point.y.alignment_to_coordinate(height, screen_rect.height);
gdk::Rectangle { x, y, width, height }
}

View file

@ -1,69 +0,0 @@
use crate::config::xml_ext::XmlElement;
use anyhow::*;
pub use backend::*;
#[cfg(feature = "x11")]
mod backend {
use super::*;
use crate::config::{EwwWindowType, StrutDefinition};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendWindowOptions {
pub wm_ignore: bool,
pub sticky: bool,
pub window_type: EwwWindowType,
pub struts: StrutDefinition,
}
impl BackendWindowOptions {
pub fn from_xml_element(xml: &XmlElement) -> Result<Self> {
let struts: Option<StrutDefinition> = xml
.child("reserve")
.ok()
.map(StrutDefinition::from_xml_element)
.transpose()
.context("Failed to parse <reserve>")?;
let window_type = xml.parse_optional_attr("windowtype")?;
Ok(BackendWindowOptions {
wm_ignore: xml.parse_optional_attr("wm-ignore")?.unwrap_or(window_type.is_none() && struts.is_none()),
window_type: window_type.unwrap_or_default(),
sticky: xml.parse_optional_attr("sticky")?.unwrap_or(true),
struts: struts.unwrap_or_default(),
})
}
}
}
#[cfg(feature = "wayland")]
mod backend {
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendWindowOptions {
pub exclusive: bool,
pub focusable: bool,
}
impl BackendWindowOptions {
pub fn from_xml_element(xml: &XmlElement) -> Result<Self> {
Ok(BackendWindowOptions {
exclusive: xml.parse_optional_attr("exclusive")?.unwrap_or(false),
focusable: xml.parse_optional_attr("focusable")?.unwrap_or(false),
})
}
}
}
#[cfg(feature = "no-x11-wayland")]
mod backend {
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendWindowOptions;
impl BackendWindowOptions {
pub fn from_xml_element(xml: &XmlElement) -> Result<Self> {
Ok(BackendWindowOptions)
}
}
}

View file

@ -1,176 +0,0 @@
use super::*;
use lazy_static::lazy_static;
use regex::Regex;
use std::ops::Range;
use crate::{
dynval::{AttrName, AttrVal},
with_text_pos_context,
};
use maplit::hashmap;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct WidgetDefinition {
pub name: String,
pub structure: WidgetUse,
pub size: Option<(i32, i32)>,
}
impl WidgetDefinition {
pub fn from_xml_element(xml: &XmlElement) -> Result<Self> {
with_text_pos_context! { xml =>
if xml.tag_name() != "def" {
bail!(
"{} | Illegal element: only <def> may be used in definition block, but found '{}'",
xml.text_pos(),
xml.as_tag_string()
);
}
WidgetDefinition {
name: xml.attr("name")?,
size: Option::zip(xml.parse_optional_attr("width")?, xml.parse_optional_attr("height")?),
structure: WidgetUse::from_xml_node(xml.only_child()?)?,
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct WidgetUse {
pub name: String,
pub children: Vec<WidgetUse>,
pub attrs: HashMap<AttrName, AttrVal>,
pub text_pos: Option<TextPos>,
}
#[derive(Debug, Clone)]
pub struct PositionData {
pub range: Range<usize>,
}
impl PartialEq for WidgetUse {
fn eq(&self, other: &WidgetUse) -> bool {
self.name == other.name && self.children == other.children && self.attrs == other.attrs
}
}
impl WidgetUse {
pub fn new(name: String, children: Vec<WidgetUse>) -> Self {
WidgetUse { name, children, attrs: HashMap::new(), ..WidgetUse::default() }
}
pub fn from_xml_node(xml: XmlNode) -> Result<Self> {
lazy_static! {
static ref PATTERN: Regex = Regex::new("\\{\\{(.*)\\}\\}").unwrap();
};
let text_pos = xml.text_pos();
let widget_use = match xml {
XmlNode::Text(text) => WidgetUse::simple_text(AttrVal::parse_string(&text.text())),
XmlNode::Element(elem) => WidgetUse {
name: elem.tag_name().to_owned(),
children: with_text_pos_context! { elem => elem.children().map(WidgetUse::from_xml_node).collect::<Result<_>>()?}?,
attrs: elem
.attributes()
.iter()
.map(|attr| {
(
AttrName(attr.name().to_owned()),
AttrVal::parse_string(&xml_ext::resolve_escaped_symbols(attr.value())),
)
})
.collect::<HashMap<_, _>>(),
..WidgetUse::default()
},
XmlNode::Ignored(_) => bail!("{} | Failed to parse node {:?} as widget use", xml.text_pos(), xml),
};
Ok(widget_use.at_pos(text_pos))
}
pub fn simple_text(text: AttrVal) -> Self {
WidgetUse {
name: "label".to_owned(),
children: vec![],
attrs: hashmap! { AttrName("text".to_owned()) => text }, // TODO this hardcoded "text" is dumdum
..WidgetUse::default()
}
}
pub fn at_pos(mut self, text_pos: TextPos) -> Self {
self.text_pos = Some(text_pos);
self
}
}
#[cfg(test)]
mod test {
use super::*;
use maplit::hashmap;
use pretty_assertions::assert_eq;
#[test]
fn test_simple_text() {
let expected_attr_value = AttrVal::from_primitive("my text");
let widget = WidgetUse::simple_text(expected_attr_value.clone());
assert_eq!(
widget,
WidgetUse {
name: "label".to_owned(),
children: Vec::new(),
attrs: hashmap! { AttrName("text".to_owned()) => expected_attr_value},
..WidgetUse::default()
},
);
}
#[test]
fn test_parse_widget_use() {
let input = r#"
<widget_name attr1="hi" attr2="12">
<child_widget/>
foo
</widget_name>
"#;
let document = roxmltree::Document::parse(input).unwrap();
let xml = XmlNode::from(document.root_element().clone());
let expected = WidgetUse {
name: "widget_name".to_owned(),
attrs: hashmap! {
AttrName("attr1".to_owned()) => AttrVal::from_primitive("hi"),
AttrName("attr2".to_owned()) => AttrVal::from_primitive("12"),
},
children: vec![
WidgetUse::new("child_widget".to_owned(), Vec::new()),
WidgetUse::simple_text(AttrVal::from_primitive("foo".to_owned())),
],
..WidgetUse::default()
};
assert_eq!(expected, WidgetUse::from_xml_node(xml).unwrap());
}
#[test]
fn test_parse_widget_definition() {
let input = r#"
<def name="foo" width="12" height="20">
<layout>test</layout>
</def>
"#;
let document = roxmltree::Document::parse(input).unwrap();
let xml = XmlNode::from(document.root_element().clone());
let expected = WidgetDefinition {
name: "foo".to_owned(),
size: Some((12, 20)),
structure: WidgetUse {
name: "layout".to_owned(),
children: vec![WidgetUse::simple_text(AttrVal::from_primitive("test"))],
attrs: HashMap::new(),
..WidgetUse::default()
},
};
assert_eq!(expected, WidgetDefinition::from_xml_element(xml.as_element().unwrap()).unwrap());
}
}

View file

@ -1,9 +1,7 @@
use anyhow::*;
use std::collections::HashMap;
use yuck::{
config::{
script_var_definition::ScriptVarDefinition, widget_definition::WidgetDefinition, window_definition::WindowDefinition,
},
config::{script_var_definition::ScriptVarDefinition, widget_definition::WidgetDefinition},
parser::from_ast::FromAst,
value::VarName,
};
@ -12,11 +10,13 @@ use simplexpr::dynval::DynVal;
use std::path::PathBuf;
use super::{script_var, EwwWindowDefinition};
/// Eww configuration structure.
#[derive(Debug, Clone)]
pub struct EwwConfig {
widgets: HashMap<String, WidgetDefinition>,
windows: HashMap<String, WindowDefinition>,
windows: HashMap<String, EwwWindowDefinition>,
initial_variables: HashMap<VarName, DynVal>,
script_vars: HashMap<VarName, ScriptVarDefinition>,
pub filepath: PathBuf,
@ -37,7 +37,7 @@ impl EwwConfig {
.map(|(name, window)| {
Ok((
name,
WindowDefinition::generate(&config.widget_definitions, window)
EwwWindowDefinition::generate(&config.widget_definitions, window)
.context("Failed expand window definition")?,
))
})
@ -51,21 +51,24 @@ impl EwwConfig {
// TODO this is kinda ugly
pub fn generate_initial_state(&self) -> Result<HashMap<VarName, DynVal>> {
let mut vars =
self.script_vars.iter().map(|var| Ok((var.0.clone(), var.1.initial_value()?))).collect::<Result<HashMap<_, _>>>()?;
let mut vars = self
.script_vars
.iter()
.map(|(name, var)| Ok((name.clone(), script_var::initial_value(var)?)))
.collect::<Result<HashMap<_, _>>>()?;
vars.extend(self.initial_variables.clone());
Ok(vars)
}
pub fn get_windows(&self) -> &HashMap<WindowName, EwwWindowDefinition> {
pub fn get_windows(&self) -> &HashMap<String, EwwWindowDefinition> {
&self.windows
}
pub fn get_window(&self, name: &WindowName) -> Result<&EwwWindowDefinition> {
pub fn get_window(&self, name: &String) -> Result<&EwwWindowDefinition> {
self.windows.get(name).with_context(|| format!("No window named '{}' exists", name))
}
pub fn get_script_var(&self, name: &VarName) -> Result<&ScriptVar> {
pub fn get_script_var(&self, name: &VarName) -> Result<&ScriptVarDefinition> {
self.script_vars.get(name).with_context(|| format!("No script var named '{}' exists", name))
}

View file

@ -1,14 +1,18 @@
use crate::{
config::{system_stats::*, PollScriptVar, ScriptVar, VarSource},
dynval::{DynVal as PrimitiveValue, VarName},
};
use std::{collections::HashMap, time::Duration};
use simplexpr::dynval::DynVal;
use yuck::{
config::script_var_definition::{PollScriptVar, ScriptVarDefinition, VarSource},
value::VarName,
};
use crate::config::system_stats::*;
macro_rules! builtin_vars {
($interval:expr, $($name:literal => $fun:expr),*$(,)?) => {{
maplit::hashmap! {
$(
VarName::from($name) => ScriptVar::Poll(PollScriptVar {
VarName::from($name) => ScriptVarDefinition::Poll(PollScriptVar {
name: VarName::from($name),
command: VarSource::Function($fun),
interval: $interval,
@ -17,19 +21,19 @@ macro_rules! builtin_vars {
}
}}}
pub fn get_inbuilt_vars() -> HashMap<VarName, ScriptVar> {
pub fn get_inbuilt_vars() -> HashMap<VarName, ScriptVarDefinition> {
builtin_vars! {Duration::new(2, 0),
// @desc EWW_TEMPS - Heat of the components in Celcius\nExample: `{{(CPU_TEMPS.core_1 + CPU_TEMPS.core_2) / 2}}`
"EWW_TEMPS" => || Ok(Primitivedynval::from(cores())),
"EWW_TEMPS" => || Ok(DynVal::from(cores())),
// @desc EWW_RAM - The current RAM + Swap usage
"EWW_RAM" => || Ok(Primitivedynval::from(format!("{:.2}", ram()))),
"EWW_RAM" => || Ok(DynVal::from(format!("{:.2}", ram()))),
// @desc EWW_DISK - Information on on all mounted partitions (Might report inaccurately on some filesystems, like btrfs)\nExample: `{{EWW_DISK["/"]}}`
"EWW_DISK" => || Ok(Primitivedynval::from(disk())),
"EWW_DISK" => || Ok(DynVal::from(disk())),
// @desc EWW_BATTERY - Battery capacity in procent of the main battery
"EWW_BATTERY" => || Ok(Primitivedynval::from(
"EWW_BATTERY" => || Ok(DynVal::from(
match get_battery_capacity() {
Err(e) => {
log::error!("Couldn't get the battery capacity: {:?}", e);
@ -40,9 +44,9 @@ pub fn get_inbuilt_vars() -> HashMap<VarName, ScriptVar> {
)),
// @desc EWW_CPU_USAGE - Average CPU usage (all cores) since the last update (No MacOS support)
"EWW_CPU_USAGE" => || Ok(Primitivedynval::from(get_avg_cpu_usage())),
"EWW_CPU_USAGE" => || Ok(DynVal::from(get_avg_cpu_usage())),
// @desc EWW_NET - Bytes up/down on all interfaces
"EWW_NET" => || Ok(Primitivedynval::from(net())),
"EWW_NET" => || Ok(DynVal::from(net())),
}
}

View file

@ -1,33 +1,8 @@
use crate::{
util,
dynval::{DynVal, VarName},
};
use anyhow::*;
use element::*;
use xml_ext::*;
pub mod backend_window_options;
pub mod element;
pub mod eww_config;
pub mod inbuilt;
pub mod script_var;
pub mod system_stats;
pub mod window_definition;
pub mod window_geometry;
pub mod xml_ext;
pub use eww_config::*;
pub use script_var::*;
pub use window_definition::*;
pub use window_geometry::*;
#[macro_export]
macro_rules! ensure_xml_tag_is {
($element:ident, $name:literal) => {
ensure!(
$element.tag_name() == $name,
anyhow!("{} | Tag needed to be of type '{}', but was: {}", $element.text_pos(), $name, $element.as_tag_string())
)
};
}

View file

@ -1,80 +1,22 @@
use std::process::Command;
use anyhow::*;
use simplexpr::dynval::DynVal;
use yuck::config::script_var_definition::{ScriptVarDefinition, VarSource};
use crate::ensure_xml_tag_is;
use super::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VarSource {
Shell(String),
Function(fn() -> Result<DynVal>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PollScriptVar {
pub name: VarName,
pub command: VarSource,
pub interval: std::time::Duration,
}
impl PollScriptVar {
pub fn run_once(&self) -> Result<DynVal> {
match &self.command {
VarSource::Shell(x) => run_command(x),
VarSource::Function(x) => x(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TailScriptVar {
pub name: VarName,
pub command: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ScriptVar {
Poll(PollScriptVar),
Tail(TailScriptVar),
}
impl ScriptVar {
pub fn name(&self) -> &VarName {
match self {
ScriptVar::Poll(x) => &x.name,
ScriptVar::Tail(x) => &x.name,
}
}
pub fn initial_value(&self) -> Result<DynVal> {
match self {
ScriptVar::Poll(x) => match &x.command {
VarSource::Function(f) => f().with_context(|| format!("Failed to compute initial value for {}", &self.name())),
VarSource::Shell(f) => {
run_command(f).with_context(|| format!("Failed to compute initial value for {}", &self.name()))
pub fn initial_value(var: &ScriptVarDefinition) -> Result<DynVal> {
match var {
ScriptVarDefinition::Poll(x) => match &x.command {
VarSource::Function(f) => {
f().map_err(|err| anyhow!(err)).with_context(|| format!("Failed to compute initial value for {}", &var.name()))
}
VarSource::Shell(f) => run_command(f).with_context(|| format!("Failed to compute initial value for {}", &var.name())),
},
ScriptVar::Tail(_) => Ok(DynVal::from_string(String::new())),
}
}
pub fn from_xml_element(xml: XmlElement) -> Result<Self> {
ensure_xml_tag_is!(xml, "script-var");
let name = VarName(xml.attr("name")?);
let command = xml.only_child()?.as_text()?.text();
if let Ok(interval) = xml.attr("interval") {
let interval = util::parse_duration(&interval)?;
Ok(ScriptVar::Poll(PollScriptVar { name, command: crate::config::VarSource::Shell(command), interval }))
} else {
Ok(ScriptVar::Tail(TailScriptVar { name, command }))
}
ScriptVarDefinition::Tail(_) => Ok(DynVal::from_string(String::new())),
}
}
/// Run a command and get the output
fn run_command(cmd: &str) -> Result<DynVal> {
pub fn run_command(cmd: &str) -> Result<DynVal> {
log::debug!("Running command: {}", cmd);
let output = String::from_utf8(Command::new("/bin/sh").arg("-c").arg(cmd).output()?.stdout)?;
let output = output.trim_matches('\n');

View file

@ -1,41 +1,14 @@
use super::*;
use crate::{dynval::NumWithUnit, ensure_xml_tag_is, widgets::widget_node};
use derive_more::*;
use serde::{Deserialize, Serialize};
use smart_default::SmartDefault;
use std::{collections::HashMap, str::FromStr};
use std::collections::HashMap;
use anyhow::*;
use yuck::config::{
backend_window_options::StrutDefinition,
backend_window_options::BackendWindowOptions,
widget_definition::WidgetDefinition,
window_definition::{WindowDefinition, WindowStacking},
window_geometry::WindowGeometry,
};
#[derive(Debug, Clone, PartialEq)]
pub enum EwwWindowType {
Dock,
Dialog,
Toolbar,
Normal,
}
impl FromStr for EwwWindowType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"dock" => Ok(Self::Dock),
"toolbar" => Ok(Self::Toolbar),
"dialog" => Ok(Self::Dialog),
"normal" => Ok(Self::Normal),
x => Err(anyhow!("Unknown windowtype provided '{}'. Possible values are: dock, toolbar, dialog, normal", x)),
}
}
}
impl Default for EwwWindowType {
fn default() -> Self {
Self::Normal
}
}
use crate::widgets::widget_node;
/// Full window-definition containing the fully expanded widget tree.
/// **Use this** rather than `[RawEwwWindowDefinition]`.
@ -43,20 +16,12 @@ impl Default for EwwWindowType {
pub struct EwwWindowDefinition {
pub name: String,
pub geometry: WindowGeometry,
pub geometry: Option<WindowGeometry>,
pub stacking: WindowStacking,
pub monitor_number: Option<i32>,
pub widget: Box<dyn widget_node::WidgetNode>,
pub focusable: bool,
#[cfg(feature = "x11")]
pub window_type: EwwWindowType,
#[cfg(feature = "x11")]
pub struts: StrutDefinition,
#[cfg(feature = "wayland")]
pub exclusive: bool,
pub resizable: bool,
pub backend_options: BackendWindowOptions,
}
impl EwwWindowDefinition {
@ -65,15 +30,10 @@ impl EwwWindowDefinition {
name: window.name,
geometry: window.geometry,
stacking: window.stacking,
monitor_number: window.screen_number,
monitor_number: window.monitor_number,
resizable: window.resizable,
widget: widget_node::generate_generic_widget_node(defs, &HashMap::new(), window.widget)?,
focusable: window.focusable,
#[cfg(feature = "x11")]
window_type: window.window_type,
#[cfg(feature = "x11")]
struts: window.struts,
#[cfg(feature = "wayland")]
exclusive: window.exclusive,
backend_options: window.backend_options,
})
}
}

View file

@ -1,144 +0,0 @@
use crate::dynval::Coords;
use anyhow::*;
use serde::{Deserialize, Serialize};
use smart_default::SmartDefault;
use std::fmt;
use super::xml_ext::XmlElement;
#[derive(Debug, derive_more::Display, Clone, Copy, Eq, PartialEq, SmartDefault, Serialize, Deserialize)]
pub enum AnchorAlignment {
#[display(fmt = "start")]
#[default]
START,
#[display(fmt = "center")]
CENTER,
#[display(fmt = "end")]
END,
}
impl AnchorAlignment {
pub fn from_x_alignment(s: &str) -> Result<AnchorAlignment> {
match s {
"l" | "left" => Ok(AnchorAlignment::START),
"c" | "center" => Ok(AnchorAlignment::CENTER),
"r" | "right" => Ok(AnchorAlignment::END),
_ => bail!(r#"couldn't parse '{}' as x-alignment. Must be one of "left", "center", "right""#, s),
}
}
pub fn from_y_alignment(s: &str) -> Result<AnchorAlignment> {
match s {
"t" | "top" => Ok(AnchorAlignment::START),
"c" | "center" => Ok(AnchorAlignment::CENTER),
"b" | "bottom" => Ok(AnchorAlignment::END),
_ => bail!(r#"couldn't parse '{}' as y-alignment. Must be one of "top", "center", "bottom""#, s),
}
}
pub fn alignment_to_coordinate(&self, size_inner: i32, size_container: i32) -> i32 {
match self {
AnchorAlignment::START => 0,
AnchorAlignment::CENTER => (size_container / 2) - (size_inner / 2),
AnchorAlignment::END => size_container - size_inner,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct AnchorPoint {
pub x: AnchorAlignment,
pub y: AnchorAlignment,
}
impl std::fmt::Display for AnchorPoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use AnchorAlignment::*;
match (self.x, self.y) {
(CENTER, CENTER) => write!(f, "center"),
(x, y) => write!(
f,
"{} {}",
match x {
START => "left",
CENTER => "center",
END => "right",
},
match y {
START => "top",
CENTER => "center",
END => "bottom",
}
),
}
}
}
impl std::str::FromStr for AnchorPoint {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "center" {
Ok(AnchorPoint { x: AnchorAlignment::CENTER, y: AnchorAlignment::CENTER })
} else {
let (first, second) = s
.split_once(' ')
.context("Failed to parse anchor: Must either be \"center\" or be formatted like \"top left\"")?;
let x_y_result: Result<_> = try {
AnchorPoint { x: AnchorAlignment::from_x_alignment(first)?, y: AnchorAlignment::from_y_alignment(second)? }
};
x_y_result.or_else(|_| {
Ok(AnchorPoint { x: AnchorAlignment::from_x_alignment(second)?, y: AnchorAlignment::from_y_alignment(first)? })
})
}
}
}
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
pub struct EwwWindowGeometry {
pub anchor_point: AnchorPoint,
pub offset: Coords,
pub size: Coords,
}
impl EwwWindowGeometry {
pub fn from_xml_element(xml: XmlElement) -> Result<Self> {
Ok(EwwWindowGeometry {
anchor_point: xml.parse_optional_attr("anchor")?.unwrap_or_default(),
size: Coords {
x: xml.parse_optional_attr("width")?.unwrap_or_default(),
y: xml.parse_optional_attr("height")?.unwrap_or_default(),
},
offset: Coords {
x: xml.parse_optional_attr("x")?.unwrap_or_default(),
y: xml.parse_optional_attr("y")?.unwrap_or_default(),
},
})
}
pub fn override_if_given(&self, anchor_point: Option<AnchorPoint>, offset: Option<Coords>, size: Option<Coords>) -> Self {
EwwWindowGeometry {
anchor_point: anchor_point.unwrap_or(self.anchor_point),
offset: offset.unwrap_or(self.offset),
size: size.unwrap_or(self.size),
}
}
}
impl std::fmt::Display for EwwWindowGeometry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{} ({})", self.offset, self.size, self.anchor_point)
}
}
impl EwwWindowGeometry {
/// Calculate the window rectangle given the configured window geometry
pub fn get_window_rectangle(&self, screen_rect: gdk::Rectangle) -> gdk::Rectangle {
let (offset_x, offset_y) = self.offset.relative_to(screen_rect.width, screen_rect.height);
let (width, height) = self.size.relative_to(screen_rect.width, screen_rect.height);
let x = screen_rect.x + offset_x + self.anchor_point.x.alignment_to_coordinate(width, screen_rect.width);
let y = screen_rect.y + offset_y + self.anchor_point.y.alignment_to_coordinate(height, screen_rect.height);
gdk::Rectangle { x, y, width, height }
}
}

View file

@ -1,307 +0,0 @@
use crate::util::StringExt;
use anyhow::*;
use itertools::Itertools;
use std::fmt;
#[macro_export]
macro_rules! with_text_pos_context {
($node:expr => $($code:tt)*) => {{
let result: Result<_> = try { $($code)* };
result.with_context(|| anyhow!("at: {}", $node.text_pos()))
}};
}
/// resolve symbols such as &quot; to replace them with the actual " symbol
pub fn resolve_escaped_symbols(s: &str) -> String {
s.replace("&quot;", "\"").replace("&lt;", "<").replace("&gt;", ">")
}
#[derive(Debug, Clone)]
pub enum XmlNode<'a, 'b> {
Element(XmlElement<'a, 'b>),
Text(XmlText<'a, 'b>),
Ignored(roxmltree::Node<'a, 'b>),
}
impl<'a, 'b> fmt::Display for XmlNode<'a, 'b> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
XmlNode::Text(text) => write!(f, "{}", text),
XmlNode::Element(elem) => write!(f, "{}", elem),
XmlNode::Ignored(node) => write!(f, "{:?}", node),
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, derive_more::Display)]
#[display(fmt = "{}:{}", row, col)]
pub struct TextPos {
pub row: u32,
pub col: u32,
}
impl std::fmt::Debug for TextPos {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl From<roxmltree::TextPos> for TextPos {
fn from(x: roxmltree::TextPos) -> Self {
TextPos { row: x.row, col: x.col }
}
}
/// Get the part of a string that is selected by the start and end TextPos.
/// Will panic if the range is out of bounds in any way.
fn get_text_from_text_range(s: &str, (start_pos, end_pos): (TextPos, TextPos)) -> String {
let mut code_text =
s.lines().dropping(start_pos.row as usize - 1).take(end_pos.row as usize - (start_pos.row as usize - 1)).collect_vec();
if let Some(first_line) = code_text.first_mut() {
*first_line = first_line.split_at(start_pos.col as usize - 1).1;
}
if let Some(last_line) = code_text.last_mut() {
*last_line = last_line.split_at(end_pos.col as usize - 1).0;
}
resolve_escaped_symbols(&code_text.join("\n"))
}
impl<'a, 'b> XmlNode<'a, 'b> {
pub fn get_sourcecode(&self) -> String {
let input_text = self.node().document().input_text();
let range = self.node().range();
let start_pos = self.node().document().text_pos_at(range.start).into();
let end_pos = self.node().document().text_pos_at(range.end).into();
get_text_from_text_range(input_text, (start_pos, end_pos))
}
pub fn as_text_or_sourcecode(&self) -> String {
self.as_text().map(|c| resolve_escaped_symbols(&c.text())).unwrap_or_else(|_| self.get_sourcecode())
}
pub fn as_text(&self) -> Result<&XmlText<'a, 'b>> {
match self {
XmlNode::Text(text) => Ok(text),
_ => Err(anyhow!("'{}' is not a text node", self)),
}
}
pub fn as_element(&self) -> Result<&XmlElement<'a, 'b>> {
match self {
XmlNode::Element(element) => Ok(element),
_ => Err(anyhow!("'{}' is not an element node", self)),
}
}
pub fn text_range(&self) -> std::ops::Range<usize> {
self.node().range()
}
pub fn text_pos(&self) -> TextPos {
let document = self.node().document();
let range = self.node().range();
document.text_pos_at(range.start).into()
}
fn node(&self) -> roxmltree::Node<'a, 'b> {
match self {
XmlNode::Text(x) => x.0,
XmlNode::Element(x) => x.0,
XmlNode::Ignored(x) => *x,
}
}
}
#[derive(Debug, Clone)]
pub struct XmlText<'a, 'b>(roxmltree::Node<'a, 'b>);
impl<'a, 'b> fmt::Display for XmlText<'a, 'b> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Text(\"{}\")", self.text())
}
}
impl<'a, 'b> XmlText<'a, 'b> {
pub fn text(&self) -> String {
self.0.text().map(resolve_escaped_symbols).unwrap_or_default().trim_lines().trim_matches('\n').to_owned()
}
pub fn text_pos(&self) -> TextPos {
let document = self.0.document();
let range = self.0.range();
document.text_pos_at(range.start).into()
}
}
#[derive(Debug, Clone)]
pub struct XmlElement<'a, 'b>(roxmltree::Node<'a, 'b>);
impl<'a, 'b> fmt::Display for XmlElement<'a, 'b> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let children = self
.children()
.map(|child| format!("{}", child))
.map(|x| x.lines().map(|line| format!(" {}", line)).join("\n"))
.join("\n");
if children.is_empty() {
write!(f, "{}</{}>", self.as_tag_string(), self.tag_name())
} else {
write!(f, "{}\n{}\n</{}>", self.as_tag_string(), children, self.tag_name())
}
}
}
impl<'a, 'b> XmlElement<'a, 'b> {
pub fn as_tag_string(&self) -> String {
let attrs = self.attributes().iter().map(|attr| format!("{}=\"{}\"", attr.name(), attr.value())).join(" ");
format!("<{} {}>", self.tag_name(), attrs)
}
pub fn tag_name(&self) -> &str {
self.0.tag_name().name()
}
pub fn child(&self, tagname: &str) -> Result<XmlElement> {
with_text_pos_context! { self =>
self.child_elements()
.find(|child| child.tag_name() == tagname)
.with_context(|| anyhow!("child element '{}' missing from {}", tagname, self.as_tag_string()))?
}
}
pub fn children(&self) -> impl Iterator<Item = XmlNode> {
self.0
.children()
.filter(|child| child.is_element() || (child.is_text() && !child.text().unwrap_or_default().is_blank()))
.map(XmlNode::from)
}
pub fn child_elements(&self) -> impl Iterator<Item = XmlElement> {
self.0.children().filter(|child| child.is_element()).map(XmlElement)
}
pub fn attributes(&self) -> &[roxmltree::Attribute] {
self.0.attributes()
}
pub fn attr(&self, key: &str) -> Result<String> {
with_text_pos_context! { self =>
self.0
.attribute(key)
.map(resolve_escaped_symbols)
.with_context(|| anyhow!("'{}' missing attribute '{}'", self.as_tag_string(), key))?
}
}
pub fn optional_attr<O, F: FnOnce(&str) -> Result<O>>(&self, key: &str, parse: F) -> Result<Option<O>> {
match self.0.attribute(key) {
Some(value) => parse(value)
.with_context(|| format!("Parsing the value of {}=\"{}\" in <{}>", key, value, self.tag_name()))
.map(Some),
None => Ok(None),
}
}
pub fn parse_optional_attr<E: Into<anyhow::Error>, O: std::str::FromStr<Err = E>>(&self, key: &str) -> Result<Option<O>> {
match self.0.attribute(key) {
Some(value) => value
.parse::<O>()
.map_err(|e| anyhow!(e))
.with_context(|| format!("Parsing the value of {}=\"{}\" in <{}>", key, value, self.tag_name()))
.map(Some),
None => Ok(None),
}
}
pub fn only_child(&self) -> Result<XmlNode> {
with_text_pos_context! { self =>
let mut children_iter = self.children();
let only_child = children_iter
.next()
.with_context(|| anyhow!("'{}' had no children", self.as_tag_string()))?;
if children_iter.next().is_some() {
bail!("'{}' had more than one child", &self);
}
only_child
}
}
pub fn only_child_element(&self) -> Result<XmlElement> {
with_text_pos_context! { self =>
self.only_child()?.as_element()?.clone()
}
}
pub fn text_pos(&self) -> TextPos {
let document = self.0.document();
let range = self.0.range();
document.text_pos_at(range.start).into()
}
}
impl<'a, 'b> From<XmlElement<'a, 'b>> for XmlNode<'a, 'b> {
fn from(elem: XmlElement<'a, 'b>) -> Self {
XmlNode::Element(elem)
}
}
impl<'a, 'b> From<XmlText<'a, 'b>> for XmlNode<'a, 'b> {
fn from(elem: XmlText<'a, 'b>) -> Self {
XmlNode::Text(elem)
}
}
impl<'a, 'b> From<roxmltree::Node<'a, 'b>> for XmlNode<'a, 'b> {
fn from(node: roxmltree::Node<'a, 'b>) -> Self {
if node.is_text() {
XmlNode::Text(XmlText(node))
} else if node.is_element() | node.is_root() {
XmlNode::Element(XmlElement(node))
} else {
XmlNode::Ignored(node)
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
pub fn test_parse_sourcecode_singleline() {
let input = "<something>whatever</something>";
let document = roxmltree::Document::parse(&input).unwrap();
let root_node = XmlNode::from(document.root_element());
assert_eq!(root_node.as_element().unwrap().only_child().unwrap().as_text_or_sourcecode(), "whatever".to_string());
}
#[test]
pub fn test_parse_sourcecode_multiline() {
let input = r#"<something>
this is
multiline
</something>"#;
let document = roxmltree::Document::parse(&input).unwrap();
let root_node = XmlNode::from(document.root_element());
assert_eq!(
root_node.as_element().unwrap().only_child().unwrap().as_text_or_sourcecode(),
"this is\nmultiline".to_string()
);
}
#[test]
pub fn test_parse_sourcecode_code() {
let input = r#"<something>
if [ "this" == '$that' ]; then
echo `hi`
fi
</something>"#;
let document = roxmltree::Document::parse(&input).unwrap();
let root_node = XmlNode::from(document.root_element());
assert_eq!(
root_node.as_element().unwrap().only_child().unwrap().as_text_or_sourcecode(),
"if [ \"this\" == '$that' ]; then\necho `hi`\nfi".to_string()
);
}
}

View file

@ -91,7 +91,6 @@ mod platform {
#[cfg(feature = "x11")]
mod platform {
use crate::config::{EwwWindowDefinition, EwwWindowType, Side, WindowStacking};
use anyhow::*;
use gdkx11;
use gtk::{self, prelude::*};
@ -103,6 +102,12 @@ mod platform {
protocol::xproto::*,
rust_connection::{DefaultStream, RustConnection},
};
use yuck::config::{
backend_window_options::{Side, WindowType},
window_definition::WindowStacking,
};
use crate::config::EwwWindowDefinition;
pub fn initialize_window(window_def: &EwwWindowDefinition, _monitor: gdk::Rectangle) -> Option<gtk::Window> {
let window_type = if window_def.backend_options.wm_ignore { gtk::WindowType::Popup } else { gtk::WindowType::Toplevel };
@ -208,11 +213,11 @@ mod platform {
self.atoms._NET_WM_WINDOW_TYPE,
self.atoms.ATOM,
&[match window_def.backend_options.window_type {
EwwWindowType::Dock => self.atoms._NET_WM_WINDOW_TYPE_DOCK,
EwwWindowType::Normal => self.atoms._NET_WM_WINDOW_TYPE_NORMAL,
EwwWindowType::Dialog => self.atoms._NET_WM_WINDOW_TYPE_DIALOG,
EwwWindowType::Toolbar => self.atoms._NET_WM_WINDOW_TYPE_TOOLBAR,
EwwWindowType::Utility => self.atoms._NET_WM_WINDOW_TYPE_UTILITY,
WindowType::Dock => self.atoms._NET_WM_WINDOW_TYPE_DOCK,
WindowType::Normal => self.atoms._NET_WM_WINDOW_TYPE_NORMAL,
WindowType::Dialog => self.atoms._NET_WM_WINDOW_TYPE_DIALOG,
WindowType::Toolbar => self.atoms._NET_WM_WINDOW_TYPE_TOOLBAR,
WindowType::Utility => self.atoms._NET_WM_WINDOW_TYPE_UTILITY,
}],
)?
.check()?;

View file

@ -1,22 +1,20 @@
use crate::{
config::window_definition::WindowName,
dynval::{AttrName, AttrValElement, VarName},
};
use anyhow::*;
use std::{collections::HashMap, sync::Arc};
use yuck::value::{AttrName, VarName};
use crate::dynval::{AttrVal, DynVal};
use simplexpr::{dynval::DynVal, SimplExpr};
/// Handler that gets executed to apply the necessary parts of the eww state to
/// a gtk widget. These are created and initialized in EwwState::resolve.
pub struct StateChangeHandler {
func: Box<dyn Fn(HashMap<AttrName, DynVal>) -> Result<()> + 'static>,
unresolved_values: HashMap<AttrName, AttrVal>,
unresolved_values: HashMap<AttrName, SimplExpr>,
}
impl StateChangeHandler {
fn used_variables(&self) -> impl Iterator<Item = &VarName> {
self.unresolved_values.iter().flat_map(|(_, value)| value.var_refs())
fn used_variables(&self) -> impl Iterator<Item = VarName> + '_ {
// TODO fix this clone
self.unresolved_values.iter().flat_map(|(_, value)| value.var_refs()).map(|x| VarName(x.to_string()))
}
/// Run the StateChangeHandler.
@ -60,7 +58,7 @@ impl EwwWindowState {
/// window-specific state-change handlers.
#[derive(Default)]
pub struct EwwState {
windows: HashMap<WindowName, EwwWindowState>,
windows: HashMap<String, EwwWindowState>,
variables_state: HashMap<VarName, DynVal>,
}
@ -80,7 +78,7 @@ impl EwwState {
}
/// remove all state stored specific to one window
pub fn clear_window_state(&mut self, window_name: &WindowName) {
pub fn clear_window_state(&mut self, window_name: &str) {
self.windows.remove(window_name);
}
@ -108,22 +106,17 @@ impl EwwState {
}
/// resolves a value if possible, using the current eww_state.
pub fn resolve_once<'a>(&'a self, value: &'a AttrVal) -> Result<DynVal> {
value
.iter()
.map(|element| match element {
AttrValElement::Primitive(primitive) => Ok(primitive.clone()),
AttrValElement::Expr(expr) => expr.clone().eval(&self.variables_state),
})
.collect()
pub fn resolve_once<'a>(&'a self, value: &'a SimplExpr) -> Result<DynVal> {
// TODO fix this clone
Ok(value.clone().eval(&self.variables_state.into_iter().map(|(k, v)| (k.0, v)).collect())?)
}
/// Resolve takes a function that applies a set of fully resolved attribute
/// values to it's gtk widget.
pub fn resolve<F: Fn(HashMap<AttrName, DynVal>) -> Result<()> + 'static + Clone>(
&mut self,
window_name: &WindowName,
required_attributes: HashMap<AttrName, AttrVal>,
window_name: &str,
required_attributes: HashMap<AttrName, SimplExpr>,
set_value: F,
) {
let handler = StateChangeHandler { func: Box::new(set_value), unresolved_values: required_attributes };
@ -132,7 +125,7 @@ impl EwwState {
// only store the handler if at least one variable is being used
if handler.used_variables().next().is_some() {
self.windows.entry(window_name.clone()).or_insert_with(EwwWindowState::default).put_handler(handler);
self.windows.entry(window_name.to_string()).or_insert_with(EwwWindowState::default).put_handler(handler);
}
}
@ -140,7 +133,7 @@ impl EwwState {
self.windows.values().flat_map(|w| w.state_change_handlers.keys())
}
pub fn vars_referenced_in(&self, window_name: &WindowName) -> std::collections::HashSet<&VarName> {
pub fn vars_referenced_in(&self, window_name: &str) -> std::collections::HashSet<&VarName> {
self.windows.get(window_name).map(|window| window.state_change_handlers.keys().collect()).unwrap_or_default()
}
}

View file

@ -1,13 +1,14 @@
use anyhow::*;
use serde::{Deserialize, Serialize};
use simplexpr::dynval::DynVal;
use structopt::StructOpt;
use crate::{
app,
config::{AnchorPoint, WindowName},
dynval::{Coords, DynVal, VarName},
use yuck::{
config::window_geometry::AnchorPoint,
value::{Coords, VarName},
};
use crate::app;
/// Struct that gets generated from `RawOpt`.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Opt {
@ -68,7 +69,7 @@ pub enum ActionWithServer {
#[structopt(name = "open", alias = "o")]
OpenWindow {
/// Name of the window you want to open.
window_name: WindowName,
window_name: String,
/// Monitor-index the window should open on
#[structopt(short, long)]
@ -90,11 +91,11 @@ pub enum ActionWithServer {
/// Open multiple windows at once.
/// NOTE: This will in the future be part of eww open, and will then be removed.
#[structopt(name = "open-many")]
OpenMany { windows: Vec<WindowName> },
OpenMany { windows: Vec<String> },
/// Close the window with the given name
#[structopt(name = "close", alias = "c")]
CloseWindow { window_name: WindowName },
CloseWindow { window_name: String },
/// Reload the configuration
#[structopt(name = "reload", alias = "r")]

View file

@ -1,17 +1,16 @@
use std::collections::HashMap;
use crate::{
app, config,
dynval::{DynVal, VarName},
};
use crate::app;
use anyhow::*;
use app::DaemonCommand;
use simplexpr::dynval::DynVal;
use tokio::{
io::{AsyncBufReadExt, BufReader},
sync::mpsc::UnboundedSender,
};
use tokio_util::sync::CancellationToken;
use yuck::{config::script_var_definition::{PollScriptVar, ScriptVarDefinition, TailScriptVar}, value::VarName};
/// Initialize the script var handler, and return a handle to that handler, which can be used to control
/// the script var execution.
@ -53,7 +52,7 @@ pub struct ScriptVarHandlerHandle {
impl ScriptVarHandlerHandle {
/// Add a new script-var that should be executed.
pub fn add(&self, script_var: config::ScriptVar) {
pub fn add(&self, script_var: ScriptVarDefinition) {
crate::print_result_err!(
"while forwarding instruction to script-var handler",
self.msg_send.send(ScriptVarHandlerMsg::AddVar(script_var))
@ -80,22 +79,22 @@ impl ScriptVarHandlerHandle {
/// Message enum used by the ScriptVarHandlerHandle to communicate to the ScriptVarHandler
#[derive(Debug, Eq, PartialEq)]
enum ScriptVarHandlerMsg {
AddVar(config::ScriptVar),
AddVar(ScriptVarDefinition),
Stop(VarName),
StopAll,
}
/// Handler that manages running and updating [ScriptVar]s
/// Handler that manages running and updating [ScriptVarDefinition]s
struct ScriptVarHandler {
tail_handler: TailVarHandler,
poll_handler: PollVarHandler,
}
impl ScriptVarHandler {
async fn add(&mut self, script_var: config::ScriptVar) {
async fn add(&mut self, script_var: ScriptVarDefinition) {
match script_var {
config::ScriptVar::Poll(var) => self.poll_handler.start(var).await,
config::ScriptVar::Tail(var) => self.tail_handler.start(var).await,
ScriptVarDefinition::Poll(var) => self.poll_handler.start(var).await,
ScriptVarDefinition::Tail(var) => self.tail_handler.start(var).await,
};
}
@ -126,14 +125,14 @@ impl PollVarHandler {
Ok(handler)
}
async fn start(&mut self, var: config::PollScriptVar) {
async fn start(&mut self, var: PollScriptVar) {
log::debug!("starting poll var {}", &var.name);
let cancellation_token = CancellationToken::new();
self.poll_handles.insert(var.name.clone(), cancellation_token.clone());
let evt_send = self.evt_send.clone();
tokio::spawn(async move {
let result: Result<_> = try {
evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), var.run_once()?)]))?;
evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), run_poll_once(&var)?)]))?;
};
crate::print_result_err!("while running script-var command", &result);
@ -141,7 +140,7 @@ impl PollVarHandler {
_ = cancellation_token.cancelled() => break,
_ = tokio::time::sleep(var.interval) => {
let result: Result<_> = try {
evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), var.run_once()?)]))?;
evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), run_poll_once(&var)?)]))?;
};
crate::print_result_err!("while running script-var command", &result);
}
@ -161,6 +160,13 @@ impl PollVarHandler {
}
}
fn run_poll_once(var: &PollScriptVar) -> Result<DynVal> {
match &var.command {
yuck::config::script_var_definition::VarSource::Shell(x) => crate::config::script_var::run_command(x),
yuck::config::script_var_definition::VarSource::Function(x) => x().map_err(|e| anyhow!(e)),
}
}
impl Drop for PollVarHandler {
fn drop(&mut self) {
self.stop_all();
@ -178,7 +184,7 @@ impl TailVarHandler {
Ok(handler)
}
async fn start(&mut self, var: config::TailScriptVar) {
async fn start(&mut self, var: TailScriptVar) {
log::debug!("starting poll var {}", &var.name);
let cancellation_token = CancellationToken::new();
self.tail_process_handles.insert(var.name.clone(), cancellation_token.clone());

View file

@ -1,12 +1,9 @@
use crate::{
config::{element::WidgetDefinition, window_definition::WindowName},
eww_state::*,
};
use crate::eww_state::*;
use anyhow::*;
use gtk::prelude::*;
use itertools::Itertools;
use std::collections::HashMap;
use yuck::value::AttrName;
use yuck::{config::widget_definition::WidgetDefinition, value::AttrName};
use std::process::Command;
use widget_definitions::*;
@ -45,7 +42,7 @@ struct BuilderArgs<'a, 'b, 'c, 'd, 'e> {
eww_state: &'a mut EwwState,
widget: &'b widget_node::Generic,
unhandled_attrs: Vec<&'c AttrName>,
window_name: &'d WindowName,
window_name: &'d str,
widget_definitions: &'e HashMap<String, WidgetDefinition>,
}
@ -60,7 +57,7 @@ struct BuilderArgs<'a, 'b, 'c, 'd, 'e> {
/// widget name.
fn build_builtin_gtk_widget(
eww_state: &mut EwwState,
window_name: &WindowName,
window_name: &str,
widget_definitions: &HashMap<String, WidgetDefinition>,
widget: &widget_node::Generic,
) -> Result<Option<gtk::Widget>> {
@ -72,7 +69,7 @@ fn build_builtin_gtk_widget(
return result.with_context(|| {
format!(
"{}Error building widget {}",
bargs.widget.text_pos.map(|x| format!("{} |", x)).unwrap_or_default(),
bargs.widget.span.map(|x| format!("{} |", x)).unwrap_or_default(),
bargs.widget.name,
)
})
@ -87,7 +84,7 @@ fn build_builtin_gtk_widget(
let child_widget = child.render(bargs.eww_state, window_name, widget_definitions).with_context(|| {
format!(
"{}error while building child '{:#?}' of '{}'",
widget.text_pos.map(|x| format!("{} |", x)).unwrap_or_default(),
widget.span.map(|x| format!("{} |", x)).unwrap_or_default(),
&child,
&gtk_widget.get_widget_name()
)
@ -108,7 +105,7 @@ fn build_builtin_gtk_widget(
if !bargs.unhandled_attrs.is_empty() {
log::error!(
"{}: Unknown attribute used in {}: {}",
widget.text_pos.map(|x| format!("{} | ", x)).unwrap_or_default(),
widget.span.map(|x| format!("{} | ", x)).unwrap_or_default(),
widget.name,
bargs.unhandled_attrs.iter().map(|x| x.to_string()).join(", ")
)

View file

@ -45,7 +45,9 @@ pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result<Option<gtk
pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: &gtk::Widget) {
let css_provider = gtk::CssProvider::new();
if let Ok(visible) = bargs.widget.get_attr("visible").and_then(|v| bargs.eww_state.resolve_once(v)?.as_bool()) {
if let Ok(visible) =
bargs.widget.get_attr("visible").and_then(|v| bargs.eww_state.resolve_once(v)?.as_bool().map_err(|e| anyhow!(e)))
{
connect_first_map(gtk_widget, move |w| {
if visible {
w.show();

View file

@ -1,18 +1,17 @@
use crate::{
config::{
element::{WidgetDefinition, WidgetUse},
xml_ext::TextPos,
WindowName,
},
eww_state::EwwState,
dynval::{AttrName, AttrVal, VarName},
};
use crate::eww_state::EwwState;
use anyhow::*;
use dyn_clone;
use simplexpr::SimplExpr;
use std::collections::HashMap;
use yuck::{
config::{widget_definition::WidgetDefinition, widget_use::WidgetUse},
parser::ast::Span,
value::{AttrName, VarName},
};
pub trait WidgetNode: std::fmt::Debug + dyn_clone::DynClone + Send + Sync {
fn get_name(&self) -> &str;
fn get_text_pos(&self) -> Option<TextPos>;
fn get_span(&self) -> Option<Span>;
/// Generate a [gtk::Widget] from a [element::WidgetUse].
///
@ -24,7 +23,7 @@ pub trait WidgetNode: std::fmt::Debug + dyn_clone::DynClone + Send + Sync {
fn render(
&self,
eww_state: &mut EwwState,
window_name: &WindowName,
window_name: &str,
widget_definitions: &HashMap<String, WidgetDefinition>,
) -> Result<gtk::Widget>;
}
@ -34,7 +33,7 @@ dyn_clone::clone_trait_object!(WidgetNode);
#[derive(Debug, Clone)]
pub struct UserDefined {
name: String,
text_pos: Option<TextPos>,
span: Option<Span>,
content: Box<dyn WidgetNode>,
}
@ -43,14 +42,14 @@ impl WidgetNode for UserDefined {
&self.name
}
fn get_text_pos(&self) -> Option<TextPos> {
self.text_pos
fn get_span(&self) -> Option<Span> {
self.span
}
fn render(
&self,
eww_state: &mut EwwState,
window_name: &WindowName,
window_name: &str,
widget_definitions: &HashMap<String, WidgetDefinition>,
) -> Result<gtk::Widget> {
self.content.render(eww_state, window_name, widget_definitions)
@ -60,19 +59,20 @@ impl WidgetNode for UserDefined {
#[derive(Debug, Clone)]
pub struct Generic {
pub name: String,
pub text_pos: Option<TextPos>,
pub span: Option<Span>,
pub children: Vec<Box<dyn WidgetNode>>,
pub attrs: HashMap<AttrName, AttrVal>,
pub attrs: HashMap<AttrName, SimplExpr>,
}
impl Generic {
pub fn get_attr(&self, key: &str) -> Result<&AttrVal> {
pub fn get_attr(&self, key: &str) -> Result<&SimplExpr> {
self.attrs.get(key).context(format!("attribute '{}' missing from use of '{}'", key, &self.name))
}
/// returns all the variables that are referenced in this widget
pub fn referenced_vars(&self) -> impl Iterator<Item = &VarName> {
self.attrs.iter().flat_map(|(_, value)| value.var_refs())
pub fn referenced_vars(&self) -> impl Iterator<Item = VarName> + '_ {
// TODO fix this clone
self.attrs.iter().flat_map(|(_, value)| value.var_refs()).map(|x| VarName(x.to_string()))
}
}
@ -81,14 +81,14 @@ impl WidgetNode for Generic {
&self.name
}
fn get_text_pos(&self) -> Option<TextPos> {
self.text_pos
fn get_span(&self) -> Option<Span> {
self.span
}
fn render(
&self,
eww_state: &mut EwwState,
window_name: &WindowName,
window_name: &str,
widget_definitions: &HashMap<String, WidgetDefinition>,
) -> Result<gtk::Widget> {
crate::widgets::build_builtin_gtk_widget(eww_state, window_name, widget_definitions, self)?
@ -98,25 +98,42 @@ impl WidgetNode for Generic {
pub fn generate_generic_widget_node(
defs: &HashMap<String, WidgetDefinition>,
local_env: &HashMap<VarName, AttrVal>,
local_env: &HashMap<VarName, SimplExpr>,
w: WidgetUse,
) -> Result<Box<dyn WidgetNode>> {
if let Some(def) = defs.get(&w.name) {
ensure!(w.children.is_empty(), "User-defined widgets cannot be given children.");
let new_local_env = w
.attrs
.attrs
.into_iter()
.map(|(name, value)| (VarName(name.0), value.resolve_one_level(local_env)))
.collect::<HashMap<_, _>>();
.map(|(name, value)| {
Ok((
VarName(name.0),
SimplExpr::Literal(value.value.span().into(), value.value.as_simplexpr()?.resolve_one_level(local_env)?),
))
})
.collect::<Result<HashMap<VarName, _>>>()?;
let content = generate_generic_widget_node(defs, &new_local_env, def.structure.clone())?;
Ok(Box::new(UserDefined { name: w.name, text_pos: w.text_pos, content }))
let content = generate_generic_widget_node(defs, &new_local_env, def.widget.clone())?;
Ok(Box::new(UserDefined { name: w.name, span: Some(w.span), content }))
} else {
Ok(Box::new(Generic {
name: w.name,
text_pos: w.text_pos,
attrs: w.attrs.into_iter().map(|(name, value)| (name, value.resolve_one_level(local_env))).collect(),
span: Some(w.span),
attrs: w
.attrs
.attrs
.into_iter()
.map(|(name, value)| {
Ok((
VarName(name.0),
SimplExpr::Literal(value.value.span().into(), value.value.as_simplexpr()?.resolve_one_level(local_env)?),
))
})
.collect()?,
children: w
.children
.into_iter()
@ -126,42 +143,41 @@ pub fn generate_generic_widget_node(
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::config::xml_ext::*;
use maplit::hashmap;
#[test]
fn test_generic_generate() {
let w_def1 = {
let input = r#"<def name="foo"><box><box>{{nested1}}{{raw1}}</box></box></def>"#;
let document = roxmltree::Document::parse(input).unwrap();
let xml = XmlNode::from(document.root_element().clone());
WidgetDefinition::from_xml_element(&xml.as_element().unwrap()).unwrap()
};
let w_def2 = {
let input = r#"<def name="bar"><box><foo nested1="{{nested2}}" raw1="raw value"/></box></def>"#;
let document = roxmltree::Document::parse(input).unwrap();
let xml = XmlNode::from(document.root_element().clone());
WidgetDefinition::from_xml_element(&xml.as_element().unwrap()).unwrap()
};
let w_use = {
let input = r#"<bar nested2="{{in_root}}"/>"#;
let document = roxmltree::Document::parse(input).unwrap();
let xml = XmlNode::from(document.root_element().clone());
WidgetUse::from_xml_node(xml).unwrap()
};
//#[cfg(test)]
// mod test {
// use super::*;
// use crate::config::xml_ext::*;
// use maplit::hashmap;
//#[test]
// fn test_generic_generate() {
// let w_def1 = {
// let input = r#"<def name="foo"><box><box>{{nested1}}{{raw1}}</box></box></def>"#;
// let document = roxmltree::Document::parse(input).unwrap();
// let xml = XmlNode::from(document.root_element().clone());
// WidgetDefinition::from_xml_element(&xml.as_element().unwrap()).unwrap()
//};
// let w_def2 = {
// let input = r#"<def name="bar"><box><foo nested1="{{nested2}}" raw1="raw value"/></box></def>"#;
// let document = roxmltree::Document::parse(input).unwrap();
// let xml = XmlNode::from(document.root_element().clone());
// WidgetDefinition::from_xml_element(&xml.as_element().unwrap()).unwrap()
//};
// let w_use = {
// let input = r#"<bar nested2="{{in_root}}"/>"#;
// let document = roxmltree::Document::parse(input).unwrap();
// let xml = XmlNode::from(document.root_element().clone());
// WidgetUse::from_xml_node(xml).unwrap()
//};
let generic = generate_generic_widget_node(
&hashmap! { "foo".to_string() => w_def1, "bar".to_string() => w_def2 },
&HashMap::new(),
w_use,
)
.unwrap();
// let generic = generate_generic_widget_node(
//&hashmap! { "foo".to_string() => w_def1, "bar".to_string() => w_def2 },
//&HashMap::new(),
// w_use,
//)
//.unwrap();
// TODO actually implement this test ._.
//// TODO actually implement this test ._.
dbg!(&generic);
// panic!("REEEEEEEEEE")
}
}
// dbg!(&generic);
//// panic!("REEEEEEEEEE")
//}

View file

@ -74,11 +74,16 @@ impl SimplExpr {
Self::Literal(span, DynVal(s, Some(span)))
}
/// Construct a synthetic simplexpr from a literal value, without adding any relevant span information (uses [DUMMY_SPAN])
pub fn synth_literal(s: String) -> Self {
/// Construct a synthetic simplexpr from a literal string, without adding any relevant span information (uses [DUMMY_SPAN])
pub fn synth_string(s: String) -> Self {
Self::Literal(DUMMY_SPAN, DynVal(s, Some(DUMMY_SPAN)))
}
/// Construct a synthetic simplexpr from a literal dynval, without adding any relevant span information (uses [DUMMY_SPAN])
pub fn synth_literal<T: Into<DynVal>>(s: T) -> Self {
Self::Literal(DUMMY_SPAN, s.into())
}
pub fn span(&self) -> Span {
match self {
SimplExpr::Literal(span, _) => *span,

View file

@ -58,6 +58,7 @@ impl AttrEntry {
}
}
// TODO maybe make this generic over the contained content
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize)]
pub struct Attributes {
pub span: Span,

View file

@ -21,7 +21,7 @@ pub type BackendWindowOptions = X11WindowOptions;
pub struct X11WindowOptions {
pub wm_ignore: bool,
pub sticky: bool,
pub window_type: EwwWindowType,
pub window_type: WindowType,
pub struts: StrutDefinition,
}
@ -39,7 +39,7 @@ impl X11WindowOptions {
}
#[derive(Debug, Clone, PartialEq, Eq, smart_default::SmartDefault, serde::Serialize)]
pub enum EwwWindowType {
pub enum WindowType {
#[default]
Dock,
Dialog,
@ -47,7 +47,7 @@ pub enum EwwWindowType {
Normal,
Utility,
}
impl FromStr for EwwWindowType {
impl FromStr for WindowType {
type Err = EnumParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {

View file

@ -32,7 +32,7 @@ pub enum VarSource {
// TODO allow for other executors? (python, etc)
Shell(String),
#[serde(skip)]
Function(fn() -> Result<DynVal, Box<dyn std::error::Error + Sync>>),
Function(fn() -> Result<DynVal, Box<dyn std::error::Error + Sync + Send + 'static>>),
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct PollScriptVar {

View file

@ -1,167 +0,0 @@
use super::{backend_window_options::*, *};
use crate::{ensure_xml_tag_is, enum_parse, value::NumWithUnit, widgets::widget_node};
use derive_more::*;
use serde::{Deserialize, Serialize};
use smart_default::SmartDefault;
use std::{collections::HashMap, str::FromStr};
/// Full window-definition containing the fully expanded widget tree.
/// **Use this** rather than [RawEwwWindowDefinition].
#[derive(Debug, Clone)]
pub struct EwwWindowDefinition {
pub name: WindowName,
pub geometry: Option<EwwWindowGeometry>,
pub stacking: WindowStacking,
pub screen_number: Option<i32>,
pub widget: Box<dyn widget_node::WidgetNode>,
pub resizable: bool,
pub backend_options: BackendWindowOptions,
}
impl EwwWindowDefinition {
pub fn generate(defs: &HashMap<String, WidgetDefinition>, window: RawEwwWindowDefinition) -> Result<Self> {
Ok(EwwWindowDefinition {
name: window.name,
geometry: window.geometry,
stacking: window.stacking,
screen_number: window.screen_number,
resizable: window.resizable,
widget: widget_node::generate_generic_widget_node(defs, &HashMap::new(), window.widget)?,
backend_options: window.backend_options,
})
}
}
/// Window-definition storing the raw WidgetUse, as received directly from parsing.
#[derive(Debug, Clone, PartialEq)]
pub struct RawEwwWindowDefinition {
pub name: WindowName,
pub geometry: Option<EwwWindowGeometry>,
pub stacking: WindowStacking,
pub widget: WidgetUse,
pub resizable: bool,
pub backend_options: BackendWindowOptions,
pub screen_number: Option<i32>,
}
impl RawEwwWindowDefinition {
pub fn from_xml_element(xml: &XmlElement) -> Result<Self> {
ensure_xml_tag_is!(xml, "window");
let geometry = match xml.child("geometry") {
Ok(node) => Some(EwwWindowGeometry::from_xml_element(node)?),
Err(_) => None,
};
Ok(RawEwwWindowDefinition {
name: WindowName(xml.attr("name")?),
geometry,
widget: WidgetUse::from_xml_node(xml.child("widget")?.only_child()?)?,
stacking: xml.parse_optional_attr("stacking")?.unwrap_or_default(),
// TODO maybe rename this to monitor?
screen_number: xml.parse_optional_attr("screen")?,
resizable: xml.parse_optional_attr("resizable")?.unwrap_or(true),
backend_options: BackendWindowOptions::from_xml_element(xml)?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, SmartDefault)]
pub enum EwwWindowType {
#[default]
Dock,
Dialog,
Toolbar,
Normal,
Utility,
}
impl FromStr for EwwWindowType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
enum_parse! { "window type", s,
"dock" => Self::Dock,
"toolbar" => Self::Toolbar,
"dialog" => Self::Dialog,
"normal" => Self::Normal,
"utility" => Self::Utility,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, smart_default::SmartDefault)]
pub enum Side {
#[default]
Top,
Left,
Right,
Bottom,
}
impl std::str::FromStr for Side {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Side> {
enum_parse! { "side", s,
"l" | "left" => Side::Left,
"r" | "right" => Side::Right,
"t" | "top" => Side::Top,
"b" | "bottom" => Side::Bottom,
}
}
}
// Surface definition if the backend for X11 is enable
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
pub struct StrutDefinition {
pub side: Side,
pub dist: NumWithUnit,
}
impl StrutDefinition {
pub fn from_xml_element(xml: XmlElement) -> Result<Self> {
Ok(StrutDefinition { side: xml.attr("side")?.parse()?, dist: xml.attr("distance")?.parse()? })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display, SmartDefault)]
pub enum WindowStacking {
#[default]
Foreground,
Background,
Bottom,
Overlay,
}
impl std::str::FromStr for WindowStacking {
type Err = anyhow::Error;
#[cfg(not(feature = "wayland"))]
fn from_str(s: &str) -> Result<Self> {
enum_parse! { "WindowStacking", s,
"foreground" | "fg" | "f" => WindowStacking::Foreground,
"background" | "bg" | "b" => WindowStacking::Background,
}
}
#[cfg(feature = "wayland")]
fn from_str(s: &str) -> Result<Self> {
enum_parse! { "WindowStacking", s,
"foreground" | "fg" => WindowStacking::Foreground,
"background" | "bg" => WindowStacking::Background,
"bottom" | "bt" => WindowStacking::Bottom,
"overlay" | "ov" => WindowStacking::Overlay,
}
}
}
#[repr(transparent)]
#[derive(Clone, Hash, PartialEq, Eq, AsRef, FromStr, Display, Serialize, Deserialize, Default, From, DebugCustom)]
#[debug(fmt = "WindowName(\".0\")")]
pub struct WindowName(String);
impl std::borrow::Borrow<str> for WindowName {
fn borrow(&self) -> &str {
&self.0
}
}

View file

@ -1,165 +0,0 @@
use anyhow::*;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, fmt, iter::FromIterator};
#[derive(Clone, Deserialize, Serialize, derive_more::From, Default)]
pub struct PrimVal(pub String);
impl fmt::Display for PrimVal {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl fmt::Debug for PrimVal {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "\"{}\"", self.0)
}
}
/// Manually implement equality, to allow for values in different formats (i.e. "1" and "1.0") to still be considered as equal.
impl std::cmp::PartialEq<Self> for PrimVal {
fn eq(&self, other: &Self) -> bool {
if let (Ok(a), Ok(b)) = (self.as_f64(), other.as_f64()) {
a == b
} else {
self.0 == other.0
}
}
}
impl FromIterator<PrimVal> for PrimVal {
fn from_iter<T: IntoIterator<Item = PrimVal>>(iter: T) -> Self {
PrimVal(iter.into_iter().join(""))
}
}
impl std::str::FromStr for PrimVal {
type Err = anyhow::Error;
/// parses the value, trying to turn it into a number and a boolean first,
/// before deciding that it is a string.
fn from_str(s: &str) -> Result<Self> {
Ok(PrimVal::from_string(s.to_string()))
}
}
macro_rules! impl_try_from {
(impl From<$typ:ty> {
$(for $for:ty => |$arg:ident| $code:expr);*;
}) => {
$(impl TryFrom<$typ> for $for {
type Error = anyhow::Error;
fn try_from($arg: $typ) -> Result<Self> { $code }
})*
};
}
macro_rules! impl_primval_from {
($($t:ty),*) => {
$(impl From<$t> for PrimVal {
fn from(x: $t) -> Self { PrimVal(x.to_string()) }
})*
};
}
impl_try_from!(impl From<PrimVal> {
for String => |x| x.as_string();
for f64 => |x| x.as_f64();
for bool => |x| x.as_bool();
for Vec<String> => |x| x.as_vec();
});
impl_primval_from!(bool, i32, u32, f32, u8, f64, &str);
impl From<&serde_json::Value> for PrimVal {
fn from(v: &serde_json::Value) -> Self {
PrimVal(
v.as_str()
.map(|x| x.to_string())
.or_else(|| serde_json::to_string(v).ok())
.unwrap_or_else(|| "<invalid json value>".to_string()),
)
}
}
impl PrimVal {
pub fn from_string(s: String) -> Self {
PrimVal(s)
}
pub fn into_inner(self) -> String {
self.0
}
/// This will never fail
pub fn as_string(&self) -> Result<String> {
Ok(self.0.to_owned())
}
pub fn as_f64(&self) -> Result<f64> {
self.0.parse().map_err(|e| anyhow!("couldn't convert {:?} to f64: {}", &self, e))
}
pub fn as_i32(&self) -> Result<i32> {
self.0.parse().map_err(|e| anyhow!("couldn't convert {:?} to i32: {}", &self, e))
}
pub fn as_bool(&self) -> Result<bool> {
self.0.parse().map_err(|e| anyhow!("couldn't convert {:?} to bool: {}", &self, e))
}
pub fn as_vec(&self) -> Result<Vec<String>> {
parse_vec(self.0.to_owned()).map_err(|e| anyhow!("Couldn't convert {:#?} to a vec: {}", &self, e))
}
pub fn as_json_value(&self) -> Result<serde_json::Value> {
serde_json::from_str::<serde_json::Value>(&self.0)
.with_context(|| format!("Couldn't convert {:#?} to a json object", &self))
}
}
fn parse_vec(a: String) -> Result<Vec<String>> {
match a.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
Some(content) => {
let mut items: Vec<String> = content.split(',').map(|x: &str| x.to_string()).collect();
let mut removed = 0;
for times_ran in 0..items.len() {
// escapes `,` if there's a `\` before em
if items[times_ran - removed].ends_with('\\') {
items[times_ran - removed].pop();
let it = items.remove((times_ran + 1) - removed);
items[times_ran - removed] += ",";
items[times_ran - removed] += &it;
removed += 1;
}
}
Ok(items)
}
None => Err(anyhow!("Is your array built like this: '[these,are,items]'?")),
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_parse_vec() {
assert_eq!(vec![""], parse_vec("[]".to_string()).unwrap(), "should be able to parse empty lists");
assert_eq!(vec!["hi"], parse_vec("[hi]".to_string()).unwrap(), "should be able to parse single element list");
assert_eq!(
vec!["hi", "ho", "hu"],
parse_vec("[hi,ho,hu]".to_string()).unwrap(),
"should be able to parse three element list"
);
assert_eq!(vec!["hi,ho"], parse_vec("[hi\\,ho]".to_string()).unwrap(), "should be able to parse list with escaped comma");
assert_eq!(
vec!["hi,ho", "hu"],
parse_vec("[hi\\,ho,hu]".to_string()).unwrap(),
"should be able to parse two element list with escaped comma"
);
assert!(parse_vec("".to_string()).is_err(), "Should fail when parsing empty string");
assert!(parse_vec("[a,b".to_string()).is_err(), "Should fail when parsing unclosed list");
assert!(parse_vec("a]".to_string()).is_err(), "Should fail when parsing unopened list");
}
}