Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 484e990c68 | |||
| 77d6994b8f | |||
| fff18bfb9d | |||
| ca934b8c36 | |||
| d11b6e634e | |||
| 4026f6dafa | |||
| 48706e5a29 |
@@ -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
|
||||||
@@ -38,22 +38,24 @@ 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, async init/claim/verify via gio futures, sync stop with 3s timeout, on_exhausted callback after MAX_FP_ATTEMPTS
|
- `fingerprint.rs` — fprintd D-Bus Listener, async init/claim/verify via gio futures, sender-validated signal handler, cleanup_dbus() für sauberen D-Bus-Lifecycle, running_flag für Race-Safety in async restarts, 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 (OnceLock-cached) 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, background_blur, fingerprint_enabled als Option<bool>) + Wallpaper-Fallback + Symlink-Rejection für background_path + Parse-Error-Logging
|
||||||
- `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking, FP-Label/Start separat verdrahtet, 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, GPU-Blur via GskBlurNode, Blur/Avatar-Cache für Multi-Monitor
|
||||||
- `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()
|
- `main.rs` — Entry Point, Panic-Hook (vor Logging), Root-Check, ext-session-lock-v1 (Pflicht in Release), Multi-Monitor mit shared Blur/Avatar-Caches, systemd-Journal-Logging, Debug-Level per `MOONLOCK_DEBUG` Env-Var, async fprintd-Init nach window.present()
|
||||||
|
|
||||||
## Sicherheit
|
## Sicherheit
|
||||||
|
|
||||||
- ext-session-lock-v1 garantiert: Compositor sperrt alle Surfaces bei lock()
|
- ext-session-lock-v1 garantiert: Compositor sperrt alle Surfaces bei lock()
|
||||||
- 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. Hook wird vor Logging installiert.
|
||||||
- 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, num_msg-Guard gegen negative Werte
|
||||||
|
- fprintd: D-Bus Signal-Sender wird gegen fprintd's unique bus name validiert (Anti-Spoofing)
|
||||||
- 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)
|
- 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
|
||||||
|
- Kein Peek-Icon am Passwortfeld (Shoulder-Surfing-Schutz)
|
||||||
- GResource-Bundle: CSS/Assets in der Binary kompiliert
|
- GResource-Bundle: CSS/Assets in der Binary kompiliert
|
||||||
|
|||||||
Generated
+2
-132
@@ -2,12 +2,6 @@
|
|||||||
# 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"
|
||||||
@@ -26,18 +20,6 @@ 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"
|
||||||
@@ -83,15 +65,6 @@ 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"
|
||||||
@@ -114,15 +87,6 @@ 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"
|
||||||
@@ -133,16 +97,6 @@ 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"
|
||||||
@@ -556,21 +510,6 @@ 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"
|
||||||
@@ -634,28 +573,18 @@ 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.5.0"
|
version = "0.6.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
"gio",
|
"gio",
|
||||||
"glib",
|
"glib",
|
||||||
"glib-build-tools",
|
"glib-build-tools",
|
||||||
|
"graphene-rs",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-session-lock",
|
"gtk4-session-lock",
|
||||||
"image",
|
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"nix",
|
"nix",
|
||||||
@@ -666,16 +595,6 @@ 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"
|
||||||
@@ -688,15 +607,6 @@ 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"
|
||||||
@@ -739,19 +649,6 @@ 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"
|
||||||
@@ -780,12 +677,6 @@ 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"
|
||||||
@@ -890,12 +781,6 @@ 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"
|
||||||
@@ -1284,18 +1169,3 @@ 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",
|
|
||||||
]
|
|
||||||
|
|||||||
+7
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moonlock"
|
name = "moonlock"
|
||||||
version = "0.5.0"
|
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"
|
||||||
@@ -14,10 +14,10 @@ gdk-pixbuf = "0.22"
|
|||||||
gio = "0.22"
|
gio = "0.22"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
graphene-rs = { version = "0.22", package = "graphene-rs" }
|
||||||
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"
|
||||||
|
|
||||||
@@ -26,3 +26,8 @@ tempfile = "3"
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
glib-build-tools = "0.22"
|
glib-build-tools = "0.22"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = "thin"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|||||||
+22
-1
@@ -2,7 +2,28 @@
|
|||||||
|
|
||||||
Architectural and design decisions for Moonlock, in reverse chronological order.
|
Architectural and design decisions for Moonlock, in reverse chronological order.
|
||||||
|
|
||||||
## 2026-03-28 – Optional background blur via `image` crate
|
## 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)
|
||||||
|
|
||||||
|
- **Who**: Nyx, Dom
|
||||||
|
- **Why**: Triple audit (quality, performance, security) revealed a critical D-Bus signal spoofing vector, fingerprint lifecycle bugs, and multi-monitor performance issues.
|
||||||
|
- **Tradeoffs**: `cleanup_dbus()` extraction adds a method but clarifies the stop/match ownership; `running_flag: Rc<Cell<bool>>` adds a field but prevents race between async restart and stop; sender validation adds a check per signal but closes the only known auth bypass.
|
||||||
|
- **How**: (1) Validate D-Bus VerifyStatus sender against fprintd's unique bus name. (2) Extract `cleanup_dbus()` from `stop()`, call it on verify-match. (3) `Rc<Cell<bool>>` running flag checked after await in `restart_verify_async`. (4) Consistent 3s D-Bus timeouts. (5) Panic hook before logging. (6) Blur and avatar caches shared across monitors. (7) Peek icon disabled. (8) Symlink rejection for background_path. (9) TOML parse errors logged.
|
||||||
|
|
||||||
|
## 2026-03-28 – GPU blur via GskBlurNode replaces CPU blur
|
||||||
|
|
||||||
|
- **Who**: Nyx, Dom
|
||||||
|
- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms–2s on 4K wallpapers at cold cache. Disk cache mitigated repeat starts but added ~100 lines of complexity.
|
||||||
|
- **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper. Removes `image` and `dirs` dependencies entirely. 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.
|
||||||
|
|
||||||
|
## 2026-03-28 – Optional background blur via `image` crate (superseded)
|
||||||
|
|
||||||
- **Who**: Nyx, Dom
|
- **Who**: Nyx, Dom
|
||||||
- **Why**: Consistent with moonset/moongreet — blurred wallpaper as lockscreen background is a common UX pattern
|
- **Why**: Consistent with moonset/moongreet — blurred wallpaper as lockscreen background is a common UX pattern
|
||||||
|
|||||||
@@ -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
@@ -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 |
+7
-2
@@ -8,6 +8,7 @@ use zeroize::Zeroizing;
|
|||||||
// PAM return codes
|
// PAM return codes
|
||||||
const PAM_SUCCESS: i32 = 0;
|
const PAM_SUCCESS: i32 = 0;
|
||||||
const PAM_BUF_ERR: i32 = 5;
|
const PAM_BUF_ERR: i32 = 5;
|
||||||
|
const PAM_AUTH_ERR: i32 = 7;
|
||||||
|
|
||||||
// PAM message styles
|
// PAM message styles
|
||||||
const PAM_PROMPT_ECHO_OFF: libc::c_int = 1;
|
const PAM_PROMPT_ECHO_OFF: libc::c_int = 1;
|
||||||
@@ -70,10 +71,14 @@ unsafe extern "C" fn pam_conv_callback(
|
|||||||
appdata_ptr: *mut libc::c_void,
|
appdata_ptr: *mut libc::c_void,
|
||||||
) -> libc::c_int {
|
) -> libc::c_int {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
if num_msg <= 0 {
|
||||||
|
return PAM_AUTH_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
// Safety: appdata_ptr was set to a valid *const CString in authenticate()
|
// Safety: appdata_ptr was set to a valid *const CString in authenticate()
|
||||||
let password = appdata_ptr as *const CString;
|
let password = appdata_ptr as *const CString;
|
||||||
if password.is_null() {
|
if password.is_null() {
|
||||||
return 7; // PAM_AUTH_ERR
|
return PAM_AUTH_ERR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safety: calloc returns zeroed memory for num_msg PamResponse structs.
|
// Safety: calloc returns zeroed memory for num_msg PamResponse structs.
|
||||||
@@ -84,7 +89,7 @@ unsafe extern "C" fn pam_conv_callback(
|
|||||||
) as *mut PamResponse;
|
) as *mut PamResponse;
|
||||||
|
|
||||||
if resp_array.is_null() {
|
if resp_array.is_null() {
|
||||||
return 7; // PAM_AUTH_ERR
|
return PAM_BUF_ERR;
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in 0..num_msg as isize {
|
for i in 0..num_msg as isize {
|
||||||
|
|||||||
+34
-10
@@ -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")];
|
||||||
@@ -49,27 +48,32 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
|||||||
let mut merged = Config::default();
|
let mut merged = Config::default();
|
||||||
for path in paths {
|
for path in paths {
|
||||||
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) {
|
match toml::from_str::<RawConfig>(&content) {
|
||||||
|
Ok(parsed) => {
|
||||||
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 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; }
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to parse {}: {e}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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() { 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)]
|
||||||
@@ -104,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();
|
||||||
@@ -115,9 +119,29 @@ 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() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let conf = dir.path().join("moonlock.toml");
|
||||||
|
fs::write(&conf, "this is not valid toml {{{{").unwrap();
|
||||||
|
let c = load_config(Some(&[conf]));
|
||||||
|
assert!(c.fingerprint_enabled);
|
||||||
|
assert!(c.background_path.is_none());
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test] fn symlink_rejected_for_background() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let real = dir.path().join("bg.jpg");
|
||||||
|
let link = dir.path().join("link.jpg");
|
||||||
|
fs::write(&real, "fake").unwrap();
|
||||||
|
std::os::unix::fs::symlink(&real, &link).unwrap();
|
||||||
|
let c = Config { background_path: Some(link.to_str().unwrap().to_string()), ..Config::default() };
|
||||||
|
// Symlink should be rejected — falls through to None
|
||||||
|
let r = resolve_background_path_with(&c, Path::new("/nonexistent"));
|
||||||
|
assert!(r.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-33
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use gio::prelude::*;
|
use gio::prelude::*;
|
||||||
use gtk4::gio;
|
use gtk4::gio;
|
||||||
use std::cell::RefCell;
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
const FPRINTD_BUS_NAME: &str = "net.reactivated.Fprint";
|
const FPRINTD_BUS_NAME: &str = "net.reactivated.Fprint";
|
||||||
@@ -12,6 +12,7 @@ const FPRINTD_MANAGER_IFACE: &str = "net.reactivated.Fprint.Manager";
|
|||||||
const FPRINTD_DEVICE_IFACE: &str = "net.reactivated.Fprint.Device";
|
const FPRINTD_DEVICE_IFACE: &str = "net.reactivated.Fprint.Device";
|
||||||
|
|
||||||
const MAX_FP_ATTEMPTS: u32 = 10;
|
const MAX_FP_ATTEMPTS: u32 = 10;
|
||||||
|
const DBUS_TIMEOUT_MS: i32 = 3000;
|
||||||
|
|
||||||
/// Retry-able statuses — finger not read properly, try again.
|
/// Retry-able statuses — finger not read properly, try again.
|
||||||
const RETRY_STATUSES: &[&str] = &[
|
const RETRY_STATUSES: &[&str] = &[
|
||||||
@@ -26,6 +27,8 @@ pub struct FingerprintListener {
|
|||||||
device_proxy: Option<gio::DBusProxy>,
|
device_proxy: Option<gio::DBusProxy>,
|
||||||
signal_id: Option<glib::SignalHandlerId>,
|
signal_id: Option<glib::SignalHandlerId>,
|
||||||
running: bool,
|
running: bool,
|
||||||
|
/// Shared flag for async tasks to detect stop() between awaits.
|
||||||
|
running_flag: Rc<Cell<bool>>,
|
||||||
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>>,
|
||||||
@@ -40,6 +43,7 @@ impl FingerprintListener {
|
|||||||
device_proxy: None,
|
device_proxy: None,
|
||||||
signal_id: None,
|
signal_id: None,
|
||||||
running: false,
|
running: false,
|
||||||
|
running_flag: Rc::new(Cell::new(false)),
|
||||||
failed_attempts: 0,
|
failed_attempts: 0,
|
||||||
on_success: None,
|
on_success: None,
|
||||||
on_failure: None,
|
on_failure: None,
|
||||||
@@ -68,7 +72,7 @@ impl FingerprintListener {
|
|||||||
|
|
||||||
// Call GetDefaultDevice
|
// Call GetDefaultDevice
|
||||||
let result = match manager
|
let result = match manager
|
||||||
.call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, -1)
|
.call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -118,7 +122,7 @@ impl FingerprintListener {
|
|||||||
|
|
||||||
let args = glib::Variant::from((&username,));
|
let args = glib::Variant::from((&username,));
|
||||||
match proxy
|
match proxy
|
||||||
.call_future("ListEnrolledFingers", Some(&args), gio::DBusCallFlags::NONE, -1)
|
.call_future("ListEnrolledFingers", Some(&args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
@@ -168,7 +172,7 @@ impl FingerprintListener {
|
|||||||
// Claim the device
|
// Claim the device
|
||||||
let args = glib::Variant::from((&username,));
|
let args = glib::Variant::from((&username,));
|
||||||
if let Err(e) = proxy
|
if let Err(e) = proxy
|
||||||
.call_future("Claim", Some(&args), gio::DBusCallFlags::NONE, -1)
|
.call_future("Claim", Some(&args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
log::error!("Failed to claim fingerprint device: {e}");
|
log::error!("Failed to claim fingerprint device: {e}");
|
||||||
@@ -178,20 +182,34 @@ impl FingerprintListener {
|
|||||||
// Start verification
|
// Start verification
|
||||||
let start_args = glib::Variant::from((&"any",));
|
let start_args = glib::Variant::from((&"any",));
|
||||||
if let Err(e) = proxy
|
if let Err(e) = proxy
|
||||||
.call_future("VerifyStart", Some(&start_args), gio::DBusCallFlags::NONE, -1)
|
.call_future("VerifyStart", Some(&start_args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
log::error!("Failed to start fingerprint verification: {e}");
|
log::error!("Failed to start fingerprint verification: {e}");
|
||||||
let _ = proxy
|
let _ = proxy
|
||||||
.call_future("Release", None, gio::DBusCallFlags::NONE, -1)
|
.call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture the unique bus name of fprintd for sender validation.
|
||||||
|
// D-Bus signals carry the sender's unique name (e.g. ":1.42"), not the
|
||||||
|
// well-known name. We validate this to prevent signal spoofing.
|
||||||
|
let expected_sender = proxy.name_owner();
|
||||||
|
|
||||||
// Connect the g-signal handler on the proxy to dispatch VerifyStatus
|
// Connect the g-signal handler on the proxy to dispatch VerifyStatus
|
||||||
let listener_weak = Rc::downgrade(listener);
|
let listener_weak = Rc::downgrade(listener);
|
||||||
let signal_id = proxy.connect_local("g-signal", false, move |values| {
|
let signal_id = proxy.connect_local("g-signal", false, move |values| {
|
||||||
// g-signal arguments: (proxy, sender_name, signal_name, parameters)
|
// g-signal arguments: (proxy, sender_name, signal_name, parameters)
|
||||||
|
let sender: String = match values[1].get() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
if expected_sender.as_ref().map(|s| s.as_str()) != Some(sender.as_str()) {
|
||||||
|
log::warn!("Ignoring D-Bus signal from unexpected sender: {sender}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let signal_name: String = match values[2].get() {
|
let signal_name: String = match values[2].get() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(_) => return None,
|
Err(_) => return None,
|
||||||
@@ -224,6 +242,7 @@ impl FingerprintListener {
|
|||||||
let mut inner = listener.borrow_mut();
|
let mut inner = listener.borrow_mut();
|
||||||
inner.signal_id = Some(signal_id);
|
inner.signal_id = Some(signal_id);
|
||||||
inner.running = true;
|
inner.running = true;
|
||||||
|
inner.running_flag.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process a VerifyStatus signal from fprintd.
|
/// Process a VerifyStatus signal from fprintd.
|
||||||
@@ -233,7 +252,7 @@ impl FingerprintListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if status == "verify-match" {
|
if status == "verify-match" {
|
||||||
self.running = false;
|
self.cleanup_dbus();
|
||||||
if let Some(ref cb) = self.on_success {
|
if let Some(ref cb) = self.on_success {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
@@ -270,17 +289,22 @@ impl FingerprintListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Restart fingerprint verification asynchronously after a completed attempt.
|
/// Restart fingerprint verification asynchronously after a completed attempt.
|
||||||
|
/// Checks running_flag after VerifyStop to avoid restarting on a released device.
|
||||||
fn restart_verify_async(&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();
|
let proxy = proxy.clone();
|
||||||
|
let running = self.running_flag.clone();
|
||||||
glib::spawn_future_local(async move {
|
glib::spawn_future_local(async move {
|
||||||
// VerifyStop before VerifyStart to avoid D-Bus errors
|
// VerifyStop before VerifyStart to avoid D-Bus errors
|
||||||
let _ = proxy
|
let _ = proxy
|
||||||
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, -1)
|
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
.await;
|
.await;
|
||||||
|
if !running.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let args = glib::Variant::from((&"any",));
|
let args = glib::Variant::from((&"any",));
|
||||||
if let Err(e) = proxy
|
if let Err(e) = proxy
|
||||||
.call_future("VerifyStart", Some(&args), gio::DBusCallFlags::NONE, -1)
|
.call_future("VerifyStart", Some(&args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
log::error!("Failed to restart fingerprint verification: {e}");
|
log::error!("Failed to restart fingerprint verification: {e}");
|
||||||
@@ -289,35 +313,37 @@ impl FingerprintListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop listening and release the device.
|
/// Disconnect the signal handler and send VerifyStop + Release to fprintd.
|
||||||
/// Uses a short timeout (3s) to avoid blocking the UI indefinitely.
|
/// Signal disconnect is synchronous to prevent further callbacks.
|
||||||
pub fn stop(&mut self) {
|
/// D-Bus cleanup is fire-and-forget to avoid blocking the UI.
|
||||||
if !self.running {
|
fn cleanup_dbus(&mut self) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.running = false;
|
self.running = false;
|
||||||
|
self.running_flag.set(false);
|
||||||
|
|
||||||
if let Some(ref proxy) = self.device_proxy {
|
if let Some(ref proxy) = self.device_proxy {
|
||||||
if let Some(id) = self.signal_id.take() {
|
if let Some(id) = self.signal_id.take() {
|
||||||
proxy.disconnect(id);
|
proxy.disconnect(id);
|
||||||
}
|
}
|
||||||
let _ = proxy.call_sync(
|
let proxy = proxy.clone();
|
||||||
"VerifyStop",
|
glib::spawn_future_local(async move {
|
||||||
None,
|
let _ = proxy
|
||||||
gio::DBusCallFlags::NONE,
|
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
3000,
|
.await;
|
||||||
gio::Cancellable::NONE,
|
let _ = proxy
|
||||||
);
|
.call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
let _ = proxy.call_sync(
|
.await;
|
||||||
"Release",
|
});
|
||||||
None,
|
|
||||||
gio::DBusCallFlags::NONE,
|
|
||||||
3000,
|
|
||||||
gio::Cancellable::NONE,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stop listening and release the device. Idempotent — safe to call multiple times.
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
if !self.running {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.cleanup_dbus();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -339,21 +365,21 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn verify_match_sets_running_false_and_calls_success() {
|
fn verify_match_sets_running_false_and_calls_success() {
|
||||||
use std::cell::Cell;
|
|
||||||
let called = Rc::new(Cell::new(false));
|
let called = Rc::new(Cell::new(false));
|
||||||
let called_clone = called.clone();
|
let called_clone = called.clone();
|
||||||
let mut listener = FingerprintListener::new();
|
let mut listener = FingerprintListener::new();
|
||||||
listener.running = true;
|
listener.running = true;
|
||||||
|
listener.running_flag.set(true);
|
||||||
listener.on_success = Some(Box::new(move || { called_clone.set(true); }));
|
listener.on_success = Some(Box::new(move || { called_clone.set(true); }));
|
||||||
|
|
||||||
listener.on_verify_status("verify-match", false);
|
listener.on_verify_status("verify-match", false);
|
||||||
assert!(called.get());
|
assert!(called.get());
|
||||||
assert!(!listener.running);
|
assert!(!listener.running);
|
||||||
|
assert!(!listener.running_flag.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn verify_no_match_calls_failure_and_stays_running() {
|
fn verify_no_match_calls_failure_and_stays_running() {
|
||||||
use std::cell::Cell;
|
|
||||||
let called = Rc::new(Cell::new(false));
|
let called = Rc::new(Cell::new(false));
|
||||||
let called_clone = called.clone();
|
let called_clone = called.clone();
|
||||||
let mut listener = FingerprintListener::new();
|
let mut listener = FingerprintListener::new();
|
||||||
@@ -368,7 +394,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn max_attempts_stops_listener_and_calls_exhausted() {
|
fn max_attempts_stops_listener_and_calls_exhausted() {
|
||||||
use std::cell::Cell;
|
|
||||||
let exhausted = Rc::new(Cell::new(false));
|
let exhausted = Rc::new(Cell::new(false));
|
||||||
let exhausted_clone = exhausted.clone();
|
let exhausted_clone = exhausted.clone();
|
||||||
let mut listener = FingerprintListener::new();
|
let mut listener = FingerprintListener::new();
|
||||||
@@ -386,7 +411,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn not_running_ignores_signals() {
|
fn not_running_ignores_signals() {
|
||||||
use std::cell::Cell;
|
|
||||||
let called = Rc::new(Cell::new(false));
|
let called = Rc::new(Cell::new(false));
|
||||||
let called_clone = called.clone();
|
let called_clone = called.clone();
|
||||||
let mut listener = FingerprintListener::new();
|
let mut listener = FingerprintListener::new();
|
||||||
|
|||||||
+112
-54
@@ -4,9 +4,9 @@
|
|||||||
use gdk4 as gdk;
|
use gdk4 as gdk;
|
||||||
use gdk_pixbuf::Pixbuf;
|
use gdk_pixbuf::Pixbuf;
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
|
use graphene_rs as graphene;
|
||||||
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;
|
||||||
@@ -41,11 +41,15 @@ struct LockscreenState {
|
|||||||
|
|
||||||
/// 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.
|
/// Fingerprint is not initialized here — use `wire_fingerprint()` after async init.
|
||||||
|
/// The `blur_cache` and `avatar_cache` are shared across monitors for multi-monitor
|
||||||
|
/// 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: >k::Application,
|
app: >k::Application,
|
||||||
unlock_callback: Rc<dyn Fn()>,
|
unlock_callback: Rc<dyn Fn()>,
|
||||||
|
blur_cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||||
|
avatar_cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||||
) -> LockscreenHandles {
|
) -> LockscreenHandles {
|
||||||
let window = gtk::ApplicationWindow::builder()
|
let window = gtk::ApplicationWindow::builder()
|
||||||
.application(app)
|
.application(app)
|
||||||
@@ -82,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);
|
if let Some(texture) = bg_texture {
|
||||||
|
let background = create_background_picture(texture, config.background_blur, blur_cache);
|
||||||
overlay.set_child(Some(&background));
|
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);
|
||||||
@@ -109,12 +115,17 @@ pub fn create_lockscreen_window(
|
|||||||
avatar_frame.append(&avatar_image);
|
avatar_frame.append(&avatar_image);
|
||||||
login_box.append(&avatar_frame);
|
login_box.append(&avatar_frame);
|
||||||
|
|
||||||
// Load avatar
|
// Load avatar — use shared cache to avoid redundant loading on multi-monitor setups.
|
||||||
|
// The cache is populated by the first monitor and reused by subsequent ones.
|
||||||
|
if let Some(ref cached) = *avatar_cache.borrow() {
|
||||||
|
avatar_image.set_paintable(Some(cached));
|
||||||
|
} else {
|
||||||
let avatar_path = users::get_avatar_path(&user.home, &user.username);
|
let avatar_path = users::get_avatar_path(&user.home, &user.username);
|
||||||
if let Some(path) = avatar_path {
|
if let Some(path) = avatar_path {
|
||||||
set_avatar_from_file(&avatar_image, &path);
|
set_avatar_from_file(&avatar_image, &path, avatar_cache);
|
||||||
} else {
|
} else {
|
||||||
set_default_avatar(&avatar_image, &window);
|
set_default_avatar(&avatar_image, &window, avatar_cache);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Username label
|
// Username label
|
||||||
@@ -125,7 +136,7 @@ pub fn create_lockscreen_window(
|
|||||||
// Password entry
|
// Password entry
|
||||||
let password_entry = gtk::PasswordEntry::builder()
|
let password_entry = gtk::PasswordEntry::builder()
|
||||||
.placeholder_text(strings.password_placeholder)
|
.placeholder_text(strings.password_placeholder)
|
||||||
.show_peek_icon(true)
|
.show_peek_icon(false)
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.build();
|
.build();
|
||||||
password_entry.add_css_class("password-entry");
|
password_entry.add_css_class("password-entry");
|
||||||
@@ -361,12 +372,18 @@ pub fn start_fingerprint(
|
|||||||
let fp_label_fail = handles.fp_label.clone();
|
let fp_label_fail = handles.fp_label.clone();
|
||||||
let unlock_cb_fp = handles.unlock_callback.clone();
|
let unlock_cb_fp = handles.unlock_callback.clone();
|
||||||
|
|
||||||
|
let fp_rc_success = fp_rc.clone();
|
||||||
let on_success = move || {
|
let on_success = move || {
|
||||||
let label = fp_label_success.clone();
|
let label = fp_label_success.clone();
|
||||||
let cb = unlock_cb_fp.clone();
|
let cb = unlock_cb_fp.clone();
|
||||||
|
let fp = fp_rc_success.clone();
|
||||||
glib::idle_add_local_once(move || {
|
glib::idle_add_local_once(move || {
|
||||||
label.set_text(load_strings(None).fingerprint_success);
|
let strings = load_strings(None);
|
||||||
|
label.set_text(strings.fingerprint_success);
|
||||||
label.add_css_class("success");
|
label.add_css_class("success");
|
||||||
|
// stop() is idempotent — cleanup_dbus() already ran inside on_verify_status,
|
||||||
|
// but this mirrors the PAM success path for defense-in-depth.
|
||||||
|
fp.borrow_mut().stop();
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -415,64 +432,99 @@ 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.
|
||||||
/// When `blur_radius` is `Some(sigma)` with sigma > 0, a Gaussian blur is applied.
|
/// Returns None if no wallpaper path is provided or the file cannot be loaded.
|
||||||
pub fn load_background_texture(bg_path: &Path, blur_radius: Option<f32>) -> gdk::Texture {
|
/// Blur is applied at render time via GPU (GskBlurNode), not here.
|
||||||
let fallback = "/dev/moonarch/moonlock/wallpaper.jpg";
|
pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
|
||||||
|
|
||||||
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);
|
let file = gio::File::for_path(bg_path);
|
||||||
gdk::Texture::from_file(&file).unwrap_or_else(|_| {
|
match gdk::Texture::from_file(&file) {
|
||||||
gdk::Texture::from_resource(fallback)
|
Ok(texture) => Some(texture),
|
||||||
})
|
Err(e) => {
|
||||||
};
|
log::warn!("Failed to load wallpaper {}: {e}", bg_path.display());
|
||||||
|
None
|
||||||
match blur_radius {
|
}
|
||||||
Some(sigma) if sigma > 0.0 => apply_blur(&texture, sigma),
|
|
||||||
_ => texture,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply Gaussian blur to a texture and return a blurred texture.
|
/// Create a Picture widget for the wallpaper background.
|
||||||
fn apply_blur(texture: &gdk::Texture, sigma: f32) -> gdk::Texture {
|
/// When `blur_radius` is `Some(sigma)` with sigma > 0, blur is applied via GPU
|
||||||
let width = texture.width() as u32;
|
/// (GskBlurNode). The blur is rendered to a concrete texture on `realize` (when
|
||||||
let height = texture.height() as u32;
|
/// the GPU renderer is available), avoiding lazy-render artifacts.
|
||||||
let stride = width as usize * 4;
|
/// The `blur_cache` is shared across monitors — the first to realize renders the
|
||||||
let mut pixel_data = vec![0u8; stride * height as usize];
|
/// blur, subsequent monitors reuse the cached texture.
|
||||||
texture.download(&mut pixel_data, stride);
|
fn create_background_picture(
|
||||||
|
texture: &gdk::Texture,
|
||||||
let img = image::RgbaImage::from_raw(width, height, pixel_data)
|
blur_radius: Option<f32>,
|
||||||
.expect("pixel buffer size matches texture dimensions");
|
blur_cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||||
let blurred = imageops::blur(&image::DynamicImage::ImageRgba8(img), sigma);
|
) -> gtk::Picture {
|
||||||
|
|
||||||
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);
|
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 {
|
||||||
|
if sigma > 0.0 {
|
||||||
|
let texture = texture.clone();
|
||||||
|
let cache = blur_cache.clone();
|
||||||
|
background.connect_realize(move |picture| {
|
||||||
|
if let Some(ref cached) = *cache.borrow() {
|
||||||
|
picture.set_paintable(Some(cached));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(blurred) = render_blurred_texture(picture, &texture, sigma) {
|
||||||
|
picture.set_paintable(Some(&blurred));
|
||||||
|
*cache.borrow_mut() = Some(blurred);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
background
|
background
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load an image file and set it as the avatar.
|
/// Render a blurred texture using the widget's GPU renderer.
|
||||||
fn set_avatar_from_file(image: >k::Image, path: &Path) {
|
/// 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(
|
||||||
|
widget: &impl IsA<gtk::Widget>,
|
||||||
|
texture: &gdk::Texture,
|
||||||
|
sigma: f32,
|
||||||
|
) -> Option<gdk::Texture> {
|
||||||
|
let native = widget.native()?;
|
||||||
|
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();
|
||||||
|
// Clip output to original texture size
|
||||||
|
snapshot.push_clip(&graphene::Rect::new(pad, pad, w, h));
|
||||||
|
snapshot.push_blur(sigma as f64);
|
||||||
|
// Render texture with padding on all sides (edges repeat via oversized bounds)
|
||||||
|
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 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.
|
||||||
|
fn set_avatar_from_file(
|
||||||
|
image: >k::Image,
|
||||||
|
path: &Path,
|
||||||
|
cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||||
|
) {
|
||||||
match Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), AVATAR_SIZE, AVATAR_SIZE, true) {
|
match Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), AVATAR_SIZE, AVATAR_SIZE, true) {
|
||||||
Ok(pixbuf) => {
|
Ok(pixbuf) => {
|
||||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||||
image.set_paintable(Some(&texture));
|
image.set_paintable(Some(&texture));
|
||||||
|
*cache.borrow_mut() = Some(texture);
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
@@ -481,7 +533,12 @@ fn set_avatar_from_file(image: >k::Image, path: &Path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load the default avatar SVG from GResources, tinted with the foreground color.
|
/// Load the default avatar SVG from GResources, tinted with the foreground color.
|
||||||
fn set_default_avatar(image: >k::Image, window: >k::ApplicationWindow) {
|
/// Stores the texture in the cache for reuse on additional monitors.
|
||||||
|
fn set_default_avatar(
|
||||||
|
image: >k::Image,
|
||||||
|
window: >k::ApplicationWindow,
|
||||||
|
cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||||
|
) {
|
||||||
let resource_path = users::get_default_avatar_path();
|
let resource_path = users::get_default_avatar_path();
|
||||||
if let Ok(bytes) =
|
if let Ok(bytes) =
|
||||||
gio::resources_lookup_data(&resource_path, gio::ResourceLookupFlags::NONE)
|
gio::resources_lookup_data(&resource_path, gio::ResourceLookupFlags::NONE)
|
||||||
@@ -503,6 +560,7 @@ fn set_default_avatar(image: >k::Image, window: >k::ApplicationWindow) {
|
|||||||
if let Some(pixbuf) = loader.pixbuf() {
|
if let Some(pixbuf) = loader.pixbuf() {
|
||||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||||
image.set_paintable(Some(&texture));
|
image.set_paintable(Some(&texture));
|
||||||
|
*cache.borrow_mut() = Some(texture);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-16
@@ -13,7 +13,7 @@ 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::cell::RefCell;
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::fingerprint::FingerprintListener;
|
use crate::fingerprint::FingerprintListener;
|
||||||
@@ -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: >k::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, config.background_blur);
|
.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: >k::Application) {
|
|||||||
fn activate_with_session_lock(
|
fn activate_with_session_lock(
|
||||||
app: >k::Application,
|
app: >k::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();
|
||||||
@@ -70,14 +70,26 @@ fn activate_with_session_lock(
|
|||||||
|
|
||||||
let monitors = display.monitors();
|
let monitors = display.monitors();
|
||||||
|
|
||||||
// Shared unlock callback — unlocks session and quits
|
// Shared unlock callback — unlocks session and quits.
|
||||||
|
// Guard prevents double-unlock if PAM and fingerprint succeed simultaneously.
|
||||||
let lock_clone = lock.clone();
|
let lock_clone = lock.clone();
|
||||||
let app_clone = app.clone();
|
let app_clone = app.clone();
|
||||||
|
let already_unlocked = Rc::new(Cell::new(false));
|
||||||
|
let au = already_unlocked.clone();
|
||||||
let unlock_callback: Rc<dyn Fn()> = Rc::new(move || {
|
let unlock_callback: Rc<dyn Fn()> = Rc::new(move || {
|
||||||
|
if au.get() {
|
||||||
|
log::debug!("Unlock already triggered, ignoring duplicate");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
au.set(true);
|
||||||
lock_clone.unlock();
|
lock_clone.unlock();
|
||||||
app_clone.quit();
|
app_clone.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Shared caches for multi-monitor — first monitor renders, rest reuse
|
||||||
|
let blur_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
|
||||||
|
let avatar_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
// Create all monitor windows immediately — no D-Bus calls here
|
// Create all monitor windows immediately — no D-Bus calls here
|
||||||
let mut all_handles = Vec::new();
|
let mut all_handles = Vec::new();
|
||||||
let mut created_any = false;
|
let mut created_any = false;
|
||||||
@@ -91,6 +103,8 @@ fn activate_with_session_lock(
|
|||||||
config,
|
config,
|
||||||
app,
|
app,
|
||||||
unlock_callback.clone(),
|
unlock_callback.clone(),
|
||||||
|
&blur_cache,
|
||||||
|
&avatar_cache,
|
||||||
);
|
);
|
||||||
lock.assign_window_to_monitor(&handles.window, &monitor);
|
lock.assign_window_to_monitor(&handles.window, &monitor);
|
||||||
handles.window.present();
|
handles.window.present();
|
||||||
@@ -144,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: >k::Application,
|
app: >k::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();
|
||||||
@@ -152,11 +166,15 @@ fn activate_without_lock(
|
|||||||
app_clone.quit();
|
app_clone.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let blur_cache = Rc::new(RefCell::new(None));
|
||||||
|
let avatar_cache = Rc::new(RefCell::new(None));
|
||||||
let handles = lockscreen::create_lockscreen_window(
|
let handles = lockscreen::create_lockscreen_window(
|
||||||
bg_texture,
|
bg_texture,
|
||||||
config,
|
config,
|
||||||
app,
|
app,
|
||||||
unlock_callback,
|
unlock_callback,
|
||||||
|
&blur_cache,
|
||||||
|
&avatar_cache,
|
||||||
);
|
);
|
||||||
handles.window.set_default_size(800, 600);
|
handles.window.set_default_size(800, 600);
|
||||||
handles.window.present();
|
handles.window.present();
|
||||||
@@ -168,11 +186,22 @@ fn activate_without_lock(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn setup_logging() {
|
fn setup_logging() {
|
||||||
systemd_journal_logger::JournalLog::new()
|
match systemd_journal_logger::JournalLog::new() {
|
||||||
.unwrap()
|
Ok(logger) => {
|
||||||
.install()
|
if let Err(e) = logger.install() {
|
||||||
.unwrap();
|
eprintln!("Failed to install journal logger: {e}");
|
||||||
log::set_max_level(log::LevelFilter::Info);
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create journal logger: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let level = if std::env::var("MOONLOCK_DEBUG").is_ok() {
|
||||||
|
log::LevelFilter::Debug
|
||||||
|
} else {
|
||||||
|
log::LevelFilter::Info
|
||||||
|
};
|
||||||
|
log::set_max_level(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_panic_hook() {
|
fn install_panic_hook() {
|
||||||
@@ -187,6 +216,7 @@ fn install_panic_hook() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
install_panic_hook();
|
||||||
setup_logging();
|
setup_logging();
|
||||||
|
|
||||||
// Root check — moonlock should not run as root
|
// Root check — moonlock should not run as root
|
||||||
@@ -194,8 +224,6 @@ fn main() {
|
|||||||
log::error!("Moonlock should not run as root");
|
log::error!("Moonlock should not run as root");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
install_panic_hook();
|
|
||||||
log::info!("Moonlock starting");
|
log::info!("Moonlock starting");
|
||||||
|
|
||||||
// Register compiled GResources
|
// Register compiled GResources
|
||||||
|
|||||||
@@ -7,14 +7,12 @@ use std::process::Command;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PowerError {
|
pub enum PowerError {
|
||||||
CommandFailed { action: &'static str, message: String },
|
CommandFailed { action: &'static str, message: String },
|
||||||
Timeout { action: &'static str },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for PowerError {
|
impl fmt::Display for PowerError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
PowerError::CommandFailed { action, message } => write!(f, "{action} failed: {message}"),
|
PowerError::CommandFailed { action, message } => write!(f, "{action} failed: {message}"),
|
||||||
PowerError::Timeout { action } => write!(f, "{action} timed out"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,7 +42,6 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test] fn power_error_display() { assert_eq!(PowerError::CommandFailed { action: "reboot", message: "fail".into() }.to_string(), "reboot failed: fail"); }
|
#[test] fn power_error_display() { assert_eq!(PowerError::CommandFailed { action: "reboot", message: "fail".into() }.to_string(), "reboot failed: fail"); }
|
||||||
#[test] fn timeout_display() { assert_eq!(PowerError::Timeout { action: "shutdown" }.to_string(), "shutdown timed out"); }
|
|
||||||
#[test] fn missing_binary() { assert!(run_command("test", "nonexistent-xyz", &[]).is_err()); }
|
#[test] fn missing_binary() { assert!(run_command("test", "nonexistent-xyz", &[]).is_err()); }
|
||||||
#[test] fn nonzero_exit() { assert!(run_command("test", "false", &[]).is_err()); }
|
#[test] fn nonzero_exit() { assert!(run_command("test", "false", &[]).is_err()); }
|
||||||
#[test] fn success() { assert!(run_command("test", "true", &[]).is_ok()); }
|
#[test] fn success() { assert!(run_command("test", "true", &[]).is_ok()); }
|
||||||
|
|||||||
Reference in New Issue
Block a user