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::hardware::rf;
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 toggle_bt: bool,
23    pub bt_rfkill_path: Option<PathBuf>,
24    pub log_level: Option<Level>,
25}
26
27// Default impl derived via #[derive(Default)]
28
29fn parse_bool(s: &str) -> bool {
30    matches!(s.to_ascii_lowercase().as_str(), "1" | "true" | "yes")
31}
32
33fn parse_value_map(content: &str) -> HashMap<String, String> {
34    let mut map = HashMap::new();
35    for line in content.lines() {
36        let line = line.trim();
37        if line.is_empty() || line.starts_with('#') {
38            continue;
39        }
40        if let Some(eq) = line.find('=') {
41            let key = line[..eq].trim().to_string();
42            let val = line[eq + 1..].trim().to_string();
43            map.insert(key, val);
44        }
45    }
46    map
47}
48
49impl Config {
50    /// Load config by overlaying env variables with values from config file.
51    /// If `path` is None, we try repo-local `./etc/uconsole-sleep/config.default` first,
52    /// then `/etc/uconsole-sleep/config`.
53    pub fn load(path: Option<PathBuf>) -> Self {
54        let mut cfg = Config::default();
55
56        // Overlay from environment variables
57        if let Ok(v) = std::env::var("DRY_RUN") {
58            cfg.dry_run = parse_bool(&v);
59        }
60        if let Ok(v) = std::env::var("POLICY_PATH") {
61            cfg.policy_path = Some(PathBuf::from(v));
62        }
63        if let Ok(v) = std::env::var("SAVING_CPU_FREQ") {
64            cfg.saving_cpu_freq = Some(v);
65        }
66        if let Ok(v) = std::env::var("HOLD_TRIGGER_SEC") {
67            cfg.hold_trigger_sec = v.parse::<f32>().ok();
68        }
69        if let Ok(v) = std::env::var("TOGGLE_WIFI") {
70            cfg.toggle_wifi = parse_bool(&v);
71        }
72        if let Ok(v) = std::env::var("WIFI_RFKILL") {
73            cfg.wifi_rfkill_path = Some(PathBuf::from(v));
74        }
75        if let Ok(v) = std::env::var("TOGGLE_BT") {
76            cfg.toggle_bt = parse_bool(&v);
77        }
78        if let Ok(v) = std::env::var("BT_RFKILL") {
79            cfg.bt_rfkill_path = Some(PathBuf::from(v));
80        }
81        if let Ok(v) = std::env::var("LOG_LEVEL")
82            && let Ok(l) = v.parse::<log::Level>()
83        {
84            cfg.log_level = Some(l);
85        }
86
87        // Determine config file path
88        let cfg_path = if let Some(p) = path {
89            p
90        } else if PathBuf::from("./etc/uconsole-sleep/config.default").exists() {
91            PathBuf::from("./etc/uconsole-sleep/config.default")
92        } else {
93            PathBuf::from("/etc/uconsole-sleep/config")
94        };
95
96        if let Ok(content) = fs::read_to_string(&cfg_path) {
97            let map = parse_value_map(&content);
98            if let Some(v) = map.get("DRY_RUN") {
99                cfg.dry_run = parse_bool(v);
100            }
101            if let Some(v) = map.get("POLICY_PATH") {
102                cfg.policy_path = Some(PathBuf::from(v));
103            }
104            if let Some(v) = map.get("SAVING_CPU_FREQ") {
105                cfg.saving_cpu_freq = Some(v.clone());
106            }
107            if let Some(v) = map.get("HOLD_TRIGGER_SEC") {
108                cfg.hold_trigger_sec = v.parse::<f32>().ok();
109            }
110            if let Some(v) = map.get("TOGGLE_WIFI") {
111                cfg.toggle_wifi = parse_bool(v);
112            }
113            if let Some(v) = map.get("WIFI_RFKILL") {
114                cfg.wifi_rfkill_path = Some(PathBuf::from(v));
115            }
116            if let Some(v) = map.get("TOGGLE_BT") {
117                cfg.toggle_bt = parse_bool(v);
118            }
119            if let Some(v) = map.get("BT_RFKILL") {
120                cfg.bt_rfkill_path = Some(PathBuf::from(v));
121            }
122            if let Some(v) = map.get("LOG_LEVEL")
123                && let Ok(l) = v.parse::<log::Level>()
124            {
125                cfg.log_level = Some(l);
126            }
127        }
128
129        // final: if wifi enabled and no rfkill path provided, set default
130        if cfg.toggle_wifi && cfg.wifi_rfkill_path.is_none() {
131            cfg.wifi_rfkill_path = Some(PathBuf::from(rf::RFKILL_PATH_WIFI));
132        }
133        // final: if bt enabled and no rfkill path provided, set default
134        if cfg.toggle_bt && cfg.bt_rfkill_path.is_none() {
135            cfg.bt_rfkill_path = Some(PathBuf::from(rf::RFKILL_PATH_BT));
136        }
137
138        cfg
139    }
140
141    #[cfg(test)]
142    pub fn load_test_file(path: &std::path::Path) -> Self {
143        Config::load(Some(path.to_path_buf()))
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use crate::hardware::rf;
150
151    use super::*;
152    use std::env;
153    use std::fs;
154
155    #[test]
156    fn test_load_from_repo_default() {
157        // `./etc/uconsole-sleep/config.default` exists in repo and contains values
158        let c = Config::load(None);
159        assert!(c.saving_cpu_freq.is_some());
160        assert_eq!(c.saving_cpu_freq.unwrap(), "100,600");
161        assert_eq!(c.hold_trigger_sec.unwrap(), 0.7_f32);
162    }
163
164    #[test]
165    fn test_wifi_default_rfkill() {
166        let tmp = env::temp_dir().join(format!(
167            "uconsole_cfg_{}",
168            std::time::SystemTime::now()
169                .duration_since(std::time::UNIX_EPOCH)
170                .unwrap()
171                .as_millis()
172        ));
173        let _ = fs::create_dir_all(&tmp);
174        let cfg_file = tmp.join("cfg");
175        fs::write(&cfg_file, "TOGGLE_WIFI=true\n").unwrap();
176        let cfg = Config::load(Some(cfg_file.clone()));
177        assert!(cfg.toggle_wifi);
178        assert_eq!(
179            cfg.wifi_rfkill_path.unwrap(),
180            PathBuf::from(rf::RFKILL_PATH_WIFI)
181        );
182    }
183
184    #[test]
185    fn test_bt_default_rfkill() {
186        let tmp = env::temp_dir().join(format!(
187            "uconsole_cfg_bt_{}",
188            std::time::SystemTime::now()
189                .duration_since(std::time::UNIX_EPOCH)
190                .unwrap()
191                .as_millis()
192        ));
193        let _ = fs::create_dir_all(&tmp);
194        let cfg_file = tmp.join("cfg_bt");
195        fs::write(&cfg_file, "TOGGLE_BT=true\n").unwrap();
196        let cfg = Config::load(Some(cfg_file.clone()));
197        assert!(cfg.toggle_bt);
198        assert_eq!(
199            cfg.bt_rfkill_path.unwrap(),
200            PathBuf::from(rf::RFKILL_PATH_BT)
201        );
202    }
203
204    #[test]
205    fn test_log_level_from_file() {
206        let tmp = env::temp_dir().join(format!(
207            "uconsole_cfg_{}",
208            std::time::SystemTime::now()
209                .duration_since(std::time::UNIX_EPOCH)
210                .unwrap()
211                .as_millis()
212        ));
213        let _ = fs::create_dir_all(&tmp);
214        let cfg_file = tmp.join("cfg_log");
215        fs::write(&cfg_file, "LOG_LEVEL=debug\n").unwrap();
216        let cfg = Config::load(Some(cfg_file.clone()));
217        assert_eq!(cfg.log_level, Some(log::Level::Debug));
218    }
219
220    // env var override test removed due to global env mutation in tests
221}