diff --git a/Cargo.lock b/Cargo.lock index 2acddd0..491a809 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,6 +317,7 @@ dependencies = [ "maplit", "notify", "num", + "popol", "pretty_assertions", "pretty_env_logger", "ref-cast", @@ -1269,6 +1270,15 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" +[[package]] +name = "popol" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bb69490e466db39a4442cc36fa3a593cbe94f688c55ecd2568dc1535c4fabf" +dependencies = [ + "libc", +] + [[package]] name = "ppv-lite86" version = "0.2.9" diff --git a/Cargo.toml b/Cargo.toml index 6d8b156..5c4deeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ pretty_env_logger = "0.4" lazy_static = "1.4.0" libc = "0.2" ref-cast = "1.0" +popol = "0.3" [dev-dependencies] pretty_assertions = "0.6.1" diff --git a/src/app.rs b/src/app.rs index ae8ae3d..ccb6f9f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -136,7 +136,7 @@ impl App { pub fn reload_all_windows(&mut self, config: config::EwwConfig) -> Result<()> { // refresh script-var poll stuff - if let Err(e) = self.script_var_handler.setup_command_poll_tasks(&config) { + if let Err(e) = self.script_var_handler.initialize_clean(config.get_script_vars().clone()) { eprintln!("Error while setting up script-var commands: {:?}", e); } diff --git a/src/config/element.rs b/src/config/element.rs index 07f5b62..430d518 100644 --- a/src/config/element.rs +++ b/src/config/element.rs @@ -28,9 +28,7 @@ impl WidgetDefinition { ); } - let size: Option<_> = try { - (xml.attr("width").ok()?, xml.attr("height").ok()?) - }; + let size: Option<_> = Option::zip(xml.attr("width").ok(), xml.attr("height").ok()); let size: Option> = size.map(|(x, y)| Ok((x.parse()?, y.parse()?))); WidgetDefinition { @@ -107,11 +105,7 @@ impl WidgetUse { pub fn from_text_with_var_refs(text: &str) -> Self { WidgetUse { - name: "layout".to_owned(), - attrs: hashmap! { - "halign".to_owned() => AttrValue::Concrete(PrimitiveValue::String("center".to_owned())), - "space-evenly".to_owned() => AttrValue::Concrete(PrimitiveValue::String("false".to_owned())), - }, + name: "text".to_owned(), children: parse_string_with_var_refs(text) .into_iter() .map(StringOrVarRef::to_attr_value) diff --git a/src/config/mod.rs b/src/config/mod.rs index c83c43c..23ae2fb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -29,20 +29,50 @@ macro_rules! ensure_xml_tag_is { } #[derive(Clone, Debug, PartialEq)] -pub struct ScriptVar { +pub struct PollScriptVar { pub name: VarName, pub command: String, pub interval: std::time::Duration, } +#[derive(Clone, Debug, PartialEq)] +pub struct TailScriptVar { + pub name: VarName, + pub command: String, +} + +#[derive(Clone, Debug, PartialEq)] +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 { + match self { + ScriptVar::Poll(x) => Ok(crate::run_command(&x.command)?), + ScriptVar::Tail(_) => Ok(PrimitiveValue::String(String::new())), + } + } + pub fn from_xml_element(xml: XmlElement) -> Result { ensure_xml_tag_is!(xml, "script-var"); let name = VarName(xml.attr("name")?.to_owned()); - let interval = util::parse_duration(xml.attr("interval")?)?; let command = xml.only_child()?.as_text()?.text(); - Ok(ScriptVar { name, interval, command }) + if let Ok(interval) = xml.attr("interval") { + let interval = util::parse_duration(interval)?; + Ok(ScriptVar::Poll(PollScriptVar { name, command, interval })) + } else { + Ok(ScriptVar::Tail(TailScriptVar { name, command })) + } } } @@ -125,7 +155,7 @@ impl EwwConfig { let mut vars = self .script_vars .iter() - .map(|var| Ok((var.name.clone(), crate::eww_state::run_command(&var.command)?))) + .map(|var| Ok((var.name().clone(), var.initial_value()?))) .collect::>>()?; vars.extend(self.get_default_vars().clone()); Ok(vars) diff --git a/src/eww_state.rs b/src/eww_state.rs index f91fa20..5bf7050 100644 --- a/src/eww_state.rs +++ b/src/eww_state.rs @@ -206,7 +206,7 @@ impl EwwState { /// Run a command and get the output pub fn run_command(cmd: &str) -> Result { - let output = String::from_utf8(Command::new("/bin/bash").arg("-c").arg(cmd).output()?.stdout)?; + let output = String::from_utf8(Command::new("/bin/sh").arg("-c").arg(cmd).output()?.stdout)?; let output = output.trim_matches('\n'); Ok(PrimitiveValue::from(output)) } diff --git a/src/main.rs b/src/main.rs index bb7178d..63a0f74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,7 @@ fn main() { } } -#[derive(StructOpt, Debug, Serialize, Deserialize)] +#[derive(StructOpt, Debug, Serialize, Deserialize, PartialEq)] pub struct Opt { #[structopt(short = "-c", parse(from_os_str))] config_file: Option, @@ -67,7 +67,7 @@ pub struct Opt { #[structopt(short = "-d", long = "--detach")] should_detach: bool, } -#[derive(StructOpt, Debug, Serialize, Deserialize)] +#[derive(StructOpt, Debug, Serialize, Deserialize, PartialEq)] pub enum OptAction { #[structopt(name = "update")] Update { fieldname: VarName, value: PrimitiveValue }, @@ -122,6 +122,10 @@ fn get_config_file_path() -> PathBuf { } fn initialize_server(opts: Opt) -> Result<()> { + if opts.action == OptAction::KillServer { + return Ok(()); + } + let config_file_path = opts.config_file.clone().unwrap_or_else(get_config_file_path); let config_dir = config_file_path .clone() @@ -138,7 +142,7 @@ fn initialize_server(opts: Opt) -> Result<()> { let (evt_send, evt_recv) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let mut script_var_handler = script_var_handler::ScriptVarHandler::new(evt_send.clone())?; - script_var_handler.setup_command_poll_tasks(&eww_config)?; + script_var_handler.initialize_clean(eww_config.get_script_vars().clone())?; let mut app = app::App { eww_state: EwwState::from_default_vars(eww_config.generate_initial_state()?.clone()), diff --git a/src/script_var_handler.rs b/src/script_var_handler.rs index 35bac1c..024ae7a 100644 --- a/src/script_var_handler.rs +++ b/src/script_var_handler.rs @@ -1,38 +1,75 @@ -use crate::{app, config, eww_state}; +use std::{ + collections::HashMap, + io::BufReader, + process::{ChildStdout, Stdio}, + time::Duration, +}; + +use crate::{ + app, config, eww_state, + value::{PrimitiveValue, VarName}, +}; use anyhow::*; use glib; use itertools::Itertools; use scheduled_executor; +use std::io::BufRead; + +/// Handler that manages running and updating [ScriptVar]s pub struct ScriptVarHandler { evt_send: glib::Sender, pub poll_handles: Vec, pub poll_executor: scheduled_executor::CoreExecutor, + pub tail_handler_thread: Option>, } impl ScriptVarHandler { pub fn new(evt_send: glib::Sender) -> Result { - log::info!("initializing handler for poll script vars"); Ok(ScriptVarHandler { evt_send, poll_handles: Vec::new(), poll_executor: scheduled_executor::CoreExecutor::new()?, + tail_handler_thread: None, }) } - /// clears and stops the currently running poll handles, then opens the new - /// ones as configured - pub fn setup_command_poll_tasks(&mut self, config: &config::EwwConfig) -> Result<()> { - log::info!("reloading handler for poll script vars"); + /// stop all running handlers + pub fn stop(&mut self) { + self.poll_handles.iter().for_each(|handle| handle.stop()); + self.poll_handles.clear(); + self.tail_handler_thread.take().map(|handle| handle.stop()); + } + + /// initialize this handler, cleaning up any previously ran executors and + /// threads. + pub fn initialize_clean(&mut self, script_vars: Vec) -> Result<()> { + self.stop(); + + let mut poll_script_vars = Vec::new(); + let mut tail_script_vars = Vec::new(); + for var in script_vars { + match var { + config::ScriptVar::Poll(x) => poll_script_vars.push(x), + config::ScriptVar::Tail(x) => tail_script_vars.push(x), + } + } + self.setup_poll_tasks(&poll_script_vars)?; + self.setup_tail_tasks(&tail_script_vars)?; + Ok(()) + } + + /// initialize the poll handler thread. + fn setup_poll_tasks(&mut self, poll_script_vars: &[config::PollScriptVar]) -> Result<()> { + log::info!("initializing handler for poll script vars"); self.poll_handles.iter().for_each(|handle| handle.stop()); self.poll_handles.clear(); let evt_send = self.evt_send.clone(); - self.poll_handles = config - .get_script_vars() + self.poll_handles = poll_script_vars .iter() .map(|var| { self.poll_executor.schedule_fixed_interval( - std::time::Duration::from_secs(0), + Duration::from_secs(0), var.interval, glib::clone!(@strong var, @strong evt_send => move |_| { let result = eww_state::run_command(&var.command) @@ -46,4 +83,73 @@ impl ScriptVarHandler { .collect_vec(); Ok(()) } + + /// initialize the tail_var handler thread + pub fn setup_tail_tasks(&mut self, tail_script_vars: &[config::TailScriptVar]) -> Result<()> { + log::info!("initializing handler for tail script vars"); + let mut sources = popol::Sources::with_capacity(tail_script_vars.len()); + + let mut command_out_readers: HashMap> = tail_script_vars + .iter() + .filter_map(|var| Some((var.name.clone(), try_run_command(&var.command)?))) + .collect(); + + for (var_name, reader) in command_out_readers.iter() { + sources.register(var_name.clone(), reader.get_ref(), popol::interest::READ); + } + + let mut events = popol::Events::with_capacity(tail_script_vars.len()); + let evt_send = self.evt_send.clone(); + // TODO this is rather ugly + let thread_handle = stoppable_thread::spawn(move |stopped| { + while !stopped.get() { + let result: Result<_> = try { + sources.wait(&mut events)?; + for (var_name, event) in events.iter() { + if event.readable { + let handle = command_out_readers + .get_mut(var_name) + .with_context(|| format!("No command output handle found for variable '{}'", var_name))?; + let mut buffer = String::new(); + handle.read_line(&mut buffer)?; + evt_send.send(app::EwwEvent::UpdateVar( + var_name.clone(), + PrimitiveValue::parse_string(&buffer), + ))?; + } else if event.hangup { + command_out_readers.remove(var_name); + } + } + }; + if let Err(err) = result { + eprintln!("Error in script-var tail handler thread: {:?}", err); + continue; + } + } + }); + self.tail_handler_thread = Some(thread_handle); + Ok(()) + } +} + +/// Run a command in sh, returning its stdout-handle wrapped in a +/// [`BufReader`]. If running the command fails, will print a warning +/// and return `None`. +fn try_run_command(command: &str) -> Option> { + let result = std::process::Command::new("sh") + .arg("-c") + .arg(command) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .stdin(Stdio::null()) + .spawn() + .map(|mut x| x.stdout.take().unwrap()); + + match result { + Ok(stdout) => Some(BufReader::new(stdout)), + Err(err) => { + eprintln!("WARN: Error running command from script-variable: {:?}", err); + None + } + } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 569dddd..e103147 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -17,7 +17,7 @@ const CMD_STRING_PLACEHODLER: &str = "{}"; /// placeholder ('{}') which will be replaced by the value provided as [arg] pub fn run_command(cmd: &str, arg: T) { let cmd = cmd.replace(CMD_STRING_PLACEHODLER, &format!("{}", arg)); - if let Err(e) = Command::new("bash").arg("-c").arg(cmd).output() { + if let Err(e) = Command::new("/bin/sh").arg("-c").arg(cmd).output() { eprintln!("{}", e); } } diff --git a/src/widgets/widget_definitions.rs b/src/widgets/widget_definitions.rs index 1cf98b7..c91a9ee 100644 --- a/src/widgets/widget_definitions.rs +++ b/src/widgets/widget_definitions.rs @@ -5,7 +5,6 @@ use crate::{ }; use anyhow::*; use gtk::{prelude::*, ImageExt}; -use maplit::hashmap; use std::{cell::RefCell, path::Path, rc::Rc}; use gdk_pixbuf; @@ -187,21 +186,10 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result { Ok(gtk_widget) } -// TODO this is rather ugly,..... -fn build_gtk_text(bargs: &mut BuilderArgs) -> Result { - let text = bargs - .widget - .children - .first() - .context("text node must contain exactly one child")? - .get_attr("text")?; - let gtk_widget = gtk::Label::new(None); - bargs.eww_state.resolve( - bargs.window_name, - bargs.local_env, - hashmap! {"text".to_owned() => text.clone() }, - glib::clone!(@strong gtk_widget => move |v| { gtk_widget.set_text(&v.get("text").unwrap().as_string().unwrap()); Ok(())}), - ); +fn build_gtk_text(_bargs: &mut BuilderArgs) -> Result { + let gtk_widget = gtk::Box::new(gtk::Orientation::Horizontal, 0); + gtk_widget.set_halign(gtk::Align::Center); + gtk_widget.set_homogeneous(false); Ok(gtk_widget) }