3 Commits

Author SHA1 Message Date
nevaforget de9a3e9e6a feat: add optional background blur, align to shared texture pattern
Gaussian blur applied at texture load time when `background_blur` is
set in moonlock.toml. Refactored wallpaper loading from per-window
Picture::for_filename() to shared gdk::Texture pattern (matching
moonset/moongreet), avoiding redundant JPEG decoding on multi-monitor.
2026-03-28 14:53:27 +01:00
nevaforget 09e0d47a38 fix: audit fixes — async restart_verify, locale caching, panic safety (v0.5.0)
- restart_verify() now async via spawn_future_local (was blocking main thread)
- stop() uses 3s timeout instead of unbounded
- load_strings() caches locale detection in OnceLock (was reading /etc/locale.conf on every call)
- child_get() replaced with child_value().get() for graceful D-Bus type mismatch handling
- Eliminate redundant password clone in auth path (direct move into spawn_blocking)
- Add on_exhausted callback: hides fp_label after MAX_FP_ATTEMPTS
- Set running=false before on_success callback (prevent double-unlock)
- Add 4 unit tests for on_verify_status state machine
- Document GLib-GString/CString zeroize limitation in CLAUDE.md
2026-03-28 10:16:06 +01:00
nevaforget 13b329cd98 perf: async fprintd initialization for instant window display
Move all fprintd D-Bus calls (init, availability check, claim, verify)
from synchronous to async using gio futures. Windows now appear
immediately without waiting for D-Bus — fingerprint label fades in
once fprintd is ready. Single shared FingerprintListener across all
monitors instead of one per monitor.
2026-03-28 09:57:56 +01:00
10 changed files with 550 additions and 191 deletions
+5 -5
View File
@@ -38,13 +38,13 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
## Architektur ## Architektur
- `auth.rs` — PAM-Authentifizierung via Raw FFI (unsafe extern "C" conv callback, msg_style-aware, Zeroizing<Vec<u8>>) - `auth.rs` — PAM-Authentifizierung via Raw FFI (unsafe extern "C" conv callback, msg_style-aware, Zeroizing<Vec<u8>>)
- `fingerprint.rs` — fprintd D-Bus Listener (Rc<RefCell<FingerprintListener>>, self-wiring g-signal via connect_local) - `fingerprint.rs` — fprintd D-Bus Listener, async init/claim/verify via gio futures, sync stop with 3s timeout, on_exhausted callback after MAX_FP_ATTEMPTS
- `users.rs` — Aktuellen User via nix getuid, Avatar-Loading mit Symlink-Rejection - `users.rs` — Aktuellen User via nix getuid, Avatar-Loading mit Symlink-Rejection
- `power.rs` — Reboot/Shutdown via /usr/bin/systemctl - `power.rs` — Reboot/Shutdown via /usr/bin/systemctl
- `i18n.rs` — Locale-Erkennung und String-Tabellen (DE/EN), faillock_warning mit konfigurierbarem max_attempts - `i18n.rs` — Locale-Erkennung (OnceLock-cached) und String-Tabellen (DE/EN), faillock_warning mit konfigurierbarem max_attempts
- `config.rs` — TOML-Config (background_path, fingerprint_enabled als Option<bool>) + Wallpaper-Fallback - `config.rs` — TOML-Config (background_path, fingerprint_enabled als Option<bool>) + Wallpaper-Fallback
- `lockscreen.rs` — GTK4 UI, PAM-Auth via gio::spawn_blocking, Fingerprint-Indikator, Zeroizing<String> für Passwort, Power-Confirm - `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking, FP-Label/Start separat verdrahtet, Zeroizing<String> für Passwort, Power-Confirm
- `main.rs` — Entry Point, Panic-Hook, Root-Check, ext-session-lock-v1 (Pflicht in Release), Multi-Monitor, systemd-Journal-Logging - `main.rs` — Entry Point, Panic-Hook, Root-Check, ext-session-lock-v1 (Pflicht in Release), Multi-Monitor, systemd-Journal-Logging, async fprintd-Init nach window.present()
## Sicherheit ## Sicherheit
@@ -52,7 +52,7 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
- Release-Build: Ohne ext-session-lock-v1 wird `exit(1)` aufgerufen — kein Fenster-Fallback - Release-Build: Ohne ext-session-lock-v1 wird `exit(1)` aufgerufen — kein Fenster-Fallback
- Panic-Hook: Bei Crash wird geloggt, aber NIEMALS unlock() aufgerufen — Screen bleibt schwarz - Panic-Hook: Bei Crash wird geloggt, aber NIEMALS unlock() aufgerufen — Screen bleibt schwarz
- PAM-Callback: msg_style-aware (Passwort nur bei PAM_PROMPT_ECHO_OFF), strdup-OOM-sicher - PAM-Callback: msg_style-aware (Passwort nur bei PAM_PROMPT_ECHO_OFF), strdup-OOM-sicher
- Passwort: Zeroizing<String> ab GTK-Entry-Extraktion, Zeroizing<Vec<u8>> im PAM-FFI-Layer - Passwort: Zeroizing<String> ab GTK-Entry-Extraktion, Zeroizing<Vec<u8>> im PAM-FFI-Layer (bekannte Einschränkung: GLib-GString und CString werden nicht gezeroized — inhärente GTK/libc-Limitierung)
- Root-Check: Exit mit Fehler wenn als root gestartet - Root-Check: Exit mit Fehler wenn als root gestartet
- Faillock: UI-Warnung nach 3 Fehlversuchen, aber PAM entscheidet über Lockout (Entry bleibt aktiv) - Faillock: UI-Warnung nach 3 Fehlversuchen, aber PAM entscheidet über Lockout (Entry bleibt aktiv)
- Kein Schließen per Escape/Alt-F4 — nur durch erfolgreiche PAM-Auth oder Fingerprint - Kein Schließen per Escape/Alt-F4 — nur durch erfolgreiche PAM-Auth oder Fingerprint
Generated
+132 -1
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"
@@ -65,6 +83,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[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"
@@ -87,6 +114,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"
@@ -97,6 +133,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"
@@ -510,6 +556,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"
@@ -573,9 +634,19 @@ 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 = "moonlock" name = "moonlock"
version = "0.4.2" version = "0.5.0"
dependencies = [ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
@@ -584,6 +655,7 @@ dependencies = [
"glib-build-tools", "glib-build-tools",
"gtk4", "gtk4",
"gtk4-session-lock", "gtk4-session-lock",
"image",
"libc", "libc",
"log", "log",
"nix", "nix",
@@ -594,6 +666,16 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.29.0" version = "0.29.0"
@@ -606,6 +688,15 @@ dependencies = [
"libc", "libc",
] ]
[[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"
@@ -648,6 +739,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"
@@ -676,6 +780,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"
@@ -780,6 +890,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"
@@ -1168,3 +1284,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",
]
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "moonlock" name = "moonlock"
version = "0.4.2" version = "0.5.0"
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"
@@ -17,6 +17,7 @@ serde = { version = "1", features = ["derive"] }
nix = { version = "0.29", features = ["user"] } nix = { version = "0.29", features = ["user"] }
zeroize = { version = "1", features = ["derive"] } zeroize = { version = "1", features = ["derive"] }
libc = "0.2" libc = "0.2"
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"
+17
View File
@@ -0,0 +1,17 @@
# Decisions
Architectural and design decisions for Moonlock, in reverse chronological order.
## 2026-03-28 Optional background blur via `image` crate
- **Who**: Nyx, Dom
- **Why**: Consistent with moonset/moongreet — blurred wallpaper as lockscreen background is a common UX pattern
- **Tradeoffs**: Adds `image` crate dependency (~15 transitive crates); CPU-side Gaussian blur at load time adds startup latency proportional to image size and sigma. Acceptable because blur runs once and the texture is shared across monitors.
- **How**: `load_background_texture(bg_path, blur_radius)` loads texture, optionally applies `imageops::blur()`, returns `gdk::Texture`. Config option `background_blur: Option<f32>` in TOML.
## 2026-03-28 Shared wallpaper texture pattern (aligned with moonset/moongreet)
- **Who**: Nyx, Dom
- **Why**: Previously loaded wallpaper per-window via `Picture::for_filename()`. Multi-monitor setups decoded the JPEG redundantly. Blur feature requires texture pixel access anyway.
- **Tradeoffs**: Slightly more code in main.rs (texture loaded before window creation), but avoids redundant decoding and enables the blur feature.
- **How**: `load_background_texture()` in lockscreen.rs decodes once, `create_background_picture()` wraps shared `gdk::Texture` in `gtk::Picture`. Same pattern as moonset/moongreet.
+2 -2
View File
@@ -7,8 +7,8 @@ Part of the Moonarch ecosystem.
- **ext-session-lock-v1** — Protocol-guaranteed screen locking (compositor keeps screen locked on crash) - **ext-session-lock-v1** — Protocol-guaranteed screen locking (compositor keeps screen locked on crash)
- **PAM authentication** — Uses system PAM stack (`/etc/pam.d/moonlock`) - **PAM authentication** — Uses system PAM stack (`/etc/pam.d/moonlock`)
- **Fingerprint unlock** — fprintd D-Bus integration (optional) - **Fingerprint unlock** — fprintd D-Bus integration, async init (optional, window appears instantly)
- **Multi-monitor** — Lockscreen on every monitor - **Multi-monitor** — Lockscreen on every monitor, single shared fingerprint listener
- **i18n** — German and English (auto-detected) - **i18n** — German and English (auto-detected)
- **Faillock warning** — UI counter + system pam_faillock - **Faillock warning** — UI counter + system pam_faillock
- **Panic safety** — Panic hook logs but never unlocks - **Panic safety** — Panic hook logs but never unlocks
+15 -3
View File
@@ -21,6 +21,7 @@ fn default_config_paths() -> Vec<PathBuf> {
#[derive(Debug, Clone, Default, Deserialize)] #[derive(Debug, Clone, Default, Deserialize)]
struct RawConfig { struct RawConfig {
pub background_path: Option<String>, pub background_path: Option<String>,
pub background_blur: Option<f32>,
pub fingerprint_enabled: Option<bool>, pub fingerprint_enabled: Option<bool>,
} }
@@ -28,6 +29,7 @@ struct RawConfig {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub background_path: Option<String>, pub background_path: Option<String>,
pub background_blur: Option<f32>,
pub fingerprint_enabled: bool, pub fingerprint_enabled: bool,
} }
@@ -35,6 +37,7 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
Config { Config {
background_path: None, background_path: None,
background_blur: None,
fingerprint_enabled: true, fingerprint_enabled: true,
} }
} }
@@ -48,6 +51,7 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
if let Ok(content) = fs::read_to_string(path) { if let Ok(content) = fs::read_to_string(path) {
if let Ok(parsed) = toml::from_str::<RawConfig>(&content) { if let Ok(parsed) = toml::from_str::<RawConfig>(&content) {
if parsed.background_path.is_some() { merged.background_path = parsed.background_path; } if parsed.background_path.is_some() { merged.background_path = parsed.background_path; }
if parsed.background_blur.is_some() { merged.background_blur = parsed.background_blur; }
if let Some(fp) = parsed.fingerprint_enabled { merged.fingerprint_enabled = fp; } if let Some(fp) = parsed.fingerprint_enabled { merged.fingerprint_enabled = fp; }
} }
} }
@@ -72,7 +76,7 @@ pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path)
mod tests { mod tests {
use super::*; use super::*;
#[test] fn default_config() { let c = Config::default(); assert!(c.background_path.is_none()); assert!(c.fingerprint_enabled); } #[test] fn default_config() { let c = Config::default(); assert!(c.background_path.is_none()); assert!(c.background_blur.is_none()); assert!(c.fingerprint_enabled); }
#[test] fn load_default_fingerprint_true() { #[test] fn load_default_fingerprint_true() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moonlock.toml"); let conf = dir.path().join("moonlock.toml");
@@ -83,15 +87,23 @@ mod tests {
#[test] fn load_background() { #[test] fn load_background() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moonlock.toml"); let conf = dir.path().join("moonlock.toml");
fs::write(&conf, "background_path = \"/custom/bg.jpg\"\nfingerprint_enabled = false\n").unwrap(); fs::write(&conf, "background_path = \"/custom/bg.jpg\"\nbackground_blur = 15.0\nfingerprint_enabled = false\n").unwrap();
let c = load_config(Some(&[conf])); let c = load_config(Some(&[conf]));
assert_eq!(c.background_path.as_deref(), Some("/custom/bg.jpg")); assert_eq!(c.background_path.as_deref(), Some("/custom/bg.jpg"));
assert_eq!(c.background_blur, Some(15.0));
assert!(!c.fingerprint_enabled); assert!(!c.fingerprint_enabled);
} }
#[test] fn load_blur_optional() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moonlock.toml");
fs::write(&conf, "background_path = \"/bg.jpg\"\n").unwrap();
let c = load_config(Some(&[conf]));
assert!(c.background_blur.is_none());
}
#[test] fn resolve_config_path() { #[test] fn resolve_config_path() {
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()), fingerprint_enabled: true }; 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")), wp);
} }
#[test] fn empty_user_config_preserves_system_fingerprint() { #[test] fn empty_user_config_preserves_system_fingerprint() {
+140 -78
View File
@@ -29,35 +29,36 @@ pub struct FingerprintListener {
failed_attempts: u32, failed_attempts: u32,
on_success: Option<Box<dyn Fn() + 'static>>, on_success: Option<Box<dyn Fn() + 'static>>,
on_failure: Option<Box<dyn Fn() + 'static>>, on_failure: Option<Box<dyn Fn() + 'static>>,
on_exhausted: Option<Box<dyn Fn() + 'static>>,
} }
impl FingerprintListener { impl FingerprintListener {
/// Create a new FingerprintListener. /// Create a lightweight FingerprintListener without any D-Bus calls.
/// Connects to fprintd synchronously — call before creating GTK windows. /// Call `init_async().await` afterwards to connect to fprintd.
pub fn new() -> Self { pub fn new() -> Self {
let mut listener = FingerprintListener { FingerprintListener {
device_proxy: None, device_proxy: None,
signal_id: None, signal_id: None,
running: false, running: false,
failed_attempts: 0, failed_attempts: 0,
on_success: None, on_success: None,
on_failure: None, on_failure: None,
}; on_exhausted: None,
listener.init_device(); }
listener
} }
/// Connect to fprintd and get the default device. /// Connect to fprintd and get the default device asynchronously.
fn init_device(&mut self) { pub async fn init_async(&mut self) {
let manager = match gio::DBusProxy::for_bus_sync( let manager = match gio::DBusProxy::for_bus_future(
gio::BusType::System, gio::BusType::System,
gio::DBusProxyFlags::NONE, gio::DBusProxyFlags::NONE,
None, None,
FPRINTD_BUS_NAME, FPRINTD_BUS_NAME,
FPRINTD_MANAGER_PATH, FPRINTD_MANAGER_PATH,
FPRINTD_MANAGER_IFACE, FPRINTD_MANAGER_IFACE,
gio::Cancellable::NONE, )
) { .await
{
Ok(m) => m, Ok(m) => m,
Err(e) => { Err(e) => {
log::debug!("fprintd manager not available: {e}"); log::debug!("fprintd manager not available: {e}");
@@ -66,13 +67,10 @@ impl FingerprintListener {
}; };
// Call GetDefaultDevice // Call GetDefaultDevice
let result = match manager.call_sync( let result = match manager
"GetDefaultDevice", .call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, -1)
None, .await
gio::DBusCallFlags::NONE, {
-1,
gio::Cancellable::NONE,
) {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
log::debug!("fprintd GetDefaultDevice failed: {e}"); log::debug!("fprintd GetDefaultDevice failed: {e}");
@@ -81,20 +79,27 @@ impl FingerprintListener {
}; };
// Extract device path from variant tuple // Extract device path from variant tuple
let device_path: String = result.child_get::<String>(0); let device_path = match result.child_value(0).get::<String>() {
Some(p) => p,
None => {
log::debug!("fprintd: unexpected GetDefaultDevice response type");
return;
}
};
if device_path.is_empty() { if device_path.is_empty() {
return; return;
} }
match gio::DBusProxy::for_bus_sync( match gio::DBusProxy::for_bus_future(
gio::BusType::System, gio::BusType::System,
gio::DBusProxyFlags::NONE, gio::DBusProxyFlags::NONE,
None, None,
FPRINTD_BUS_NAME, FPRINTD_BUS_NAME,
&device_path, &device_path,
FPRINTD_DEVICE_IFACE, FPRINTD_DEVICE_IFACE,
gio::Cancellable::NONE, )
) { .await
{
Ok(proxy) => { Ok(proxy) => {
self.device_proxy = Some(proxy); self.device_proxy = Some(proxy);
} }
@@ -104,41 +109,46 @@ impl FingerprintListener {
} }
} }
/// Check if fprintd is available and the user has enrolled fingerprints. /// Check if fprintd is available and the user has enrolled fingerprints (async).
pub fn is_available(&self, username: &str) -> bool { pub async fn is_available_async(&self, username: &str) -> bool {
let proxy = match &self.device_proxy { let proxy = match &self.device_proxy {
Some(p) => p, Some(p) => p,
None => return false, None => return false,
}; };
let args = glib::Variant::from((&username,)); let args = glib::Variant::from((&username,));
match proxy.call_sync( match proxy
"ListEnrolledFingers", .call_future("ListEnrolledFingers", Some(&args), gio::DBusCallFlags::NONE, -1)
Some(&args), .await
gio::DBusCallFlags::NONE, {
-1,
gio::Cancellable::NONE,
) {
Ok(result) => { Ok(result) => {
// Result is a tuple of (array of strings) // Result is a tuple of (array of strings)
let fingers: Vec<String> = result.child_get::<Vec<String>>(0); match result.child_value(0).get::<Vec<String>>() {
!fingers.is_empty() Some(fingers) => !fingers.is_empty(),
None => {
log::debug!("fprintd: unexpected ListEnrolledFingers response type");
false
}
}
} }
Err(_) => false, Err(_) => false,
} }
} }
/// Start listening for fingerprint verification. /// Start listening for fingerprint verification.
/// Claims the device and starts verification using async D-Bus calls.
/// Connects the D-Bus g-signal handler internally. The `listener` parameter /// Connects the D-Bus g-signal handler internally. The `listener` parameter
/// must be the same `Rc<RefCell<FingerprintListener>>` that owns `self`. /// must be the same `Rc<RefCell<FingerprintListener>>` that owns `self`.
pub fn start<F, G>( pub async fn start_async<F, G, H>(
listener: &Rc<RefCell<FingerprintListener>>, listener: &Rc<RefCell<FingerprintListener>>,
username: &str, username: &str,
on_success: F, on_success: F,
on_failure: G, on_failure: G,
on_exhausted: H,
) where ) where
F: Fn() + 'static, F: Fn() + 'static,
G: Fn() + 'static, G: Fn() + 'static,
H: Fn() + 'static,
{ {
let proxy = { let proxy = {
let inner = listener.borrow(); let inner = listener.borrow();
@@ -152,38 +162,29 @@ impl FingerprintListener {
let mut inner = listener.borrow_mut(); let mut inner = listener.borrow_mut();
inner.on_success = Some(Box::new(on_success)); inner.on_success = Some(Box::new(on_success));
inner.on_failure = Some(Box::new(on_failure)); inner.on_failure = Some(Box::new(on_failure));
inner.on_exhausted = Some(Box::new(on_exhausted));
} }
// Claim the device // Claim the device
let args = glib::Variant::from((&username,)); let args = glib::Variant::from((&username,));
if let Err(e) = proxy.call_sync( if let Err(e) = proxy
"Claim", .call_future("Claim", Some(&args), gio::DBusCallFlags::NONE, -1)
Some(&args), .await
gio::DBusCallFlags::NONE, {
-1,
gio::Cancellable::NONE,
) {
log::error!("Failed to claim fingerprint device: {e}"); log::error!("Failed to claim fingerprint device: {e}");
return; return;
} }
// Start verification // Start verification
let start_args = glib::Variant::from((&"any",)); let start_args = glib::Variant::from((&"any",));
if let Err(e) = proxy.call_sync( if let Err(e) = proxy
"VerifyStart", .call_future("VerifyStart", Some(&start_args), gio::DBusCallFlags::NONE, -1)
Some(&start_args), .await
gio::DBusCallFlags::NONE, {
-1,
gio::Cancellable::NONE,
) {
log::error!("Failed to start fingerprint verification: {e}"); log::error!("Failed to start fingerprint verification: {e}");
let _ = proxy.call_sync( let _ = proxy
"Release", .call_future("Release", None, gio::DBusCallFlags::NONE, -1)
None, .await;
gio::DBusCallFlags::NONE,
-1,
gio::Cancellable::NONE,
);
return; return;
} }
@@ -232,6 +233,7 @@ impl FingerprintListener {
} }
if status == "verify-match" { if status == "verify-match" {
self.running = false;
if let Some(ref cb) = self.on_success { if let Some(ref cb) = self.on_success {
cb(); cb();
} }
@@ -240,23 +242,26 @@ impl FingerprintListener {
if RETRY_STATUSES.contains(&status) { if RETRY_STATUSES.contains(&status) {
if done { if done {
self.restart_verify(); self.restart_verify_async();
} }
return; return;
} }
if status == "verify-no-match" { if status == "verify-no-match" {
self.failed_attempts += 1; self.failed_attempts += 1;
if let Some(ref cb) = self.on_failure {
cb();
}
if self.failed_attempts >= MAX_FP_ATTEMPTS { if self.failed_attempts >= MAX_FP_ATTEMPTS {
log::warn!("Fingerprint max attempts ({MAX_FP_ATTEMPTS}) reached, stopping"); log::warn!("Fingerprint max attempts ({MAX_FP_ATTEMPTS}) reached, stopping");
if let Some(ref cb) = self.on_exhausted {
cb();
}
self.stop(); self.stop();
return; return;
} }
if let Some(ref cb) = self.on_failure {
cb();
}
if done { if done {
self.restart_verify(); self.restart_verify_async();
} }
return; return;
} }
@@ -264,31 +269,28 @@ impl FingerprintListener {
log::debug!("Unhandled fprintd status: {status}"); log::debug!("Unhandled fprintd status: {status}");
} }
/// Restart fingerprint verification after a completed attempt. /// Restart fingerprint verification asynchronously after a completed attempt.
fn restart_verify(&self) { fn restart_verify_async(&self) {
if let Some(ref proxy) = self.device_proxy { if let Some(ref proxy) = self.device_proxy {
let proxy = proxy.clone();
glib::spawn_future_local(async move {
// VerifyStop before VerifyStart to avoid D-Bus errors // VerifyStop before VerifyStart to avoid D-Bus errors
let _ = proxy.call_sync( let _ = proxy
"VerifyStop", .call_future("VerifyStop", None, gio::DBusCallFlags::NONE, -1)
None, .await;
gio::DBusCallFlags::NONE,
-1,
gio::Cancellable::NONE,
);
let args = glib::Variant::from((&"any",)); let args = glib::Variant::from((&"any",));
if let Err(e) = proxy.call_sync( if let Err(e) = proxy
"VerifyStart", .call_future("VerifyStart", Some(&args), gio::DBusCallFlags::NONE, -1)
Some(&args), .await
gio::DBusCallFlags::NONE, {
-1,
gio::Cancellable::NONE,
) {
log::error!("Failed to restart fingerprint verification: {e}"); log::error!("Failed to restart fingerprint verification: {e}");
} }
});
} }
} }
/// Stop listening and release the device. /// Stop listening and release the device.
/// Uses a short timeout (3s) to avoid blocking the UI indefinitely.
pub fn stop(&mut self) { pub fn stop(&mut self) {
if !self.running { if !self.running {
return; return;
@@ -303,14 +305,14 @@ impl FingerprintListener {
"VerifyStop", "VerifyStop",
None, None,
gio::DBusCallFlags::NONE, gio::DBusCallFlags::NONE,
-1, 3000,
gio::Cancellable::NONE, gio::Cancellable::NONE,
); );
let _ = proxy.call_sync( let _ = proxy.call_sync(
"Release", "Release",
None, None,
gio::DBusCallFlags::NONE, gio::DBusCallFlags::NONE,
-1, 3000,
gio::Cancellable::NONE, gio::Cancellable::NONE,
); );
} }
@@ -334,4 +336,64 @@ mod tests {
fn max_attempts_constant() { fn max_attempts_constant() {
assert_eq!(MAX_FP_ATTEMPTS, 10); assert_eq!(MAX_FP_ATTEMPTS, 10);
} }
#[test]
fn verify_match_sets_running_false_and_calls_success() {
use std::cell::Cell;
let called = Rc::new(Cell::new(false));
let called_clone = called.clone();
let mut listener = FingerprintListener::new();
listener.running = true;
listener.on_success = Some(Box::new(move || { called_clone.set(true); }));
listener.on_verify_status("verify-match", false);
assert!(called.get());
assert!(!listener.running);
}
#[test]
fn verify_no_match_calls_failure_and_stays_running() {
use std::cell::Cell;
let called = Rc::new(Cell::new(false));
let called_clone = called.clone();
let mut listener = FingerprintListener::new();
listener.running = true;
listener.on_failure = Some(Box::new(move || { called_clone.set(true); }));
listener.on_verify_status("verify-no-match", false);
assert!(called.get());
assert!(listener.running);
assert_eq!(listener.failed_attempts, 1);
}
#[test]
fn max_attempts_stops_listener_and_calls_exhausted() {
use std::cell::Cell;
let exhausted = Rc::new(Cell::new(false));
let exhausted_clone = exhausted.clone();
let mut listener = FingerprintListener::new();
listener.running = true;
listener.on_failure = Some(Box::new(|| {}));
listener.on_exhausted = Some(Box::new(move || { exhausted_clone.set(true); }));
for _ in 0..MAX_FP_ATTEMPTS {
listener.on_verify_status("verify-no-match", true);
}
assert!(!listener.running);
assert!(exhausted.get());
assert_eq!(listener.failed_attempts, MAX_FP_ATTEMPTS);
}
#[test]
fn not_running_ignores_signals() {
use std::cell::Cell;
let called = Rc::new(Cell::new(false));
let called_clone = called.clone();
let mut listener = FingerprintListener::new();
listener.running = false;
listener.on_success = Some(Box::new(move || { called_clone.set(true); }));
listener.on_verify_status("verify-match", false);
assert!(!called.get());
}
} }
+9 -2
View File
@@ -4,9 +4,13 @@
use std::env; use std::env;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::sync::OnceLock;
const DEFAULT_LOCALE_CONF: &str = "/etc/locale.conf"; const DEFAULT_LOCALE_CONF: &str = "/etc/locale.conf";
/// Cached locale prefix — detected once, reused for all subsequent calls.
static CACHED_LOCALE: OnceLock<String> = OnceLock::new();
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Strings { pub struct Strings {
pub password_placeholder: &'static str, pub password_placeholder: &'static str,
@@ -86,8 +90,11 @@ pub fn detect_locale() -> String {
} }
pub fn load_strings(locale: Option<&str>) -> &'static Strings { pub fn load_strings(locale: Option<&str>) -> &'static Strings {
let locale = match locale { Some(l) => l.to_string(), None => detect_locale() }; let locale = match locale {
match locale.as_str() { "de" => &STRINGS_DE, _ => &STRINGS_EN } Some(l) => l,
None => CACHED_LOCALE.get_or_init(detect_locale),
};
match locale { "de" => &STRINGS_DE, _ => &STRINGS_EN }
} }
pub fn faillock_warning(attempt_count: u32, max_attempts: u32, strings: &Strings) -> Option<String> { pub fn faillock_warning(attempt_count: u32, max_attempts: u32, strings: &Strings) -> Option<String> {
+144 -63
View File
@@ -6,6 +6,7 @@ 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::path::Path; use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
@@ -19,23 +20,33 @@ use crate::i18n::{faillock_warning, load_strings, Strings};
use crate::power::{self, PowerError}; use crate::power::{self, PowerError};
use crate::users; use crate::users;
/// Handles returned from create_lockscreen_window for post-creation wiring.
pub struct LockscreenHandles {
pub window: gtk::ApplicationWindow,
pub fp_label: gtk::Label,
pub password_entry: gtk::PasswordEntry,
pub unlock_callback: Rc<dyn Fn()>,
pub username: String,
state: Rc<RefCell<LockscreenState>>,
}
const AVATAR_SIZE: i32 = 128; const AVATAR_SIZE: i32 = 128;
const FAILLOCK_MAX_ATTEMPTS: u32 = 3; const FAILLOCK_MAX_ATTEMPTS: u32 = 3;
/// Shared mutable state for the lockscreen. /// Shared mutable state for the lockscreen.
struct LockscreenState { struct LockscreenState {
failed_attempts: u32, failed_attempts: u32,
fp_listener: FingerprintListener,
fp_listener_rc: Option<Rc<RefCell<FingerprintListener>>>, fp_listener_rc: Option<Rc<RefCell<FingerprintListener>>>,
} }
/// Create a lockscreen window for a single monitor. /// Create a lockscreen window for a single monitor.
/// Fingerprint is not initialized here — use `wire_fingerprint()` after async init.
pub fn create_lockscreen_window( pub fn create_lockscreen_window(
bg_path: &Path, bg_texture: &gdk::Texture,
config: &Config, _config: &Config,
app: &gtk::Application, app: &gtk::Application,
unlock_callback: Rc<dyn Fn()>, unlock_callback: Rc<dyn Fn()>,
) -> gtk::ApplicationWindow { ) -> LockscreenHandles {
let window = gtk::ApplicationWindow::builder() let window = gtk::ApplicationWindow::builder()
.application(app) .application(app)
.build(); .build();
@@ -46,17 +57,24 @@ pub fn create_lockscreen_window(
Some(u) => u, Some(u) => u,
None => { None => {
log::error!("Failed to get current user"); log::error!("Failed to get current user");
return window; let fp_label = gtk::Label::new(None);
fp_label.set_visible(false);
return LockscreenHandles {
window,
fp_label,
password_entry: gtk::PasswordEntry::new(),
unlock_callback,
username: String::new(),
state: Rc::new(RefCell::new(LockscreenState {
failed_attempts: 0,
fp_listener_rc: None,
})),
};
} }
}; };
let fp_listener = FingerprintListener::new();
let fp_available = config.fingerprint_enabled
&& fp_listener.is_available(&user.username);
let state = Rc::new(RefCell::new(LockscreenState { let state = Rc::new(RefCell::new(LockscreenState {
failed_attempts: 0, failed_attempts: 0,
fp_listener,
fp_listener_rc: None, fp_listener_rc: None,
})); }));
@@ -65,7 +83,7 @@ pub fn create_lockscreen_window(
window.set_child(Some(&overlay)); window.set_child(Some(&overlay));
// Background wallpaper // Background wallpaper
let background = create_background_picture(bg_path); let background = create_background_picture(bg_texture);
overlay.set_child(Some(&background)); overlay.set_child(Some(&background));
// Centered vertical box // Centered vertical box
@@ -119,15 +137,10 @@ pub fn create_lockscreen_window(
error_label.set_visible(false); error_label.set_visible(false);
login_box.append(&error_label); login_box.append(&error_label);
// Fingerprint label // Fingerprint label — hidden until async fprintd init completes
let fp_label = gtk::Label::new(None); let fp_label = gtk::Label::new(None);
fp_label.add_css_class("fingerprint-label"); fp_label.add_css_class("fingerprint-label");
if fp_available {
fp_label.set_text(strings.fingerprint_prompt);
fp_label.set_visible(true);
} else {
fp_label.set_visible(false); fp_label.set_visible(false);
}
login_box.append(&fp_label); login_box.append(&fp_label);
// Confirm box area (for power confirm) // Confirm box area (for power confirm)
@@ -227,9 +240,8 @@ pub fn create_lockscreen_window(
password_entry, password_entry,
async move { async move {
let user = username.clone(); let user = username.clone();
let pass = Zeroizing::new((*password).clone());
let result = gio::spawn_blocking(move || { let result = gio::spawn_blocking(move || {
auth::authenticate(&user, &pass) auth::authenticate(&user, &password)
}).await; }).await;
match result { match result {
@@ -293,11 +305,61 @@ pub fn create_lockscreen_window(
)); ));
window.add_controller(key_controller); window.add_controller(key_controller);
// Start fingerprint listener // Fade-in on map
if fp_available { window.connect_map(|w| {
let unlock_cb_fp = unlock_callback.clone(); glib::idle_add_local_once(clone!(
let fp_label_success = fp_label.clone(); #[weak]
let fp_label_fail = fp_label.clone(); w,
move || {
w.add_css_class("visible");
}
));
});
// Focus password entry on realize
window.connect_realize(clone!(
#[weak]
password_entry,
move |_| {
glib::idle_add_local_once(move || {
password_entry.grab_focus();
});
}
));
LockscreenHandles {
window,
fp_label,
password_entry: password_entry.clone(),
unlock_callback,
username: user.username,
state: state.clone(),
}
}
/// Show the fingerprint label and store the listener reference for stop-on-unlock.
/// Does NOT start verification — call `start_fingerprint()` on one monitor for that.
pub fn show_fingerprint_label(
handles: &LockscreenHandles,
fp_rc: &Rc<RefCell<FingerprintListener>>,
) {
let strings = load_strings(None);
handles.fp_label.set_text(strings.fingerprint_prompt);
handles.fp_label.set_visible(true);
// Store the Rc reference for stop() on unlock
handles.state.borrow_mut().fp_listener_rc = Some(fp_rc.clone());
}
/// Start fingerprint verification on a single monitor's handles.
/// Wires up on_success/on_failure callbacks and calls start_async.
pub fn start_fingerprint(
handles: &LockscreenHandles,
fp_rc: &Rc<RefCell<FingerprintListener>>,
) {
let fp_label_success = handles.fp_label.clone();
let fp_label_fail = handles.fp_label.clone();
let unlock_cb_fp = handles.unlock_callback.clone();
let on_success = move || { let on_success = move || {
let label = fp_label_success.clone(); let label = fp_label_success.clone();
@@ -335,51 +397,70 @@ pub fn create_lockscreen_window(
)); ));
}; };
// Extract the fp_listener into its own Rc<RefCell<>> for signal self-wiring let fp_label_exhausted = handles.fp_label.clone();
let fp_rc = { let on_exhausted = move || {
let mut s = state.borrow_mut(); let label = fp_label_exhausted.clone();
let listener = std::mem::replace(&mut s.fp_listener, FingerprintListener::new());
Rc::new(RefCell::new(listener))
};
FingerprintListener::start(&fp_rc, &user.username, on_success, on_failure);
// Store back the Rc reference for stop() on unlock
state.borrow_mut().fp_listener_rc = Some(fp_rc);
}
// Fade-in on map
window.connect_map(|w| {
glib::idle_add_local_once(clone!(
#[weak]
w,
move || {
w.add_css_class("visible");
}
));
});
// Focus password entry on realize
window.connect_realize(clone!(
#[weak]
password_entry,
move |_| {
glib::idle_add_local_once(move || { glib::idle_add_local_once(move || {
password_entry.grab_focus(); label.set_visible(false);
});
};
let username = handles.username.clone();
let fp_rc_clone = fp_rc.clone();
glib::spawn_future_local(async move {
FingerprintListener::start_async(
&fp_rc_clone, &username, on_success, on_failure, on_exhausted,
).await;
}); });
} }
));
window /// Load the wallpaper as a texture once, for sharing across all windows.
/// 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>) -> gdk::Texture {
let fallback = "/dev/moonarch/moonlock/wallpaper.jpg";
let texture = if bg_path.starts_with("/dev/moonarch/moonlock") {
let resource_path = bg_path.to_str().unwrap_or(fallback);
gdk::Texture::from_resource(resource_path)
} else {
let file = gio::File::for_path(bg_path);
gdk::Texture::from_file(&file).unwrap_or_else(|_| {
gdk::Texture::from_resource(fallback)
})
};
match blur_radius {
Some(sigma) if sigma > 0.0 => apply_blur(&texture, sigma),
_ => texture,
}
} }
/// Create a Picture widget for the wallpaper background. /// Apply Gaussian blur to a texture and return a blurred texture.
fn create_background_picture(bg_path: &Path) -> gtk::Picture { fn apply_blur(texture: &gdk::Texture, sigma: f32) -> gdk::Texture {
let background = if bg_path.starts_with("/dev/moonarch/moonlock") { let width = texture.width() as u32;
gtk::Picture::for_resource(bg_path.to_str().unwrap_or("")) let height = texture.height() as u32;
} else { let stride = width as usize * 4;
gtk::Picture::for_filename(bg_path.to_str().unwrap_or("")) 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 Picture widget for the wallpaper background from a shared texture.
fn create_background_picture(texture: &gdk::Texture) -> gtk::Picture {
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);
+61 -13
View File
@@ -13,9 +13,11 @@ use gdk4 as gdk;
use gtk4::prelude::*; use gtk4::prelude::*;
use gtk4::{self as gtk, gio}; use gtk4::{self as gtk, gio};
use gtk4_session_lock; use gtk4_session_lock;
use std::path::PathBuf; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use crate::fingerprint::FingerprintListener;
fn load_css(display: &gdk::Display) { fn load_css(display: &gdk::Display) {
let css_provider = gtk::CssProvider::new(); let css_provider = gtk::CssProvider::new();
css_provider.load_from_resource("/dev/moonarch/moonlock/style.css"); css_provider.load_from_resource("/dev/moonarch/moonlock/style.css");
@@ -39,14 +41,15 @@ fn activate(app: &gtk::Application) {
let config = config::load_config(None); let config = config::load_config(None);
let bg_path = config::resolve_background_path(&config); let bg_path = config::resolve_background_path(&config);
let bg_texture = lockscreen::load_background_texture(&bg_path, config.background_blur);
if gtk4_session_lock::is_supported() { if gtk4_session_lock::is_supported() {
activate_with_session_lock(app, &display, &bg_path, &config); activate_with_session_lock(app, &display, &bg_texture, &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_path, &config); activate_without_lock(app, &bg_texture, &config);
} }
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
{ {
@@ -59,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_path: &PathBuf, bg_texture: &gdk::Texture,
config: &config::Config, config: &config::Config,
) { ) {
let lock = gtk4_session_lock::Instance::new(); let lock = gtk4_session_lock::Instance::new();
@@ -75,33 +78,73 @@ fn activate_with_session_lock(
app_clone.quit(); app_clone.quit();
}); });
// Create all monitor windows immediately — no D-Bus calls here
let mut all_handles = Vec::new();
let mut created_any = false; let mut created_any = false;
for i in 0..monitors.n_items() { for i in 0..monitors.n_items() {
if let Some(monitor) = monitors if let Some(monitor) = monitors
.item(i) .item(i)
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok()) .and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
{ {
let window = lockscreen::create_lockscreen_window( let handles = lockscreen::create_lockscreen_window(
bg_path, bg_texture,
config, config,
app, app,
unlock_callback.clone(), unlock_callback.clone(),
); );
lock.assign_window_to_monitor(&window, &monitor); lock.assign_window_to_monitor(&handles.window, &monitor);
window.present(); handles.window.present();
all_handles.push(handles);
created_any = true; created_any = true;
} }
} }
if !created_any { if !created_any {
log::error!("No lockscreen windows created — screen stays locked (compositor policy)"); log::error!("No lockscreen windows created — screen stays locked (compositor policy)");
return;
} }
// Async fprintd initialization — runs after windows are visible
if config.fingerprint_enabled {
init_fingerprint_async(all_handles);
}
}
/// Initialize fprintd asynchronously after windows are visible.
/// Uses a single FingerprintListener shared across all monitors —
/// only the first monitor's handles get the fingerprint UI wired up.
fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) {
glib::spawn_future_local(async move {
let mut listener = FingerprintListener::new();
listener.init_async().await;
// Use the first monitor's username to check enrollment
let username = &all_handles[0].username;
if username.is_empty() {
return;
}
if !listener.is_available_async(username).await {
log::debug!("fprintd not available or no enrolled fingers");
return;
}
let fp_rc = Rc::new(RefCell::new(listener));
// Show fingerprint label on all monitors
for handles in &all_handles {
lockscreen::show_fingerprint_label(handles, &fp_rc);
}
// Start verification listener on the first monitor only
lockscreen::start_fingerprint(&all_handles[0], &fp_rc);
});
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
fn activate_without_lock( fn activate_without_lock(
app: &gtk::Application, app: &gtk::Application,
bg_path: &PathBuf, bg_texture: &gdk::Texture,
config: &config::Config, config: &config::Config,
) { ) {
let app_clone = app.clone(); let app_clone = app.clone();
@@ -109,14 +152,19 @@ fn activate_without_lock(
app_clone.quit(); app_clone.quit();
}); });
let window = lockscreen::create_lockscreen_window( let handles = lockscreen::create_lockscreen_window(
bg_path, bg_texture,
config, config,
app, app,
unlock_callback, unlock_callback,
); );
window.set_default_size(800, 600); handles.window.set_default_size(800, 600);
window.present(); handles.window.present();
// Async fprintd initialization for development mode
if config.fingerprint_enabled {
init_fingerprint_async(vec![handles]);
}
} }
fn setup_logging() { fn setup_logging() {