1use 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 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 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 fn get_search_dirs() -> Vec<PathBuf> {
60 let mut dirs = Vec::new();
61
62 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 if let Ok(cwd) = std::env::current_dir() {
71 dirs.push(cwd);
72 }
73
74 #[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 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 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 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 #[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 PathBuf::from("config.ini")
184 }
185
186 pub fn load(config_file: &str) -> Self {
187 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 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 }
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}