summaryrefslogtreecommitdiff
path: root/service/src/config.rs
diff options
context:
space:
mode:
Diffstat (limited to 'service/src/config.rs')
-rw-r--r--service/src/config.rs244
1 files changed, 244 insertions, 0 deletions
diff --git a/service/src/config.rs b/service/src/config.rs
new file mode 100644
index 0000000..dd98ec7
--- /dev/null
+++ b/service/src/config.rs
@@ -0,0 +1,244 @@
+use anyhow::Result;
+use configparser::ini::Ini;
+use std::path::PathBuf;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tokio_util::sync::CancellationToken;
+use tracing::{debug, info, warn};
+use zbus::Connection;
+
+#[derive(Clone, Debug)]
+pub struct Config {
+ pub endpoints: Vec<(String, u16)>,
+ pub username: Option<String>,
+ pub password: Option<String>,
+ pub client_id: String,
+ pub refresh_interval_seconds: u64,
+}
+
+fn default_port() -> u16 {
+ 1883
+}
+fn default_client_id() -> String {
+ "camper-widget-refresh".to_string()
+}
+fn default_refresh_interval() -> u64 {
+ 60
+}
+// log level is now controlled via RUST_LOG in main; no per-config default needed
+
+fn plasma_applet_src_path() -> Option<PathBuf> {
+ dirs::config_dir().map(|d| d.join("plasma-org.kde.plasma.desktop-appletsrc"))
+}
+
+fn find_applet_section_id(ini: &Ini) -> Option<String> {
+ for section in ini.sections() {
+ if let Some(plugin) = ini.get(&section, "plugin")
+ && plugin == "craftknight.camper_widget"
+ {
+ return Some(section);
+ }
+ }
+ warn!("No applet section id found");
+ None
+}
+
+fn unescape_kde_value(s: &str) -> String {
+ s.replace("\\s", " ")
+ .replace("\\t", "\t")
+ .replace("\\n", "\n")
+ .replace("\\\\", "\\")
+}
+
+fn parse_mqtt_hosts(raw: &str) -> Vec<(String, u16)> {
+ let unescaped = unescape_kde_value(raw);
+ let mut out: Vec<(String, u16)> = Vec::new();
+ for entry in unescaped.split(',') {
+ let part = entry.trim();
+ if part.is_empty() {
+ continue;
+ }
+ if let Some((host, port_str)) = part.rsplit_once(':')
+ && let Ok(port) = port_str.parse::<u16>()
+ {
+ out.push((host.trim().to_string(), port));
+ continue;
+ }
+ out.push((part.to_string(), default_port()));
+ }
+ if out.is_empty() {
+ out.push(("127.0.0.1".to_string(), default_port()));
+ }
+ out
+}
+
+pub async fn load_config() -> Config {
+ // Defaults
+ let mut endpoints: Vec<(String, u16)> = vec![("127.0.0.1".to_string(), default_port())];
+ let mut username: Option<String> = None;
+ let mut password: Option<String> = None;
+ let client_id = default_client_id();
+ let mut refresh_interval_seconds = default_refresh_interval();
+
+ if let Some(path) = plasma_applet_src_path() {
+ debug!("Reading Plasma INI config from {:?}", path);
+ match tokio::fs::read_to_string(&path).await {
+ Ok(contents) => {
+ let mut ini = Ini::new();
+ if let Err(e) = ini.read(contents) {
+ warn!("Failed to parse Plasma INI config at {:?}: {}", path, e);
+ }
+ let sections = ini.sections();
+ debug!("Found {} INI sections", sections.len());
+ if let Some(base) = find_applet_section_id(&ini) {
+ debug!("Matched applet section id: {}", base);
+ // Build the [Configuration][General] section name from the found applet base
+ let sec_general = format!("{}][configuration][general", base);
+ if ini.sections().contains(&sec_general) {
+ debug!("Using section: {}", sec_general);
+ debug!("Looking under primary section: {}", sec_general);
+ let mut read_any = false;
+ let section = &sec_general;
+ debug!("Attempt reading values from section: {}", section);
+ if let Some(raw_hosts) = ini.get(section, "mqttHosts") {
+ debug!("Found mqttHosts='{}'", raw_hosts);
+ endpoints = parse_mqtt_hosts(&raw_hosts);
+ debug!("Parsed endpoints: {:?}", endpoints);
+ read_any = true;
+ }
+ if let Some(u) = ini.get(section, "mqttUsername")
+ && !u.is_empty()
+ {
+ username = Some(u);
+ debug!("Found mqttUsername set");
+ read_any = true;
+ }
+ if let Some(p) = ini.get(section, "mqttPassword")
+ && !p.is_empty()
+ {
+ password = Some(p);
+ debug!("Found mqttPassword set (redacted)");
+ read_any = true;
+ }
+ if let Some(sec) = ini.get(section, "refreshIntervalSeconds")
+ && let Ok(v) = sec.trim().parse::<u64>()
+ {
+ refresh_interval_seconds = v;
+ debug!("Found refreshIntervalSeconds={}", v);
+ read_any = true;
+ }
+ if !read_any {
+ warn!("No configuration keys found under {}", section);
+ }
+ } else {
+ warn!("Could not find target configuration section");
+ }
+ } else {
+ warn!("No applet section id found");
+ }
+ }
+ Err(e) => {
+ warn!("Failed to read Plasma INI config at {:?}: {}", path, e);
+ }
+ }
+ } else {
+ warn!("No config dir found (dirs::config_dir returned None)");
+ }
+
+ Config {
+ endpoints,
+ username,
+ password,
+ client_id,
+ refresh_interval_seconds,
+ }
+}
+
+pub async fn listen_for_config_changes(
+ _conn: &Connection,
+ config_state: Arc<RwLock<Config>>,
+ shared_state: &crate::dbus::SharedState,
+ token: CancellationToken,
+) -> Result<()> {
+ info!("Starting config reload monitor");
+
+ loop {
+ tokio::select! {
+ _ = shared_state.config_reload_notify.notified() => {}
+ _ = token.cancelled() => {
+ info!("Config reload monitor shutting down");
+ return Ok(());
+ }
+ }
+
+ info!("Config reload triggered, reloading configuration");
+
+ let new_config = load_config().await;
+
+ {
+ let mut config_guard = config_state.write().await;
+ *config_guard = new_config;
+ }
+
+ info!("Configuration reloaded successfully");
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_mqtt_hosts_various() {
+ assert_eq!(
+ parse_mqtt_hosts("127.0.0.1"),
+ vec![("127.0.0.1".to_string(), 1883)]
+ );
+ assert_eq!(
+ parse_mqtt_hosts("127.0.0.1:1883"),
+ vec![("127.0.0.1".to_string(), 1883)]
+ );
+ assert_eq!(
+ parse_mqtt_hosts("127.0.0.1,192.168.1.111"),
+ vec![
+ ("127.0.0.1".to_string(), 1883),
+ ("192.168.1.111".to_string(), 1883)
+ ]
+ );
+ assert_eq!(
+ parse_mqtt_hosts("127.0.0.1:1883,192.168.1.111"),
+ vec![
+ ("127.0.0.1".to_string(), 1883),
+ ("192.168.1.111".to_string(), 1883)
+ ]
+ );
+ assert_eq!(
+ parse_mqtt_hosts("127.0.0.1:1883,192.168.1.111:1833"),
+ vec![
+ ("127.0.0.1".to_string(), 1883),
+ ("192.168.1.111".to_string(), 1833)
+ ]
+ );
+ // KDE config escapes leading spaces as \s
+ assert_eq!(
+ parse_mqtt_hosts("\\s192.168.10.202 :1883"),
+ vec![("192.168.10.202".to_string(), 1883)]
+ );
+ }
+
+ #[test]
+ fn test_find_applet_section_id() {
+ let mut ini = Ini::new();
+ // Base section like [Containments][85][Applets][133]
+ ini.set(
+ "Containments][85][Applets][133",
+ "plugin",
+ Some("craftknight.camper_widget".to_string()),
+ );
+ let found = find_applet_section_id(&ini);
+ // configparser normalizes section names to lowercase
+ assert_eq!(found, Some("containments][85][applets][133".to_string()));
+ }
+
+ // We avoid testing load_config() directly because it depends on user's plasma file.
+}