uconsole_sleep/
config.rs

1//! Simple config file parsing helpers
2//!
3//! Supports reading simple KEY=VALUE pairs from a config file (shell-style
4//! comments with #). Loads environment variables first and then overlays the
5//! values from a config file if present. This is intentionally lightweight.
6
7use std::collections::HashMap;
8use std::fs;
9use std::path::PathBuf;
10
11use crate::wifi;
12use log::Level;
13
14#[derive(Clone, Debug, Default)]
15pub struct Config {
16    pub dry_run: bool,
17    pub policy_path: Option<PathBuf>,
18    pub saving_cpu_freq: Option<String>,
19    pub hold_trigger_sec: Option<f32>,
20    pub toggle_wifi: bool,
21    pub wifi_rfkill_path: Option<PathBuf>,
22    pub log_level: Option<Level>,
23}
24
25// Default impl derived via #[derive(Default)]
26
27fn parse_bool(s: &str) -> bool {
28    matches!(s.to_ascii_lowercase().as_str(), "1" | "true" | "yes")
29}
30
31fn parse_value_map(content: &str) -> HashMap<String, String> {
32    let mut map = HashMap::new();
33    for line in content.lines() {
34        let line = line.trim();
35        if line.is_empty() || line.starts_with('#') {
36            continue;
37        }
38        if let Some(eq) = line.find('=') {
39            let key = line[..eq].trim().to_string();
40            let val = line[eq + 1..].trim().to_string();
41            map.insert(key, val);
42        }
43    }
44    map
45}
46
47impl Config {
48    /// Load config by overlaying env variables with values from config file.
49    /// If `path` is None, we try repo-local `./etc/uconsole-sleep/config.default` first,
50    /// then `/etc/uconsole-sleep/config`.
51    pub fn load(path: Option<PathBuf>) -> Self {
52        let mut cfg = Config::default();
53
54        // Overlay from environment variables
55        if let Ok(v) = std::env::var("DRY_RUN") {
56            cfg.dry_run = parse_bool(&v);
57        }
58        if let Ok(v) = std::env::var("POLICY_PATH") {
59            cfg.policy_path = Some(PathBuf::from(v));
60        }
61        if let Ok(v) = std::env::var("SAVING_CPU_FREQ") {
62            cfg.saving_cpu_freq = Some(v);
63        }
64        if let Ok(v) = std::env::var("HOLD_TRIGGER_SEC") {
65            cfg.hold_trigger_sec = v.parse::<f32>().ok();
66        }
67        if let Ok(v) = std::env::var("TOGGLE_WIFI") {
68            cfg.toggle_wifi = parse_bool(&v);
69        }
70        if let Ok(v) = std::env::var("WIFI_RFKILL") {
71            cfg.wifi_rfkill_path = Some(PathBuf::from(v));
72        }
73        if let Ok(v) = std::env::var("LOG_LEVEL")
74            && let Ok(l) = v.parse::<log::Level>()
75        {
76            cfg.log_level = Some(l);
77        }
78
79        // Determine config file path
80        let cfg_path = if let Some(p) = path {
81            p
82        } else if PathBuf::from("./etc/uconsole-sleep/config.default").exists() {
83            PathBuf::from("./etc/uconsole-sleep/config.default")
84        } else {
85            PathBuf::from("/etc/uconsole-sleep/config")
86        };
87
88        if let Ok(content) = fs::read_to_string(&cfg_path) {
89            let map = parse_value_map(&content);
90            if let Some(v) = map.get("DRY_RUN") {
91                cfg.dry_run = parse_bool(v);
92            }
93            if let Some(v) = map.get("POLICY_PATH") {
94                cfg.policy_path = Some(PathBuf::from(v));
95            }
96            if let Some(v) = map.get("SAVING_CPU_FREQ") {
97                cfg.saving_cpu_freq = Some(v.clone());
98            }
99            if let Some(v) = map.get("HOLD_TRIGGER_SEC") {
100                cfg.hold_trigger_sec = v.parse::<f32>().ok();
101            }
102            if let Some(v) = map.get("TOGGLE_WIFI") {
103                cfg.toggle_wifi = parse_bool(v);
104            }
105            if let Some(v) = map.get("WIFI_RFKILL") {
106                cfg.wifi_rfkill_path = Some(PathBuf::from(v));
107            }
108            if let Some(v) = map.get("LOG_LEVEL")
109                && let Ok(l) = v.parse::<log::Level>()
110            {
111                cfg.log_level = Some(l);
112            }
113        }
114
115        // final: if wifi enabled and no rfkill path provided, set default
116        if cfg.toggle_wifi && cfg.wifi_rfkill_path.is_none() {
117            cfg.wifi_rfkill_path = Some(PathBuf::from(wifi::RFKILL_PATH));
118        }
119
120        cfg
121    }
122
123    #[cfg(test)]
124    pub fn load_test_file(path: &std::path::Path) -> Self {
125        Config::load(Some(path.to_path_buf()))
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use crate::wifi;
132
133    use super::*;
134    use std::env;
135    use std::fs;
136
137    #[test]
138    fn test_load_from_repo_default() {
139        // `./etc/uconsole-sleep/config.default` exists in repo and contains values
140        let c = Config::load(None);
141        assert!(c.saving_cpu_freq.is_some());
142        assert_eq!(c.saving_cpu_freq.unwrap(), "100,600");
143        assert_eq!(c.hold_trigger_sec.unwrap(), 0.7_f32);
144    }
145
146    #[test]
147    fn test_wifi_default_rfkill() {
148        let tmp = env::temp_dir().join(format!(
149            "uconsole_cfg_{}",
150            std::time::SystemTime::now()
151                .duration_since(std::time::UNIX_EPOCH)
152                .unwrap()
153                .as_millis()
154        ));
155        let _ = fs::create_dir_all(&tmp);
156        let cfg_file = tmp.join("cfg");
157        fs::write(&cfg_file, "TOGGLE_WIFI=true\n").unwrap();
158        let cfg = Config::load(Some(cfg_file.clone()));
159        assert!(cfg.toggle_wifi);
160        assert_eq!(
161            cfg.wifi_rfkill_path.unwrap(),
162            PathBuf::from(wifi::RFKILL_PATH)
163        );
164    }
165
166    #[test]
167    fn test_log_level_from_file() {
168        let tmp = env::temp_dir().join(format!(
169            "uconsole_cfg_{}",
170            std::time::SystemTime::now()
171                .duration_since(std::time::UNIX_EPOCH)
172                .unwrap()
173                .as_millis()
174        ));
175        let _ = fs::create_dir_all(&tmp);
176        let cfg_file = tmp.join("cfg_log");
177        fs::write(&cfg_file, "LOG_LEVEL=debug\n").unwrap();
178        let cfg = Config::load(Some(cfg_file.clone()));
179        assert_eq!(cfg.log_level, Some(log::Level::Debug));
180    }
181
182    // env var override test removed due to global env mutation in tests
183}