diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ee205..b13dc49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). ### Added - Optional background blur via `background_blur` config option (Gaussian blur, `image` crate) +- Disk cache for blurred wallpaper (`~/.cache/moonset/`) — avoids re-blurring on subsequent starts ## [0.1.1] - 2026-03-28 diff --git a/Cargo.lock b/Cargo.lock index 5c644c2..35f4d13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -805,7 +805,7 @@ dependencies = [ [[package]] name = "moonset" -version = "0.1.1" +version = "0.4.0" dependencies = [ "dirs", "env_logger", diff --git a/src/panel.rs b/src/panel.rs index b15d86b..3db2554 100644 --- a/src/panel.rs +++ b/src/panel.rs @@ -8,8 +8,12 @@ use gtk4::prelude::*; use gtk4::{self as gtk, gio}; use image::imageops; use std::cell::RefCell; -use std::path::Path; +use std::fs; +use std::io::Write; +use std::os::unix::fs::OpenOptionsExt; +use std::path::{Path, PathBuf}; use std::rc::Rc; +use std::time::SystemTime; use crate::i18n::{load_strings, Strings}; use crate::power::{self, PowerError}; @@ -96,11 +100,118 @@ pub fn load_background_texture(bg_path: &Path, blur_radius: Option) -> gdk: }; match blur_radius { - Some(sigma) if sigma > 0.0 => apply_blur(&texture, sigma), + Some(sigma) if sigma > 0.0 => load_blurred_with_cache(bg_path, &texture, sigma), _ => texture, } } +// -- Blur cache ---------------------------------------------------------------- + +const CACHE_PNG: &str = "blur-cache.png"; +const CACHE_META: &str = "blur-cache.meta"; + +fn blur_cache_dir() -> Option { + dirs::cache_dir().map(|d| d.join("moonset")) +} + +/// Build the cache key string for the current wallpaper + sigma. +fn build_cache_meta(bg_path: &Path, sigma: f32) -> Option { + if bg_path.starts_with("/dev/moonarch/") { + let binary = std::env::current_exe().ok()?; + let binary_mtime = fs::metadata(&binary) + .ok()? + .modified() + .ok()? + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_secs(); + Some(format!( + "path={}\nbinary_mtime={}\nsigma={}\n", + bg_path.display(), binary_mtime, sigma, + )) + } else { + let meta = fs::metadata(bg_path).ok()?; + let mtime = meta + .modified() + .ok()? + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_secs(); + Some(format!( + "path={}\nsize={}\nmtime={}\nsigma={}\n", + bg_path.display(), meta.len(), mtime, sigma, + )) + } +} + +/// Try to load a cached blurred texture if the cache key matches. +fn load_cached_blur(cache_dir: &Path, expected_meta: &str) -> Option { + let stored_meta = fs::read_to_string(cache_dir.join(CACHE_META)).ok()?; + if stored_meta != expected_meta { + log::debug!("Blur cache meta mismatch, will re-blur"); + return None; + } + let file = gio::File::for_path(cache_dir.join(CACHE_PNG)); + match gdk::Texture::from_file(&file) { + Ok(texture) => { + log::debug!("Loaded blurred wallpaper from cache"); + Some(texture) + } + Err(e) => { + log::debug!("Failed to load cached blur PNG: {e}"); + None + } + } +} + +/// Save a blurred texture to the cache directory. +fn save_blur_cache(cache_dir: &Path, texture: &gdk::Texture, meta: &str) { + if let Err(e) = save_blur_cache_inner(cache_dir, texture, meta) { + log::debug!("Failed to save blur cache: {e}"); + } +} + +fn save_blur_cache_inner( + cache_dir: &Path, + texture: &gdk::Texture, + meta: &str, +) -> Result<(), Box> { + fs::create_dir_all(cache_dir)?; + + let png_bytes = texture.save_to_png_bytes(); + + let mut f = fs::OpenOptions::new() + .create(true).write(true).truncate(true).mode(0o600) + .open(cache_dir.join(CACHE_PNG))?; + f.write_all(&png_bytes)?; + + // Meta last — incomplete cache is treated as a miss on next start + let mut f = fs::OpenOptions::new() + .create(true).write(true).truncate(true).mode(0o600) + .open(cache_dir.join(CACHE_META))?; + f.write_all(meta.as_bytes())?; + + log::debug!("Saved blur cache to {}", cache_dir.display()); + Ok(()) +} + +/// Load blurred texture, using disk cache when available. +fn load_blurred_with_cache(bg_path: &Path, texture: &gdk::Texture, sigma: f32) -> gdk::Texture { + if let Some(cache_dir) = blur_cache_dir() { + if let Some(meta) = build_cache_meta(bg_path, sigma) { + if let Some(cached) = load_cached_blur(&cache_dir, &meta) { + return cached; + } + let blurred = apply_blur(texture, sigma); + save_blur_cache(&cache_dir, &blurred, &meta); + return blurred; + } + } + apply_blur(texture, sigma) +} + +// -- Blur implementation ------------------------------------------------------- + /// Apply Gaussian blur to a texture and return a blurred texture. fn apply_blur(texture: &gdk::Texture, sigma: f32) -> gdk::Texture { let width = texture.width() as u32; @@ -627,4 +738,61 @@ mod tests { let confirm_fn = defs[1].confirm_attr.unwrap(); assert_eq!(confirm_fn(strings), "Wirklich abmelden?"); } + + // -- Blur cache tests -- + + #[test] + fn build_cache_meta_for_file() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("wallpaper.jpg"); + fs::write(&file, b"fake image").unwrap(); + let meta = build_cache_meta(&file, 20.0); + assert!(meta.is_some()); + let meta = meta.unwrap(); + assert!(meta.contains("path=")); + assert!(meta.contains("size=10")); + assert!(meta.contains("sigma=20")); + assert!(meta.contains("mtime=")); + } + + #[test] + fn build_cache_meta_for_gresource() { + let path = Path::new("/dev/moonarch/moonset/wallpaper.jpg"); + let meta = build_cache_meta(path, 15.0); + assert!(meta.is_some()); + let meta = meta.unwrap(); + assert!(meta.contains("binary_mtime=")); + assert!(meta.contains("sigma=15")); + assert!(!meta.contains("size=")); + } + + #[test] + fn build_cache_meta_missing_file() { + let meta = build_cache_meta(Path::new("/nonexistent/wallpaper.jpg"), 20.0); + assert!(meta.is_none()); + } + + #[test] + fn cache_meta_mismatch_returns_none() { + let dir = tempfile::tempdir().unwrap(); + fs::write( + dir.path().join(CACHE_META), + "path=/old.jpg\nsize=100\nmtime=1\nsigma=20\n", + ).unwrap(); + let result = load_cached_blur( + dir.path(), + "path=/new.jpg\nsize=200\nmtime=2\nsigma=20\n", + ); + assert!(result.is_none()); + } + + #[test] + fn cache_missing_meta_returns_none() { + let dir = tempfile::tempdir().unwrap(); + let result = load_cached_blur( + dir.path(), + "path=/any.jpg\nsize=1\nmtime=1\nsigma=20\n", + ); + assert!(result.is_none()); + } }