From ded50cca6f01f08a8e44257394fdde634d421e8e Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Thu, 22 Jun 2023 23:07:40 +0100 Subject: [PATCH] feat: support for 'ironvar' dynamic variables --- src/config/common.rs | 20 +- src/config/mod.rs | 2 + src/dynamic_string.rs | 160 -------------- src/dynamic_value/dynamic_bool.rs | 78 +++++++ src/dynamic_value/dynamic_string.rs | 321 ++++++++++++++++++++++++++++ src/dynamic_value/mod.rs | 7 + src/ipc/commands.rs | 18 +- src/ipc/responses.rs | 1 + src/ipc/server.rs | 16 ++ src/ironvar.rs | 107 ++++++++++ src/macros.rs | 42 +++- src/main.rs | 16 +- src/modules/custom/button.rs | 4 +- src/modules/custom/image.rs | 4 +- src/modules/custom/label.rs | 4 +- src/modules/custom/progress.rs | 4 +- src/modules/label.rs | 4 +- 17 files changed, 613 insertions(+), 195 deletions(-) delete mode 100644 src/dynamic_string.rs create mode 100644 src/dynamic_value/dynamic_bool.rs create mode 100644 src/dynamic_value/dynamic_string.rs create mode 100644 src/dynamic_value/mod.rs create mode 100644 src/ironvar.rs diff --git a/src/config/common.rs b/src/config/common.rs index 3bfe085..b22b38e 100644 --- a/src/config/common.rs +++ b/src/config/common.rs @@ -1,11 +1,9 @@ -use crate::dynamic_string::DynamicString; +use crate::dynamic_value::{dynamic_string, DynamicBool}; use crate::script::{Script, ScriptInput}; -use crate::send; use gtk::gdk::ScrollDirection; use gtk::prelude::*; use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType}; use serde::Deserialize; -use tokio::spawn; use tracing::trace; /// Common configuration options @@ -15,7 +13,7 @@ pub struct CommonConfig { pub class: Option, pub name: Option, - pub show_if: Option, + pub show_if: Option, pub transition_type: Option, pub transition_duration: Option, @@ -114,7 +112,7 @@ impl CommonConfig { if let Some(tooltip) = self.tooltip { let container = container.clone(); - DynamicString::new(&tooltip, move |string| { + dynamic_string(&tooltip, move |string| { container.set_tooltip_text(Some(&string)); Continue(true) }); @@ -127,23 +125,13 @@ impl CommonConfig { container.show_all(); }, |show_if| { - let script = Script::new_polling(show_if); let container = container.clone(); - let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); - - spawn(async move { - script - .run(None, |_, success| { - send!(tx, success); - }) - .await; - }); { let revealer = revealer.clone(); let container = container.clone(); - rx.attach(None, move |success| { + show_if.subscribe(move |success| { if success { container.show_all(); } diff --git a/src/config/mod.rs b/src/config/mod.rs index 1bc04c6..0ed2652 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -100,6 +100,8 @@ pub struct Config { /// GTK icon theme to use. pub icon_theme: Option, + pub ironvar_defaults: Option, String>>, + pub start: Option>, pub center: Option>, pub end: Option>, diff --git a/src/dynamic_string.rs b/src/dynamic_string.rs deleted file mode 100644 index b27e2d3..0000000 --- a/src/dynamic_string.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::script::{OutputStream, Script}; -use crate::{lock, send}; -use gtk::prelude::*; -use std::sync::{Arc, Mutex}; -use tokio::spawn; - -/// A segment of a dynamic string, -/// containing either a static string -/// or a script. -#[derive(Debug)] -enum DynamicStringSegment { - Static(String), - Dynamic(Script), -} - -/// A string with embedded scripts for dynamic content. -pub struct DynamicString; - -impl DynamicString { - /// Creates a new dynamic string, based off the input template. - /// Runs `f` with the compiled string each time one of the scripts updates. - /// - /// # Example - /// - /// ```rs - /// DynamicString::new(&text, move |string| { - /// label.set_markup(&string); - /// Continue(true) - /// }); - /// ``` - pub fn new(input: &str, f: F) -> Self - where - F: FnMut(String) -> Continue + 'static, - { - let segments = Self::parse_input(input); - - let label_parts = Arc::new(Mutex::new(Vec::new())); - let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); - - for (i, segment) in segments.into_iter().enumerate() { - match segment { - DynamicStringSegment::Static(str) => { - lock!(label_parts).push(str); - } - DynamicStringSegment::Dynamic(script) => { - let tx = tx.clone(); - let label_parts = label_parts.clone(); - - // insert blank value to preserve segment order - lock!(label_parts).push(String::new()); - - spawn(async move { - script - .run(None, |out, _| { - if let OutputStream::Stdout(out) = out { - let mut label_parts = lock!(label_parts); - - let _: String = std::mem::replace(&mut label_parts[i], out); - - let string = label_parts.join(""); - send!(tx, string); - } - }) - .await; - }); - } - } - } - - // initialize - { - let label_parts = lock!(label_parts).join(""); - send!(tx, label_parts); - } - - rx.attach(None, f); - - Self - } - - /// Parses the input string into static and dynamic segments - fn parse_input(input: &str) -> Vec { - if !input.contains("{{") { - return vec![DynamicStringSegment::Static(input.to_string())]; - } - - let mut segments = vec![]; - - let mut chars = input.chars().collect::>(); - while !chars.is_empty() { - let char_pair = if chars.len() > 1 { - Some(&chars[..=1]) - } else { - None - }; - - let (token, skip) = if let Some(['{', '{']) = char_pair { - const SKIP_BRACKETS: usize = 4; // two braces either side - - let str = chars - .windows(2) - .skip(2) - .take_while(|win| win != &['}', '}']) - .map(|w| w[0]) - .collect::(); - - let len = str.len(); - - ( - DynamicStringSegment::Dynamic(Script::from(str.as_str())), - len + SKIP_BRACKETS, - ) - } else { - let mut str = chars - .windows(2) - .take_while(|win| win != &['{', '{']) - .map(|w| w[0]) - .collect::(); - - // if segment is at end of string, last char gets missed above due to uneven window. - if chars.len() == str.len() + 1 { - let remaining_char = *chars.get(str.len()).expect("Failed to find last char"); - str.push(remaining_char); - } - - let len = str.len(); - - (DynamicStringSegment::Static(str), len) - }; - - // quick runtime check to make sure the parser is working as expected - assert_ne!(skip, 0); - - segments.push(token); - chars.drain(..skip); - } - - segments - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test() { - // TODO: see if we can run gtk tests in ci - if gtk::init().is_ok() { - let label = gtk::Label::new(None); - DynamicString::new( - "Uptime: {{1000:uptime -p | cut -d ' ' -f2-}}", - move |string| { - label.set_label(&string); - Continue(true) - }, - ); - } - } -} diff --git a/src/dynamic_value/dynamic_bool.rs b/src/dynamic_value/dynamic_bool.rs new file mode 100644 index 0000000..094408f --- /dev/null +++ b/src/dynamic_value/dynamic_bool.rs @@ -0,0 +1,78 @@ +#[cfg(feature = "ipc")] +use crate::ironvar::get_variable_manager; +use crate::script::Script; +use crate::send; +use cfg_if::cfg_if; +use glib::Continue; +use serde::Deserialize; +use tokio::spawn; + +#[derive(Debug, Deserialize, Clone)] +#[serde(untagged)] +pub enum DynamicBool { + /// Either a script or variable, to be determined. + Unknown(String), + Script(Script), + #[cfg(feature = "ipc")] + Variable(Box), +} + +impl DynamicBool { + pub fn subscribe(self, f: F) + where + F: FnMut(bool) -> Continue + 'static, + { + let value = match self { + Self::Unknown(input) => { + if input.starts_with('#') { + cfg_if! { + if #[cfg(feature = "ipc")] { + Self::Variable(input.into()) + } else { + Self::Unknown(input) + } + } + } else { + let script = Script::from(input.as_str()); + Self::Script(script) + } + } + _ => self, + }; + + let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); + + rx.attach(None, f); + + spawn(async move { + match value { + DynamicBool::Script(script) => { + script + .run(None, |_, success| { + send!(tx, success); + }) + .await; + } + #[cfg(feature = "ipc")] + DynamicBool::Variable(variable) => { + let variable_manager = get_variable_manager(); + + let variable_name = variable[1..].into(); // remove hash + let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name); + + while let Ok(value) = rx.recv().await { + let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default(); + send!(tx, has_value); + } + } + DynamicBool::Unknown(_) => unreachable!(), + } + }); + } +} + +/// Check if a string ironvar is 'truthy' +#[cfg(feature = "ipc")] +fn is_truthy(string: &str) -> bool { + !(string.is_empty() || string == "0" || string == "false") +} diff --git a/src/dynamic_value/dynamic_string.rs b/src/dynamic_value/dynamic_string.rs new file mode 100644 index 0000000..d9332f0 --- /dev/null +++ b/src/dynamic_value/dynamic_string.rs @@ -0,0 +1,321 @@ +#[cfg(feature = "ipc")] +use crate::ironvar::get_variable_manager; +use crate::script::{OutputStream, Script}; +use crate::{lock, send}; +use gtk::prelude::*; +use std::sync::{Arc, Mutex}; +use tokio::spawn; + +/// A segment of a dynamic string, +/// containing either a static string +/// or a script. +#[derive(Debug)] +enum DynamicStringSegment { + Static(String), + Script(Script), + #[cfg(feature = "ipc")] + Variable(Box), +} + +/// Creates a new dynamic string, based off the input template. +/// Runs `f` with the compiled string each time one of the scripts or variables updates. +/// +/// # Example +/// +/// ```rs +/// dynamic_string(&text, move |string| { +/// label.set_markup(&string); +/// Continue(true) +/// }); +/// ``` +pub fn dynamic_string(input: &str, f: F) +where + F: FnMut(String) -> Continue + 'static, +{ + let tokens = parse_input(input); + + let label_parts = Arc::new(Mutex::new(Vec::new())); + let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); + + for (i, segment) in tokens.into_iter().enumerate() { + match segment { + DynamicStringSegment::Static(str) => { + lock!(label_parts).push(str); + } + DynamicStringSegment::Script(script) => { + let tx = tx.clone(); + let label_parts = label_parts.clone(); + + // insert blank value to preserve segment order + lock!(label_parts).push(String::new()); + + spawn(async move { + script + .run(None, |out, _| { + if let OutputStream::Stdout(out) = out { + let mut label_parts = lock!(label_parts); + + let _: String = std::mem::replace(&mut label_parts[i], out); + + let string = label_parts.join(""); + send!(tx, string); + } + }) + .await; + }); + } + #[cfg(feature = "ipc")] + DynamicStringSegment::Variable(name) => { + let tx = tx.clone(); + let label_parts = label_parts.clone(); + + // insert blank value to preserve segment order + lock!(label_parts).push(String::new()); + + spawn(async move { + let variable_manager = get_variable_manager(); + let mut rx = crate::write_lock!(variable_manager).subscribe(name); + + while let Ok(value) = rx.recv().await { + if let Some(value) = value { + let mut label_parts = lock!(label_parts); + + let _: String = std::mem::replace(&mut label_parts[i], value); + + let string = label_parts.join(""); + send!(tx, string); + } + } + }); + } + } + } + + rx.attach(None, f); + + // initialize + { + let label_parts = lock!(label_parts).join(""); + send!(tx, label_parts); + } +} + +/// Parses the input string into static and dynamic segments +fn parse_input(input: &str) -> Vec { + // short-circuit parser if it's all static + if !input.contains("{{") && !input.contains('#') { + return vec![DynamicStringSegment::Static(input.to_string())]; + } + + let mut tokens = vec![]; + + let mut chars = input.chars().collect::>(); + while !chars.is_empty() { + let char_pair = if chars.len() > 1 { + Some(&chars[..=1]) + } else { + None + }; + + let (token, skip) = match char_pair { + Some(['{', '{']) => parse_script(&chars), + Some(['#', '#']) => (DynamicStringSegment::Static("#".to_string()), 2), + #[cfg(feature = "ipc")] + Some(['#', _]) => parse_variable(&chars), + _ => parse_static(&chars), + }; + + // quick runtime check to make sure the parser is working as expected + assert_ne!(skip, 0); + + tokens.push(token); + chars.drain(..skip); + } + + tokens +} + +fn parse_script(chars: &[char]) -> (DynamicStringSegment, usize) { + const SKIP_BRACKETS: usize = 4; // two braces either side + + let str = chars + .windows(2) + .skip(2) + .take_while(|win| win != &['}', '}']) + .map(|w| w[0]) + .collect::(); + + let len = str.len() + SKIP_BRACKETS; + let script = Script::from(str.as_str()); + + (DynamicStringSegment::Script(script), len) +} + +#[cfg(feature = "ipc")] +fn parse_variable(chars: &[char]) -> (DynamicStringSegment, usize) { + const SKIP_HASH: usize = 1; + + let str = chars + .iter() + .skip(1) + .take_while(|&c| !c.is_whitespace()) + .collect::(); + + let len = str.len() + SKIP_HASH; + let value = str.into(); + + (DynamicStringSegment::Variable(value), len) +} + +fn parse_static(chars: &[char]) -> (DynamicStringSegment, usize) { + let mut str = chars + .windows(2) + .take_while(|&win| win != ['{', '{'] && win[0] != '#') + .map(|w| w[0]) + .collect::(); + + // if segment is at end of string, last char gets missed above due to uneven window. + if chars.len() == str.len() + 1 { + let remaining_char = *chars.get(str.len()).expect("Failed to find last char"); + str.push(remaining_char); + } + + let len = str.len(); + + (DynamicStringSegment::Static(str), len) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_static() { + const INPUT: &str = "hello world"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 1); + assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT)) + } + + #[test] + fn test_static_odd_char_count() { + const INPUT: &str = "hello"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 1); + assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT)) + } + + #[test] + fn test_script() { + const INPUT: &str = "{{echo hello}}"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 1); + assert!( + matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo hello") + ); + } + + #[test] + fn test_variable() { + const INPUT: &str = "#variable"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 1); + assert!( + matches!(&tokens[0], DynamicStringSegment::Variable(name) if name.to_string() == "variable") + ); + } + + #[test] + fn test_static_script() { + const INPUT: &str = "hello {{echo world}}"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 2); + assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello ")); + assert!( + matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world") + ); + } + + #[test] + fn test_static_variable() { + const INPUT: &str = "hello #subject"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 2); + assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello ")); + assert!( + matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject") + ); + } + + #[test] + fn test_static_script_static() { + const INPUT: &str = "hello {{echo world}} foo"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 3); + assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello ")); + assert!( + matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world") + ); + assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo")); + } + + #[test] + fn test_static_variable_static() { + const INPUT: &str = "hello #subject foo"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 3); + assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello ")); + assert!( + matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject") + ); + assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo")); + } + + #[test] + fn test_static_script_variable() { + const INPUT: &str = "hello {{echo world}} #foo"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 4); + assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello ")); + assert!( + matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world") + ); + assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " ")); + assert!( + matches!(&tokens[3], DynamicStringSegment::Variable(name) if name.to_string() == "foo") + ); + } + + #[test] + fn test_escape_hash() { + const INPUT: &str = "number ###num"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 3); + assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "number ")); + assert!(matches!(&tokens[1], DynamicStringSegment::Static(str) if str == "#")); + assert!( + matches!(&tokens[2], DynamicStringSegment::Variable(name) if name.to_string() == "num") + ); + } + + #[test] + fn test_script_with_hash() { + const INPUT: &str = "{{echo #hello}}"; + let tokens = parse_input(INPUT); + + assert_eq!(tokens.len(), 1); + assert!( + matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo #hello") + ); + } +} diff --git a/src/dynamic_value/mod.rs b/src/dynamic_value/mod.rs new file mode 100644 index 0000000..83ad88c --- /dev/null +++ b/src/dynamic_value/mod.rs @@ -0,0 +1,7 @@ +#![doc = include_str!("../../docs/Dynamic values.md")] + +mod dynamic_bool; +mod dynamic_string; + +pub use dynamic_bool::DynamicBool; +pub use dynamic_string::dynamic_string; diff --git a/src/ipc/commands.rs b/src/ipc/commands.rs index 51fc4d3..11e1cfb 100644 --- a/src/ipc/commands.rs +++ b/src/ipc/commands.rs @@ -10,5 +10,21 @@ pub enum Command { /// Open the GTK inspector Inspect, -} + /// Set an `ironvar` value. + /// This creates it if it does not already exist, and updates it if it does. + /// Any references to this variable are automatically and immediately updated. + /// Keys and values can be any valid UTF-8 string. + Set { + /// Variable key. Can be any valid UTF-8 string. + key: Box, + /// Variable value. Can be any valid UTF-8 string. + value: String, + }, + + /// Get the current value of an `ironvar`. + Get { + /// Variable key. + key: Box, + }, +} diff --git a/src/ipc/responses.rs b/src/ipc/responses.rs index 506732c..df5a802 100644 --- a/src/ipc/responses.rs +++ b/src/ipc/responses.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; #[serde(tag = "type")] pub enum Response { Ok, + OkValue { value: String }, Err { message: Option }, } diff --git a/src/ipc/server.rs b/src/ipc/server.rs index d24ed48..816e624 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -106,6 +106,22 @@ impl Ipc { Command::Inspect => { gtk::Window::set_interactive_debugging(true); Response::Ok + }, + Command::Set { key, value } => { + let variable_manager = get_variable_manager(); + let mut variable_manager = write_lock!(variable_manager); + match variable_manager.set(key, value) { + Ok(_) => Response::Ok, + Err(err) => Response::error(&format!("{err}")), + } + } + Command::Get { key } => { + let variable_manager = get_variable_manager(); + let value = read_lock!(variable_manager).get(&key); + match value { + Some(value) => Response::OkValue { value }, + None => Response::error("Variable not found"), + } } Command::Ping => Response::Ok, } diff --git a/src/ironvar.rs b/src/ironvar.rs new file mode 100644 index 0000000..a83f07f --- /dev/null +++ b/src/ironvar.rs @@ -0,0 +1,107 @@ +#![doc = include_str!("../docs/Ironvars.md")] + +use crate::{arc_rw, send}; +use color_eyre::{Report, Result}; +use lazy_static::lazy_static; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use tokio::sync::broadcast; + +lazy_static! { + static ref VARIABLE_MANAGER: Arc> = arc_rw!(VariableManager::new()); +} + +pub fn get_variable_manager() -> Arc> { + VARIABLE_MANAGER.clone() +} + +/// Global singleton manager for `IronVar` variables. +pub struct VariableManager { + variables: HashMap, IronVar>, +} + +impl VariableManager { + pub fn new() -> Self { + Self { + variables: HashMap::new(), + } + } + + /// Sets the value for a variable, + /// creating it if it does not exist. + pub fn set(&mut self, key: Box, value: String) -> Result<()> { + if Self::key_is_valid(&key) { + if let Some(var) = self.variables.get_mut(&key) { + var.set(Some(value)); + } else { + let var = IronVar::new(Some(value)); + self.variables.insert(key, var); + } + + Ok(()) + } else { + Err(Report::msg("Invalid key")) + } + } + + /// Gets the current value of an `ironvar`. + /// Prefer to use `subscribe` where possible. + pub fn get(&self, key: &str) -> Option { + self.variables.get(key).and_then(IronVar::get) + } + + /// Subscribes to an `ironvar`, creating it if it does not exist. + /// Any time the var is set, its value is sent on the channel. + pub fn subscribe(&mut self, key: Box) -> broadcast::Receiver> { + self.variables + .entry(key) + .or_insert_with(|| IronVar::new(None)) + .subscribe() + } + + fn key_is_valid(key: &str) -> bool { + !key.is_empty() + && key + .chars() + .all(|char| char.is_alphanumeric() || char == '_' || char == '-') + } +} + +/// Ironbar dynamic variable representation. +/// Interact with them through the `VARIABLE_MANAGER` `VariableManager` singleton. +#[derive(Debug)] +struct IronVar { + value: Option, + tx: broadcast::Sender>, + _rx: broadcast::Receiver>, +} + +impl IronVar { + /// Creates a new variable. + fn new(value: Option) -> Self { + let (tx, rx) = broadcast::channel(32); + + Self { value, tx, _rx: rx } + } + + /// Gets the current variable value. + /// Prefer to subscribe to changes where possible. + fn get(&self) -> Option { + self.value.clone() + } + + /// Sets the current variable value. + /// The change is broadcast to all receivers. + fn set(&mut self, value: Option) { + self.value = value.clone(); + send!(self.tx, value); + } + + /// Subscribes to the variable. + /// The latest value is immediately sent to all receivers. + fn subscribe(&self) -> broadcast::Receiver> { + let rx = self.tx.subscribe(); + send!(self.tx, self.value.clone()); + rx + } +} diff --git a/src/macros.rs b/src/macros.rs index 49e91a2..fa2ea30 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,7 +1,7 @@ /// Sends a message on an asynchronous `Sender` using `send()` /// Panics if the message cannot be sent. /// -/// Usage: +/// # Usage: /// /// ```rs /// send_async!(tx, "my message"); @@ -16,7 +16,7 @@ macro_rules! send_async { /// Sends a message on an synchronous `Sender` using `send()` /// Panics if the message cannot be sent. /// -/// Usage: +/// # Usage: /// /// ```rs /// send!(tx, "my message"); @@ -31,7 +31,7 @@ macro_rules! send { /// Sends a message on an synchronous `Sender` using `try_send()` /// Panics if the message cannot be sent. /// -/// Usage: +/// # Usage: /// /// ```rs /// try_send!(tx, "my message"); @@ -46,7 +46,7 @@ macro_rules! try_send { /// Locks a `Mutex`. /// Panics if the `Mutex` cannot be locked. /// -/// Usage: +/// # Usage: /// /// ```rs /// let mut val = lock!(my_mutex); @@ -62,7 +62,7 @@ macro_rules! lock { /// Gets a read lock on a `RwLock`. /// Panics if the `RwLock` cannot be locked. /// -/// Usage: +/// # Usage: /// /// ```rs /// let val = read_lock!(my_rwlock); @@ -77,7 +77,7 @@ macro_rules! read_lock { /// Gets a write lock on a `RwLock`. /// Panics if the `RwLock` cannot be locked. /// -/// Usage: +/// # Usage: /// /// ```rs /// let mut val = write_lock!(my_rwlock); @@ -88,3 +88,33 @@ macro_rules! write_lock { $rwlock.write().expect($crate::error::ERR_WRITE_LOCK) }; } + +/// Wraps `val` in a new `Arc>`. +/// +/// # Usage: +/// +/// ```rs +/// let val = arc_mut!(MyService::new()); +/// ``` +/// +#[macro_export] +macro_rules! arc_mut { + ($val:expr) => { + std::sync::Arc::new(std::Sync::Mutex::new($val)) + }; +} + +/// Wraps `val` in a new `Arc>`. +/// +/// # Usage: +/// +/// ```rs +/// let val = arc_rw!(MyService::new()); +/// ``` +/// +#[macro_export] +macro_rules! arc_rw { + ($val:expr) => { + std::sync::Arc::new(std::sync::RwLock::new($val)) + }; +} diff --git a/src/main.rs b/src/main.rs index 6ddcfb0..6cd5022 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,12 +7,14 @@ mod cli; mod clients; mod config; mod desktop_file; -mod dynamic_string; +mod dynamic_value; mod error; mod gtk_helpers; mod image; #[cfg(feature = "ipc")] mod ipc; +#[cfg(feature = "ipc")] +mod ironvar; mod logging; mod macros; mod modules; @@ -119,7 +121,7 @@ async fn start_ironbar() { ConfigLoader::load, ); - let config = match config_res { + let mut config: Config = match config_res { Ok(config) => config, Err(err) => { error!("{:?}", err); @@ -129,6 +131,16 @@ async fn start_ironbar() { debug!("Loaded config file"); + #[cfg(feature = "ipc")] + if let Some(ironvars) = config.ironvar_defaults.take() { + let variable_manager = ironvar::get_variable_manager(); + for (k, v) in ironvars { + if write_lock!(variable_manager).set(k.clone(), v).is_err() { + tracing::warn!("Ignoring invalid ironvar: '{k}'"); + } + } + } + if let Err(err) = create_bars(app, &display, wayland_client, &config) { error!("{:?}", err); exit(ExitCode::CreateBars as i32); diff --git a/src/modules/custom/button.rs b/src/modules/custom/button.rs index bb67a88..f9ea7a2 100644 --- a/src/modules/custom/button.rs +++ b/src/modules/custom/button.rs @@ -1,5 +1,5 @@ use super::{CustomWidget, CustomWidgetContext, ExecEvent}; -use crate::dynamic_string::DynamicString; +use crate::dynamic_value::dynamic_string; use crate::popup::Popup; use crate::{build, try_send}; use gtk::prelude::*; @@ -25,7 +25,7 @@ impl CustomWidget for ButtonWidget { label.set_use_markup(true); button.add(&label); - DynamicString::new(&text, move |string| { + dynamic_string(&text, move |string| { label.set_markup(&string); Continue(true) }); diff --git a/src/modules/custom/image.rs b/src/modules/custom/image.rs index 3ae11fb..edbcc26 100644 --- a/src/modules/custom/image.rs +++ b/src/modules/custom/image.rs @@ -1,6 +1,6 @@ use super::{CustomWidget, CustomWidgetContext}; use crate::build; -use crate::dynamic_string::DynamicString; +use crate::dynamic_value::dynamic_string; use crate::image::ImageProvider; use gtk::prelude::*; use gtk::Image; @@ -29,7 +29,7 @@ impl CustomWidget for ImageWidget { let gtk_image = gtk_image.clone(); let icon_theme = context.icon_theme.clone(); - DynamicString::new(&self.src, move |src| { + dynamic_string(&self.src, move |src| { ImageProvider::parse(&src, &icon_theme, self.size) .map(|image| image.load_into_image(gtk_image.clone())); diff --git a/src/modules/custom/label.rs b/src/modules/custom/label.rs index 4b9d682..8fa3d27 100644 --- a/src/modules/custom/label.rs +++ b/src/modules/custom/label.rs @@ -1,6 +1,6 @@ use super::{CustomWidget, CustomWidgetContext}; use crate::build; -use crate::dynamic_string::DynamicString; +use crate::dynamic_value::dynamic_string; use gtk::prelude::*; use gtk::Label; use serde::Deserialize; @@ -22,7 +22,7 @@ impl CustomWidget for LabelWidget { { let label = label.clone(); - DynamicString::new(&self.label, move |string| { + dynamic_string(&self.label, move |string| { label.set_markup(&string); Continue(true) }); diff --git a/src/modules/custom/progress.rs b/src/modules/custom/progress.rs index d6bd285..c7d1eba 100644 --- a/src/modules/custom/progress.rs +++ b/src/modules/custom/progress.rs @@ -1,5 +1,5 @@ use super::{try_get_orientation, CustomWidget, CustomWidgetContext}; -use crate::dynamic_string::DynamicString; +use crate::dynamic_value::dynamic_string; use crate::modules::custom::set_length; use crate::script::{OutputStream, Script, ScriptInput}; use crate::{build, send}; @@ -69,7 +69,7 @@ impl CustomWidget for ProgressWidget { let progress = progress.clone(); progress.set_show_text(true); - DynamicString::new(&text, move |string| { + dynamic_string(&text, move |string| { progress.set_text(Some(&string)); Continue(true) }); diff --git a/src/modules/label.rs b/src/modules/label.rs index 0ca67c7..a10a362 100644 --- a/src/modules/label.rs +++ b/src/modules/label.rs @@ -1,5 +1,5 @@ use crate::config::CommonConfig; -use crate::dynamic_string::DynamicString; +use crate::dynamic_value::dynamic_string; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::try_send; use color_eyre::Result; @@ -31,7 +31,7 @@ impl Module