diff --git a/CLAUDE.md b/CLAUDE.md index e7f466f..a2908bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,12 +56,13 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git--x86_64.pkg.tar.z - **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads - **Socket-Cancellation**: `Arc>>` + `AtomicBool` für saubere Abbrüche - **Avatar-Cache**: `HashMap` in `Rc>` -- **Symmetrie mit moonset**: Gleiche Patterns (i18n, config, users, power, GResource) +- **GPU-Blur via GskBlurNode**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` im `connect_realize` Callback — kein CPU-Blur, kein Disk-Cache, kein `image`-Crate +- **Symmetrie mit moonlock/moonset**: Gleiche Patterns (i18n, config, users, power, GResource, GPU-Blur) - **Session-Validierung**: Relative Pfade erlaubt (greetd löst PATH auf), nur `..`/Null-Bytes werden abgelehnt - **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config - **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var - **File Permissions**: Cache-Dateien 0o600 - **Testbare Persistence**: `save_*_to`/`load_*_from` Varianten mit konfigurierbarem Pfad für Unit-Tests -- **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster (Greeter + Wallpaper-Windows) geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor +- **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor - **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None` - **Error-Detail-Filterung**: GDK/greetd-Fehlerdetails nur auf `debug!`-Level, `warn!` ohne interne Details — verhindert Systeminfo-Leak ins Journal diff --git a/Cargo.lock b/Cargo.lock index df6bcd4..bf7038c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "anyhow" version = "1.0.102" @@ -26,18 +20,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - [[package]] name = "cairo-rs" version = "0.22.0" @@ -77,15 +59,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -108,15 +81,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - [[package]] name = "field-offset" version = "0.3.6" @@ -127,16 +91,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "foldhash" version = "0.1.5" @@ -550,21 +504,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "png", - "zune-core", - "zune-jpeg", -] - [[package]] name = "indexmap" version = "2.13.0" @@ -628,28 +567,18 @@ dependencies = [ "autocfg", ] -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - [[package]] name = "moongreet" -version = "0.4.0" +version = "0.5.0" dependencies = [ "gdk-pixbuf", "gdk4", "gio", "glib", "glib-build-tools", + "graphene-rs", "gtk4", "gtk4-layer-shell", - "image", "log", "serde", "serde_json", @@ -658,25 +587,6 @@ dependencies = [ "toml 0.8.23", ] -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -719,19 +629,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -760,12 +657,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pxfm" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" - [[package]] name = "quote" version = "1.0.45" @@ -870,12 +761,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - [[package]] name = "slab" version = "0.4.12" @@ -1244,18 +1129,3 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/Cargo.toml b/Cargo.toml index 704b06d..7921612 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moongreet" -version = "0.4.0" +version = "0.5.0" edition = "2024" description = "A greetd greeter for Wayland with GTK4 and Layer Shell" license = "MIT" @@ -15,7 +15,7 @@ gio = "0.22" toml = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" -image = { version = "0.25", default-features = false, features = ["jpeg", "png"] } +graphene-rs = { version = "0.22", package = "graphene-rs" } log = "0.4" systemd-journal-logger = "2.2" diff --git a/DECISIONS.md b/DECISIONS.md index 4d051a5..010a943 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,6 +1,13 @@ # Decisions -## 2026-03-28 – Optional background blur via `image` crate +## 2026-03-28 – GPU blur via GskBlurNode replaces CPU blur + +- **Who**: Ragnar, Dom +- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms–2s on 4K wallpapers at cold cache. Disk cache and async orchestration added significant complexity. +- **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper backgrounds. Removes `image` crate dependency entirely (~15 transitive crates eliminated). No disk cache needed. +- **How**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` on `connect_realize`. Blur happens once on the GPU when the widget gets its renderer, producing a concrete `gdk::Texture`. Zero startup latency. Symmetric with moonlock and moonset. + +## 2026-03-28 – Optional background blur via `image` crate (superseded) - **Who**: Selene, Dom - **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens diff --git a/src/greeter.rs b/src/greeter.rs index 9729275..346257c 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -6,17 +6,12 @@ use gdk_pixbuf::Pixbuf; use glib::clone; use gtk4::prelude::*; use gtk4::{self as gtk, gio}; -use image::imageops; use std::cell::RefCell; use std::collections::HashMap; -use std::fs; -use std::io::Write; -use std::os::unix::fs::OpenOptionsExt; use std::os::unix::net::UnixStream; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use std::time::SystemTime; use crate::config::Config; use crate::i18n::{faillock_warning, load_strings, Strings}; @@ -97,14 +92,13 @@ fn is_valid_username(name: &str) -> bool { return false; } name.chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-') + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-' || c == '@') } -/// Load the background image as a shared texture (decode once, reuse everywhere). -/// When `blur_radius` is `Some(sigma)` with sigma > 0, a Gaussian blur is applied. -pub fn load_background_texture(bg_path: &Path, blur_radius: Option) -> Option { +/// Load background texture from GResource or filesystem. +pub fn load_background_texture(bg_path: &Path) -> Option { let path_str = bg_path.to_str()?; - let texture = if bg_path.starts_with("/dev/moonarch/moongreet") { + if bg_path.starts_with("/dev/moonarch/moongreet") { match gio::resources_lookup_data(path_str, gio::ResourceLookupFlags::NONE) { Ok(bytes) => match gdk::Texture::from_bytes(&bytes) { Ok(texture) => Some(texture), @@ -138,146 +132,34 @@ pub fn load_background_texture(bg_path: &Path, blur_radius: Option) -> Opti None } } - }?; - - match blur_radius { - Some(sigma) if sigma > 0.0 => Some(load_blurred_with_cache(bg_path, &texture, sigma)), - _ => Some(texture), } } -// -- Blur cache ---------------------------------------------------------------- +// -- GPU blur via GskBlurNode ------------------------------------------------- -const BLUR_CACHE_PNG: &str = "blur-cache.png"; -const BLUR_CACHE_META: &str = "blur-cache.meta"; - -fn blur_cache_dir() -> Option { - Some(PathBuf::from("/var/cache/moongreet")) -} - -/// 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(BLUR_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(BLUR_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, +/// Render a blurred texture using the GPU via GskBlurNode. +fn render_blurred_texture( + widget: &impl IsA, 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(BLUR_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(BLUR_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; - let height = texture.height() as u32; - let stride = width as usize * 4; - let mut pixel_data = vec![0u8; stride * height as usize]; - texture.download(&mut pixel_data, stride); - - let img = image::RgbaImage::from_raw(width, height, pixel_data) - .expect("pixel buffer size matches texture dimensions"); - let blurred = imageops::blur(&image::DynamicImage::ImageRgba8(img), sigma); - - let bytes = glib::Bytes::from(blurred.as_raw()); - let mem_texture = gdk::MemoryTexture::new( - width as i32, - height as i32, - gdk::MemoryFormat::B8g8r8a8Premultiplied, - &bytes, - stride, + sigma: f32, +) -> Option { + let native = widget.native()?; + let renderer = native.renderer()?; + let snapshot = gtk::Snapshot::new(); + let bounds = graphene_rs::Rect::new( + 0.0, 0.0, texture.width() as f32, texture.height() as f32, ); - mem_texture.upcast() + snapshot.push_blur(sigma as f64); + snapshot.append_texture(texture, &bounds); + snapshot.pop(); + let node = snapshot.to_node()?; + Some(renderer.render_texture(&node, None)) } /// Create a wallpaper-only window for secondary monitors. pub fn create_wallpaper_window( texture: &gdk::Texture, + blur_radius: Option, app: >k::Application, ) -> gtk::ApplicationWindow { let window = gtk::ApplicationWindow::builder() @@ -285,18 +167,28 @@ pub fn create_wallpaper_window( .build(); window.add_css_class("wallpaper"); - let background = create_background_picture(texture); + let background = create_background_picture(texture, blur_radius); window.set_child(Some(&background)); window } -/// Create a Picture widget for the wallpaper background from a pre-loaded texture. -fn create_background_picture(texture: &gdk::Texture) -> gtk::Picture { +/// Create a Picture widget for the wallpaper background, optionally with GPU blur. +pub fn create_background_picture(texture: &gdk::Texture, blur_radius: Option) -> gtk::Picture { let background = gtk::Picture::for_paintable(texture); background.set_content_fit(gtk::ContentFit::Cover); background.set_hexpand(true); background.set_vexpand(true); + + if let Some(sigma) = blur_radius.filter(|s| *s > 0.0) { + let texture = texture.clone(); + background.connect_realize(move |picture| { + if let Some(blurred) = render_blurred_texture(picture, &texture, sigma) { + picture.set_paintable(Some(&blurred)); + } + }); + } + background } @@ -360,7 +252,7 @@ pub fn create_greeter_window( // Background wallpaper if let Some(texture) = texture { - overlay.set_child(Some(&create_background_picture(texture))); + overlay.set_child(Some(&create_background_picture(texture, config.background_blur))); } // Main layout: 3 rows (top spacer, center login, bottom bar) @@ -508,7 +400,7 @@ pub fn create_greeter_window( error_label, move |btn| { btn.set_sensitive(false); - execute_power_action(power::reboot, strings.reboot_failed, &error_label); + execute_power_action(power::reboot, strings.reboot_failed, &error_label, btn); } )); power_box.append(&reboot_btn); @@ -522,7 +414,7 @@ pub fn create_greeter_window( error_label, move |btn| { btn.set_sensitive(false); - execute_power_action(power::shutdown, strings.shutdown_failed, &error_label); + execute_power_action(power::shutdown, strings.shutdown_failed, &error_label, btn); } )); power_box.append(&shutdown_btn); @@ -742,13 +634,24 @@ fn set_avatar_from_file( username: Option<&str>, state: &Rc>, ) { - // Reject oversized files - if let Ok(meta) = std::fs::metadata(path) { - if meta.len() > MAX_AVATAR_FILE_SIZE { + // Re-check symlink status to narrow TOCTOU window from get_avatar_path_with() + match std::fs::symlink_metadata(path) { + Ok(meta) if meta.file_type().is_symlink() => { + log::warn!("Rejecting symlink avatar at load time: {}", path.display()); + image.set_icon_name(Some("avatar-default-symbolic")); + return; + } + Ok(meta) if meta.len() > MAX_AVATAR_FILE_SIZE => { log::debug!("Avatar file too large ({} bytes): {}", meta.len(), path.display()); image.set_icon_name(Some("avatar-default-symbolic")); return; } + Err(e) => { + log::debug!("Cannot stat avatar {}: {e}", path.display()); + image.set_icon_name(Some("avatar-default-symbolic")); + return; + } + Ok(_) => {} } match Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), AVATAR_SIZE, AVATAR_SIZE, true) { @@ -869,6 +772,15 @@ fn show_error( password_entry.grab_focus(); } +/// Extract and length-check a greetd error description from a JSON response. +fn extract_greetd_description<'a>(response: &'a serde_json::Value, fallback: &'a str) -> &'a str { + response + .get("description") + .and_then(|v| v.as_str()) + .filter(|d| !d.is_empty() && d.len() <= MAX_GREETD_ERROR_LENGTH) + .unwrap_or(fallback) +} + /// Display a greetd error, using a fallback for missing or oversized descriptions. fn show_greetd_error( error_label: >k::Label, @@ -876,15 +788,8 @@ fn show_greetd_error( response: &serde_json::Value, fallback: &str, ) { - let description = response - .get("description") - .and_then(|v| v.as_str()) - .unwrap_or(""); - if !description.is_empty() && description.len() <= MAX_GREETD_ERROR_LENGTH { - show_error(error_label, password_entry, description); - } else { - show_error(error_label, password_entry, fallback); - } + let message = extract_greetd_description(response, fallback); + show_error(error_label, password_entry, message); } /// Cancel any in-progress greetd session. @@ -1113,15 +1018,7 @@ fn login_worker( return Ok(LoginResult::Cancelled); } if response.get("type").and_then(|v| v.as_str()) == Some("error") { - let description = response - .get("description") - .and_then(|v| v.as_str()) - .unwrap_or(""); - let message = if !description.is_empty() && description.len() <= MAX_GREETD_ERROR_LENGTH { - description.to_string() - } else { - strings.auth_failed.to_string() - }; + let message = extract_greetd_description(&response, strings.auth_failed).to_string(); return Ok(LoginResult::Error { message }); } } @@ -1190,10 +1087,7 @@ fn login_worker( }); } else { return Ok(LoginResult::Error { - message: response - .get("description") - .and_then(|v| v.as_str()) - .unwrap_or(strings.session_start_failed) + message: extract_greetd_description(&response, strings.session_start_failed) .to_string(), }); } @@ -1209,10 +1103,13 @@ fn execute_power_action( action_fn: fn() -> Result<(), PowerError>, error_message: &'static str, error_label: >k::Label, + button: >k::Button, ) { glib::spawn_future_local(clone!( #[weak] error_label, + #[weak] + button, async move { let result = gio::spawn_blocking(move || action_fn()).await; @@ -1222,11 +1119,13 @@ fn execute_power_action( log::error!("Power action failed: {e}"); error_label.set_text(error_message); error_label.set_visible(true); + button.set_sensitive(true); } Err(_) => { log::error!("Power action panicked"); error_label.set_text(error_message); error_label.set_visible(true); + button.set_sensitive(true); } } } @@ -1333,6 +1232,7 @@ mod tests { assert!(is_valid_username("test-user")); assert!(is_valid_username("test.user")); assert!(is_valid_username("_admin")); + assert!(is_valid_username("user@domain")); } #[test] @@ -1340,6 +1240,7 @@ mod tests { assert!(!is_valid_username("")); assert!(!is_valid_username(".hidden")); assert!(!is_valid_username("-dash")); + assert!(!is_valid_username("@domain")); assert!(!is_valid_username("user/name")); assert!(!is_valid_username(&"a".repeat(MAX_USERNAME_LENGTH + 1))); } @@ -1768,7 +1669,7 @@ mod tests { #[test] fn load_background_texture_missing_file_returns_none() { - let result = load_background_texture(Path::new("/nonexistent/wallpaper.jpg"), None); + let result = load_background_texture(Path::new("/nonexistent/wallpaper.jpg")); assert!(result.is_none()); } @@ -1779,7 +1680,7 @@ mod tests { // Create a sparse file that exceeds MAX_WALLPAPER_FILE_SIZE let f = std::fs::File::create(&path).unwrap(); f.set_len(MAX_WALLPAPER_FILE_SIZE + 1).unwrap(); - let result = load_background_texture(&path, None); + let result = load_background_texture(&path); assert!(result.is_none()); } @@ -1790,63 +1691,32 @@ mod tests { // 0xFF is not valid UTF-8 let non_utf8 = OsStr::from_bytes(&[0xff, 0xfe, 0xfd]); let path = Path::new(non_utf8); - let result = load_background_texture(path, None); - assert!(result.is_none()); - } - - // -- Blur cache tests -- - - #[test] - fn build_cache_meta_for_file() { - let dir = tempfile::tempdir().unwrap(); - let file = dir.path().join("wallpaper.jpg"); - std::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/moongreet/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(); - std::fs::write( - dir.path().join(BLUR_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", - ); + let result = load_background_texture(path); 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()); + fn extract_greetd_description_normal() { + let resp = serde_json::json!({"type": "error", "description": "bad password"}); + assert_eq!(extract_greetd_description(&resp, "fallback"), "bad password"); + } + + #[test] + fn extract_greetd_description_oversized() { + let long = "x".repeat(MAX_GREETD_ERROR_LENGTH + 1); + let resp = serde_json::json!({"type": "error", "description": long}); + assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback"); + } + + #[test] + fn extract_greetd_description_empty() { + let resp = serde_json::json!({"type": "error", "description": ""}); + assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback"); + } + + #[test] + fn extract_greetd_description_missing() { + let resp = serde_json::json!({"type": "error"}); + assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback"); } } diff --git a/src/i18n.rs b/src/i18n.rs index 85eab27..2d36c7e 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -28,7 +28,6 @@ pub struct Strings { pub session_start_failed: &'static str, pub reboot_failed: &'static str, pub shutdown_failed: &'static str, - pub connection_error: &'static str, pub socket_error: &'static str, pub unexpected_greetd_response: &'static str, @@ -53,7 +52,6 @@ const STRINGS_DE: Strings = Strings { session_start_failed: "Session konnte nicht gestartet werden", reboot_failed: "Neustart fehlgeschlagen", shutdown_failed: "Herunterfahren fehlgeschlagen", - connection_error: "Verbindungsfehler", socket_error: "Socket-Fehler", unexpected_greetd_response: "Unerwartete Antwort von greetd", faillock_attempts_remaining: "Noch {n} Versuch(e) vor Kontosperrung!", @@ -76,7 +74,6 @@ const STRINGS_EN: Strings = Strings { session_start_failed: "Failed to start session", reboot_failed: "Reboot failed", shutdown_failed: "Shutdown failed", - connection_error: "Connection error", socket_error: "Socket error", unexpected_greetd_response: "Unexpected response from greetd", faillock_attempts_remaining: "{n} attempt(s) remaining before lockout!", diff --git a/src/main.rs b/src/main.rs index aa074bb..a70331c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,7 +55,8 @@ fn activate(app: >k::Application) { log::debug!("Background path: {}", bg_path.display()); // Load background texture once — shared across all windows - let bg_texture = greeter::load_background_texture(&bg_path, config.background_blur); + // Blur is applied on the GPU via GskBlurNode at widget realization time. + let bg_texture = greeter::load_background_texture(&bg_path); if bg_texture.is_none() { log::error!("Failed to load background texture — greeter will start without wallpaper"); } @@ -81,7 +82,7 @@ fn activate(app: >k::Application) { .item(i) .and_then(|obj| obj.downcast::().ok()) { - let wallpaper = greeter::create_wallpaper_window(texture, app); + let wallpaper = greeter::create_wallpaper_window(texture, config.background_blur, app); setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom); wallpaper.set_monitor(Some(&monitor)); wallpaper.present(); diff --git a/src/power.rs b/src/power.rs index 53f40b9..8ed862c 100644 --- a/src/power.rs +++ b/src/power.rs @@ -41,8 +41,7 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), if output.status.success() { log::debug!("Power action {action} completed successfully"); - } - if !output.status.success() { + } else { let stderr = String::from_utf8_lossy(&output.stderr); return Err(PowerError::CommandFailed { action, @@ -100,7 +99,7 @@ mod tests { #[test] fn run_command_passes_args() { - let result = run_command("test", "echo", &["hello", "world"]); + let result = run_command("test", "true", &["--ignored-arg"]); assert!(result.is_ok()); } } diff --git a/src/sessions.rs b/src/sessions.rs index 9f9acbd..e0f6844 100644 --- a/src/sessions.rs +++ b/src/sessions.rs @@ -12,6 +12,7 @@ const DEFAULT_XSESSION_DIRS: &[&str] = &["/usr/share/xsessions"]; pub struct Session { pub name: String, pub exec_cmd: String, + #[allow(dead_code)] // Retained for future Wayland-only filtering pub session_type: String, } diff --git a/src/users.rs b/src/users.rs index 49a2b15..0cb4f65 100644 --- a/src/users.rs +++ b/src/users.rs @@ -23,9 +23,11 @@ const NOLOGIN_SHELLS: &[&str] = &[ #[derive(Debug, Clone)] pub struct User { pub username: String, + #[allow(dead_code)] // Retained for debugging and future UID-based features pub uid: u32, pub gecos: String, pub home: PathBuf, + #[allow(dead_code)] // Retained for debugging and future shell-based filtering pub shell: String, } @@ -55,16 +57,13 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec { let mut users = Vec::new(); for line in content.lines() { - let parts: Vec<&str> = line.split(':').collect(); - if parts.len() < 7 { + let mut fields = line.splitn(7, ':'); + let (Some(username), Some(_pw), Some(uid_str), Some(_gid), Some(gecos), Some(home), Some(shell)) = + (fields.next(), fields.next(), fields.next(), fields.next(), + fields.next(), fields.next(), fields.next()) + else { continue; - } - - let username = parts[0]; - let uid_str = parts[2]; - let gecos = parts[4]; - let home = parts[5]; - let shell = parts[6]; + }; let uid = match uid_str.parse::() { Ok(u) => u,