Skip to main content

memory_monitor/
config.rs

1//! File: src\config.rs
2//! Author: Hadi Cahyadi <cumulus13@gmail.com>
3//! Date: 2026-05-06
4//! Description: 
5//! License: MIT
6
7use configparser::ini::Ini;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
11pub struct MonitorConfig {
12    pub threshold: f64,
13    pub check_interval: u64,
14    pub cleanmem_path: String,
15    pub growl_host: String,
16    pub growl_port: u16,
17    pub growl_password: String,
18    pub growl_app_name: String,
19    pub icon_path: String,
20}
21
22impl Default for MonitorConfig {
23    fn default() -> Self {
24        MonitorConfig {
25            threshold: 98.0,
26            check_interval: 60,
27            cleanmem_path: "cleanmem.exe".to_string(),
28            growl_host: "localhost".to_string(),
29            growl_port: 23053,
30            growl_password: String::new(),
31            growl_app_name: "Memory Monitor".to_string(),
32            icon_path: "memory-monitor.png".to_string(),
33        }
34    }
35}
36
37impl MonitorConfig {
38    /// Get candidate config filenames to search for
39    fn get_candidate_filenames() -> Vec<String> {
40        let mut candidates = vec![
41            "config.ini".to_string(),
42            "memory-monitor.ini".to_string(),
43            "memory_monitor.ini".to_string(),
44        ];
45        
46        // Add exe name based candidates
47        if let Ok(exe_path) = std::env::current_exe() {
48            if let Some(exe_name) = exe_path.file_stem().and_then(|n| n.to_str()) {
49                candidates.push(format!("{}.ini", exe_name));
50                candidates.push(format!("{}.conf", exe_name));
51                candidates.push(format!("{}_config.ini", exe_name));
52            }
53        }
54        
55        candidates
56    }
57
58    /// Get candidate search directories in priority order
59    fn get_search_dirs() -> Vec<PathBuf> {
60        let mut dirs = Vec::new();
61        
62        // 1. Executable directory (highest priority)
63        if let Ok(exe_path) = std::env::current_exe() {
64            if let Some(exe_dir) = exe_path.parent() {
65                dirs.push(exe_dir.to_path_buf());
66            }
67        }
68        
69        // 2. Current working directory
70        if let Ok(cwd) = std::env::current_dir() {
71            dirs.push(cwd);
72        }
73        
74        // 3. Platform-specific config directories
75        #[cfg(target_os = "windows")]
76        {
77            if let Ok(appdata) = std::env::var("APPDATA") {
78                dirs.push(Path::new(&appdata).join("memory-monitor"));
79            }
80            if let Ok(programdata) = std::env::var("PROGRAMDATA") {
81                dirs.push(Path::new(&programdata).join("memory-monitor"));
82            }
83        }
84        
85        #[cfg(target_os = "linux")]
86        {
87            let xdg_config = std::env::var("XDG_CONFIG_HOME")
88                .map(PathBuf::from)
89                .unwrap_or_else(|_| {
90                    let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
91                    Path::new(&home).join(".config")
92                });
93            dirs.push(xdg_config.join("memory-monitor"));
94            dirs.push(PathBuf::from("/etc/memory-monitor"));
95        }
96        
97        #[cfg(target_os = "macos")]
98        {
99            if let Ok(home) = std::env::var("HOME") {
100                dirs.push(Path::new(&home)
101                    .join("Library")
102                    .join("Application Support")
103                    .join("memory-monitor"));
104            }
105            dirs.push(PathBuf::from("/Library/Application Support/memory-monitor"));
106        }
107        
108        dirs
109    }
110
111    /// Find config file by searching through directories and candidate filenames
112    fn find_config_file() -> Option<PathBuf> {
113        let dirs = Self::get_search_dirs();
114        let filenames = Self::get_candidate_filenames();
115        
116        for dir in &dirs {
117            for filename in &filenames {
118                let config_path = dir.join(filename);
119                if config_path.exists() {
120                    return Some(config_path);
121                }
122            }
123        }
124        
125        None
126    }
127    
128    /// Get the path where default config should be created
129    fn get_default_config_path() -> PathBuf {
130        let exe_name = std::env::current_exe()
131            .ok()
132            .and_then(|p| p.file_stem().and_then(|n| n.to_str()).map(String::from))
133            .unwrap_or_else(|| "memory-monitor".to_string());
134        
135        // Prefer exe directory
136        if let Ok(exe_path) = std::env::current_exe() {
137            if let Some(exe_dir) = exe_path.parent() {
138                if let Ok(metadata) = std::fs::metadata(exe_dir) {
139                    if !metadata.permissions().readonly() {
140                        return exe_dir.join("config.ini");
141                    }
142                }
143            }
144        }
145        
146        // Fallback to platform-specific
147        #[cfg(target_os = "windows")]
148        {
149            if let Ok(appdata) = std::env::var("APPDATA") {
150                let dir = Path::new(&appdata).join(&exe_name);
151                std::fs::create_dir_all(&dir).ok();
152                return dir.join("config.ini");
153            }
154        }
155        
156        #[cfg(target_os = "linux")]
157        {
158            let xdg_config = std::env::var("XDG_CONFIG_HOME")
159                .map(PathBuf::from)
160                .unwrap_or_else(|_| {
161                    let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
162                    Path::new(&home).join(".config")
163                });
164            
165            let dir = xdg_config.join(&exe_name);
166            std::fs::create_dir_all(&dir).ok();
167            return dir.join("config.ini");
168        }
169        
170        #[cfg(target_os = "macos")]
171        {
172            if let Ok(home) = std::env::var("HOME") {
173                let dir = Path::new(&home)
174                    .join("Library")
175                    .join("Application Support")
176                    .join(&exe_name);
177                std::fs::create_dir_all(&dir).ok();
178                return dir.join("config.ini");
179            }
180        }
181        
182        // Last fallback
183        PathBuf::from("config.ini")
184    }
185    
186    pub fn load(config_file: &str) -> Self {
187        // If user specified an explicit file, try that directly
188        if config_file != "config.ini" || std::path::Path::new(config_file).is_absolute() {
189            let explicit_path = Path::new(config_file);
190            if explicit_path.exists() {
191                return Self::load_from_path(explicit_path);
192            }
193        }
194        
195        // Search for config file
196        let config_path = Self::find_config_file()
197            .unwrap_or_else(|| Self::get_default_config_path());
198        
199        if !config_path.exists() {
200            Self::create_default_at_path(&config_path);
201        }
202        
203        Self::load_from_path(&config_path)
204    }
205    
206    fn load_from_path(path: &Path) -> Self {
207        let mut config = Ini::new();
208        config.load(path).unwrap_or_default();
209        
210        MonitorConfig {
211            threshold: config
212                .get("Settings", "threshold")
213                .and_then(|v| v.parse().ok())
214                .unwrap_or(98.0),
215            
216            check_interval: config
217                .get("Settings", "check_interval")
218                .and_then(|v| v.parse().ok())
219                .unwrap_or(60),
220            
221            cleanmem_path: config
222                .get("Settings", "cleanmem_path")
223                .unwrap_or_else(|| "cleanmem.exe".to_string()),
224            
225            icon_path: config
226                .get("Settings", "icon_path")
227                .unwrap_or_else(|| "memory-monitor.png".to_string()),
228            
229            growl_host: config
230                .get("Growl", "host")
231                .unwrap_or_else(|| "localhost".to_string()),
232            
233            growl_port: config
234                .get("Growl", "port")
235                .and_then(|v| v.parse().ok())
236                .unwrap_or(23053),
237            
238            growl_password: config
239                .get("Growl", "password")
240                .unwrap_or_default(),
241            
242            growl_app_name: config
243                .get("Growl", "app_name")
244                .unwrap_or_else(|| "Memory Monitor".to_string()),
245        }
246    }
247    
248    fn create_default_at_path(path: &Path) {
249        let mut config = Ini::new();
250        config.set("Settings", "threshold", Some("98.0".to_string()));
251        config.set("Settings", "check_interval", Some("60".to_string()));
252        config.set("Settings", "cleanmem_path", Some("cleanmem.exe".to_string()));
253        config.set("Settings", "icon_path", Some("memory-monitor.png".to_string()));
254        config.set("Growl", "host", Some("localhost".to_string()));
255        config.set("Growl", "port", Some("23053".to_string()));
256        config.set("Growl", "password", Some(String::new()));
257        config.set("Growl", "app_name", Some("Memory Monitor".to_string()));
258        
259        if let Some(parent) = path.parent() {
260            std::fs::create_dir_all(parent).ok();
261        }
262        
263        config.write(path).expect("Failed to write config file");
264        println!("✅ Created default config file: {}", path.display());
265    }
266    
267    // pub fn create_default(config_file: &str) {
268    //     let path = Self::get_default_config_path();
269    //     Self::create_default_at_path(&path);
270    // }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use tempfile::NamedTempFile;
277    
278    #[test]
279    fn test_default_config() {
280        let config = MonitorConfig::default();
281        assert_eq!(config.threshold, 98.0);
282        assert_eq!(config.check_interval, 60);
283        assert_eq!(config.icon_path, "memory-monitor.png");
284    }
285    
286    #[test]
287    fn test_candidate_filenames() {
288        let candidates = MonitorConfig::get_candidate_filenames();
289        assert!(candidates.contains(&"config.ini".to_string()));
290        assert!(candidates.contains(&"memory-monitor.ini".to_string()));
291        assert!(candidates.contains(&"memory_monitor.ini".to_string()));
292    }
293    
294    #[test]
295    fn test_create_and_load_config() {
296        let temp_file = NamedTempFile::new().unwrap();
297        let path = temp_file.path();
298        
299        MonitorConfig::create_default_at_path(path);
300        let config = MonitorConfig::load_from_path(path);
301        
302        assert_eq!(config.threshold, 98.0);
303        assert_eq!(config.growl_port, 23053);
304    }
305}