Compare commits

..

No commits in common. "77b94a560dcbc0098dcd0d7b1f05a52594f58559" and "3c39467508a832dbd4abcd9d1a96c2b6dffad940" have entirely different histories.

13 changed files with 441 additions and 214 deletions

View File

@ -17,7 +17,7 @@ Teil des Moonarch-Ökosystems.
## Projektstruktur ## Projektstruktur
- `src/` — Rust-Quellcode (main.rs, greeter.rs, ipc.rs, config.rs, users.rs, sessions.rs, i18n.rs, power.rs) - `src/` — Rust-Quellcode (main.rs, greeter.rs, ipc.rs, config.rs, users.rs, sessions.rs, i18n.rs, power.rs)
- `resources/` — GResource-Assets (style.css, default-avatar.svg) - `resources/` — GResource-Assets (style.css, wallpaper.jpg, default-avatar.svg)
- `config/` — Beispiel-Konfigurationsdateien für `/etc/moongreet/` und `/etc/greetd/` - `config/` — Beispiel-Konfigurationsdateien für `/etc/moongreet/` und `/etc/greetd/`
- `pkg/` — PKGBUILD für Arch-Linux-Paketierung (`makepkg -sf`) - `pkg/` — PKGBUILD für Arch-Linux-Paketierung (`makepkg -sf`)
@ -56,13 +56,12 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
- **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads - **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads
- **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche - **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche
- **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>` - **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>`
- **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 moonset**: Gleiche Patterns (i18n, config, users, power, GResource)
- **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 - **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 - **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 - **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var
- **File Permissions**: Cache-Dateien 0o600 - **File Permissions**: Cache-Dateien 0o600
- **Testbare Persistence**: `save_*_to`/`load_*_from` Varianten mit konfigurierbarem Pfad für Unit-Tests - **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 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 (Greeter + Wallpaper-Windows) 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` - **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 - **Error-Detail-Filterung**: GDK/greetd-Fehlerdetails nur auf `debug!`-Level, `warn!` ohne interne Details — verhindert Systeminfo-Leak ins Journal

134
Cargo.lock generated
View File

@ -2,6 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.102"
@ -20,6 +26,18 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" 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]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.22.0" version = "0.22.0"
@ -59,6 +77,15 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@ -81,6 +108,15 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 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]] [[package]]
name = "field-offset" name = "field-offset"
version = "0.3.6" version = "0.3.6"
@ -91,6 +127,16 @@ dependencies = [
"rustc_version", "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]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.1.5" version = "0.1.5"
@ -504,6 +550,21 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" 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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.13.0" version = "2.13.0"
@ -567,18 +628,28 @@ dependencies = [
"autocfg", "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]] [[package]]
name = "moongreet" name = "moongreet"
version = "0.5.3" version = "0.4.0"
dependencies = [ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
"gio", "gio",
"glib", "glib",
"glib-build-tools", "glib-build-tools",
"graphene-rs",
"gtk4", "gtk4",
"gtk4-layer-shell", "gtk4-layer-shell",
"image",
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
@ -587,6 +658,25 @@ dependencies = [
"toml 0.8.23", "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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
@ -629,6 +719,19 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 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]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@ -657,6 +760,12 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pxfm"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@ -761,6 +870,12 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
@ -1129,3 +1244,18 @@ name = "zmij"
version = "1.0.21" version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" 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",
]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "moongreet" name = "moongreet"
version = "0.5.3" version = "0.4.0"
edition = "2024" edition = "2024"
description = "A greetd greeter for Wayland with GTK4 and Layer Shell" description = "A greetd greeter for Wayland with GTK4 and Layer Shell"
license = "MIT" license = "MIT"
@ -15,7 +15,7 @@ gio = "0.22"
toml = "0.8" toml = "0.8"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
graphene-rs = { version = "0.22", package = "graphene-rs" } image = { version = "0.25", default-features = false, features = ["jpeg", "png"] }
log = "0.4" log = "0.4"
systemd-journal-logger = "2.2" systemd-journal-logger = "2.2"

View File

@ -1,20 +1,6 @@
# Decisions # Decisions
## 2026-03-28 Remove embedded wallpaper from binary ## 2026-03-28 Optional background blur via `image` crate
- **Who**: Selene, Dom
- **Why**: Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg. Embedding a 374K JPEG in the binary is redundant. GTK background color (Catppuccin Mocha base) is a clean fallback.
- **Tradeoffs**: Without moonarch installed AND without config, greeter shows plain dark background instead of wallpaper. Acceptable — that's the expected minimal state.
- **How**: Remove wallpaper.jpg from GResources, return None from resolve_background_path when no file found, skip wallpaper window creation and background picture when no path available.
## 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 500ms2s 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 - **Who**: Selene, Dom
- **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens - **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens

View File

@ -2,6 +2,7 @@
<gresources> <gresources>
<gresource prefix="/dev/moonarch/moongreet"> <gresource prefix="/dev/moonarch/moongreet">
<file>style.css</file> <file>style.css</file>
<file>wallpaper.jpg</file>
<file>default-avatar.svg</file> <file>default-avatar.svg</file>
</gresource> </gresource>
</gresources> </gresources>

BIN
resources/wallpaper.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

View File

@ -6,6 +6,7 @@ use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
const MOONARCH_WALLPAPER: &str = "/usr/share/moonarch/wallpaper.jpg"; const MOONARCH_WALLPAPER: &str = "/usr/share/moonarch/wallpaper.jpg";
const GRESOURCE_PREFIX: &str = "/dev/moonarch/moongreet";
/// Default config search path: system-wide config. /// Default config search path: system-wide config.
fn default_config_paths() -> Vec<PathBuf> { fn default_config_paths() -> Vec<PathBuf> {
@ -83,19 +84,19 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
/// Resolve the wallpaper path using the fallback hierarchy. /// Resolve the wallpaper path using the fallback hierarchy.
/// ///
/// Priority: config background_path > Moonarch system default > None (GTK background color). /// Priority: config background_path > Moonarch system default > gresource fallback.
pub fn resolve_background_path(config: &Config) -> Option<PathBuf> { pub fn resolve_background_path(config: &Config) -> PathBuf {
resolve_background_path_with(config, Path::new(MOONARCH_WALLPAPER)) resolve_background_path_with(config, Path::new(MOONARCH_WALLPAPER))
} }
/// Resolve with configurable moonarch wallpaper path (for testing). /// Resolve with configurable moonarch wallpaper path (for testing).
pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> Option<PathBuf> { pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> PathBuf {
// User-configured path // User-configured path
if let Some(ref bg) = config.background_path { if let Some(ref bg) = config.background_path {
let path = PathBuf::from(bg); let path = PathBuf::from(bg);
if path.is_file() { if path.is_file() {
log::debug!("Wallpaper: using config path {}", path.display()); log::debug!("Wallpaper: using config path {}", path.display());
return Some(path); return path;
} }
log::debug!("Wallpaper: config path {} not found, trying fallbacks", path.display()); log::debug!("Wallpaper: config path {} not found, trying fallbacks", path.display());
} }
@ -103,11 +104,12 @@ pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path)
// Moonarch ecosystem default // Moonarch ecosystem default
if moonarch_wallpaper.is_file() { if moonarch_wallpaper.is_file() {
log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display()); log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display());
return Some(moonarch_wallpaper.to_path_buf()); return moonarch_wallpaper.to_path_buf();
} }
log::debug!("Wallpaper: no wallpaper found, using GTK background color"); // GResource fallback path (loaded from compiled resources at runtime)
None log::debug!("Wallpaper: using GResource fallback");
PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg"))
} }
#[cfg(test)] #[cfg(test)]
@ -216,7 +218,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
resolve_background_path_with(&config, Path::new("/nonexistent")), resolve_background_path_with(&config, Path::new("/nonexistent")),
Some(wallpaper) wallpaper
); );
} }
@ -227,7 +229,7 @@ mod tests {
..Config::default() ..Config::default()
}; };
let result = resolve_background_path_with(&config, Path::new("/nonexistent")); let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
assert!(result.is_none()); assert!(result.to_str().unwrap().contains("moongreet"));
} }
#[test] #[test]
@ -238,14 +240,14 @@ mod tests {
let config = Config::default(); let config = Config::default();
assert_eq!( assert_eq!(
resolve_background_path_with(&config, &moonarch_wp), resolve_background_path_with(&config, &moonarch_wp),
Some(moonarch_wp) moonarch_wp
); );
} }
#[test] #[test]
fn resolve_returns_none_when_no_wallpaper_found() { fn resolve_uses_gresource_fallback_as_last_resort() {
let config = Config::default(); let config = Config::default();
let result = resolve_background_path_with(&config, Path::new("/nonexistent")); let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
assert!(result.is_none()); assert!(result.to_str().unwrap().contains("wallpaper.jpg"));
} }
} }

View File

@ -6,12 +6,17 @@ use gdk_pixbuf::Pixbuf;
use glib::clone; use glib::clone;
use gtk4::prelude::*; use gtk4::prelude::*;
use gtk4::{self as gtk, gio}; use gtk4::{self as gtk, gio};
use image::imageops;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; 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::os::unix::net::UnixStream;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use crate::config::Config; use crate::config::Config;
use crate::i18n::{faillock_warning, load_strings, Strings}; use crate::i18n::{faillock_warning, load_strings, Strings};
@ -92,11 +97,30 @@ fn is_valid_username(name: &str) -> bool {
return false; return false;
} }
name.chars() name.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-' || c == '@') .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')
} }
/// Load background texture from filesystem. /// Load the background image as a shared texture (decode once, reuse everywhere).
pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> { /// 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<f32>) -> Option<gdk::Texture> {
let path_str = bg_path.to_str()?;
let texture = 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),
Err(e) => {
log::debug!("GResource texture decode error: {e}");
log::warn!("Failed to decode background texture from GResource {path_str}");
None
}
},
Err(e) => {
log::debug!("GResource lookup error: {e}");
log::warn!("Failed to load background texture from GResource {path_str}");
None
}
}
} else {
if let Ok(meta) = std::fs::metadata(bg_path) if let Ok(meta) = std::fs::metadata(bg_path)
&& meta.len() > MAX_WALLPAPER_FILE_SIZE && meta.len() > MAX_WALLPAPER_FILE_SIZE
{ {
@ -114,46 +138,146 @@ pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
None None
} }
} }
}?;
match blur_radius {
Some(sigma) if sigma > 0.0 => Some(load_blurred_with_cache(bg_path, &texture, sigma)),
_ => Some(texture),
}
} }
// -- GPU blur via GskBlurNode ------------------------------------------------- // -- Blur cache ----------------------------------------------------------------
/// Render a blurred texture using the GPU via GskBlurNode. const BLUR_CACHE_PNG: &str = "blur-cache.png";
/// const BLUR_CACHE_META: &str = "blur-cache.meta";
/// To avoid edge darkening (blur samples transparent pixels outside bounds),
/// the texture is rendered with padding equal to 3x the blur sigma. The blur fn blur_cache_dir() -> Option<PathBuf> {
/// is applied to the padded area, then cropped back to the original size. Some(PathBuf::from("/var/cache/moongreet"))
fn render_blurred_texture( }
widget: &impl IsA<gtk::Widget>,
/// Build the cache key string for the current wallpaper + sigma.
fn build_cache_meta(bg_path: &Path, sigma: f32) -> Option<String> {
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<gdk::Texture> {
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,
texture: &gdk::Texture, texture: &gdk::Texture,
sigma: f32, meta: &str,
) -> Option<gdk::Texture> { ) -> Result<(), Box<dyn std::error::Error>> {
let native = widget.native()?; fs::create_dir_all(cache_dir)?;
let renderer = native.renderer()?;
let w = texture.width() as f32; let png_bytes = texture.save_to_png_bytes();
let h = texture.height() as f32;
// Padding must cover the blur kernel radius (typically ~3x sigma)
let pad = (sigma * 3.0).ceil();
let snapshot = gtk::Snapshot::new(); let mut f = fs::OpenOptions::new()
// Clip output to original texture size .create(true).write(true).truncate(true).mode(0o600)
snapshot.push_clip(&graphene_rs::Rect::new(pad, pad, w, h)); .open(cache_dir.join(BLUR_CACHE_PNG))?;
snapshot.push_blur(sigma as f64); f.write_all(&png_bytes)?;
// Render texture with padding on all sides (edges repeat via oversized bounds)
snapshot.append_texture(texture, &graphene_rs::Rect::new(0.0, 0.0, w + 2.0 * pad, h + 2.0 * pad));
snapshot.pop(); // blur
snapshot.pop(); // clip
let node = snapshot.to_node()?; let mut f = fs::OpenOptions::new()
let viewport = graphene_rs::Rect::new(pad, pad, w, h); .create(true).write(true).truncate(true).mode(0o600)
Some(renderer.render_texture(&node, Some(&viewport))) .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,
);
mem_texture.upcast()
} }
/// Create a wallpaper-only window for secondary monitors. /// Create a wallpaper-only window for secondary monitors.
pub fn create_wallpaper_window( pub fn create_wallpaper_window(
texture: &gdk::Texture, texture: &gdk::Texture,
blur_radius: Option<f32>,
app: &gtk::Application, app: &gtk::Application,
) -> gtk::ApplicationWindow { ) -> gtk::ApplicationWindow {
let window = gtk::ApplicationWindow::builder() let window = gtk::ApplicationWindow::builder()
@ -161,28 +285,18 @@ pub fn create_wallpaper_window(
.build(); .build();
window.add_css_class("wallpaper"); window.add_css_class("wallpaper");
let background = create_background_picture(texture, blur_radius); let background = create_background_picture(texture);
window.set_child(Some(&background)); window.set_child(Some(&background));
window window
} }
/// Create a Picture widget for the wallpaper background, optionally with GPU blur. /// Create a Picture widget for the wallpaper background from a pre-loaded texture.
fn create_background_picture(texture: &gdk::Texture, blur_radius: Option<f32>) -> gtk::Picture { fn create_background_picture(texture: &gdk::Texture) -> gtk::Picture {
let background = gtk::Picture::for_paintable(texture); let background = gtk::Picture::for_paintable(texture);
background.set_content_fit(gtk::ContentFit::Cover); background.set_content_fit(gtk::ContentFit::Cover);
background.set_hexpand(true); background.set_hexpand(true);
background.set_vexpand(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 background
} }
@ -246,7 +360,7 @@ pub fn create_greeter_window(
// Background wallpaper // Background wallpaper
if let Some(texture) = texture { if let Some(texture) = texture {
overlay.set_child(Some(&create_background_picture(texture, config.background_blur))); overlay.set_child(Some(&create_background_picture(texture)));
} }
// Main layout: 3 rows (top spacer, center login, bottom bar) // Main layout: 3 rows (top spacer, center login, bottom bar)
@ -394,7 +508,7 @@ pub fn create_greeter_window(
error_label, error_label,
move |btn| { move |btn| {
btn.set_sensitive(false); btn.set_sensitive(false);
execute_power_action(power::reboot, strings.reboot_failed, &error_label, btn); execute_power_action(power::reboot, strings.reboot_failed, &error_label);
} }
)); ));
power_box.append(&reboot_btn); power_box.append(&reboot_btn);
@ -408,7 +522,7 @@ pub fn create_greeter_window(
error_label, error_label,
move |btn| { move |btn| {
btn.set_sensitive(false); btn.set_sensitive(false);
execute_power_action(power::shutdown, strings.shutdown_failed, &error_label, btn); execute_power_action(power::shutdown, strings.shutdown_failed, &error_label);
} }
)); ));
power_box.append(&shutdown_btn); power_box.append(&shutdown_btn);
@ -628,33 +742,16 @@ fn set_avatar_from_file(
username: Option<&str>, username: Option<&str>,
state: &Rc<RefCell<GreeterState>>, state: &Rc<RefCell<GreeterState>>,
) { ) {
// Re-check symlink status to narrow TOCTOU window from get_avatar_path_with() // Reject oversized files
match std::fs::symlink_metadata(path) { if let Ok(meta) = std::fs::metadata(path) {
Ok(meta) if meta.file_type().is_symlink() => { if meta.len() > MAX_AVATAR_FILE_SIZE {
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()); log::debug!("Avatar file too large ({} bytes): {}", meta.len(), path.display());
image.set_icon_name(Some("avatar-default-symbolic")); image.set_icon_name(Some("avatar-default-symbolic"));
return; return;
} }
Err(e) => {
log::debug!("Cannot stat avatar {}: {e}", path.display());
image.set_icon_name(Some("avatar-default-symbolic"));
return;
}
Ok(_) => {}
} }
let Some(path_str) = path.to_str() else { match Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), AVATAR_SIZE, AVATAR_SIZE, true) {
log::debug!("Non-UTF-8 avatar path, skipping: {}", path.display());
image.set_icon_name(Some("avatar-default-symbolic"));
return;
};
match Pixbuf::from_file_at_scale(path_str, AVATAR_SIZE, AVATAR_SIZE, true) {
Ok(pixbuf) => { Ok(pixbuf) => {
let texture = gdk::Texture::for_pixbuf(&pixbuf); let texture = gdk::Texture::for_pixbuf(&pixbuf);
if let Some(name) = username { if let Some(name) = username {
@ -772,15 +869,6 @@ fn show_error(
password_entry.grab_focus(); 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. /// Display a greetd error, using a fallback for missing or oversized descriptions.
fn show_greetd_error( fn show_greetd_error(
error_label: &gtk::Label, error_label: &gtk::Label,
@ -788,8 +876,15 @@ fn show_greetd_error(
response: &serde_json::Value, response: &serde_json::Value,
fallback: &str, fallback: &str,
) { ) {
let message = extract_greetd_description(response, fallback); let description = response
show_error(error_label, password_entry, message); .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);
}
} }
/// Cancel any in-progress greetd session. /// Cancel any in-progress greetd session.
@ -1018,7 +1113,15 @@ fn login_worker(
return Ok(LoginResult::Cancelled); return Ok(LoginResult::Cancelled);
} }
if response.get("type").and_then(|v| v.as_str()) == Some("error") { if response.get("type").and_then(|v| v.as_str()) == Some("error") {
let message = extract_greetd_description(&response, strings.auth_failed).to_string(); 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()
};
return Ok(LoginResult::Error { message }); return Ok(LoginResult::Error { message });
} }
} }
@ -1087,7 +1190,10 @@ fn login_worker(
}); });
} else { } else {
return Ok(LoginResult::Error { return Ok(LoginResult::Error {
message: extract_greetd_description(&response, strings.session_start_failed) message: response
.get("description")
.and_then(|v| v.as_str())
.unwrap_or(strings.session_start_failed)
.to_string(), .to_string(),
}); });
} }
@ -1103,13 +1209,10 @@ fn execute_power_action(
action_fn: fn() -> Result<(), PowerError>, action_fn: fn() -> Result<(), PowerError>,
error_message: &'static str, error_message: &'static str,
error_label: &gtk::Label, error_label: &gtk::Label,
button: &gtk::Button,
) { ) {
glib::spawn_future_local(clone!( glib::spawn_future_local(clone!(
#[weak] #[weak]
error_label, error_label,
#[weak]
button,
async move { async move {
let result = gio::spawn_blocking(move || action_fn()).await; let result = gio::spawn_blocking(move || action_fn()).await;
@ -1119,13 +1222,11 @@ fn execute_power_action(
log::error!("Power action failed: {e}"); log::error!("Power action failed: {e}");
error_label.set_text(error_message); error_label.set_text(error_message);
error_label.set_visible(true); error_label.set_visible(true);
button.set_sensitive(true);
} }
Err(_) => { Err(_) => {
log::error!("Power action panicked"); log::error!("Power action panicked");
error_label.set_text(error_message); error_label.set_text(error_message);
error_label.set_visible(true); error_label.set_visible(true);
button.set_sensitive(true);
} }
} }
} }
@ -1156,24 +1257,18 @@ fn save_last_user(username: &str) {
fn save_last_user_to(path: &Path, username: &str) { fn save_last_user_to(path: &Path, username: &str) {
log::debug!("Saving last user: {username}"); log::debug!("Saving last user: {username}");
if let Some(parent) = path.parent() if let Some(parent) = path.parent() {
&& let Err(e) = std::fs::create_dir_all(parent) let _ = std::fs::create_dir_all(parent);
{
log::warn!("Failed to create cache dir {}: {e}", parent.display());
return;
} }
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::io::Write; use std::io::Write;
if let Err(e) = std::fs::OpenOptions::new() let _ = std::fs::OpenOptions::new()
.create(true) .create(true)
.write(true) .write(true)
.truncate(true) .truncate(true)
.mode(0o600) .mode(0o600)
.open(path) .open(path)
.and_then(|mut f| f.write_all(username.as_bytes())) .and_then(|mut f| f.write_all(username.as_bytes()));
{
log::warn!("Failed to save last user to {}: {e}", path.display());
}
} }
fn load_last_session(username: &str) -> Option<String> { fn load_last_session(username: &str) -> Option<String> {
@ -1218,16 +1313,13 @@ fn save_last_session_to(path: &Path, session_name: &str) {
log::debug!("Saving last session: {session_name}"); log::debug!("Saving last session: {session_name}");
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::io::Write; use std::io::Write;
if let Err(e) = std::fs::OpenOptions::new() let _ = std::fs::OpenOptions::new()
.create(true) .create(true)
.write(true) .write(true)
.truncate(true) .truncate(true)
.mode(0o600) .mode(0o600)
.open(path) .open(path)
.and_then(|mut f| f.write_all(session_name.as_bytes())) .and_then(|mut f| f.write_all(session_name.as_bytes()));
{
log::warn!("Failed to save last session to {}: {e}", path.display());
}
} }
#[cfg(test)] #[cfg(test)]
@ -1241,8 +1333,6 @@ mod tests {
assert!(is_valid_username("test-user")); assert!(is_valid_username("test-user"));
assert!(is_valid_username("test.user")); assert!(is_valid_username("test.user"));
assert!(is_valid_username("_admin")); assert!(is_valid_username("_admin"));
assert!(is_valid_username("user@domain"));
assert!(is_valid_username(&"a".repeat(MAX_USERNAME_LENGTH)));
} }
#[test] #[test]
@ -1250,7 +1340,6 @@ mod tests {
assert!(!is_valid_username("")); assert!(!is_valid_username(""));
assert!(!is_valid_username(".hidden")); assert!(!is_valid_username(".hidden"));
assert!(!is_valid_username("-dash")); assert!(!is_valid_username("-dash"));
assert!(!is_valid_username("@domain"));
assert!(!is_valid_username("user/name")); assert!(!is_valid_username("user/name"));
assert!(!is_valid_username(&"a".repeat(MAX_USERNAME_LENGTH + 1))); assert!(!is_valid_username(&"a".repeat(MAX_USERNAME_LENGTH + 1)));
} }
@ -1611,18 +1700,6 @@ mod tests {
assert!(matches!(result, LoginResult::Cancelled)); assert!(matches!(result, LoginResult::Cancelled));
} }
#[test]
fn login_worker_connect_failure() {
let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
let result = login_worker(
"alice", "pass", "/usr/bin/niri",
"/nonexistent/sock", &default_greetd_sock(), &cancelled,
load_strings(Some("en")),
);
assert!(result.is_err());
}
#[test] #[test]
fn login_worker_invalid_exec_cmd() { fn login_worker_invalid_exec_cmd() {
let (sock_path, handle) = fake_greetd(|stream| { let (sock_path, handle) = fake_greetd(|stream| {
@ -1691,7 +1768,7 @@ mod tests {
#[test] #[test]
fn load_background_texture_missing_file_returns_none() { fn load_background_texture_missing_file_returns_none() {
let result = load_background_texture(Path::new("/nonexistent/wallpaper.jpg")); let result = load_background_texture(Path::new("/nonexistent/wallpaper.jpg"), None);
assert!(result.is_none()); assert!(result.is_none());
} }
@ -1702,7 +1779,7 @@ mod tests {
// Create a sparse file that exceeds MAX_WALLPAPER_FILE_SIZE // Create a sparse file that exceeds MAX_WALLPAPER_FILE_SIZE
let f = std::fs::File::create(&path).unwrap(); let f = std::fs::File::create(&path).unwrap();
f.set_len(MAX_WALLPAPER_FILE_SIZE + 1).unwrap(); f.set_len(MAX_WALLPAPER_FILE_SIZE + 1).unwrap();
let result = load_background_texture(&path); let result = load_background_texture(&path, None);
assert!(result.is_none()); assert!(result.is_none());
} }
@ -1713,32 +1790,63 @@ mod tests {
// 0xFF is not valid UTF-8 // 0xFF is not valid UTF-8
let non_utf8 = OsStr::from_bytes(&[0xff, 0xfe, 0xfd]); let non_utf8 = OsStr::from_bytes(&[0xff, 0xfe, 0xfd]);
let path = Path::new(non_utf8); let path = Path::new(non_utf8);
let result = load_background_texture(path); 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",
);
assert!(result.is_none()); assert!(result.is_none());
} }
#[test] #[test]
fn extract_greetd_description_normal() { fn cache_missing_meta_returns_none() {
let resp = serde_json::json!({"type": "error", "description": "bad password"}); let dir = tempfile::tempdir().unwrap();
assert_eq!(extract_greetd_description(&resp, "fallback"), "bad password"); let result = load_cached_blur(
} dir.path(),
"path=/any.jpg\nsize=1\nmtime=1\nsigma=20\n",
#[test] );
fn extract_greetd_description_oversized() { assert!(result.is_none());
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");
} }
} }

View File

@ -28,6 +28,7 @@ pub struct Strings {
pub session_start_failed: &'static str, pub session_start_failed: &'static str,
pub reboot_failed: &'static str, pub reboot_failed: &'static str,
pub shutdown_failed: &'static str, pub shutdown_failed: &'static str,
pub connection_error: &'static str,
pub socket_error: &'static str, pub socket_error: &'static str,
pub unexpected_greetd_response: &'static str, pub unexpected_greetd_response: &'static str,
@ -52,6 +53,7 @@ const STRINGS_DE: Strings = Strings {
session_start_failed: "Session konnte nicht gestartet werden", session_start_failed: "Session konnte nicht gestartet werden",
reboot_failed: "Neustart fehlgeschlagen", reboot_failed: "Neustart fehlgeschlagen",
shutdown_failed: "Herunterfahren fehlgeschlagen", shutdown_failed: "Herunterfahren fehlgeschlagen",
connection_error: "Verbindungsfehler",
socket_error: "Socket-Fehler", socket_error: "Socket-Fehler",
unexpected_greetd_response: "Unerwartete Antwort von greetd", unexpected_greetd_response: "Unerwartete Antwort von greetd",
faillock_attempts_remaining: "Noch {n} Versuch(e) vor Kontosperrung!", faillock_attempts_remaining: "Noch {n} Versuch(e) vor Kontosperrung!",
@ -74,6 +76,7 @@ const STRINGS_EN: Strings = Strings {
session_start_failed: "Failed to start session", session_start_failed: "Failed to start session",
reboot_failed: "Reboot failed", reboot_failed: "Reboot failed",
shutdown_failed: "Shutdown failed", shutdown_failed: "Shutdown failed",
connection_error: "Connection error",
socket_error: "Socket error", socket_error: "Socket error",
unexpected_greetd_response: "Unexpected response from greetd", unexpected_greetd_response: "Unexpected response from greetd",
faillock_attempts_remaining: "{n} attempt(s) remaining before lockout!", faillock_attempts_remaining: "{n} attempt(s) remaining before lockout!",

View File

@ -51,11 +51,14 @@ fn activate(app: &gtk::Application) {
// Load config and resolve wallpaper // Load config and resolve wallpaper
let config = config::load_config(None); let config = config::load_config(None);
let bg_texture = config::resolve_background_path(&config) let bg_path = config::resolve_background_path(&config);
.and_then(|path| { log::debug!("Background path: {}", bg_path.display());
log::debug!("Background path: {}", path.display());
greeter::load_background_texture(&path) // Load background texture once — shared across all windows
}); let bg_texture = greeter::load_background_texture(&bg_path, config.background_blur);
if bg_texture.is_none() {
log::error!("Failed to load background texture — greeter will start without wallpaper");
}
let use_layer_shell = std::env::var("MOONGREET_NO_LAYER_SHELL").is_err(); let use_layer_shell = std::env::var("MOONGREET_NO_LAYER_SHELL").is_err();
log::debug!("Layer shell: {use_layer_shell}"); log::debug!("Layer shell: {use_layer_shell}");
@ -78,7 +81,7 @@ fn activate(app: &gtk::Application) {
.item(i) .item(i)
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok()) .and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
{ {
let wallpaper = greeter::create_wallpaper_window(texture, config.background_blur, app); let wallpaper = greeter::create_wallpaper_window(texture, app);
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom); setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom);
wallpaper.set_monitor(Some(&monitor)); wallpaper.set_monitor(Some(&monitor));
wallpaper.present(); wallpaper.present();
@ -88,16 +91,10 @@ fn activate(app: &gtk::Application) {
} }
fn setup_logging() { fn setup_logging() {
match systemd_journal_logger::JournalLog::new() { systemd_journal_logger::JournalLog::new()
Ok(logger) => { .unwrap()
if let Err(e) = logger.install() { .install()
eprintln!("Failed to install journal logger: {e}"); .unwrap();
}
}
Err(e) => {
eprintln!("Failed to create journal logger: {e}");
}
}
let level = if std::env::var("MOONGREET_DEBUG").is_ok() { let level = if std::env::var("MOONGREET_DEBUG").is_ok() {
log::LevelFilter::Debug log::LevelFilter::Debug
} else { } else {

View File

@ -41,7 +41,8 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
if output.status.success() { if output.status.success() {
log::debug!("Power action {action} completed successfully"); log::debug!("Power action {action} completed successfully");
} else { }
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
return Err(PowerError::CommandFailed { return Err(PowerError::CommandFailed {
action, action,
@ -99,7 +100,7 @@ mod tests {
#[test] #[test]
fn run_command_passes_args() { fn run_command_passes_args() {
let result = run_command("test", "true", &["--ignored-arg"]); let result = run_command("test", "echo", &["hello", "world"]);
assert!(result.is_ok()); assert!(result.is_ok());
} }
} }

View File

@ -12,7 +12,6 @@ const DEFAULT_XSESSION_DIRS: &[&str] = &["/usr/share/xsessions"];
pub struct Session { pub struct Session {
pub name: String, pub name: String,
pub exec_cmd: String, pub exec_cmd: String,
#[allow(dead_code)] // Retained for future Wayland-only filtering
pub session_type: String, pub session_type: String,
} }

View File

@ -23,11 +23,9 @@ const NOLOGIN_SHELLS: &[&str] = &[
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct User { pub struct User {
pub username: String, pub username: String,
#[allow(dead_code)] // Retained for debugging and future UID-based features
pub uid: u32, pub uid: u32,
pub gecos: String, pub gecos: String,
pub home: PathBuf, pub home: PathBuf,
#[allow(dead_code)] // Retained for debugging and future shell-based filtering
pub shell: String, pub shell: String,
} }
@ -57,13 +55,16 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec<User> {
let mut users = Vec::new(); let mut users = Vec::new();
for line in content.lines() { for line in content.lines() {
let mut fields = line.splitn(7, ':'); let parts: Vec<&str> = line.split(':').collect();
let (Some(username), Some(_pw), Some(uid_str), Some(_gid), Some(gecos), Some(home), Some(shell)) = if parts.len() < 7 {
(fields.next(), fields.next(), fields.next(), fields.next(),
fields.next(), fields.next(), fields.next())
else {
continue; 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::<u32>() { let uid = match uid_str.parse::<u32>() {
Ok(u) => u, Ok(u) => u,