3 Commits

Author SHA1 Message Date
nevaforget 484e990c68 fix: elevate CSS priority to override GTK4 user theme (v0.6.4)
Colloid-Catppuccin theme loaded via ~/.config/gtk-4.0/gtk.css at
PRIORITY_USER (800) was overriding moonlock's PRIORITY_APPLICATION (600),
causing avatar to lose its circular border-radius.

- Use STYLE_PROVIDER_PRIORITY_USER for app CSS provider
- Replace border-radius: 50% with 9999px (GTK4 CSS percentage quirk)
2026-03-29 14:24:26 +02:00
nevaforget 77d6994b8f fix: prevent edge darkening on GPU-blurred wallpaper (v0.6.3)
GskBlurNode samples pixels outside texture bounds as transparent,
causing visible darkening at wallpaper edges. Fix renders the texture
with 3x-sigma padding before blur, then clips back to original size.
Symmetric fix with moonset v0.7.1.
2026-03-28 23:28:40 +01:00
nevaforget fff18bfb9d refactor: remove embedded wallpaper from binary (v0.6.2)
Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg.
Embedding a 374K JPEG in the binary was redundant. Without a wallpaper
file, GTK background color (Catppuccin Mocha base) shows through.
2026-03-28 23:23:02 +01:00
10 changed files with 62 additions and 47 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ Teil des Moonarch-Ökosystems.
## Projektstruktur ## Projektstruktur
- `src/` — Rust-Quellcode (main.rs, lockscreen.rs, auth.rs, fingerprint.rs, config.rs, i18n.rs, users.rs, power.rs) - `src/` — Rust-Quellcode (main.rs, lockscreen.rs, auth.rs, fingerprint.rs, config.rs, i18n.rs, users.rs, power.rs)
- `resources/` — GResource-Assets (style.css, wallpaper.jpg, default-avatar.svg) - `resources/` — GResource-Assets (style.css, default-avatar.svg)
- `config/` — PAM-Konfiguration und Beispiel-Config - `config/` — PAM-Konfiguration und Beispiel-Config
## Kommandos ## Kommandos
Generated
+1 -1
View File
@@ -575,7 +575,7 @@ dependencies = [
[[package]] [[package]]
name = "moonlock" name = "moonlock"
version = "0.6.0" version = "0.6.4"
dependencies = [ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "moonlock" name = "moonlock"
version = "0.6.1" version = "0.6.4"
edition = "2024" edition = "2024"
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support" description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
license = "MIT" license = "MIT"
+7
View File
@@ -2,6 +2,13 @@
Architectural and design decisions for Moonlock, in reverse chronological order. Architectural and design decisions for Moonlock, in reverse chronological order.
## 2026-03-28 Remove embedded wallpaper from binary
- **Who**: Nyx, 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, lockscreen 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 background picture creation when no texture available.
## 2026-03-28 Audit-driven security and lifecycle fixes (v0.6.0) ## 2026-03-28 Audit-driven security and lifecycle fixes (v0.6.0)
- **Who**: Nyx, Dom - **Who**: Nyx, Dom
-1
View File
@@ -2,7 +2,6 @@
<gresources> <gresources>
<gresource prefix="/dev/moonarch/moonlock"> <gresource prefix="/dev/moonarch/moonlock">
<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>
+1 -1
View File
@@ -23,7 +23,7 @@ window.lockscreen.visible {
/* Round avatar image */ /* Round avatar image */
.avatar { .avatar {
border-radius: 50%; border-radius: 9999px;
min-width: 128px; min-width: 128px;
min-height: 128px; min-height: 128px;
background-color: @theme_selected_bg_color; background-color: @theme_selected_bg_color;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

+10 -11
View File
@@ -6,7 +6,6 @@ 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/moonlock";
fn default_config_paths() -> Vec<PathBuf> { fn default_config_paths() -> Vec<PathBuf> {
let mut paths = vec![PathBuf::from("/etc/moonlock/moonlock.toml")]; let mut paths = vec![PathBuf::from("/etc/moonlock/moonlock.toml")];
@@ -64,17 +63,17 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
merged merged
} }
pub fn resolve_background_path(config: &Config) -> PathBuf { pub fn resolve_background_path(config: &Config) -> Option<PathBuf> {
resolve_background_path_with(config, Path::new(MOONARCH_WALLPAPER)) resolve_background_path_with(config, Path::new(MOONARCH_WALLPAPER))
} }
pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> PathBuf { pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> Option<PathBuf> {
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() && !path.is_symlink() { return path; } if path.is_file() && !path.is_symlink() { return Some(path); }
} }
if moonarch_wallpaper.is_file() { return moonarch_wallpaper.to_path_buf(); } if moonarch_wallpaper.is_file() { return Some(moonarch_wallpaper.to_path_buf()); }
PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg")) None
} }
#[cfg(test)] #[cfg(test)]
@@ -109,7 +108,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let wp = dir.path().join("bg.jpg"); fs::write(&wp, "fake").unwrap(); let wp = dir.path().join("bg.jpg"); fs::write(&wp, "fake").unwrap();
let c = Config { background_path: Some(wp.to_str().unwrap().to_string()), ..Config::default() }; let c = Config { background_path: Some(wp.to_str().unwrap().to_string()), ..Config::default() };
assert_eq!(resolve_background_path_with(&c, Path::new("/nonexistent")), wp); assert_eq!(resolve_background_path_with(&c, Path::new("/nonexistent")), Some(wp));
} }
#[test] fn empty_user_config_preserves_system_fingerprint() { #[test] fn empty_user_config_preserves_system_fingerprint() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
@@ -120,10 +119,10 @@ mod tests {
let c = load_config(Some(&[sys_conf, usr_conf])); let c = load_config(Some(&[sys_conf, usr_conf]));
assert!(!c.fingerprint_enabled); assert!(!c.fingerprint_enabled);
} }
#[test] fn resolve_gresource_fallback() { #[test] fn resolve_no_wallpaper_returns_none() {
let c = Config::default(); let c = Config::default();
let r = resolve_background_path_with(&c, Path::new("/nonexistent")); let r = resolve_background_path_with(&c, Path::new("/nonexistent"));
assert!(r.to_str().unwrap().contains("moonlock")); assert!(r.is_none());
} }
#[test] fn toml_parse_error_returns_default() { #[test] fn toml_parse_error_returns_default() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
@@ -141,8 +140,8 @@ mod tests {
fs::write(&real, "fake").unwrap(); fs::write(&real, "fake").unwrap();
std::os::unix::fs::symlink(&real, &link).unwrap(); std::os::unix::fs::symlink(&real, &link).unwrap();
let c = Config { background_path: Some(link.to_str().unwrap().to_string()), ..Config::default() }; let c = Config { background_path: Some(link.to_str().unwrap().to_string()), ..Config::default() };
// Symlink should be rejected — falls through to moonarch wallpaper or gresource // Symlink should be rejected — falls through to None
let r = resolve_background_path_with(&c, Path::new("/nonexistent")); let r = resolve_background_path_with(&c, Path::new("/nonexistent"));
assert_ne!(r, link); assert!(r.is_none());
} }
} }
+34 -24
View File
@@ -44,7 +44,7 @@ struct LockscreenState {
/// The `blur_cache` and `avatar_cache` are shared across monitors for multi-monitor /// The `blur_cache` and `avatar_cache` are shared across monitors for multi-monitor
/// setups, avoiding redundant GPU renders and SVG rasterizations. /// setups, avoiding redundant GPU renders and SVG rasterizations.
pub fn create_lockscreen_window( pub fn create_lockscreen_window(
bg_texture: &gdk::Texture, bg_texture: Option<&gdk::Texture>,
config: &Config, config: &Config,
app: &gtk::Application, app: &gtk::Application,
unlock_callback: Rc<dyn Fn()>, unlock_callback: Rc<dyn Fn()>,
@@ -86,9 +86,11 @@ pub fn create_lockscreen_window(
let overlay = gtk::Overlay::new(); let overlay = gtk::Overlay::new();
window.set_child(Some(&overlay)); window.set_child(Some(&overlay));
// Background wallpaper // Background wallpaper (if available — otherwise GTK background color shows through)
let background = create_background_picture(bg_texture, config.background_blur, blur_cache); if let Some(texture) = bg_texture {
overlay.set_child(Some(&background)); let background = create_background_picture(texture, config.background_blur, blur_cache);
overlay.set_child(Some(&background));
}
// Centered vertical box // Centered vertical box
let main_box = gtk::Box::new(gtk::Orientation::Vertical, 0); let main_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
@@ -430,18 +432,16 @@ pub fn start_fingerprint(
} }
/// Load the wallpaper as a texture once, for sharing across all windows. /// Load the wallpaper as a texture once, for sharing across all windows.
/// Returns None if no wallpaper path is provided or the file cannot be loaded.
/// Blur is applied at render time via GPU (GskBlurNode), not here. /// Blur is applied at render time via GPU (GskBlurNode), not here.
pub fn load_background_texture(bg_path: &Path) -> gdk::Texture { pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
let fallback = "/dev/moonarch/moonlock/wallpaper.jpg"; let file = gio::File::for_path(bg_path);
match gdk::Texture::from_file(&file) {
if bg_path.starts_with("/dev/moonarch/moonlock") { Ok(texture) => Some(texture),
let resource_path = bg_path.to_str().unwrap_or(fallback); Err(e) => {
gdk::Texture::from_resource(resource_path) log::warn!("Failed to load wallpaper {}: {e}", bg_path.display());
} else { None
let file = gio::File::for_path(bg_path); }
gdk::Texture::from_file(&file).unwrap_or_else(|_| {
gdk::Texture::from_resource(fallback)
})
} }
} }
@@ -483,6 +483,10 @@ fn create_background_picture(
/// Render a blurred texture using the widget's GPU renderer. /// Render a blurred texture using the widget's GPU renderer.
/// Returns None if the renderer is not available. /// Returns None if the renderer is not available.
///
/// To avoid edge darkening (blur samples transparent pixels outside bounds),
/// the texture is rendered with padding equal to 3x the blur sigma. The blur
/// is applied to the padded area, then cropped back to the original size.
fn render_blurred_texture( fn render_blurred_texture(
widget: &impl IsA<gtk::Widget>, widget: &impl IsA<gtk::Widget>,
texture: &gdk::Texture, texture: &gdk::Texture,
@@ -490,18 +494,24 @@ fn render_blurred_texture(
) -> Option<gdk::Texture> { ) -> Option<gdk::Texture> {
let native = widget.native()?; let native = widget.native()?;
let renderer = native.renderer()?; let renderer = native.renderer()?;
let w = texture.width() as f32;
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 snapshot = gtk::Snapshot::new();
let bounds = graphene::Rect::new( // Clip output to original texture size
0.0, snapshot.push_clip(&graphene::Rect::new(pad, pad, w, h));
0.0,
texture.width() as f32,
texture.height() as f32,
);
snapshot.push_blur(sigma as f64); snapshot.push_blur(sigma as f64);
snapshot.append_texture(texture, &bounds); // Render texture with padding on all sides (edges repeat via oversized bounds)
snapshot.pop(); snapshot.append_texture(texture, &graphene::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 node = snapshot.to_node()?;
Some(renderer.render_texture(&node, None)) let viewport = graphene::Rect::new(pad, pad, w, h);
Some(renderer.render_texture(&node, Some(&viewport)))
} }
/// Load an image file and set it as the avatar. Stores the texture in the cache. /// Load an image file and set it as the avatar. Stores the texture in the cache.
+7 -7
View File
@@ -24,7 +24,7 @@ fn load_css(display: &gdk::Display) {
gtk::style_context_add_provider_for_display( gtk::style_context_add_provider_for_display(
display, display,
&css_provider, &css_provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, gtk::STYLE_PROVIDER_PRIORITY_USER,
); );
} }
@@ -40,16 +40,16 @@ fn activate(app: &gtk::Application) {
load_css(&display); load_css(&display);
let config = config::load_config(None); let config = config::load_config(None);
let bg_path = config::resolve_background_path(&config); let bg_texture = config::resolve_background_path(&config)
let bg_texture = lockscreen::load_background_texture(&bg_path); .and_then(|path| lockscreen::load_background_texture(&path));
if gtk4_session_lock::is_supported() { if gtk4_session_lock::is_supported() {
activate_with_session_lock(app, &display, &bg_texture, &config); activate_with_session_lock(app, &display, bg_texture.as_ref(), &config);
} else { } else {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
log::warn!("ext-session-lock-v1 not supported — running in development mode"); log::warn!("ext-session-lock-v1 not supported — running in development mode");
activate_without_lock(app, &bg_texture, &config); activate_without_lock(app, bg_texture.as_ref(), &config);
} }
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
{ {
@@ -62,7 +62,7 @@ fn activate(app: &gtk::Application) {
fn activate_with_session_lock( fn activate_with_session_lock(
app: &gtk::Application, app: &gtk::Application,
display: &gdk::Display, display: &gdk::Display,
bg_texture: &gdk::Texture, bg_texture: Option<&gdk::Texture>,
config: &config::Config, config: &config::Config,
) { ) {
let lock = gtk4_session_lock::Instance::new(); let lock = gtk4_session_lock::Instance::new();
@@ -158,7 +158,7 @@ fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
fn activate_without_lock( fn activate_without_lock(
app: &gtk::Application, app: &gtk::Application,
bg_texture: &gdk::Texture, bg_texture: Option<&gdk::Texture>,
config: &config::Config, config: &config::Config,
) { ) {
let app_clone = app.clone(); let app_clone = app.clone();