Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de9a3e9e6a | |||
| 09e0d47a38 | |||
| 13b329cd98 | |||
| 58c076198f | |||
| 78bcf90492 |
@@ -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/loginctl
|
- `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
|
- `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
+131
-164
@@ -3,63 +3,10 @@
|
|||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "adler2"
|
||||||
version = "1.1.4"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstream"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
|
||||||
dependencies = [
|
|
||||||
"anstyle",
|
|
||||||
"anstyle-parse",
|
|
||||||
"anstyle-query",
|
|
||||||
"anstyle-wincon",
|
|
||||||
"colorchoice",
|
|
||||||
"is_terminal_polyfill",
|
|
||||||
"utf8parse",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle"
|
|
||||||
version = "1.0.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-parse"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
|
||||||
dependencies = [
|
|
||||||
"utf8parse",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-query"
|
|
||||||
version = "1.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-wincon"
|
|
||||||
version = "3.0.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
|
||||||
dependencies = [
|
|
||||||
"anstyle",
|
|
||||||
"once_cell_polyfill",
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
@@ -79,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"
|
||||||
@@ -125,32 +84,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "crc32fast"
|
||||||
version = "1.0.5"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "env_filter"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"cfg-if",
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "env_logger"
|
|
||||||
version = "0.11.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
|
|
||||||
dependencies = [
|
|
||||||
"anstream",
|
|
||||||
"anstyle",
|
|
||||||
"env_filter",
|
|
||||||
"jiff",
|
|
||||||
"log",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -175,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"
|
||||||
@@ -185,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"
|
||||||
@@ -598,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"
|
||||||
@@ -610,42 +583,12 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "is_terminal_polyfill"
|
|
||||||
version = "1.70.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiff"
|
|
||||||
version = "0.2.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
|
|
||||||
dependencies = [
|
|
||||||
"jiff-static",
|
|
||||||
"log",
|
|
||||||
"portable-atomic",
|
|
||||||
"portable-atomic-util",
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiff-static"
|
|
||||||
version = "0.2.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "khronos_api"
|
name = "khronos_api"
|
||||||
version = "3.1.0"
|
version = "3.1.0"
|
||||||
@@ -692,10 +635,19 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moonlock"
|
name = "miniz_oxide"
|
||||||
version = "0.4.0"
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "moonlock"
|
||||||
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"env_logger",
|
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
"gio",
|
"gio",
|
||||||
@@ -703,15 +655,27 @@ dependencies = [
|
|||||||
"glib-build-tools",
|
"glib-build-tools",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-session-lock",
|
"gtk4-session-lock",
|
||||||
|
"image",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"nix",
|
"nix",
|
||||||
"serde",
|
"serde",
|
||||||
|
"systemd-journal-logger",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"toml 0.8.23",
|
"toml 0.8.23",
|
||||||
"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"
|
||||||
@@ -724,18 +688,21 @@ 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"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell_polyfill"
|
|
||||||
version = "1.70.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@@ -773,18 +740,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "png"
|
||||||
version = "1.13.1"
|
version = "0.18.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "portable-atomic-util"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"portable-atomic",
|
"bitflags",
|
||||||
|
"crc32fast",
|
||||||
|
"fdeflate",
|
||||||
|
"flate2",
|
||||||
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -815,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"
|
||||||
@@ -830,35 +801,6 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex"
|
|
||||||
version = "1.12.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-automata",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-automata"
|
|
||||||
version = "0.4.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.8.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -948,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"
|
||||||
@@ -984,6 +932,16 @@ dependencies = [
|
|||||||
"version-compare",
|
"version-compare",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "systemd-journal-logger"
|
||||||
|
version = "2.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7266304d24ca5a4b230545fc558c80e18bd3e1d2eb1be149b6bcd04398d3e79c"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.13.3"
|
version = "0.13.3"
|
||||||
@@ -1116,12 +1074,6 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8parse"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -1332,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",
|
||||||
|
]
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moonlock"
|
name = "moonlock"
|
||||||
version = "0.4.1"
|
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,8 +17,9 @@ 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"
|
||||||
env_logger = "0.11"
|
systemd-journal-logger = "2.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,7 @@
|
|||||||
# Maintainer: Dominik Kressler
|
# Maintainer: Dominik Kressler
|
||||||
|
|
||||||
pkgname=moonlock-git
|
pkgname=moonlock-git
|
||||||
pkgver=0.4.0.r0.g0000000
|
pkgver=0.4.1.r1.g78bcf90
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
pkgdesc="A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
@@ -15,6 +15,7 @@ depends=(
|
|||||||
'gtk4-layer-shell'
|
'gtk4-layer-shell'
|
||||||
'gtk-session-lock'
|
'gtk-session-lock'
|
||||||
'pam'
|
'pam'
|
||||||
|
'systemd-libs'
|
||||||
)
|
)
|
||||||
makedepends=(
|
makedepends=(
|
||||||
'git'
|
'git'
|
||||||
|
|||||||
+15
-3
@@ -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
@@ -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
@@ -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
@@ -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: >k::Application,
|
app: >k::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);
|
||||||
|
|||||||
+66
-29
@@ -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: >k::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: >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_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: >k::Application,
|
app: >k::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,33 +152,27 @@ 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() {
|
||||||
let mut builder = env_logger::Builder::from_default_env();
|
systemd_journal_logger::JournalLog::new()
|
||||||
builder.filter_level(log::LevelFilter::Info);
|
.unwrap()
|
||||||
|
.install()
|
||||||
let log_dir = PathBuf::from("/var/cache/moonlock");
|
.unwrap();
|
||||||
if log_dir.is_dir() {
|
log::set_max_level(log::LevelFilter::Info);
|
||||||
let log_file = log_dir.join("moonlock.log");
|
|
||||||
if let Ok(file) = std::fs::OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.append(true)
|
|
||||||
.open(&log_file)
|
|
||||||
{
|
|
||||||
builder.target(env_logger::Target::Pipe(Box::new(file)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_panic_hook() {
|
fn install_panic_hook() {
|
||||||
|
|||||||
+6
-7
@@ -1,4 +1,4 @@
|
|||||||
// ABOUTME: Power actions — reboot and shutdown via loginctl.
|
// ABOUTME: Power actions — reboot and shutdown via systemctl.
|
||||||
// ABOUTME: Wrappers around system commands for the lockscreen UI.
|
// ABOUTME: Wrappers around system commands for the lockscreen UI.
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -22,11 +22,10 @@ impl fmt::Display for PowerError {
|
|||||||
impl std::error::Error for PowerError {}
|
impl std::error::Error for PowerError {}
|
||||||
|
|
||||||
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
|
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
|
||||||
let child = Command::new(program)
|
let output = Command::new(program)
|
||||||
.args(args)
|
.args(args)
|
||||||
.spawn()
|
.stderr(std::process::Stdio::piped())
|
||||||
.map_err(|e| PowerError::CommandFailed { action, message: e.to_string() })?;
|
.output()
|
||||||
let output = child.wait_with_output()
|
|
||||||
.map_err(|e| PowerError::CommandFailed { action, message: e.to_string() })?;
|
.map_err(|e| PowerError::CommandFailed { action, message: e.to_string() })?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
@@ -37,8 +36,8 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reboot() -> Result<(), PowerError> { run_command("reboot", "/usr/bin/loginctl", &["reboot"]) }
|
pub fn reboot() -> Result<(), PowerError> { run_command("reboot", "/usr/bin/systemctl", &["--no-ask-password", "reboot"]) }
|
||||||
pub fn shutdown() -> Result<(), PowerError> { run_command("shutdown", "/usr/bin/loginctl", &["poweroff"]) }
|
pub fn shutdown() -> Result<(), PowerError> { run_command("shutdown", "/usr/bin/systemctl", &["--no-ask-password", "poweroff"]) }
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
Reference in New Issue
Block a user