// ABOUTME: Configuration loading for the greeter. // ABOUTME: Reads moongreet.toml for wallpaper and GTK theme settings with fallback hierarchy. use serde::Deserialize; use std::fs; use std::path::{Path, PathBuf}; const MOONARCH_WALLPAPER: &str = "/usr/share/moonarch/wallpaper.jpg"; const GRESOURCE_PREFIX: &str = "/dev/moonarch/moongreet"; /// Default config search path: system-wide config. fn default_config_paths() -> Vec { vec![PathBuf::from("/etc/moongreet/moongreet.toml")] } /// Raw TOML structure for deserialization. #[derive(Debug, Clone, Default, Deserialize)] struct TomlConfig { appearance: Option, } #[derive(Debug, Clone, Default, Deserialize)] struct Appearance { background: Option, #[serde(rename = "background-blur")] background_blur: Option, #[serde(rename = "gtk-theme")] gtk_theme: Option, } /// Greeter configuration. #[derive(Debug, Clone, Default)] pub struct Config { pub background_path: Option, pub background_blur: Option, pub gtk_theme: 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 { match fs::read_to_string(path) { Ok(content) => { match toml::from_str::(&content) { Ok(parsed) => { log::debug!("Config loaded: {}", path.display()); if let Some(appearance) = parsed.appearance { if let Some(bg) = appearance.background { // Resolve relative paths against config file directory let bg_path = PathBuf::from(&bg); if bg_path.is_absolute() { merged.background_path = Some(bg); } else if let Some(parent) = path.parent() { merged.background_path = Some(parent.join(&bg).to_string_lossy().to_string()); } } if appearance.background_blur.is_some() { merged.background_blur = appearance.background_blur; } if appearance.gtk_theme.is_some() { merged.gtk_theme = appearance.gtk_theme; } } } Err(e) => { log::warn!("Config parse error in {}: {e}", path.display()); } } } Err(_) => { log::debug!("Config not found: {}", path.display()); } } } log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}", merged.background_path, merged.background_blur, merged.gtk_theme); merged } /// Resolve the wallpaper path using the fallback hierarchy. /// /// Priority: config background_path > Moonarch system default > gresource fallback. pub fn resolve_background_path(config: &Config) -> PathBuf { 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) -> PathBuf { // User-configured path if let Some(ref bg) = config.background_path { let path = PathBuf::from(bg); if path.is_file() { log::debug!("Wallpaper: using config path {}", path.display()); return path; } log::debug!("Wallpaper: config path {} not found, trying fallbacks", path.display()); } // Moonarch ecosystem default if moonarch_wallpaper.is_file() { log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display()); return moonarch_wallpaper.to_path_buf(); } // GResource fallback path (loaded from compiled resources at runtime) log::debug!("Wallpaper: using GResource fallback"); PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg")) } #[cfg(test)] mod tests { use super::*; #[test] fn default_config_has_none_fields() { let config = Config::default(); assert!(config.background_path.is_none()); assert!(config.background_blur.is_none()); assert!(config.gtk_theme.is_none()); } #[test] fn load_config_returns_default_when_no_files_exist() { let paths = vec![PathBuf::from("/nonexistent/moongreet.toml")]; let config = load_config(Some(&paths)); assert!(config.background_path.is_none()); assert!(config.gtk_theme.is_none()); } #[test] fn load_config_reads_appearance_section() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("moongreet.toml"); fs::write( &conf, "[appearance]\nbackground = \"/custom/wallpaper.jpg\"\nbackground-blur = 20.0\ngtk-theme = \"catppuccin\"\n", ) .unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert_eq!( config.background_path.as_deref(), Some("/custom/wallpaper.jpg") ); assert_eq!(config.background_blur, Some(20.0)); assert_eq!(config.gtk_theme.as_deref(), Some("catppuccin")); } #[test] fn load_config_blur_optional() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("moongreet.toml"); fs::write(&conf, "[appearance]\nbackground = \"/bg.jpg\"\n").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); assert!(config.background_blur.is_none()); } #[test] fn load_config_resolves_relative_background() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("moongreet.toml"); fs::write(&conf, "[appearance]\nbackground = \"bg.jpg\"\n").unwrap(); let paths = vec![conf]; let config = load_config(Some(&paths)); let expected = dir.path().join("bg.jpg").to_string_lossy().to_string(); assert_eq!(config.background_path.as_deref(), Some(expected.as_str())); } #[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, "[appearance]\nbackground = \"/first.jpg\"\ngtk-theme = \"first\"\n", ) .unwrap(); fs::write( &conf2, "[appearance]\nbackground = \"/second.jpg\"\ngtk-theme = \"second\"\n", ) .unwrap(); let paths = vec![conf1, conf2]; let config = load_config(Some(&paths)); assert_eq!(config.background_path.as_deref(), Some("/second.jpg")); assert_eq!(config.gtk_theme.as_deref(), Some("second")); } #[test] fn load_config_skips_missing_files() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("exists.toml"); fs::write( &conf, "[appearance]\nbackground = \"/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")), 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!(result.to_str().unwrap().contains("moongreet")); } #[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), moonarch_wp ); } #[test] fn resolve_uses_gresource_fallback_as_last_resort() { let config = Config::default(); let result = resolve_background_path_with(&config, Path::new("/nonexistent")); assert!(result.to_str().unwrap().contains("wallpaper.jpg")); } }