diff --git a/Cargo.lock b/Cargo.lock index bd4293a..77c25e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -269,6 +290,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -607,6 +639,15 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -648,6 +689,7 @@ dependencies = [ name = "moonlock" version = "0.5.0" dependencies = [ + "dirs", "gdk-pixbuf", "gdk4", "gio", @@ -703,6 +745,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "pango" version = "0.22.0" @@ -801,6 +849,17 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -955,12 +1014,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.23" @@ -1080,6 +1159,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" diff --git a/Cargo.toml b/Cargo.toml index 7f5f8f0..0f9caae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ gdk-pixbuf = "0.22" gio = "0.22" toml = "0.8" serde = { version = "1", features = ["derive"] } +dirs = "6" nix = { version = "0.29", features = ["user"] } zeroize = { version = "1", features = ["derive"] } libc = "0.2" diff --git a/src/lockscreen.rs b/src/lockscreen.rs index 165f4e6..5d9f0ba 100644 --- a/src/lockscreen.rs +++ b/src/lockscreen.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 zeroize::Zeroizing; @@ -430,11 +434,117 @@ 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("moonlock")) +} + +/// 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)?; + + 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; @@ -617,4 +727,60 @@ mod tests { fn avatar_size_matches_css() { assert_eq!(AVATAR_SIZE, 128); } + + // -- 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")); + } + + #[test] + fn build_cache_meta_for_gresource() { + let path = Path::new("/dev/moonarch/moonlock/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()); + } }