a lightweight, interval-based utility to combat digital strain through "Ma" (intentional pauses) for the eyes and body.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: adds settings struct and various tests

+193 -28
+193 -28
src/settings/mod.rs
··· 182 182 } 183 183 } 184 184 185 - // Reads the settings window state back into `cfg`. Also usable from callbacks 186 - // that capture `SettingsWindow` and `VecModel<LevelEntry>` directly. 187 - pub fn read_settings_into( 188 - window: &SettingsWindow, 189 - levels_model: &VecModel<LevelEntry>, 190 - cfg: &mut AppConfig, 191 - ) { 192 - let active_name = window.get_active_profile().to_string(); 185 + pub(crate) struct SettingsFormData { 186 + pub active_profile: String, 187 + pub enforced_mode: bool, 188 + pub idle_detection_enabled: bool, 189 + pub idle_threshold_mins: i32, 190 + pub levels: Vec<(i32, i32, i32, String)>, 191 + pub long_break_enabled: bool, 192 + pub long_break_after_cycles: i32, 193 + pub long_break_duration_mins: i32, 194 + pub long_break_gap_mins: i32, 195 + pub long_break_label: String, 196 + pub sound_enabled: bool, 197 + pub sound_volume: f32, 198 + pub autostart: bool, 199 + pub theme_mode: i32, 200 + pub text_size_mode: i32, 201 + } 202 + 203 + fn collect_form_data(window: &SettingsWindow, levels_model: &VecModel<LevelEntry>) -> SettingsFormData { 204 + SettingsFormData { 205 + active_profile: window.get_active_profile().to_string(), 206 + enforced_mode: window.get_enforced_mode(), 207 + idle_detection_enabled: window.get_idle_detection_enabled(), 208 + idle_threshold_mins: window.get_idle_threshold_mins(), 209 + levels: (0..levels_model.row_count()) 210 + .filter_map(|i| levels_model.row_data(i)) 211 + .map(|e| (e.work_mins, e.break_mins, e.break_extra_secs, e.label.to_string())) 212 + .collect(), 213 + long_break_enabled: window.get_long_break_enabled(), 214 + long_break_after_cycles: window.get_long_break_after_cycles(), 215 + long_break_duration_mins: window.get_long_break_duration_mins(), 216 + long_break_gap_mins: window.get_long_break_gap_mins(), 217 + long_break_label: window.get_long_break_label().to_string(), 218 + sound_enabled: window.get_sound_enabled(), 219 + sound_volume: window.get_sound_volume(), 220 + autostart: window.get_autostart(), 221 + theme_mode: window.get_theme_mode(), 222 + text_size_mode: window.get_text_size_mode(), 223 + } 224 + } 225 + 226 + pub(crate) fn apply_form_data(data: &SettingsFormData, cfg: &mut AppConfig) { 193 227 let active_key = cfg 194 228 .profiles 195 229 .iter() 196 - .find(|(_, p)| p.name == active_name) 230 + .find(|(_, p)| p.name == data.active_profile) 197 231 .map(|(k, _)| k.clone()); 198 232 199 233 if let Some(key) = active_key { 200 234 cfg.app.active_profile = key.clone(); 201 235 202 236 if let Some(prof) = cfg.profiles.get_mut(&key) { 203 - prof.levels = (0..levels_model.row_count()) 204 - .filter_map(|i| levels_model.row_data(i)) 205 - .map(|e| BreakLevelConfig { 206 - work_secs: e.work_mins as u64 * 60, 207 - break_secs: e.break_mins as u64 * 60 + e.break_extra_secs as u64, 208 - label: e.label.to_string(), 237 + prof.levels = data 238 + .levels 239 + .iter() 240 + .map(|(work_mins, break_mins, extra_secs, label)| BreakLevelConfig { 241 + work_secs: *work_mins as u64 * 60, 242 + break_secs: *break_mins as u64 * 60 + *extra_secs as u64, 243 + label: label.clone(), 209 244 }) 210 245 .collect(); 211 246 212 - prof.long_break = if window.get_long_break_enabled() { 247 + prof.long_break = if data.long_break_enabled { 213 248 Some(LongBreakConfig { 214 - after_cycles: window.get_long_break_after_cycles() as u32, 215 - max_cycle_gap_secs: window.get_long_break_gap_mins() as u64 * 60, 216 - break_secs: window.get_long_break_duration_mins() as u64 * 60, 217 - label: window.get_long_break_label().to_string(), 249 + after_cycles: data.long_break_after_cycles as u32, 250 + max_cycle_gap_secs: data.long_break_gap_mins as u64 * 60, 251 + break_secs: data.long_break_duration_mins as u64 * 60, 252 + label: data.long_break_label.clone(), 218 253 }) 219 254 } else { 220 255 None 221 256 }; 222 257 223 - prof.idle_detection_enabled = window.get_idle_detection_enabled(); 224 - prof.idle_threshold_secs = window.get_idle_threshold_mins() as u64 * 60; 225 - prof.mode = if window.get_enforced_mode() { 258 + prof.idle_detection_enabled = data.idle_detection_enabled; 259 + prof.idle_threshold_secs = data.idle_threshold_mins as u64 * 60; 260 + prof.mode = if data.enforced_mode { 226 261 BreakModeConfig::Enforced 227 262 } else { 228 263 BreakModeConfig::Reminder ··· 230 265 } 231 266 } 232 267 233 - cfg.appearance.sound_enabled = window.get_sound_enabled(); 234 - cfg.appearance.sound_volume = window.get_sound_volume(); 235 - cfg.app.autostart = window.get_autostart(); 236 - cfg.appearance.overlay_theme = OverlayTheme::from_mode_index(window.get_theme_mode()); 237 - cfg.appearance.text_size_mode = window.get_text_size_mode() as u32; 268 + cfg.appearance.sound_enabled = data.sound_enabled; 269 + cfg.appearance.sound_volume = data.sound_volume; 270 + cfg.app.autostart = data.autostart; 271 + cfg.appearance.overlay_theme = OverlayTheme::from_mode_index(data.theme_mode); 272 + cfg.appearance.text_size_mode = data.text_size_mode as u32; 273 + } 274 + 275 + pub fn read_settings_into( 276 + window: &SettingsWindow, 277 + levels_model: &VecModel<LevelEntry>, 278 + cfg: &mut AppConfig, 279 + ) { 280 + let data = collect_form_data(window, levels_model); 281 + apply_form_data(&data, cfg); 282 + } 283 + 284 + #[cfg(test)] 285 + mod tests { 286 + use super::*; 287 + use crate::config::AppConfig; 288 + 289 + fn pomodoro_form(cfg: &AppConfig) -> SettingsFormData { 290 + let prof = cfg.profiles.get("pomodoro").unwrap(); 291 + let level = &prof.levels[0]; 292 + SettingsFormData { 293 + active_profile: prof.name.clone(), 294 + enforced_mode: false, 295 + idle_detection_enabled: true, 296 + idle_threshold_mins: 5, 297 + levels: vec![( 298 + (level.work_secs / 60) as i32, 299 + (level.break_secs / 60) as i32, 300 + (level.break_secs % 60) as i32, 301 + level.label.clone(), 302 + )], 303 + long_break_enabled: true, 304 + long_break_after_cycles: 4, 305 + long_break_duration_mins: 15, 306 + long_break_gap_mins: 30, 307 + long_break_label: "Long break".to_string(), 308 + sound_enabled: true, 309 + sound_volume: 0.5, 310 + autostart: false, 311 + theme_mode: 2, 312 + text_size_mode: 1, 313 + } 314 + } 315 + 316 + #[test] 317 + fn apply_sets_active_profile_key() { 318 + let mut cfg = AppConfig::default(); 319 + let data = pomodoro_form(&cfg); 320 + apply_form_data(&data, &mut cfg); 321 + assert_eq!(cfg.app.active_profile, "pomodoro"); 322 + } 323 + 324 + #[test] 325 + fn apply_enforced_mode() { 326 + let mut cfg = AppConfig::default(); 327 + let mut data = pomodoro_form(&cfg); 328 + data.enforced_mode = true; 329 + apply_form_data(&data, &mut cfg); 330 + assert_eq!(cfg.profiles["pomodoro"].mode, BreakModeConfig::Enforced); 331 + } 332 + 333 + #[test] 334 + fn apply_reminder_mode() { 335 + let mut cfg = AppConfig::default(); 336 + let mut data = pomodoro_form(&cfg); 337 + data.enforced_mode = false; 338 + apply_form_data(&data, &mut cfg); 339 + assert_eq!(cfg.profiles["pomodoro"].mode, BreakModeConfig::Reminder); 340 + } 341 + 342 + #[test] 343 + fn apply_long_break_enabled() { 344 + let mut cfg = AppConfig::default(); 345 + let mut data = pomodoro_form(&cfg); 346 + data.long_break_enabled = true; 347 + data.long_break_after_cycles = 3; 348 + data.long_break_duration_mins = 20; 349 + data.long_break_gap_mins = 45; 350 + data.long_break_label = "Rest".to_string(); 351 + apply_form_data(&data, &mut cfg); 352 + let lb = cfg.profiles["pomodoro"].long_break.as_ref().unwrap(); 353 + assert_eq!(lb.after_cycles, 3); 354 + assert_eq!(lb.break_secs, 20 * 60); 355 + assert_eq!(lb.max_cycle_gap_secs, 45 * 60); 356 + assert_eq!(lb.label, "Rest"); 357 + } 358 + 359 + #[test] 360 + fn apply_long_break_disabled() { 361 + let mut cfg = AppConfig::default(); 362 + let mut data = pomodoro_form(&cfg); 363 + data.long_break_enabled = false; 364 + apply_form_data(&data, &mut cfg); 365 + assert!(cfg.profiles["pomodoro"].long_break.is_none()); 366 + } 367 + 368 + #[test] 369 + fn apply_levels_conversion() { 370 + let mut cfg = AppConfig::default(); 371 + let mut data = pomodoro_form(&cfg); 372 + data.levels = vec![(30, 5, 30, "Focus break".to_string())]; 373 + apply_form_data(&data, &mut cfg); 374 + let levels = &cfg.profiles["pomodoro"].levels; 375 + assert_eq!(levels.len(), 1); 376 + assert_eq!(levels[0].work_secs, 30 * 60); 377 + assert_eq!(levels[0].break_secs, 5 * 60 + 30); 378 + assert_eq!(levels[0].label, "Focus break"); 379 + } 380 + 381 + #[test] 382 + fn apply_appearance_settings() { 383 + let mut cfg = AppConfig::default(); 384 + let data = pomodoro_form(&cfg); 385 + apply_form_data(&data, &mut cfg); 386 + assert!(cfg.appearance.sound_enabled); 387 + assert!((cfg.appearance.sound_volume - 0.5).abs() < 0.001); 388 + assert_eq!(cfg.appearance.overlay_theme, OverlayTheme::Dark); 389 + assert_eq!(cfg.appearance.text_size_mode, 1); 390 + } 391 + 392 + #[test] 393 + fn apply_idle_settings() { 394 + let mut cfg = AppConfig::default(); 395 + let mut data = pomodoro_form(&cfg); 396 + data.idle_detection_enabled = false; 397 + data.idle_threshold_mins = 10; 398 + apply_form_data(&data, &mut cfg); 399 + let prof = &cfg.profiles["pomodoro"]; 400 + assert!(!prof.idle_detection_enabled); 401 + assert_eq!(prof.idle_threshold_secs, 10 * 60); 402 + } 238 403 } 239 404 240 405 fn get_font() -> &'static Font {