diff --git a/.vimspector.json b/.vimspector.json deleted file mode 100644 index 0907b190..00000000 --- a/.vimspector.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "configurations": { - "launch": { - "adapter": "CodeLLDB", - "configuration": { - "request": "launch", - "program": "${workspaceRoot}/target/debug/eww", - "args": ["open", "main_window"] - } - } - } -} diff --git a/crates/eww/src/state/scope_graph.rs b/crates/eww/src/state/scope_graph.rs index 052a1c41..ab102c2f 100644 --- a/crates/eww/src/state/scope_graph.rs +++ b/crates/eww/src/state/scope_graph.rs @@ -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, @@ -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); + } + } } } diff --git a/crates/eww/src/widgets/build_widget.rs b/crates/eww/src/widgets/build_widget.rs index da2172c9..6b5e2a95 100644 --- a/crates/eww/src/widgets/build_widget.rs +++ b/crates/eww/src/widgets/build_widget.rs @@ -120,11 +120,23 @@ fn build_let_special_widget( custom_widget_invocation: Option>, ) -> Result { let child = widget_use.body.first().expect("no child in let"); + + // 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.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, - widget_use.defined_vars.into_iter().map(|(k, v)| (AttrName(k.to_string()), v)).collect(), + defined_vars.into_iter().map(|(k, v)| (AttrName(k.to_string()), SimplExpr::Literal(v))).collect(), )?; let gtk_widget = build_gtk_widget(graph, widget_defs, let_scope, child.clone(), custom_widget_invocation)?; let scope_graph_sender = graph.event_sender.clone(); diff --git a/crates/eww/src/widgets/def_widget_macro.rs b/crates/eww/src/widgets/def_widget_macro.rs index 73c1938a..47d4f91a 100644 --- a/crates/eww/src/widgets/def_widget_macro.rs +++ b/crates/eww/src/widgets/def_widget_macro.rs @@ -32,16 +32,16 @@ macro_rules! def_widget { // Get all the variables that are referred to in any of the attributes expressions let required_vars: Vec = attr_map .values() - .flat_map(|expr| expr.as_ref().and_then(|x| x.try_into_simplexpr()).map(|x| x.collect_var_refs()).unwrap_or_default()) + .flat_map(|expr| expr.as_ref().map(|x| x.collect_var_refs()).unwrap_or_default()) .collect(); $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| { + 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. @@ -79,7 +79,8 @@ macro_rules! def_widget { (@value_depending_on_type $values:expr, $attr_name:ident : as_action $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { match $attr_name { - Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action), + Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action.eval_exprs(&$values)?), + Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(ExecutableAction::Shell(expr.eval(&$values)?.as_string()?)), _ => None, } }; diff --git a/crates/eww/src/widgets/mod.rs b/crates/eww/src/widgets/mod.rs index a96050a9..32961c89 100644 --- a/crates/eww/src/widgets/mod.rs +++ b/crates/eww/src/widgets/mod.rs @@ -1,9 +1,8 @@ use std::process::Command; -use anyhow::Result; -use yuck::config::attr_value::Action; +use yuck::config::attr_value::ExecutableAction; -use crate::state::scope_graph::{ScopeGraph, ScopeIndex}; +use crate::state::scope_graph::{ScopeGraphEvent, ScopeIndex}; pub mod build_widget; pub mod circular_progressbar; @@ -66,13 +65,25 @@ mod test { } } -pub(self) fn run_action(graph: &mut ScopeGraph, scope: ScopeIndex, action: &Action) -> Result<()> { +pub(self) fn run_action( + sender: tokio::sync::mpsc::UnboundedSender, + scope: ScopeIndex, + timeout: std::time::Duration, + action: &ExecutableAction, + args: &[T], +) where + T: 'static + std::fmt::Display + Send + Sync + Clone, +{ match action { - Action::Update(varname, expr) => { - let value = graph.evaluate_simplexpr_in_scope(scope, expr)?; - graph.update_value(scope, varname, value)?; + ExecutableAction::Update(varname, value) => { + let res = sender.send(ScopeGraphEvent::UpdateValue(scope, varname.clone(), value.clone())); + if let Err(e) = res { + log::error!("{}", e); + } + } + ExecutableAction::Shell(command) => { + run_command(timeout, command, args); } - Action::Noop => {} + ExecutableAction::Noop => {} } - Ok(()) } diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index e82e37d3..e8251cd1 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1,5 +1,5 @@ #![allow(clippy::option_map_unit_fn)] -use super::{build_widget::BuilderArgs, circular_progressbar::*, run_command, transform::*}; +use super::{build_widget::BuilderArgs, circular_progressbar::*, transform::*}; use crate::{ def_widget, enum_parse, error::DiagError, @@ -25,7 +25,7 @@ use std::{ time::Duration, }; use yuck::{ - config::{validate::ValidationError, attr_value::Action}, + config::{attr_value::ExecutableAction, validate::ValidationError}, error::{AstError, AstResult}, gen_diagnostic, parser::from_ast::FromAst, @@ -195,6 +195,7 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Wi /// @widget !range pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Range) -> Result<()> { gtk_widget.set_sensitive(false); + let calling_scope = bargs.calling_scope.clone(); // only allow changing the value via the value property if the user isn't currently dragging let is_being_dragged = Rc::new(RefCell::new(false)); @@ -207,7 +208,7 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Ran gtk::Inhibit(false) })); - def_widget!(bargs, _g, gtk_widget, { + def_widget!(bargs, graph, gtk_widget, { // @prop value - the value prop(value: as_f64) { if !*is_being_dragged.borrow() { @@ -220,11 +221,12 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Ran prop(max: as_f64) { gtk_widget.adjustment().set_upper(max)}, // @prop timeout - timeout of the command // @prop onchange - command executed once the value is changes. The placeholder `{}`, used in the command will be replaced by the new value. - prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { + let scope_sender = graph.event_sender.clone(); gtk_widget.set_sensitive(true); gtk_widget.add_events(gdk::EventMask::PROPERTY_CHANGE_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_value_changed(move |gtk_widget| { - run_command(timeout, &onchange, &[gtk_widget.value()]); + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[gtk_widget.value()]); })); } }); @@ -246,7 +248,8 @@ pub(super) fn resolve_orientable_attrs(bargs: &mut BuilderArgs, gtk_widget: >k /// @desc A combo box allowing the user to choose between several items. fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::ComboBoxText::new(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop items - Items that should be displayed in the combo box prop(items: as_vec) { gtk_widget.remove_all(); @@ -256,9 +259,10 @@ fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result Result { /// @desc A checkbox that can trigger events on checked / unchecked. fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::CheckButton::new(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop timeout - timeout of the command // @prop onchecked - action (command) to be executed when checked by the user // @prop onunchecked - similar to onchecked but when the widget is unchecked - prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_string = "", onunchecked: as_string = "") { + prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_action?, onunchecked: as_action?) { + let scope_sender = graph.event_sender.clone(); + let onchecked = onchecked.unwrap_or(ExecutableAction::Noop); + let onunchecked = onunchecked.unwrap_or(ExecutableAction::Noop); connect_signal_handler!(gtk_widget, gtk_widget.connect_toggled(move |gtk_widget| { - run_command(timeout, if gtk_widget.is_active() { &onchecked } else { &onunchecked }, &[""]); + run_action(scope_sender.clone(), calling_scope, timeout, if gtk_widget.is_active() { &onchecked } else { &onunchecked }, &[""]); })); } }); @@ -314,15 +322,17 @@ fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result { /// @desc A button opening a color chooser window fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::ColorButtonBuilder::new().build(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop use-alpha - bool to whether or not use alpha prop(use_alpha: as_bool) {gtk_widget.set_use_alpha(use_alpha);}, // @prop onchange - runs the code when the color was selected // @prop timeout - timeout of the command - prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { + let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_color_set(move |gtk_widget| { - run_command(timeout, &onchange, &[gtk_widget.rgba()]); + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[gtk_widget.rgba()]); })); } }); @@ -334,15 +344,17 @@ fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result { /// @desc A color chooser widget fn build_gtk_color_chooser(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::ColorChooserWidget::new(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop use-alpha - bool to wether or not use alpha prop(use_alpha: as_bool) {gtk_widget.set_use_alpha(use_alpha);}, // @prop onchange - runs the code when the color was selected // @prop timeout - timeout of the command - prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { + let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_color_activated(move |_a, color| { - run_command(timeout, &onchange, &[*color]); + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[*color]); })); } }); @@ -394,7 +406,8 @@ fn build_gtk_progress(bargs: &mut BuilderArgs) -> Result { /// @desc An input field. For this to be useful, set `focusable="true"` on the window. fn build_gtk_input(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Entry::new(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop value - the content of the text field prop(value: as_string) { gtk_widget.set_text(&value); @@ -402,9 +415,10 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result { // @prop onchange - Command to run when the text changes. The placeholder `{}` will be replaced by the value // @prop timeout - timeout of the command - prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { + let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| { - run_command(timeout, &onchange, &[gtk_widget.text().to_string()]); + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[gtk_widget.text().to_string()]); })); } }); @@ -415,8 +429,9 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result { /// @desc A button fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Button::new(); + let calling_scope = bargs.calling_scope.clone(); - def_widget!(bargs, scope_graph, gtk_widget, { + def_widget!(bargs, graph, gtk_widget, { // @prop onclick - a command that get's run when the button is clicked // @prop onmiddleclick - a command that get's run when the button is middleclicked // @prop onrightclick - a command that get's run when the button is rightclicked @@ -424,20 +439,25 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { prop( timeout: as_duration = Duration::from_millis(200), onclick: as_action?, - onmiddleclick: as_string = "", - onrightclick: as_string = "" + onmiddleclick: as_action?, + onrightclick: as_action? ) { + let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); - let onclick = onclick.cloned().unwrap_or(Action::Noop); + let onclick = onclick.unwrap_or(ExecutableAction::Noop); + let onmiddleclick = onmiddleclick.unwrap_or(ExecutableAction::Noop); + let onrightclick = onrightclick.unwrap_or(ExecutableAction::Noop); connect_signal_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { match evt.button() { 1 => { - if let Err(e) = run_action(scope_graph, bargs.calling_scope, &onclick) { - log::error!("{}", e); - } - } - 2 => run_command(timeout, &onmiddleclick, &[""]), - 3 => run_command(timeout, &onrightclick, &[""]), + run_action(scope_sender.clone(), calling_scope, timeout, &onclick, &[""]); + }, + 2 => { + run_action(scope_sender.clone(), calling_scope, timeout, &onmiddleclick, &[""]); + }, + 3 => { + run_action(scope_sender.clone(), calling_scope, timeout, &onrightclick, &[""]); + }, _ => {}, } gtk::Inhibit(false) @@ -556,6 +576,7 @@ fn build_gtk_scrolledwindow(bargs: &mut BuilderArgs) -> Result Result { let gtk_widget = gtk::EventBox::new(); + let calling_scope = bargs.calling_scope.clone(); // Support :hover selector gtk_widget.connect_enter_notify_event(|gtk_widget, evt| { @@ -572,38 +593,41 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { gtk::Inhibit(false) }); - def_widget!(bargs, _g, gtk_widget, { + def_widget!(bargs, graph, gtk_widget, { // @prop timeout - timeout of the command // @prop onscroll - event to execute when the user scrolls with the mouse over the widget. The placeholder `{}` used in the command will be replaced with either `up` or `down`. - prop(timeout: as_duration = Duration::from_millis(200), onscroll: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onscroll: as_action) { + let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::SCROLL_MASK); gtk_widget.add_events(gdk::EventMask::SMOOTH_SCROLL_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_scroll_event(move |_, evt| { let delta = evt.delta().1; if delta != 0f64 { // Ignore the first event https://bugzilla.gnome.org/show_bug.cgi?id=675959 - run_command(timeout, &onscroll, &[if delta < 0f64 { "up" } else { "down" }]); + run_action(scope_sender.clone(), calling_scope, timeout, &onscroll, &[if delta < 0f64 { "up" } else { "down" }]); } gtk::Inhibit(false) })); }, // @prop timeout - timeout of the command // @prop onhover - event to execute when the user hovers over the widget - prop(timeout: as_duration = Duration::from_millis(200), onhover: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onhover: as_action) { + let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_enter_notify_event(move |_, evt| { if evt.detail() != NotifyType::Inferior { - run_command(timeout, &onhover, &[evt.position().0, evt.position().1]); + run_action(scope_sender.clone(), calling_scope, timeout, &onhover, &[evt.position().0, evt.position().1]); } gtk::Inhibit(false) })); }, // @prop timeout - timeout of the command // @prop onhoverlost - event to execute when the user losts hovers over the widget - prop(timeout: as_duration = Duration::from_millis(200), onhoverlost: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onhoverlost: as_action) { + let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::LEAVE_NOTIFY_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |_, evt| { if evt.detail() != NotifyType::Inferior { - run_command(timeout, &onhoverlost, &[evt.position().0, evt.position().1]); + run_action(scope_sender.clone(), calling_scope, timeout, &onhoverlost, &[evt.position().0, evt.position().1]); } gtk::Inhibit(false) })); @@ -635,7 +659,8 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { }, // @prop timeout - timeout of the command // @prop on_dropped - Command to execute when something is dropped on top of this element. The placeholder `{}` used in the command will be replaced with the uri to the dropped thing. - prop(timeout: as_duration = Duration::from_millis(200), ondropped: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), ondropped: as_action) { + let scope_sender = graph.event_sender.clone(); gtk_widget.drag_dest_set( DestDefaults::ALL, &[ @@ -646,9 +671,9 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { ); connect_signal_handler!(gtk_widget, gtk_widget.connect_drag_data_received(move |_, _, _x, _y, selection_data, _target_type, _timestamp| { if let Some(data) = selection_data.uris().first(){ - run_command(timeout, &ondropped, &[data.to_string(), "file".to_string()]); + run_action(scope_sender.clone(), calling_scope, timeout, &ondropped, &[data.to_string(), "file".to_string()]); } else if let Some(data) = selection_data.text(){ - run_command(timeout, &ondropped, &[data.to_string(), "text".to_string()]); + run_action(scope_sender.clone(), calling_scope, timeout, &ondropped, &[data.to_string(), "text".to_string()]); } })); }, @@ -768,7 +793,8 @@ fn build_gtk_literal(bargs: &mut BuilderArgs) -> Result { /// @desc A widget that displays a calendar fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Calendar::new(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop day - the selected day prop(day: as_f64) { gtk_widget.set_day(day as i32) }, // @prop month - the selected month @@ -785,10 +811,13 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { prop(show_week_numbers: as_bool) { gtk_widget.set_show_week_numbers(show_week_numbers) }, // @prop onclick - command to run when the user selects a date. The `{0}` placeholder will be replaced by the selected day, `{1}` will be replaced by the month, and `{2}` by the year. // @prop timeout - timeout of the command - prop(timeout: as_duration = Duration::from_millis(200), onclick: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onclick: as_action) { + let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_day_selected(move |w| { log::warn!("BREAKING CHANGE: The date is now provided via three values, set by the placeholders {{0}}, {{1}} and {{2}}. If you're currently using the onclick date, you will need to change this."); - run_command( + run_action( + scope_sender.clone(), + calling_scope, timeout, &onclick, &[w.day(), w.month(), w.year()] diff --git a/crates/simplexpr/src/eval.rs b/crates/simplexpr/src/eval.rs index 21c950b8..7f74ed11 100644 --- a/crates/simplexpr/src/eval.rs +++ b/crates/simplexpr/src/eval.rs @@ -154,6 +154,7 @@ impl SimplExpr { } } + pub fn eval(&self, values: &HashMap) -> Result { let span = self.span(); let value = match self { diff --git a/crates/yuck/src/config/attr_value.rs b/crates/yuck/src/config/attr_value.rs index 920c6417..ccbcd98e 100644 --- a/crates/yuck/src/config/attr_value.rs +++ b/crates/yuck/src/config/attr_value.rs @@ -5,7 +5,6 @@ use simplexpr::{dynval::DynVal, eval::EvalError, SimplExpr}; pub static ACTION_NAMES: &[&str] = &["update"]; -// TODO: Maybe separate that into another file #[derive(Debug, Clone)] pub enum AttrValue { Action(Action), @@ -19,10 +18,43 @@ impl AttrValue { _ => None, } } + + pub fn collect_var_refs(&self) -> Vec { + match self { + Self::SimplExpr(expr) => expr.collect_var_refs(), + Self::Action(action) => action.collect_var_refs(), + } + } } #[derive(Debug, Clone, Eq, PartialEq)] pub enum Action { Update(VarName, SimplExpr), + Shell(SimplExpr), + Noop, +} + +impl Action { + pub fn eval_exprs(&self, values: &HashMap) -> Result { + Ok(match self { + Self::Update(varname, expr) => ExecutableAction::Update(varname.clone(), expr.eval(values)?), + Self::Shell(expr) => ExecutableAction::Shell(expr.eval(values)?.as_string()?), + Self::Noop => ExecutableAction::Noop, + }) + } + + pub fn collect_var_refs(&self) -> Vec { + match self { + Self::Update(_, expr) => expr.collect_var_refs(), + Self::Shell(expr) => expr.collect_var_refs(), + Self::Noop => vec![], + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ExecutableAction { + Update(VarName, DynVal), + Shell(String), Noop, }