greetd-moongreet/src/config.rs
nevaforget 293bba32a6 feat: add optional background blur via image crate
Gaussian blur applied at texture load time when `background-blur` is
set in the [appearance] section of moongreet.toml. Blur runs once,
result is shared across monitors.
2026-03-28 14:53:16 +01:00

254 lines
9.1 KiB
Rust

// 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<PathBuf> {
vec![PathBuf::from("/etc/moongreet/moongreet.toml")]
}
/// Raw TOML structure for deserialization.
#[derive(Debug, Clone, Default, Deserialize)]
struct TomlConfig {
appearance: Option<Appearance>,
}
#[derive(Debug, Clone, Default, Deserialize)]
struct Appearance {
background: Option<String>,
#[serde(rename = "background-blur")]
background_blur: Option<f32>,
#[serde(rename = "gtk-theme")]
gtk_theme: Option<String>,
}
/// Greeter configuration.
#[derive(Debug, Clone, Default)]
pub struct Config {
pub background_path: Option<String>,
pub background_blur: Option<f32>,
pub gtk_theme: Option<String>,
}
/// 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::<TomlConfig>(&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"));
}
}