// ABOUTME: Configuration loading for the session power menu. // ABOUTME: Reads moonset.toml for wallpaper settings with fallback hierarchy. use serde::Deserialize; use std::fs; use std::path::{Path, PathBuf}; const MOONARCH_WALLPAPER: &str = "/usr/share/moonarch/wallpaper.jpg"; /// Default config search paths: system-wide, then user-specific. fn default_config_paths() -> Vec { let mut paths = vec![PathBuf::from("/etc/moonset/moonset.toml")]; if let Some(config_dir) = dirs::config_dir() { paths.push(config_dir.join("moonset").join("moonset.toml")); } paths } /// Power menu configuration. #[derive(Debug, Clone, Default, Deserialize)] pub struct Config { pub background_path: Option, pub background_blur: Option, } /// Load config from TOML files. Later paths override earlier ones. pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config { let default_paths = default_config_paths(); let paths = config_paths.unwrap_or(&default_paths); let mut merged = Config::default(); for path in paths { if let Ok(content) = fs::read_to_string(path) { match toml::from_str::(&content) { Ok(parsed) => { log::debug!("Config loaded: {}", path.display()); if parsed.background_path.is_some() { merged.background_path = parsed.background_path; } if parsed.background_blur.is_some() { merged.background_blur = parsed.background_blur; } } Err(e) => { log::warn!("Failed to parse {}: {e}", path.display()); } } } } // Validate blur range if let Some(blur) = merged.background_blur { if !blur.is_finite() || blur < 0.0 || blur > 200.0 { log::warn!("Invalid background_blur value {blur}, ignoring"); merged.background_blur = None; } } merged } /// Resolve the wallpaper path using the fallback hierarchy. /// /// Priority: config background_path > Moonarch system default. /// Returns None if no wallpaper is available (CSS background shows through). pub fn resolve_background_path(config: &Config) -> Option { resolve_background_path_with(config, Path::new(MOONARCH_WALLPAPER)) } /// Resolve with configurable moonarch wallpaper path (for testing). pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> Option { // User-configured path if let Some(ref bg) = config.background_path { let path = PathBuf::from(bg); if path.is_file() { log::debug!("Wallpaper source: config ({})", path.display()); return Some(path); } } // Moonarch ecosystem default if moonarch_wallpaper.is_file() { log::debug!("Wallpaper source: moonarch default ({})", moonarch_wallpaper.display()); return Some(moonarch_wallpaper.to_path_buf()); } log::debug!("No wallpaper found, using CSS background"); None } #[cfg(test)] mod tests { use super::*; #[test] fn default_config_has_none_background() { let config = Config::default(); assert!(config.background_path.is_none()); assert!(config.background_blur.is_none()); } #[test] fn load_config_returns_default_when_no_files_exist() { let paths = vec![PathBuf::from("/nonexistent/moonset.toml")]; let config = load_config(Some(&paths)); assert!(config.background_path.is_none()); } #[test] fn load_config_reads_background_path() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("moonset.toml"); fs::write(&conf, "background_path = \"/custom/wallpaper.jpg\"\n").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert_eq!(config.background_path.as_deref(), Some("/custom/wallpaper.jpg")); } #[test] fn load_config_reads_background_blur() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("moonset.toml"); fs::write(&conf, "background_blur = 20.0\n").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert_eq!(config.background_blur, Some(20.0)); } #[test] fn load_config_blur_override() { let dir = tempfile::tempdir().unwrap(); let conf1 = dir.path().join("first.toml"); let conf2 = dir.path().join("second.toml"); fs::write(&conf1, "background_blur = 10.0\n").unwrap(); fs::write(&conf2, "background_blur = 25.0\n").unwrap(); let paths = vec![conf1, conf2]; let config = load_config(Some(&paths)); assert_eq!(config.background_blur, Some(25.0)); } #[test] fn load_config_later_paths_override_earlier() { let dir = tempfile::tempdir().unwrap(); let conf1 = dir.path().join("first.toml"); let conf2 = dir.path().join("second.toml"); fs::write(&conf1, "background_path = \"/first.jpg\"\n").unwrap(); fs::write(&conf2, "background_path = \"/second.jpg\"\n").unwrap(); let paths = vec![conf1, conf2]; let config = load_config(Some(&paths)); assert_eq!(config.background_path.as_deref(), Some("/second.jpg")); } #[test] fn load_config_skips_missing_files() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("exists.toml"); fs::write(&conf, "background_path = \"/exists.jpg\"\n").unwrap(); let paths = vec![PathBuf::from("/nonexistent.toml"), conf]; let config = load_config(Some(&paths)); assert_eq!(config.background_path.as_deref(), Some("/exists.jpg")); } #[test] fn resolve_uses_config_path_when_file_exists() { let dir = tempfile::tempdir().unwrap(); let wallpaper = dir.path().join("custom.jpg"); fs::write(&wallpaper, "fake").unwrap(); let config = Config { background_path: Some(wallpaper.to_str().unwrap().to_string()), ..Config::default() }; assert_eq!( resolve_background_path_with(&config, Path::new("/nonexistent")), Some(wallpaper) ); } #[test] fn resolve_ignores_config_path_when_file_missing() { let config = Config { background_path: Some("/nonexistent/wallpaper.jpg".to_string()), ..Config::default() }; let result = resolve_background_path_with(&config, Path::new("/nonexistent")); assert_eq!(result, None); } #[test] fn resolve_uses_moonarch_wallpaper_as_second_fallback() { let dir = tempfile::tempdir().unwrap(); let moonarch_wp = dir.path().join("wallpaper.jpg"); fs::write(&moonarch_wp, "fake").unwrap(); let config = Config::default(); assert_eq!(resolve_background_path_with(&config, &moonarch_wp), Some(moonarch_wp)); } #[test] fn resolve_returns_none_when_no_wallpaper_available() { let config = Config::default(); let result = resolve_background_path_with(&config, Path::new("/nonexistent")); assert_eq!(result, None); } #[test] fn load_config_ignores_invalid_toml_syntax() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("bad.toml"); fs::write(&conf, "this is not valid [[[ toml").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert!(config.background_path.is_none()); assert!(config.background_blur.is_none()); } #[test] fn load_config_ignores_wrong_field_types() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("wrong_type.toml"); fs::write(&conf, "background_blur = \"not_a_number\"\n").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert!(config.background_blur.is_none()); } #[test] fn load_config_rejects_negative_blur() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("negative.toml"); fs::write(&conf, "background_blur = -5.0\n").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert_eq!(config.background_blur, None); } #[test] fn load_config_rejects_excessive_blur() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("huge.toml"); fs::write(&conf, "background_blur = 999.0\n").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert_eq!(config.background_blur, None); } #[test] fn load_config_accepts_valid_blur_range() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("valid.toml"); fs::write(&conf, "background_blur = 50.0\n").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert_eq!(config.background_blur, Some(50.0)); } #[test] fn load_config_accepts_zero_blur() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("zero.toml"); fs::write(&conf, "background_blur = 0.0\n").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert_eq!(config.background_blur, Some(0.0)); } #[test] fn load_config_accepts_max_blur() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("max.toml"); fs::write(&conf, "background_blur = 200.0\n").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert_eq!(config.background_blur, Some(200.0)); } }