power_rules_daemon/
config.rs1use 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#[derive(Debug)]
35pub struct ConfigWatcher {
36 path: PathBuf,
37 last_modified: Option<SystemTime>,
38}
39
40impl ConfigWatcher {
41 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 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
65fn validate_default_profile(profile: &str) -> bool {
67 matches!(
68 profile.to_lowercase().as_str(),
69 "performance" | "balanced" | "power-saver" | "power_saver"
70 )
71}
72
73pub 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
80pub 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 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 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
118pub fn build_rule_map(config: &Config) -> HashMap<String, String> {
120 config.rule.iter().map(|r| (r.name.clone(), r.profile.clone())).collect()
121}