diff --git a/docs/Ironvars.md b/docs/Ironvars.md index 975fd55..2af055f 100644 --- a/docs/Ironvars.md +++ b/docs/Ironvars.md @@ -6,4 +6,19 @@ Any UTF-8 string is a valid value. Reference values using `#my_variable`. These update as soon as the value changes. -You can set defaults using the `ironvar_defaults` key in your top-level config. \ No newline at end of file +You can set defaults using the `ironvar_defaults` key in your top-level config. + +Some modules (such as `sys_info`) expose their values over the Ironvar interface, +allowing you to build custom interfaces and integrate into scripts. +These present their values inside read-only namespaces. + +Some examples below: + +```shell +ironbar var list +ironbar var list sysinfo +ironbar var list sysinfo.disk_percent +ironbar var get sysinfo.disk_percent./home +ironbar var get sysinfo.disk_percent.mean +ironbar var get sysinfo.memory_percent +``` \ No newline at end of file diff --git a/src/clients/mod.rs b/src/clients/mod.rs index 1458b8b..418faf3 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -1,4 +1,4 @@ -use crate::await_sync; +use crate::{await_sync, Ironbar}; use color_eyre::Result; use std::collections::HashMap; use std::path::Path; @@ -192,7 +192,11 @@ impl Clients { #[cfg(feature = "sys_info")] pub fn sys_info(&mut self) -> Arc { self.sys_info - .get_or_insert_with(|| Arc::new(sysinfo::Client::new())) + .get_or_insert_with(|| { + let client = Arc::new(sysinfo::Client::new()); + Ironbar::variable_manager().register_namespace("sysinfo", client.clone()); + client + }) .clone() } diff --git a/src/clients/sysinfo.rs b/src/clients/sysinfo.rs index fbb0931..847d2a8 100644 --- a/src/clients/sysinfo.rs +++ b/src/clients/sysinfo.rs @@ -1,9 +1,12 @@ +use crate::ironvar::Namespace; use crate::modules::sysinfo::Interval; use crate::{lock, register_client}; +use color_eyre::{Report, Result}; use std::cmp::Ordering; use std::collections::HashMap; use std::fmt::Debug; -use std::sync::Mutex; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; use sysinfo::{Components, Disks, LoadAvg, Networks, RefreshKind, System}; #[repr(u64)] @@ -43,6 +46,21 @@ pub enum Function { Name(String), } +impl FromStr for Function { + type Err = (); + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "sum" => Ok(Self::Sum), + "min" => Ok(Self::Min), + "max" => Ok(Self::Max), + "mean" => Ok(Self::Mean), + "" => Err(()), + _ => Ok(Self::Name(s.to_string())), + } + } +} + #[derive(Debug)] pub struct ValueSet { values: HashMap, Value>, @@ -388,3 +406,212 @@ register_client!(Client, sys_info); const fn c_to_f(c: f64) -> f64 { c / 5.0 * 9.0 + 32.0 } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TokenType { + CpuFrequency, + CpuPercent, + + MemoryFree, + MemoryAvailable, + MemoryTotal, + MemoryUsed, + MemoryPercent, + + SwapFree, + SwapTotal, + SwapUsed, + SwapPercent, + + TempC, + TempF, + + DiskFree, + DiskTotal, + DiskUsed, + DiskPercent, + DiskRead, + DiskWrite, + + NetDown, + NetUp, + + LoadAverage1, + LoadAverage5, + LoadAverage15, + Uptime, +} + +impl FromStr for TokenType { + type Err = Report; + + fn from_str(s: &str) -> Result { + match s { + "cpu_frequency" => Ok(Self::CpuFrequency), + "cpu_percent" => Ok(Self::CpuPercent), + + "memory_free" => Ok(Self::MemoryFree), + "memory_available" => Ok(Self::MemoryAvailable), + "memory_total" => Ok(Self::MemoryTotal), + "memory_used" => Ok(Self::MemoryUsed), + "memory_percent" => Ok(Self::MemoryPercent), + + "swap_free" => Ok(Self::SwapFree), + "swap_total" => Ok(Self::SwapTotal), + "swap_used" => Ok(Self::SwapUsed), + "swap_percent" => Ok(Self::SwapPercent), + + "temp_c" => Ok(Self::TempC), + "temp_f" => Ok(Self::TempF), + + "disk_free" => Ok(Self::DiskFree), + "disk_total" => Ok(Self::DiskTotal), + "disk_used" => Ok(Self::DiskUsed), + "disk_percent" => Ok(Self::DiskPercent), + "disk_read" => Ok(Self::DiskRead), + "disk_write" => Ok(Self::DiskWrite), + + "net_down" => Ok(Self::NetDown), + "net_up" => Ok(Self::NetUp), + + "load_average_1" => Ok(Self::LoadAverage1), + "load_average_5" => Ok(Self::LoadAverage5), + "load_average_15" => Ok(Self::LoadAverage15), + "uptime" => Ok(Self::Uptime), + _ => Err(Report::msg(format!("invalid token type: '{s}'"))), + } + } +} + +impl Namespace for Client { + fn get(&self, key: &str) -> Option { + let get = |value: Value| Some(value.get(Prefix::None).to_string()); + + let token = TokenType::from_str(key).ok()?; + match token { + TokenType::CpuFrequency => None, + TokenType::CpuPercent => None, + TokenType::MemoryFree => get(self.memory_free()), + TokenType::MemoryAvailable => get(self.memory_available()), + TokenType::MemoryTotal => get(self.memory_total()), + TokenType::MemoryUsed => get(self.memory_used()), + TokenType::MemoryPercent => get(self.memory_percent()), + TokenType::SwapFree => get(self.swap_free()), + TokenType::SwapTotal => get(self.swap_total()), + TokenType::SwapUsed => get(self.swap_used()), + TokenType::SwapPercent => get(self.swap_percent()), + TokenType::TempC => None, + TokenType::TempF => None, + TokenType::DiskFree => None, + TokenType::DiskTotal => None, + TokenType::DiskUsed => None, + TokenType::DiskPercent => None, + TokenType::DiskRead => None, + TokenType::DiskWrite => None, + TokenType::NetDown => None, + TokenType::NetUp => None, + TokenType::LoadAverage1 => get(self.load_average_1()), + TokenType::LoadAverage5 => get(self.load_average_5()), + TokenType::LoadAverage15 => get(self.load_average_15()), + TokenType::Uptime => Some(self.uptime()), + } + } + + fn list(&self) -> Vec { + vec![ + "memory_free", + "memory_available", + "memory_total", + "memory_used", + "memory_percent", + "swap_free", + "swap_total", + "swap_used", + "swap_percent", + "load_average_1", + "load_average_5", + "load_average_15", + "uptime", + ] + .into_iter() + .map(ToString::to_string) + .collect() + } + + fn namespaces(&self) -> Vec { + vec![ + "cpu_frequency", + "cpu_percent", + "temp_c", + "temp_f", + "disk_free", + "disk_total", + "disk_used", + "disk_percent", + "disk_read", + "disk_write", + "net_down", + "net_up", + ] + .into_iter() + .map(ToString::to_string) + .collect() + } + + fn get_namespace(&self, key: &str) -> Option> { + let token = TokenType::from_str(key).ok()?; + + match token { + TokenType::CpuFrequency => Some(Arc::new(self.cpu_frequency())), + TokenType::CpuPercent => Some(Arc::new(self.cpu_percent())), + TokenType::MemoryFree => None, + TokenType::MemoryAvailable => None, + TokenType::MemoryTotal => None, + TokenType::MemoryUsed => None, + TokenType::MemoryPercent => None, + TokenType::SwapFree => None, + TokenType::SwapTotal => None, + TokenType::SwapUsed => None, + TokenType::SwapPercent => None, + TokenType::TempC => Some(Arc::new(self.temp_c())), + TokenType::TempF => Some(Arc::new(self.temp_f())), + TokenType::DiskFree => Some(Arc::new(self.disk_free())), + TokenType::DiskTotal => Some(Arc::new(self.disk_total())), + TokenType::DiskUsed => Some(Arc::new(self.disk_used())), + TokenType::DiskPercent => Some(Arc::new(self.disk_percent())), + TokenType::DiskRead => Some(Arc::new(self.disk_read(Interval::All(1)))), + TokenType::DiskWrite => Some(Arc::new(self.disk_write(Interval::All(1)))), + TokenType::NetDown => Some(Arc::new(self.net_down(Interval::All(1)))), + TokenType::NetUp => Some(Arc::new(self.net_up(Interval::All(1)))), + TokenType::LoadAverage1 => None, + TokenType::LoadAverage5 => None, + TokenType::LoadAverage15 => None, + TokenType::Uptime => None, + } + } +} + +impl Namespace for ValueSet { + fn get(&self, key: &str) -> Option { + let function = Function::from_str(key).ok()?; + Some(self.apply(&function, Prefix::None).to_string()) + } + + fn list(&self) -> Vec { + let mut vec = vec!["sum", "min", "max", "mean"] + .into_iter() + .map(ToString::to_string) + .collect::>(); + + vec.extend(self.values.keys().map(ToString::to_string)); + vec + } + + fn namespaces(&self) -> Vec { + vec![] + } + + fn get_namespace(&self, _key: &str) -> Option> { + None + } +} diff --git a/src/dynamic_value/dynamic_bool.rs b/src/dynamic_value/dynamic_bool.rs index c2162fc..4b69cc3 100644 --- a/src/dynamic_value/dynamic_bool.rs +++ b/src/dynamic_value/dynamic_bool.rs @@ -58,7 +58,7 @@ impl DynamicBool { let variable_manager = Ironbar::variable_manager(); let variable_name = variable[1..].into(); // remove hash - let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name); + let mut rx = variable_manager.subscribe(variable_name); while let Ok(value) = rx.recv().await { let has_value = value.is_some_and(|s| is_truthy(&s)); diff --git a/src/dynamic_value/dynamic_string.rs b/src/dynamic_value/dynamic_string.rs index 7c0dbdb..a33f89c 100644 --- a/src/dynamic_value/dynamic_string.rs +++ b/src/dynamic_value/dynamic_string.rs @@ -71,7 +71,7 @@ where spawn(async move { let variable_manager = Ironbar::variable_manager(); - let mut rx = crate::write_lock!(variable_manager).subscribe(name); + let mut rx = variable_manager.subscribe(name); while let Ok(value) = rx.recv().await { if let Some(value) = value { diff --git a/src/ipc/commands.rs b/src/ipc/commands.rs index 93b183e..53f6d21 100644 --- a/src/ipc/commands.rs +++ b/src/ipc/commands.rs @@ -52,7 +52,7 @@ pub enum IronvarCommand { }, /// Gets the current value of all `ironvar`s. - List, + List { namespace: Option> }, } #[derive(Args, Debug, Serialize, Deserialize)] diff --git a/src/ipc/server/ironvar.rs b/src/ipc/server/ironvar.rs index ced8e77..2689c53 100644 --- a/src/ipc/server/ironvar.rs +++ b/src/ipc/server/ironvar.rs @@ -1,36 +1,73 @@ use crate::ipc::commands::IronvarCommand; use crate::ipc::Response; -use crate::{read_lock, write_lock, Ironbar}; +use crate::ironvar::{Namespace, WritableNamespace}; +use crate::Ironbar; +use std::sync::Arc; pub fn handle_command(command: IronvarCommand) -> Response { match command { IronvarCommand::Set { key, value } => { let variable_manager = Ironbar::variable_manager(); - let mut variable_manager = write_lock!(variable_manager); - match variable_manager.set(key, value) { + match variable_manager.set(&key, value) { Ok(()) => Response::Ok, Err(err) => Response::error(&format!("{err}")), } } - IronvarCommand::Get { key } => { + IronvarCommand::Get { mut key } => { let variable_manager = Ironbar::variable_manager(); - let value = read_lock!(variable_manager).get(&key); + let mut ns: Arc = variable_manager; + + if key.contains('.') { + for part in key.split('.') { + ns = match ns.get_namespace(part) { + Some(ns) => ns.clone(), + None => { + key = part.into(); + break; + } + }; + } + } + + let value = ns.get(&key); match value { Some(value) => Response::OkValue { value }, None => Response::error("Variable not found"), } } - IronvarCommand::List => { + IronvarCommand::List { namespace } => { let variable_manager = Ironbar::variable_manager(); + let mut ns: Arc = variable_manager; - let mut values = read_lock!(variable_manager) + if let Some(namespace) = namespace { + for part in namespace.split('.') { + ns = match ns.get_namespace(part) { + Some(ns) => ns.clone(), + None => return Response::error("Namespace not found"), + }; + } + } + + let mut namespaces = ns + .namespaces() + .iter() + .map(|ns| format!("<{ns}>")) + .collect::>(); + + namespaces.sort(); + + let mut value = namespaces.join("\n"); + + let mut values = ns .get_all() .iter() - .map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default())) + .map(|(k, v)| format!("{k}: {v}")) .collect::>(); values.sort(); - let value = values.join("\n"); + + value.push('\n'); + value.push_str(&values.join("\n")); Response::OkValue { value } } diff --git a/src/ironvar.rs b/src/ironvar.rs index 190fea2..067ffef 100644 --- a/src/ironvar.rs +++ b/src/ironvar.rs @@ -1,13 +1,36 @@ #![doc = include_str!("../docs/Ironvars.md")] -use crate::send; +use crate::{arc_rw, read_lock, send, write_lock}; use color_eyre::{Report, Result}; use std::collections::HashMap; +use std::sync::{Arc, RwLock}; use tokio::sync::broadcast; +type NamespaceTrait = Arc; + +pub trait Namespace { + fn get(&self, key: &str) -> Option; + fn list(&self) -> Vec; + + fn get_all(&self) -> HashMap, String> { + self.list() + .into_iter() + .filter_map(|name| self.get(&name).map(|value| (name.into(), value))) + .collect() + } + + fn namespaces(&self) -> Vec; + fn get_namespace(&self, key: &str) -> Option; +} + +pub trait WritableNamespace: Namespace { + fn set(&self, key: &str, value: String) -> Result<()>; +} + /// Global singleton manager for `IronVar` variables. pub struct VariableManager { - variables: HashMap, IronVar>, + variables: Arc, IronVar>>>, + namespaces: Arc, NamespaceTrait>>>, } impl Default for VariableManager { @@ -19,41 +42,15 @@ impl Default for VariableManager { impl VariableManager { pub fn new() -> Self { Self { - variables: HashMap::new(), + variables: arc_rw!(HashMap::new()), + namespaces: arc_rw!(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) - } - - pub fn get_all(&self) -> &HashMap, IronVar> { - &self.variables - } - /// 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 + pub fn subscribe(&self, key: Box) -> broadcast::Receiver> { + write_lock!(self.variables) .entry(key) .or_insert_with(|| IronVar::new(None)) .subscribe() @@ -65,6 +62,76 @@ impl VariableManager { .chars() .all(|char| char.is_alphanumeric() || char == '_' || char == '-') } + + pub fn register_namespace(&self, name: &str, namespace: Arc) + where + N: Namespace + Sync + Send + 'static, + { + write_lock!(self.namespaces).insert(name.into(), namespace); + } +} + +impl Namespace for VariableManager { + fn get(&self, key: &str) -> Option { + if key.contains('.') { + let Some((ns, key)) = key.split_once('.') else { + return None; + }; + + let namespaces = read_lock!(self.namespaces); + let Some(ns) = namespaces.get(ns) else { + return None; + }; + + ns.get(key).map(|v| v.to_owned()) + } else { + read_lock!(self.variables).get(key).and_then(IronVar::get) + } + } + + fn list(&self) -> Vec { + read_lock!(self.variables) + .keys() + .map(ToString::to_string) + .collect() + } + + fn get_all(&self) -> HashMap, String> { + read_lock!(self.variables) + .iter() + .filter_map(|(k, v)| v.get().map(|value| (k.clone(), value))) + .collect() + } + + fn namespaces(&self) -> Vec { + read_lock!(self.namespaces) + .keys() + .map(ToString::to_string) + .collect() + } + + fn get_namespace(&self, key: &str) -> Option { + read_lock!(self.namespaces).get(key).cloned() + } +} + +impl WritableNamespace for VariableManager { + /// Sets the value for a variable, + /// creating it if it does not exist. + fn set(&self, key: &str, value: String) -> Result<()> { + if Self::key_is_valid(key) { + if let Some(var) = write_lock!(self.variables).get_mut(&Box::from(key)) { + var.set(Some(value)); + } else { + let var = IronVar::new(Some(value)); + write_lock!(self.variables).insert(key.into(), var); + } + + Ok(()) + } else { + Err(Report::msg("Invalid key")) + } + } } /// Ironbar dynamic variable representation. diff --git a/src/main.rs b/src/main.rs index 83ece42..6456a05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,6 @@ use std::path::PathBuf; use std::process::exit; use std::rc::Rc; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -#[cfg(feature = "ipc")] -use std::sync::RwLock; use std::sync::{mpsc, Arc, Mutex, OnceLock}; use cfg_if::cfg_if; @@ -32,7 +30,7 @@ use crate::clients::Clients; use crate::config::{Config, MonitorConfig}; use crate::error::ExitCode; #[cfg(feature = "ipc")] -use crate::ironvar::VariableManager; +use crate::ironvar::{VariableManager, WritableNamespace}; use crate::style::load_css; mod bar; @@ -263,10 +261,10 @@ impl Ironbar { /// Gets the `Ironvar` manager singleton. #[cfg(feature = "ipc")] #[must_use] - pub fn variable_manager() -> Arc> { - static VARIABLE_MANAGER: OnceLock>> = OnceLock::new(); + pub fn variable_manager() -> Arc { + static VARIABLE_MANAGER: OnceLock> = OnceLock::new(); VARIABLE_MANAGER - .get_or_init(|| arc_rw!(VariableManager::new())) + .get_or_init(|| Arc::new(VariableManager::new())) .clone() } @@ -336,7 +334,7 @@ fn load_config() -> (Config, PathBuf) { if let Some(ironvars) = config.ironvar_defaults.take() { let variable_manager = Ironbar::variable_manager(); for (k, v) in ironvars { - if write_lock!(variable_manager).set(k.clone(), v).is_err() { + if variable_manager.set(&k, v).is_err() { warn!("Ignoring invalid ironvar: '{k}'"); } } diff --git a/src/modules/sysinfo/mod.rs b/src/modules/sysinfo/mod.rs index 9e205eb..fbab5b0 100644 --- a/src/modules/sysinfo/mod.rs +++ b/src/modules/sysinfo/mod.rs @@ -2,9 +2,10 @@ mod parser; mod renderer; mod token; +use crate::clients::sysinfo::TokenType; use crate::config::{CommonConfig, ModuleOrientation}; use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt}; -use crate::modules::sysinfo::token::{Part, TokenType}; +use crate::modules::sysinfo::token::Part; use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::{clients, glib_recv, module_impl, send_async, spawn, try_send}; use color_eyre::Result; diff --git a/src/modules/sysinfo/parser.rs b/src/modules/sysinfo/parser.rs index af1d918..6ae695b 100644 --- a/src/modules/sysinfo/parser.rs +++ b/src/modules/sysinfo/parser.rs @@ -1,65 +1,9 @@ -use crate::clients::sysinfo::{Function, Prefix}; -use crate::modules::sysinfo::token::{Alignment, Formatting, Part, Token, TokenType}; +use crate::clients::sysinfo::{Function, Prefix, TokenType}; +use crate::modules::sysinfo::token::{Alignment, Formatting, Part, Token}; use color_eyre::{Report, Result}; use std::iter::Peekable; use std::str::{Chars, FromStr}; -impl FromStr for TokenType { - type Err = Report; - - fn from_str(s: &str) -> Result { - match s { - "cpu_frequency" => Ok(Self::CpuFrequency), - "cpu_percent" => Ok(Self::CpuPercent), - - "memory_free" => Ok(Self::MemoryFree), - "memory_available" => Ok(Self::MemoryAvailable), - "memory_total" => Ok(Self::MemoryTotal), - "memory_used" => Ok(Self::MemoryUsed), - "memory_percent" => Ok(Self::MemoryPercent), - - "swap_free" => Ok(Self::SwapFree), - "swap_total" => Ok(Self::SwapTotal), - "swap_used" => Ok(Self::SwapUsed), - "swap_percent" => Ok(Self::SwapPercent), - - "temp_c" => Ok(Self::TempC), - "temp_f" => Ok(Self::TempF), - - "disk_free" => Ok(Self::DiskFree), - "disk_total" => Ok(Self::DiskTotal), - "disk_used" => Ok(Self::DiskUsed), - "disk_percent" => Ok(Self::DiskPercent), - "disk_read" => Ok(Self::DiskRead), - "disk_write" => Ok(Self::DiskWrite), - - "net_down" => Ok(Self::NetDown), - "net_up" => Ok(Self::NetUp), - - "load_average_1" => Ok(Self::LoadAverage1), - "load_average_5" => Ok(Self::LoadAverage5), - "load_average_15" => Ok(Self::LoadAverage15), - "uptime" => Ok(Self::Uptime), - _ => Err(Report::msg(format!("invalid token type: '{s}'"))), - } - } -} - -impl FromStr for Function { - type Err = (); - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "sum" => Ok(Self::Sum), - "min" => Ok(Self::Min), - "max" => Ok(Self::Max), - "mean" => Ok(Self::Mean), - "" => Err(()), - _ => Ok(Self::Name(s.to_string())), - } - } -} - impl Function { pub(crate) fn default_for(token_type: TokenType) -> Self { match token_type { diff --git a/src/modules/sysinfo/renderer.rs b/src/modules/sysinfo/renderer.rs index f835878..050fac6 100644 --- a/src/modules/sysinfo/renderer.rs +++ b/src/modules/sysinfo/renderer.rs @@ -1,7 +1,7 @@ -use super::token::{Alignment, Part, Token, TokenType}; +use super::token::{Alignment, Part, Token}; use super::Interval; use crate::clients; -use crate::clients::sysinfo::{Value, ValueSet}; +use crate::clients::sysinfo::{TokenType, Value, ValueSet}; pub enum TokenValue { Number(f64), diff --git a/src/modules/sysinfo/token.rs b/src/modules/sysinfo/token.rs index 499662e..0dea390 100644 --- a/src/modules/sysinfo/token.rs +++ b/src/modules/sysinfo/token.rs @@ -1,39 +1,4 @@ -use crate::clients::sysinfo::{Function, Prefix}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TokenType { - CpuFrequency, - CpuPercent, - - MemoryFree, - MemoryAvailable, - MemoryTotal, - MemoryUsed, - MemoryPercent, - - SwapFree, - SwapTotal, - SwapUsed, - SwapPercent, - - TempC, - TempF, - - DiskFree, - DiskTotal, - DiskUsed, - DiskPercent, - DiskRead, - DiskWrite, - - NetDown, - NetUp, - - LoadAverage1, - LoadAverage5, - LoadAverage15, - Uptime, -} +use crate::clients::sysinfo::{Function, Prefix, TokenType}; #[derive(Debug, Clone)] pub struct Token {