Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] AttrValue / Update #440

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
12 changes: 0 additions & 12 deletions .vimspector.json

This file was deleted.

7 changes: 7 additions & 0 deletions crates/eww/src/state/scope_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ impl ScopeIndex {
}
}

#[derive(Debug)]
pub enum ScopeGraphEvent {
RemoveScope(ScopeIndex),
UpdateValue(ScopeIndex, VarName, DynVal),
}

/// A graph structure of scopes where each scope may inherit from another scope,
Expand Down Expand Up @@ -87,6 +89,11 @@ impl ScopeGraph {
ScopeGraphEvent::RemoveScope(scope_index) => {
self.remove_scope(scope_index);
}
ScopeGraphEvent::UpdateValue(scope_index, name, value) => {
if let Err(e) = self.update_value(scope_index, &name, value) {
log::error!("{}", e);
}
}
}
}

Expand Down
112 changes: 91 additions & 21 deletions crates/eww/src/widgets/build_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
use yuck::{
config::{
widget_definition::WidgetDefinition,
widget_use::{BasicWidgetUse, ChildrenWidgetUse, LoopWidgetUse, WidgetUse},
widget_use::{BasicWidgetUse, ChildrenWidgetUse, LetWidgetUse, LoopWidgetUse, WidgetUse},
},
gen_diagnostic,
};
Expand Down Expand Up @@ -56,6 +56,9 @@ pub fn build_gtk_widget(
WidgetUse::Basic(widget_use) => {
build_basic_gtk_widget(graph, widget_defs, calling_scope, widget_use, custom_widget_invocation)
}
WidgetUse::Let(let_use) => {
build_let_special_widget_single_child(graph, widget_defs, calling_scope, let_use, custom_widget_invocation)
}
WidgetUse::Loop(_) | WidgetUse::Children(_) => Err(anyhow::anyhow!(DiagError::new(gen_diagnostic! {
msg = "This widget can only be used as a child of some container widget such as box",
label = widget_use.span(),
Expand Down Expand Up @@ -111,6 +114,68 @@ fn build_basic_gtk_widget(
}
}

fn init_let_widget_scope(graph: &mut ScopeGraph, calling_scope: ScopeIndex, widget_use: &LetWidgetUse) -> Result<ScopeIndex> {
// Evaluate explicitly here, so we don't keep linking the state changes here.
// If that was desired, it'd suffice to just pass the simplexprs as attributes to register_new_scope,
// rather than converting them into literals explicitly.
let mut defined_vars = HashMap::new();
for (name, expr) in widget_use.defined_vars.clone().into_iter() {
let mut needed_vars = graph.lookup_variables_in_scope(calling_scope, &expr.collect_var_refs())?;
needed_vars.extend(defined_vars.clone().into_iter());
let value = expr.eval(&needed_vars)?;
defined_vars.insert(name, value);
}

let let_scope = graph.register_new_scope(
"let-widget".to_string(),
Some(calling_scope),
calling_scope,
defined_vars.into_iter().map(|(k, v)| (AttrName(k.to_string()), SimplExpr::Literal(v))).collect(),
)?;
Ok(let_scope)
}

fn build_let_special_widget_single_child(
graph: &mut ScopeGraph,
widget_defs: Rc<HashMap<String, WidgetDefinition>>,
calling_scope: ScopeIndex,
widget_use: LetWidgetUse,
custom_widget_invocation: Option<Rc<CustomWidgetInvocation>>,
) -> Result<gtk::Widget> {
assert!(widget_use.body.len() == 1, "build_let_special_widget_single_child called with let with more than one child");
let child = widget_use.body.first().cloned().expect("no child in let");
let let_scope = init_let_widget_scope(graph, calling_scope, &widget_use)?;
let child_widget = build_gtk_widget(graph, widget_defs, let_scope, child.clone(), custom_widget_invocation)?;
let scope_graph_sender = graph.event_sender.clone();
child_widget.connect_destroy(move |_| {
let _ = scope_graph_sender.send(ScopeGraphEvent::RemoveScope(let_scope));
});
Ok(child_widget)
}

fn build_let_special_widget_multiple_children(
graph: &mut ScopeGraph,
widget_defs: Rc<HashMap<String, WidgetDefinition>>,
calling_scope: ScopeIndex,
widget_use: LetWidgetUse,
gtk_container: &gtk::Container,
custom_widget_invocation: Option<Rc<CustomWidgetInvocation>>,
) -> Result<()> {
let let_scope = init_let_widget_scope(graph, calling_scope, &widget_use)?;
let scope_graph_sender = graph.event_sender.clone();

for child in &widget_use.body {
let child_widget =
build_gtk_widget(graph, widget_defs.clone(), let_scope, child.clone(), custom_widget_invocation.clone())?;
gtk_container.add(&child_widget);
}

gtk_container.connect_destroy(move |_| {
let _ = scope_graph_sender.send(ScopeGraphEvent::RemoveScope(let_scope));
});
Ok(())
}

/// build a [`gtk::Widget`] out of a [`WidgetUse`] that uses a
/// **builtin widget**. User defined widgets are handled by [`widget_definitions::widget_use_to_gtk_widget`].
///
Expand Down Expand Up @@ -184,26 +249,31 @@ fn populate_widget_children(
) -> Result<()> {
for child in widget_use_children {
match child {
WidgetUse::Children(child) => {
build_children_special_widget(
tree,
widget_defs.clone(),
calling_scope,
child,
gtk_container,
custom_widget_invocation.clone().context("Not in a custom widget invocation")?,
)?;
}
WidgetUse::Loop(child) => {
build_loop_special_widget(
tree,
widget_defs.clone(),
calling_scope,
child,
gtk_container,
custom_widget_invocation.clone(),
)?;
}
WidgetUse::Children(child) => build_children_special_widget(
tree,
widget_defs.clone(),
calling_scope,
child,
gtk_container,
custom_widget_invocation.clone().context("Not in a custom widget invocation")?,
)?,

WidgetUse::Let(child) => build_let_special_widget_multiple_children(
tree,
widget_defs.clone(),
calling_scope,
child,
gtk_container,
custom_widget_invocation.clone(),
)?,
WidgetUse::Loop(child) => build_loop_special_widget(
tree,
widget_defs.clone(),
calling_scope,
child,
gtk_container,
custom_widget_invocation.clone(),
)?,
_ => {
let child_widget =
build_gtk_widget(tree, widget_defs.clone(), calling_scope, child, custom_widget_invocation.clone())?;
Expand Down
71 changes: 50 additions & 21 deletions crates/eww/src/widgets/def_widget_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ macro_rules! def_widget {
// If an attribute is explicitly marked as optional (? appended to type)
// the attribute will still show up here, as a `None` value. Otherwise, all values in this map
// will be `Some`.
let attr_map: Result<HashMap<eww_shared_util::AttrName, Option<simplexpr::SimplExpr>>> = try {
let attr_map: Result<HashMap<eww_shared_util::AttrName, Option<yuck::config::attr_value::AttrValue>>> = try {
::maplit::hashmap! {
$(
eww_shared_util::AttrName(::std::stringify!($attr_name).to_owned()) =>
def_widget!(@get_value $args, &::std::stringify!($attr_name).replace('_', "-"), $(? $($optional)?)? $(= $default)?)
def_widget!(@get_value $args, &::std::stringify!($attr_name).replace('_', "-"), $typecast_func, $(? $($optional)?)? $(= $default)?)
),*
}
};
Expand All @@ -37,12 +37,12 @@ macro_rules! def_widget {

$args.scope_graph.register_listener(
$args.calling_scope,
crate::state::scope::Listener {
crate::state::scope::Listener {
needed_variables: required_vars,
f: Box::new({
let $gtk_widget = gdk::glib::clone::Downgrade::downgrade(&$gtk_widget);
move |$scope_graph, values| {
let $gtk_widget = gdk::glib::clone::Upgrade::upgrade(&$gtk_widget).unwrap();
move |#[allow(unused)] $scope_graph, values| {
let $gtk_widget = gdk::glib::clone::Upgrade::upgrade(&$gtk_widget).expect("Failed to upgrade widget ref");
// values is a map of all the variables that are required to evaluate the
// attributes expression.

Expand All @@ -53,15 +53,16 @@ macro_rules! def_widget {
let $attr_name = attr_map.get(::std::stringify!($attr_name))
.context("Missing attribute, this should never happen")?;

// if the value is Some, evaluate and typecast it as expected
let $attr_name = if let Some(x) = $attr_name {
Some(x.eval(&values)?.$typecast_func()?)
} else {
None
};


// If the value is some, evaluate and typecast it.
// This now uses a new macro, to match on the type cast function:
// if we're casting into an action, we wanna do a different thing than if we where casting into an expr
let $attr_name = def_widget!(@value_depending_on_type values, $attr_name : $typecast_func);

// If the attribute is optional, keep it as Option<T>, otherwise unwrap
// because we _know_ the value in the attr_map is Some if the attribute is not optional.
def_widget!(@unwrap_if_required $attr_name $(? $($optional)?)?);
def_widget!(@unwrap_if_required $attr_name : $typecast_func $(? $($optional)?)?);
)*

// And then run the provided code with those attributes in scope.
Expand All @@ -76,23 +77,51 @@ macro_rules! def_widget {
})+
};

(@unwrap_if_required $value:ident ?) => { };
(@unwrap_if_required $value:ident) => {
let $value = $value.unwrap();
(@value_depending_on_type $values:expr, $attr_name:ident : as_action) => {
match $attr_name {
Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action.clone().resolve_to_executable(&$values)),
Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(ExecutableAction::Shell(expr.clone().resolve_refs_lenient(&$values))),
_ => None,
}
};

(@value_depending_on_type $values:expr, $attr_name:ident : $typecast_func:ident) => {
match $attr_name {
Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(expr.eval(&$values)?.$typecast_func()?),
_ => None,
}
};

// optional actions are a special case, as those should default to Noop rather than being represented as options.
(@unwrap_if_required $value:ident : as_action ?) => {
let $value = $value.expect("No value was provided, eventhough value was required");
};
// Optional values don't need unwrapping, they're supposed to be optional
(@unwrap_if_required $value:ident : $typecast_func:ident ?) => { };
// required values will still be stored as option at this point (because typechecking) -- but they are known to exist, and thus we can unwrap-
(@unwrap_if_required $value:ident : $typecast_func:ident) => {
let $value = $value.expect("No value was provided, eventhough value was required");
};
// The attribute is explicitly marked as optional and is an action. Optional actions should just default to Noop
(@get_value $args:ident, $name:expr, as_action, ?) => {
Some($args.widget_use.attrs.ast_optional::<yuck::config::attr_value::AttrValue>($name)?
.clone()
.unwrap_or_else(|| yuck::config::attr_value::AttrValue::Action(yuck::config::attr_value::Action::Noop)))
};
// The attribute is explicitly marked as optional - the value should be provided to the prop function body as Option<T>
(@get_value $args:ident, $name:expr, ?) => {
$args.widget_use.attrs.ast_optional::<simplexpr::SimplExpr>($name)?.clone()
(@get_value $args:ident, $name:expr, $typecast_func:ident, ?) => {
$args.widget_use.attrs.ast_optional::<yuck::config::attr_value::AttrValue>($name)?.clone()
};

// The attribute has a default value
(@get_value $args:ident, $name:expr, = $default:expr) => {
Some($args.widget_use.attrs.ast_optional::<simplexpr::SimplExpr>($name)?.clone().unwrap_or_else(|| simplexpr::SimplExpr::synth_literal($default)))
(@get_value $args:ident, $name:expr, $_typecast_func:ident, = $default:expr) => {
Some($args.widget_use.attrs.ast_optional::<yuck::config::attr_value::AttrValue>($name)?
.clone()
.unwrap_or_else(|| yuck::config::attr_value::AttrValue::SimplExpr(simplexpr::SimplExpr::synth_literal($default))))
};

// The attribute is required - the prop will only be ran if this attribute is actually provided.
(@get_value $args:ident, $name:expr,) => {
Some($args.widget_use.attrs.ast_required::<simplexpr::SimplExpr>($name)?.clone())
(@get_value $args:ident, $name:expr, $typecast_func:ident,) => {
Some($args.widget_use.attrs.ast_required::<yuck::config::attr_value::AttrValue>($name)?.clone())
}
}
80 changes: 45 additions & 35 deletions crates/eww/src/widgets/mod.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,65 @@
use std::process::Command;

use eww_shared_util::VarName;
use simplexpr::dynval::DynVal;
use yuck::config::attr_value::ExecutableAction;

use crate::state::scope_graph::{ScopeGraphEvent, ScopeIndex};

pub mod build_widget;
pub mod circular_progressbar;
pub mod def_widget_macro;
pub mod graph;
pub mod transform;
pub mod widget_definitions;

/// Run a command that was provided as an attribute.
/// This command may use placeholders which will be replaced by the values of the arguments given.
/// This can either be the placeholder `{}`, which will be replaced by the first argument,
/// Or a placeholder like `{0}`, `{1}`, etc, which will refer to the respective argument.
pub(self) fn run_command<T>(timeout: std::time::Duration, cmd: &str, args: &[T])
where
T: 'static + std::fmt::Display + Send + Sync + Clone,
{
#[macro_export]
macro_rules! action_args {
($($key:literal => $value:expr),* $(,)?) => {
serde_json::json!({
$($key.to_string(): $value),*
})
}
}

/// Run an action
pub(self) fn run_action(
sender: tokio::sync::mpsc::UnboundedSender<ScopeGraphEvent>,
scope: ScopeIndex,
timeout: std::time::Duration,
action: &ExecutableAction,
args: &serde_json::Value,
) {
let result: anyhow::Result<()> = try {
let event_arg = maplit::hashmap! { VarName("event".to_string()) => DynVal::try_from(args)? };
match action {
ExecutableAction::Update(varname, value) => {
let value = value.eval(&event_arg)?;
sender.send(ScopeGraphEvent::UpdateValue(scope, varname.clone(), value.clone()))?;
}
ExecutableAction::Shell(command) => {
let command = command.eval(&event_arg)?;
run_command(timeout, command.to_string());
}
ExecutableAction::Noop => {}
}
};
if let Err(e) = result {
log::error!("{}", e);
}
}

/// Run a command with a given timeout
fn run_command(timeout: std::time::Duration, cmd: String) {
use wait_timeout::ChildExt;
let cmd = replace_placeholders(cmd, args);
std::thread::spawn(move || {
log::debug!("Running command from widget: {}", cmd);
let child = Command::new("/bin/sh").arg("-c").arg(&cmd).spawn();
match child {
Ok(mut child) => match child.wait_timeout(timeout) {
// child timed out
Ok(None) => {
log::error!("WARNING: command {} timed out", &cmd);
log::warn!(": command {} timed out", &cmd);
let _ = child.kill();
let _ = child.wait();
}
Expand All @@ -35,28 +70,3 @@ where
}
});
}

fn replace_placeholders<T>(cmd: &str, args: &[T]) -> String
where
T: 'static + std::fmt::Display + Send + Sync + Clone,
{
if !args.is_empty() {
let cmd = cmd.replace("{}", &format!("{}", args[0]));
args.iter().enumerate().fold(cmd.to_string(), |acc, (i, arg)| acc.replace(&format!("{{{}}}", i), &format!("{}", arg)))
} else {
cmd.to_string()
}
}

#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_replace_placeholders() {
assert_eq!("foo", replace_placeholders("foo", &[""]),);
assert_eq!("foo hi", replace_placeholders("foo {}", &["hi"]),);
assert_eq!("foo hi", replace_placeholders("foo {}", &["hi", "ho"]),);
assert_eq!("bar foo baz", replace_placeholders("{0} foo {1}", &["bar", "baz"]),);
assert_eq!("baz foo bar", replace_placeholders("{1} foo {0}", &["bar", "baz"]),);
}
}
Loading