Skip to main content

power_rules_daemon/
config.rs

1//! Configuration handling with real-time reload support.
2
3use serde::Deserialize;
4use std::collections::HashMap;
5use std::path::PathBuf;
6use std::time::SystemTime;
7use anyhow::{Context, Result};
8use toml::from_str;
9
10pub const DEFAULT_POLLING_INTERVAL: u64 = 5;
11pub const DEFAULT_PAUSE_MINUTES: u64 = 60;
12pub const DEFAULT_DEFAULT_PROFILE: &str = "balanced";
13
14#[derive(Debug, Deserialize, Clone)]
15pub struct Config {
16    pub config: Option<ConfigSection>,
17    pub rule: Vec<Rule>,
18}
19
20#[derive(Debug, Deserialize, Clone)]
21pub struct ConfigSection {
22    pub polling_interval: Option<u64>,
23    pub pause_on_manual_change: Option<u64>,
24    pub default_profile: Option<String>,
25}
26
27#[derive(Debug, Deserialize, Clone)]
28pub struct Rule {
29    pub name: String,
30    pub profile: String,
31}
32
33/// Configuration watcher for real-time reloading
34#[derive(Debug)]
35pub struct ConfigWatcher {
36    path: PathBuf,
37    last_modified: Option<SystemTime>,
38}
39
40impl ConfigWatcher {
41    /// Creates a new watcher for the given config file path
42    pub fn new(path: PathBuf) -> Result<Self> {
43        let last_modified = if path.exists() {
44            Some(std::fs::metadata(&path)?.modified()?)
45        } else {
46            None
47        };
48        Ok(Self { path, last_modified })
49    }
50
51    /// Checks if the config file has been modified since last check
52    pub fn has_changed(&mut self) -> Result<bool> {
53        let current_modified = if self.path.exists() {
54            Some(std::fs::metadata(&self.path)?.modified()?)
55        } else {
56            None
57        };
58
59        let changed = current_modified != self.last_modified;
60        self.last_modified = current_modified;
61        Ok(changed)
62    }
63}
64
65/// Validates the default power profile string.
66fn validate_default_profile(profile: &str) -> bool {
67    matches!(
68        profile.to_lowercase().as_str(),
69        "performance" | "balanced" | "power-saver" | "power_saver"
70    )
71}
72
73/// Gets the default config file path
74pub fn get_config_path() -> Result<PathBuf> {
75    Ok(dirs::home_dir()
76        .context("Could not find home directory")?
77        .join(".config/power-rules/config.toml"))
78}
79
80/// Loads and validates the configuration
81pub fn load_config(config_path: &PathBuf) -> Result<Config> {
82    if !config_path.exists() {
83        return Ok(Config {
84            config: None,
85            rule: Vec::new(),
86        });
87    }
88
89    let config_data = std::fs::read_to_string(config_path)
90        .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
91
92    let config: Config = from_str(&config_data).context("Failed to parse TOML configuration")?;
93
94    // Validate default_profile if specified
95    if let Some(ConfigSection { default_profile: Some(profile), .. }) = &config.config {
96        if !validate_default_profile(profile) {
97            return Err(anyhow::anyhow!(
98                "Invalid default_profile '{}' in config. Must be one of: performance, balanced, power-saver",
99                profile
100            ));
101        }
102    }
103
104    // Validate all rule profiles
105    for rule in &config.rule {
106        if !validate_default_profile(&rule.profile) {
107            return Err(anyhow::anyhow!(
108                "Invalid profile '{}' for rule '{}'. Must be one of: performance, balanced, power-saver",
109                rule.profile,
110                rule.name
111            ));
112        }
113    }
114
115    Ok(config)
116}
117
118/// Helper to convert config rules to a hashmap
119pub fn build_rule_map(config: &Config) -> HashMap<String, String> {
120    config.rule.iter().map(|r| (r.name.clone(), r.profile.clone())).collect()
121}