Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f09a04a115 | |||
| a462b2cf06 | |||
| 77b94a560d | |||
| b06b02faac | |||
| 9a89da8b13 | |||
| d5e431d37e | |||
| 7c10516473 | |||
| 09371b5fd2 | |||
| 3c39467508 |
@@ -17,7 +17,7 @@ Teil des Moonarch-Ökosystems.
|
|||||||
## Projektstruktur
|
## Projektstruktur
|
||||||
|
|
||||||
- `src/` — Rust-Quellcode (main.rs, greeter.rs, ipc.rs, config.rs, users.rs, sessions.rs, i18n.rs, power.rs)
|
- `src/` — Rust-Quellcode (main.rs, greeter.rs, ipc.rs, config.rs, users.rs, sessions.rs, i18n.rs, power.rs)
|
||||||
- `resources/` — GResource-Assets (style.css, wallpaper.jpg, default-avatar.svg)
|
- `resources/` — GResource-Assets (style.css, default-avatar.svg)
|
||||||
- `config/` — Beispiel-Konfigurationsdateien für `/etc/moongreet/` und `/etc/greetd/`
|
- `config/` — Beispiel-Konfigurationsdateien für `/etc/moongreet/` und `/etc/greetd/`
|
||||||
- `pkg/` — PKGBUILD für Arch-Linux-Paketierung (`makepkg -sf`)
|
- `pkg/` — PKGBUILD für Arch-Linux-Paketierung (`makepkg -sf`)
|
||||||
|
|
||||||
@@ -44,8 +44,9 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
|
|||||||
- `sessions.rs` — Wayland/X11 Sessions aus .desktop Files
|
- `sessions.rs` — Wayland/X11 Sessions aus .desktop Files
|
||||||
- `power.rs` — Reboot/Shutdown via loginctl
|
- `power.rs` — Reboot/Shutdown via loginctl
|
||||||
- `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen
|
- `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen
|
||||||
- `config.rs` — TOML-Config ([appearance] background, gtk-theme) + Wallpaper-Fallback
|
- `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback
|
||||||
- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC, Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o600 Permissions)
|
- `config.rs` — TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback
|
||||||
|
- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC (Multi-Stage-Auth für fprintd), Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o600 Permissions)
|
||||||
- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, systemd-journal-logger
|
- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, systemd-journal-logger
|
||||||
- `resources/style.css` — Catppuccin-inspiriertes Theme
|
- `resources/style.css` — Catppuccin-inspiriertes Theme
|
||||||
|
|
||||||
@@ -56,12 +57,14 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
|
|||||||
- **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads
|
- **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads
|
||||||
- **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche
|
- **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche
|
||||||
- **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>`
|
- **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>`
|
||||||
- **Symmetrie mit moonset**: Gleiche Patterns (i18n, config, users, power, GResource)
|
- **GPU-Blur via GskBlurNode**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` im `connect_realize` Callback — kein CPU-Blur, kein Disk-Cache, kein `image`-Crate
|
||||||
|
- **Fingerprint via greetd Multi-Stage PAM**: fprintd D-Bus nur als Probe (Gerät/Enrollment), eigentliche Verifizierung läuft über PAM im greetd-Auth-Loop. `auth_message_type: "secret"` → Passwort, alles andere → `None` (PAM entscheidet). 60s Socket-Timeout bei fprintd.
|
||||||
|
- **Symmetrie mit moonlock/moonset**: Gleiche Patterns (i18n, config, users, power, GResource, GPU-Blur)
|
||||||
- **Session-Validierung**: Relative Pfade erlaubt (greetd löst PATH auf), nur `..`/Null-Bytes werden abgelehnt
|
- **Session-Validierung**: Relative Pfade erlaubt (greetd löst PATH auf), nur `..`/Null-Bytes werden abgelehnt
|
||||||
- **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config
|
- **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config
|
||||||
- **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var
|
- **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var
|
||||||
- **File Permissions**: Cache-Dateien 0o600
|
- **File Permissions**: Cache-Dateien 0o600
|
||||||
- **Testbare Persistence**: `save_*_to`/`load_*_from` Varianten mit konfigurierbarem Pfad für Unit-Tests
|
- **Testbare Persistence**: `save_*_to`/`load_*_from` Varianten mit konfigurierbarem Pfad für Unit-Tests
|
||||||
- **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster (Greeter + Wallpaper-Windows) geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor
|
- **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor
|
||||||
- **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None`
|
- **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None`
|
||||||
- **Error-Detail-Filterung**: GDK/greetd-Fehlerdetails nur auf `debug!`-Level, `warn!` ohne interne Details — verhindert Systeminfo-Leak ins Journal
|
- **Error-Detail-Filterung**: GDK/greetd-Fehlerdetails nur auf `debug!`-Level, `warn!` ohne interne Details — verhindert Systeminfo-Leak ins Journal
|
||||||
|
|||||||
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"
|
||||||
@@ -77,15 +59,6 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crc32fast"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -108,15 +81,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"
|
||||||
@@ -127,16 +91,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"
|
||||||
@@ -550,21 +504,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"
|
||||||
@@ -628,28 +567,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 = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.4.1"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
"gio",
|
"gio",
|
||||||
"glib",
|
"glib",
|
||||||
"glib-build-tools",
|
"glib-build-tools",
|
||||||
|
"graphene-rs",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-layer-shell",
|
"gtk4-layer-shell",
|
||||||
"image",
|
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -658,25 +587,6 @@ dependencies = [
|
|||||||
"toml 0.8.23",
|
"toml 0.8.23",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "moxcms"
|
|
||||||
version = "0.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
"pxfm",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.2.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -719,19 +629,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"
|
||||||
@@ -760,12 +657,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"
|
||||||
@@ -870,12 +761,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"
|
||||||
@@ -1244,18 +1129,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",
|
|
||||||
]
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.4.0"
|
version = "0.6.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A greetd greeter for Wayland with GTK4 and Layer Shell"
|
description = "A greetd greeter for Wayland with GTK4 and Layer Shell"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -15,7 +15,7 @@ gio = "0.22"
|
|||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png"] }
|
graphene-rs = { version = "0.22", package = "graphene-rs" }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
systemd-journal-logger = "2.2"
|
systemd-journal-logger = "2.2"
|
||||||
|
|
||||||
|
|||||||
+22
-1
@@ -1,6 +1,27 @@
|
|||||||
# Decisions
|
# Decisions
|
||||||
|
|
||||||
## 2026-03-28 – Optional background blur via `image` crate
|
## 2026-03-29 – Fingerprint authentication via greetd multi-stage PAM
|
||||||
|
|
||||||
|
- **Who**: Ragnar, Dom
|
||||||
|
- **Why**: moonlock supports fprintd but moongreet rejected multi-stage auth. Users with enrolled fingerprints couldn't use them at the login screen.
|
||||||
|
- **Tradeoffs**: Direct fprintd D-Bus verification (like moonlock) can't start a greetd session — greetd controls session creation via PAM. Using greetd multi-stage means PAM decides the auth order (fingerprint first, then password fallback), not truly parallel. Acceptable — matches standard pam_fprintd behavior.
|
||||||
|
- **How**: Replace single-pass auth with a loop over auth_message rounds. Secret prompts get the password, non-secret prompts (fprintd) get None and block until PAM resolves. fprintd D-Bus probe (gio::DBusProxy) only for UI — detecting device availability and enrolled fingers. 60s socket timeout when fingerprint available. Config option `fingerprint-enabled` (default true).
|
||||||
|
|
||||||
|
## 2026-03-28 – Remove embedded wallpaper from binary
|
||||||
|
|
||||||
|
- **Who**: Selene, Dom
|
||||||
|
- **Why**: Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg. Embedding a 374K JPEG in the binary is redundant. GTK background color (Catppuccin Mocha base) is a clean fallback.
|
||||||
|
- **Tradeoffs**: Without moonarch installed AND without config, greeter shows plain dark background instead of wallpaper. Acceptable — that's the expected minimal state.
|
||||||
|
- **How**: Remove wallpaper.jpg from GResources, return None from resolve_background_path when no file found, skip wallpaper window creation and background picture when no path available.
|
||||||
|
|
||||||
|
## 2026-03-28 – GPU blur via GskBlurNode replaces CPU blur
|
||||||
|
|
||||||
|
- **Who**: Ragnar, Dom
|
||||||
|
- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms–2s on 4K wallpapers at cold cache. Disk cache and async orchestration added significant complexity.
|
||||||
|
- **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper backgrounds. Removes `image` crate dependency entirely (~15 transitive crates eliminated). No disk cache needed.
|
||||||
|
- **How**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` on `connect_realize`. Blur happens once on the GPU when the widget gets its renderer, producing a concrete `gdk::Texture`. Zero startup latency. Symmetric with moonlock and moonset.
|
||||||
|
|
||||||
|
## 2026-03-28 – Optional background blur via `image` crate (superseded)
|
||||||
|
|
||||||
- **Who**: Selene, Dom
|
- **Who**: Selene, Dom
|
||||||
- **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens
|
- **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Part of the Moonarch ecosystem.
|
|||||||
- **Multi-monitor** — Greeter on primary, wallpaper on all monitors
|
- **Multi-monitor** — Greeter on primary, wallpaper on all monitors
|
||||||
- **i18n** — German and English (auto-detected from system locale)
|
- **i18n** — German and English (auto-detected from system locale)
|
||||||
- **Faillock warning** — Warns after 2 failed attempts, locked message after 3
|
- **Faillock warning** — Warns after 2 failed attempts, locked message after 3
|
||||||
|
- **Fingerprint** — fprintd support via greetd multi-stage PAM (configurable)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# Maintainer: Dominik Kressler
|
# Maintainer: Dominik Kressler
|
||||||
|
|
||||||
pkgname=moongreet-git
|
pkgname=moongreet-git
|
||||||
pkgver=0.3.1.r5.g4c9b436
|
pkgver=0.4.0.r7.g77b94a5
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="A greetd greeter for Wayland with GTK4 and Layer Shell"
|
pkgdesc="A greetd greeter for Wayland with GTK4 and Layer Shell"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="/dev/moonarch/moongreet">
|
<gresource prefix="/dev/moonarch/moongreet">
|
||||||
<file>style.css</file>
|
<file>style.css</file>
|
||||||
<file>wallpaper.jpg</file>
|
|
||||||
<file>default-avatar.svg</file>
|
<file>default-avatar.svg</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
|
|||||||
+8
-1
@@ -22,7 +22,7 @@ window.wallpaper {
|
|||||||
|
|
||||||
/* Round avatar image — size is set via set_size_request() in code */
|
/* Round avatar image — size is set via set_size_request() in code */
|
||||||
.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;
|
||||||
@@ -54,6 +54,13 @@ window.wallpaper {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fingerprint prompt label */
|
||||||
|
.fingerprint-label {
|
||||||
|
color: alpha(white, 0.6);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* User list on the bottom left */
|
/* User list on the bottom left */
|
||||||
.user-list {
|
.user-list {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 366 KiB |
+49
-16
@@ -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/moongreet";
|
|
||||||
|
|
||||||
/// Default config search path: system-wide config.
|
/// Default config search path: system-wide config.
|
||||||
fn default_config_paths() -> Vec<PathBuf> {
|
fn default_config_paths() -> Vec<PathBuf> {
|
||||||
@@ -26,14 +25,28 @@ struct Appearance {
|
|||||||
background_blur: Option<f32>,
|
background_blur: Option<f32>,
|
||||||
#[serde(rename = "gtk-theme")]
|
#[serde(rename = "gtk-theme")]
|
||||||
gtk_theme: Option<String>,
|
gtk_theme: Option<String>,
|
||||||
|
#[serde(rename = "fingerprint-enabled")]
|
||||||
|
fingerprint_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Greeter configuration.
|
/// Greeter configuration.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[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 background_blur: Option<f32>,
|
||||||
pub gtk_theme: Option<String>,
|
pub gtk_theme: Option<String>,
|
||||||
|
pub fingerprint_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Config {
|
||||||
|
background_path: None,
|
||||||
|
background_blur: None,
|
||||||
|
gtk_theme: None,
|
||||||
|
fingerprint_enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load config from TOML files. Later paths override earlier ones.
|
/// Load config from TOML files. Later paths override earlier ones.
|
||||||
@@ -65,6 +78,9 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
|||||||
if appearance.gtk_theme.is_some() {
|
if appearance.gtk_theme.is_some() {
|
||||||
merged.gtk_theme = appearance.gtk_theme;
|
merged.gtk_theme = appearance.gtk_theme;
|
||||||
}
|
}
|
||||||
|
if let Some(fp) = appearance.fingerprint_enabled {
|
||||||
|
merged.fingerprint_enabled = fp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -78,25 +94,25 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}", merged.background_path, merged.background_blur, merged.gtk_theme);
|
log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}, fingerprint={}", merged.background_path, merged.background_blur, merged.gtk_theme, merged.fingerprint_enabled);
|
||||||
merged
|
merged
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the wallpaper path using the fallback hierarchy.
|
/// Resolve the wallpaper path using the fallback hierarchy.
|
||||||
///
|
///
|
||||||
/// Priority: config background_path > Moonarch system default > gresource fallback.
|
/// Priority: config background_path > Moonarch system default > None (GTK background color).
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve with configurable moonarch wallpaper path (for testing).
|
/// Resolve with configurable moonarch wallpaper path (for testing).
|
||||||
pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> PathBuf {
|
pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> Option<PathBuf> {
|
||||||
// User-configured path
|
// User-configured path
|
||||||
if let Some(ref bg) = config.background_path {
|
if let Some(ref bg) = config.background_path {
|
||||||
let path = PathBuf::from(bg);
|
let path = PathBuf::from(bg);
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
log::debug!("Wallpaper: using config path {}", path.display());
|
log::debug!("Wallpaper: using config path {}", path.display());
|
||||||
return path;
|
return Some(path);
|
||||||
}
|
}
|
||||||
log::debug!("Wallpaper: config path {} not found, trying fallbacks", path.display());
|
log::debug!("Wallpaper: config path {} not found, trying fallbacks", path.display());
|
||||||
}
|
}
|
||||||
@@ -104,12 +120,11 @@ pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path)
|
|||||||
// Moonarch ecosystem default
|
// Moonarch ecosystem default
|
||||||
if moonarch_wallpaper.is_file() {
|
if moonarch_wallpaper.is_file() {
|
||||||
log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display());
|
log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display());
|
||||||
return moonarch_wallpaper.to_path_buf();
|
return Some(moonarch_wallpaper.to_path_buf());
|
||||||
}
|
}
|
||||||
|
|
||||||
// GResource fallback path (loaded from compiled resources at runtime)
|
log::debug!("Wallpaper: no wallpaper found, using GTK background color");
|
||||||
log::debug!("Wallpaper: using GResource fallback");
|
None
|
||||||
PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -122,6 +137,7 @@ mod tests {
|
|||||||
assert!(config.background_path.is_none());
|
assert!(config.background_path.is_none());
|
||||||
assert!(config.background_blur.is_none());
|
assert!(config.background_blur.is_none());
|
||||||
assert!(config.gtk_theme.is_none());
|
assert!(config.gtk_theme.is_none());
|
||||||
|
assert!(config.fingerprint_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -218,7 +234,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_background_path_with(&config, Path::new("/nonexistent")),
|
resolve_background_path_with(&config, Path::new("/nonexistent")),
|
||||||
wallpaper
|
Some(wallpaper)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +245,7 @@ mod tests {
|
|||||||
..Config::default()
|
..Config::default()
|
||||||
};
|
};
|
||||||
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
||||||
assert!(result.to_str().unwrap().contains("moongreet"));
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -240,14 +256,31 @@ mod tests {
|
|||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_background_path_with(&config, &moonarch_wp),
|
resolve_background_path_with(&config, &moonarch_wp),
|
||||||
moonarch_wp
|
Some(moonarch_wp)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_uses_gresource_fallback_as_last_resort() {
|
fn resolve_returns_none_when_no_wallpaper_found() {
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
||||||
assert!(result.to_str().unwrap().contains("wallpaper.jpg"));
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_fingerprint_enabled_default_true() {
|
||||||
|
let paths = vec![PathBuf::from("/nonexistent/moongreet.toml")];
|
||||||
|
let config = load_config(Some(&paths));
|
||||||
|
assert!(config.fingerprint_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_fingerprint_disabled() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let conf = dir.path().join("moongreet.toml");
|
||||||
|
fs::write(&conf, "[appearance]\nfingerprint-enabled = false\n").unwrap();
|
||||||
|
let paths = vec![conf];
|
||||||
|
let config = load_config(Some(&paths));
|
||||||
|
assert!(!config.fingerprint_enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
// ABOUTME: fprintd D-Bus probe for fingerprint device availability.
|
||||||
|
// ABOUTME: Checks if fprintd is running and the user has enrolled fingerprints.
|
||||||
|
|
||||||
|
use gio::prelude::*;
|
||||||
|
use gtk4::gio;
|
||||||
|
|
||||||
|
const FPRINTD_BUS_NAME: &str = "net.reactivated.Fprint";
|
||||||
|
const FPRINTD_MANAGER_PATH: &str = "/net/reactivated/Fprint/Manager";
|
||||||
|
const FPRINTD_MANAGER_IFACE: &str = "net.reactivated.Fprint.Manager";
|
||||||
|
const FPRINTD_DEVICE_IFACE: &str = "net.reactivated.Fprint.Device";
|
||||||
|
|
||||||
|
const DBUS_TIMEOUT_MS: i32 = 3000;
|
||||||
|
|
||||||
|
/// Lightweight fprintd probe — detects device availability and finger enrollment.
|
||||||
|
/// Does NOT perform verification (that happens through greetd/PAM).
|
||||||
|
pub struct FingerprintProbe {
|
||||||
|
device_proxy: Option<gio::DBusProxy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FingerprintProbe {
|
||||||
|
/// Create a probe without any D-Bus connections.
|
||||||
|
/// Call `init_async().await` to connect to fprintd.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
FingerprintProbe {
|
||||||
|
device_proxy: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to fprintd on the system bus and discover the default device.
|
||||||
|
pub async fn init_async(&mut self) {
|
||||||
|
let manager = match gio::DBusProxy::for_bus_future(
|
||||||
|
gio::BusType::System,
|
||||||
|
gio::DBusProxyFlags::NONE,
|
||||||
|
None,
|
||||||
|
FPRINTD_BUS_NAME,
|
||||||
|
FPRINTD_MANAGER_PATH,
|
||||||
|
FPRINTD_MANAGER_IFACE,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("fprintd manager not available: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match manager
|
||||||
|
.call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("fprintd GetDefaultDevice failed: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match gio::DBusProxy::for_bus_future(
|
||||||
|
gio::BusType::System,
|
||||||
|
gio::DBusProxyFlags::NONE,
|
||||||
|
None,
|
||||||
|
FPRINTD_BUS_NAME,
|
||||||
|
&device_path,
|
||||||
|
FPRINTD_DEVICE_IFACE,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(proxy) => {
|
||||||
|
self.device_proxy = Some(proxy);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("fprintd device proxy failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the user has enrolled fingerprints on the default device.
|
||||||
|
/// Returns false if fprintd is unavailable or the user has no enrollments.
|
||||||
|
pub async fn is_available_async(&self, username: &str) -> bool {
|
||||||
|
let proxy = match &self.device_proxy {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let args = glib::Variant::from((&username,));
|
||||||
|
match proxy
|
||||||
|
.call_future(
|
||||||
|
"ListEnrolledFingers",
|
||||||
|
Some(&args),
|
||||||
|
gio::DBusCallFlags::NONE,
|
||||||
|
DBUS_TIMEOUT_MS,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => match result.child_value(0).get::<Vec<String>>() {
|
||||||
|
Some(fingers) => !fingers.is_empty(),
|
||||||
|
None => {
|
||||||
|
log::debug!("fprintd: unexpected ListEnrolledFingers response type");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_probe_has_no_device() {
|
||||||
|
let probe = FingerprintProbe::new();
|
||||||
|
assert!(probe.device_proxy.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constants_are_defined() {
|
||||||
|
assert!(!FPRINTD_BUS_NAME.is_empty());
|
||||||
|
assert!(!FPRINTD_MANAGER_PATH.is_empty());
|
||||||
|
assert!(!FPRINTD_MANAGER_IFACE.is_empty());
|
||||||
|
assert!(!FPRINTD_DEVICE_IFACE.is_empty());
|
||||||
|
assert!(DBUS_TIMEOUT_MS > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+343
-142
@@ -6,7 +6,6 @@ use gdk_pixbuf::Pixbuf;
|
|||||||
use glib::clone;
|
use glib::clone;
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use gtk4::{self as gtk, gio};
|
use gtk4::{self as gtk, gio};
|
||||||
use image::imageops;
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
@@ -93,81 +92,68 @@ fn is_valid_username(name: &str) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
name.chars()
|
name.chars()
|
||||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-' || c == '@')
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the background image as a shared texture (decode once, reuse everywhere).
|
/// Load background texture from filesystem.
|
||||||
/// When `blur_radius` is `Some(sigma)` with sigma > 0, a Gaussian blur is applied.
|
pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
|
||||||
pub fn load_background_texture(bg_path: &Path, blur_radius: Option<f32>) -> Option<gdk::Texture> {
|
if let Ok(meta) = std::fs::metadata(bg_path)
|
||||||
let path_str = bg_path.to_str()?;
|
&& meta.len() > MAX_WALLPAPER_FILE_SIZE
|
||||||
let texture = if bg_path.starts_with("/dev/moonarch/moongreet") {
|
{
|
||||||
match gio::resources_lookup_data(path_str, gio::ResourceLookupFlags::NONE) {
|
log::warn!(
|
||||||
Ok(bytes) => match gdk::Texture::from_bytes(&bytes) {
|
"Wallpaper file too large ({} bytes), skipping: {}",
|
||||||
Ok(texture) => Some(texture),
|
meta.len(), bg_path.display()
|
||||||
Err(e) => {
|
);
|
||||||
log::debug!("GResource texture decode error: {e}");
|
return None;
|
||||||
log::warn!("Failed to decode background texture from GResource {path_str}");
|
}
|
||||||
None
|
match gdk::Texture::from_filename(bg_path) {
|
||||||
}
|
Ok(texture) => Some(texture),
|
||||||
},
|
Err(e) => {
|
||||||
Err(e) => {
|
log::debug!("Wallpaper load error: {e}");
|
||||||
log::debug!("GResource lookup error: {e}");
|
log::warn!("Failed to load background texture from {}", bg_path.display());
|
||||||
log::warn!("Failed to load background texture from GResource {path_str}");
|
None
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if let Ok(meta) = std::fs::metadata(bg_path)
|
|
||||||
&& meta.len() > MAX_WALLPAPER_FILE_SIZE
|
|
||||||
{
|
|
||||||
log::warn!(
|
|
||||||
"Wallpaper file too large ({} bytes), skipping: {}",
|
|
||||||
meta.len(), bg_path.display()
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
match gdk::Texture::from_filename(bg_path) {
|
|
||||||
Ok(texture) => Some(texture),
|
|
||||||
Err(e) => {
|
|
||||||
log::debug!("Wallpaper load error: {e}");
|
|
||||||
log::warn!("Failed to load background texture from {}", bg_path.display());
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
|
|
||||||
match blur_radius {
|
|
||||||
Some(sigma) if sigma > 0.0 => Some(apply_blur(&texture, sigma)),
|
|
||||||
_ => Some(texture),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply Gaussian blur to a texture and return a blurred texture.
|
// -- GPU blur via GskBlurNode -------------------------------------------------
|
||||||
fn apply_blur(texture: &gdk::Texture, sigma: f32) -> gdk::Texture {
|
|
||||||
let width = texture.width() as u32;
|
|
||||||
let height = texture.height() as u32;
|
|
||||||
let stride = width as usize * 4;
|
|
||||||
let mut pixel_data = vec![0u8; stride * height as usize];
|
|
||||||
texture.download(&mut pixel_data, stride);
|
|
||||||
|
|
||||||
let img = image::RgbaImage::from_raw(width, height, pixel_data)
|
/// Render a blurred texture using the GPU via GskBlurNode.
|
||||||
.expect("pixel buffer size matches texture dimensions");
|
///
|
||||||
let blurred = imageops::blur(&image::DynamicImage::ImageRgba8(img), sigma);
|
/// 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 bytes = glib::Bytes::from(blurred.as_raw());
|
let w = texture.width() as f32;
|
||||||
let mem_texture = gdk::MemoryTexture::new(
|
let h = texture.height() as f32;
|
||||||
width as i32,
|
// Padding must cover the blur kernel radius (typically ~3x sigma)
|
||||||
height as i32,
|
let pad = (sigma * 3.0).ceil();
|
||||||
gdk::MemoryFormat::B8g8r8a8Premultiplied,
|
|
||||||
&bytes,
|
let snapshot = gtk::Snapshot::new();
|
||||||
stride,
|
// Clip output to original texture size
|
||||||
);
|
snapshot.push_clip(&graphene_rs::Rect::new(pad, pad, w, h));
|
||||||
mem_texture.upcast()
|
snapshot.push_blur(sigma as f64);
|
||||||
|
// Render texture with padding on all sides (edges repeat via oversized bounds)
|
||||||
|
snapshot.append_texture(texture, &graphene_rs::Rect::new(0.0, 0.0, w + 2.0 * pad, h + 2.0 * pad));
|
||||||
|
snapshot.pop(); // blur
|
||||||
|
snapshot.pop(); // clip
|
||||||
|
|
||||||
|
let node = snapshot.to_node()?;
|
||||||
|
let viewport = graphene_rs::Rect::new(pad, pad, w, h);
|
||||||
|
Some(renderer.render_texture(&node, Some(&viewport)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a wallpaper-only window for secondary monitors.
|
/// Create a wallpaper-only window for secondary monitors.
|
||||||
pub fn create_wallpaper_window(
|
pub fn create_wallpaper_window(
|
||||||
texture: &gdk::Texture,
|
texture: &gdk::Texture,
|
||||||
|
blur_radius: Option<f32>,
|
||||||
app: >k::Application,
|
app: >k::Application,
|
||||||
) -> gtk::ApplicationWindow {
|
) -> gtk::ApplicationWindow {
|
||||||
let window = gtk::ApplicationWindow::builder()
|
let window = gtk::ApplicationWindow::builder()
|
||||||
@@ -175,18 +161,28 @@ pub fn create_wallpaper_window(
|
|||||||
.build();
|
.build();
|
||||||
window.add_css_class("wallpaper");
|
window.add_css_class("wallpaper");
|
||||||
|
|
||||||
let background = create_background_picture(texture);
|
let background = create_background_picture(texture, blur_radius);
|
||||||
window.set_child(Some(&background));
|
window.set_child(Some(&background));
|
||||||
|
|
||||||
window
|
window
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a Picture widget for the wallpaper background from a pre-loaded texture.
|
/// Create a Picture widget for the wallpaper background, optionally with GPU blur.
|
||||||
fn create_background_picture(texture: &gdk::Texture) -> gtk::Picture {
|
fn create_background_picture(texture: &gdk::Texture, blur_radius: Option<f32>) -> gtk::Picture {
|
||||||
let background = gtk::Picture::for_paintable(texture);
|
let background = gtk::Picture::for_paintable(texture);
|
||||||
background.set_content_fit(gtk::ContentFit::Cover);
|
background.set_content_fit(gtk::ContentFit::Cover);
|
||||||
background.set_hexpand(true);
|
background.set_hexpand(true);
|
||||||
background.set_vexpand(true);
|
background.set_vexpand(true);
|
||||||
|
|
||||||
|
if let Some(sigma) = blur_radius.filter(|s| *s > 0.0) {
|
||||||
|
let texture = texture.clone();
|
||||||
|
background.connect_realize(move |picture| {
|
||||||
|
if let Some(blurred) = render_blurred_texture(picture, &texture, sigma) {
|
||||||
|
picture.set_paintable(Some(&blurred));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
background
|
background
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +194,7 @@ struct GreeterState {
|
|||||||
failed_attempts: HashMap<String, u32>,
|
failed_attempts: HashMap<String, u32>,
|
||||||
greetd_sock: Arc<Mutex<Option<UnixStream>>>,
|
greetd_sock: Arc<Mutex<Option<UnixStream>>>,
|
||||||
login_cancelled: Arc<std::sync::atomic::AtomicBool>,
|
login_cancelled: Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
fingerprint_available: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create the main greeter window with login UI.
|
/// Create the main greeter window with login UI.
|
||||||
@@ -228,6 +225,7 @@ pub fn create_greeter_window(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let strings = load_strings(None);
|
let strings = load_strings(None);
|
||||||
|
let fingerprint_enabled = config.fingerprint_enabled;
|
||||||
let all_users = users::get_users(None);
|
let all_users = users::get_users(None);
|
||||||
let all_sessions = sessions::get_sessions(None, None);
|
let all_sessions = sessions::get_sessions(None, None);
|
||||||
log::debug!("Greeter window: {} user(s), {} session(s)", all_users.len(), all_sessions.len());
|
log::debug!("Greeter window: {} user(s), {} session(s)", all_users.len(), all_sessions.len());
|
||||||
@@ -242,6 +240,7 @@ pub fn create_greeter_window(
|
|||||||
failed_attempts: HashMap::new(),
|
failed_attempts: HashMap::new(),
|
||||||
greetd_sock: Arc::new(Mutex::new(None)),
|
greetd_sock: Arc::new(Mutex::new(None)),
|
||||||
login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
|
fingerprint_available: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Root overlay for layering
|
// Root overlay for layering
|
||||||
@@ -250,7 +249,7 @@ pub fn create_greeter_window(
|
|||||||
|
|
||||||
// Background wallpaper
|
// Background wallpaper
|
||||||
if let Some(texture) = texture {
|
if let Some(texture) = texture {
|
||||||
overlay.set_child(Some(&create_background_picture(texture)));
|
overlay.set_child(Some(&create_background_picture(texture, config.background_blur)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main layout: 3 rows (top spacer, center login, bottom bar)
|
// Main layout: 3 rows (top spacer, center login, bottom bar)
|
||||||
@@ -312,6 +311,12 @@ pub fn create_greeter_window(
|
|||||||
error_label.set_visible(false);
|
error_label.set_visible(false);
|
||||||
login_box.append(&error_label);
|
login_box.append(&error_label);
|
||||||
|
|
||||||
|
// Fingerprint label (hidden until probe confirms availability)
|
||||||
|
let fp_label = gtk::Label::new(None);
|
||||||
|
fp_label.add_css_class("fingerprint-label");
|
||||||
|
fp_label.set_visible(false);
|
||||||
|
login_box.append(&fp_label);
|
||||||
|
|
||||||
login_box.set_halign(gtk::Align::Center);
|
login_box.set_halign(gtk::Align::Center);
|
||||||
main_box.append(&login_box);
|
main_box.append(&login_box);
|
||||||
|
|
||||||
@@ -352,6 +357,8 @@ pub fn create_greeter_window(
|
|||||||
#[weak]
|
#[weak]
|
||||||
error_label,
|
error_label,
|
||||||
#[weak]
|
#[weak]
|
||||||
|
fp_label,
|
||||||
|
#[weak]
|
||||||
session_dropdown,
|
session_dropdown,
|
||||||
#[weak]
|
#[weak]
|
||||||
window,
|
window,
|
||||||
@@ -368,9 +375,12 @@ pub fn create_greeter_window(
|
|||||||
&username_label,
|
&username_label,
|
||||||
&password_entry,
|
&password_entry,
|
||||||
&error_label,
|
&error_label,
|
||||||
|
&fp_label,
|
||||||
&session_dropdown,
|
&session_dropdown,
|
||||||
&sessions_rc,
|
&sessions_rc,
|
||||||
&window,
|
&window,
|
||||||
|
fingerprint_enabled,
|
||||||
|
strings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
@@ -398,7 +408,7 @@ pub fn create_greeter_window(
|
|||||||
error_label,
|
error_label,
|
||||||
move |btn| {
|
move |btn| {
|
||||||
btn.set_sensitive(false);
|
btn.set_sensitive(false);
|
||||||
execute_power_action(power::reboot, strings.reboot_failed, &error_label);
|
execute_power_action(power::reboot, strings.reboot_failed, &error_label, btn);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
power_box.append(&reboot_btn);
|
power_box.append(&reboot_btn);
|
||||||
@@ -412,7 +422,7 @@ pub fn create_greeter_window(
|
|||||||
error_label,
|
error_label,
|
||||||
move |btn| {
|
move |btn| {
|
||||||
btn.set_sensitive(false);
|
btn.set_sensitive(false);
|
||||||
execute_power_action(power::shutdown, strings.shutdown_failed, &error_label);
|
execute_power_action(power::shutdown, strings.shutdown_failed, &error_label, btn);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
power_box.append(&shutdown_btn);
|
power_box.append(&shutdown_btn);
|
||||||
@@ -501,6 +511,8 @@ pub fn create_greeter_window(
|
|||||||
#[weak]
|
#[weak]
|
||||||
error_label,
|
error_label,
|
||||||
#[weak]
|
#[weak]
|
||||||
|
fp_label,
|
||||||
|
#[weak]
|
||||||
session_dropdown,
|
session_dropdown,
|
||||||
#[weak]
|
#[weak]
|
||||||
window,
|
window,
|
||||||
@@ -518,6 +530,8 @@ pub fn create_greeter_window(
|
|||||||
#[weak]
|
#[weak]
|
||||||
error_label,
|
error_label,
|
||||||
#[weak]
|
#[weak]
|
||||||
|
fp_label,
|
||||||
|
#[weak]
|
||||||
session_dropdown,
|
session_dropdown,
|
||||||
#[weak]
|
#[weak]
|
||||||
window,
|
window,
|
||||||
@@ -529,9 +543,12 @@ pub fn create_greeter_window(
|
|||||||
&username_label,
|
&username_label,
|
||||||
&password_entry,
|
&password_entry,
|
||||||
&error_label,
|
&error_label,
|
||||||
|
&fp_label,
|
||||||
&session_dropdown,
|
&session_dropdown,
|
||||||
&sessions_rc,
|
&sessions_rc,
|
||||||
&window,
|
&window,
|
||||||
|
fingerprint_enabled,
|
||||||
|
strings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
@@ -549,9 +566,12 @@ fn select_initial_user(
|
|||||||
username_label: >k::Label,
|
username_label: >k::Label,
|
||||||
password_entry: >k::PasswordEntry,
|
password_entry: >k::PasswordEntry,
|
||||||
error_label: >k::Label,
|
error_label: >k::Label,
|
||||||
|
fp_label: >k::Label,
|
||||||
session_dropdown: >k::DropDown,
|
session_dropdown: >k::DropDown,
|
||||||
sessions: &[Session],
|
sessions: &[Session],
|
||||||
window: >k::ApplicationWindow,
|
window: >k::ApplicationWindow,
|
||||||
|
fingerprint_enabled: bool,
|
||||||
|
strings: &'static Strings,
|
||||||
) {
|
) {
|
||||||
if users.is_empty() {
|
if users.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -571,9 +591,12 @@ fn select_initial_user(
|
|||||||
username_label,
|
username_label,
|
||||||
password_entry,
|
password_entry,
|
||||||
error_label,
|
error_label,
|
||||||
|
fp_label,
|
||||||
session_dropdown,
|
session_dropdown,
|
||||||
sessions,
|
sessions,
|
||||||
window,
|
window,
|
||||||
|
fingerprint_enabled,
|
||||||
|
strings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,19 +608,24 @@ fn switch_to_user(
|
|||||||
username_label: >k::Label,
|
username_label: >k::Label,
|
||||||
password_entry: >k::PasswordEntry,
|
password_entry: >k::PasswordEntry,
|
||||||
error_label: >k::Label,
|
error_label: >k::Label,
|
||||||
|
fp_label: >k::Label,
|
||||||
session_dropdown: >k::DropDown,
|
session_dropdown: >k::DropDown,
|
||||||
sessions: &[Session],
|
sessions: &[Session],
|
||||||
window: >k::ApplicationWindow,
|
window: >k::ApplicationWindow,
|
||||||
|
fingerprint_enabled: bool,
|
||||||
|
strings: &'static Strings,
|
||||||
) {
|
) {
|
||||||
log::debug!("Switching to user: {}", user.username);
|
log::debug!("Switching to user: {}", user.username);
|
||||||
{
|
{
|
||||||
let mut s = state.borrow_mut();
|
let mut s = state.borrow_mut();
|
||||||
s.selected_user = Some(user.clone());
|
s.selected_user = Some(user.clone());
|
||||||
|
s.fingerprint_available = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
username_label.set_text(user.display_name());
|
username_label.set_text(user.display_name());
|
||||||
password_entry.set_text("");
|
password_entry.set_text("");
|
||||||
error_label.set_visible(false);
|
error_label.set_visible(false);
|
||||||
|
fp_label.set_visible(false);
|
||||||
|
|
||||||
// Update avatar
|
// Update avatar
|
||||||
let cached = {
|
let cached = {
|
||||||
@@ -622,6 +650,27 @@ fn switch_to_user(
|
|||||||
// Pre-select last used session for this user
|
// Pre-select last used session for this user
|
||||||
select_last_session(&user.username, session_dropdown, sessions);
|
select_last_session(&user.username, session_dropdown, sessions);
|
||||||
|
|
||||||
|
// Probe fprintd for fingerprint availability
|
||||||
|
if fingerprint_enabled {
|
||||||
|
let username = user.username.clone();
|
||||||
|
glib::spawn_future_local(clone!(
|
||||||
|
#[weak]
|
||||||
|
fp_label,
|
||||||
|
#[strong]
|
||||||
|
state,
|
||||||
|
async move {
|
||||||
|
let mut probe = crate::fingerprint::FingerprintProbe::new();
|
||||||
|
probe.init_async().await;
|
||||||
|
let available = probe.is_available_async(&username).await;
|
||||||
|
state.borrow_mut().fingerprint_available = available;
|
||||||
|
fp_label.set_visible(available);
|
||||||
|
if available {
|
||||||
|
fp_label.set_text(strings.fingerprint_prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
password_entry.grab_focus();
|
password_entry.grab_focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,16 +681,33 @@ fn set_avatar_from_file(
|
|||||||
username: Option<&str>,
|
username: Option<&str>,
|
||||||
state: &Rc<RefCell<GreeterState>>,
|
state: &Rc<RefCell<GreeterState>>,
|
||||||
) {
|
) {
|
||||||
// Reject oversized files
|
// Re-check symlink status to narrow TOCTOU window from get_avatar_path_with()
|
||||||
if let Ok(meta) = std::fs::metadata(path) {
|
match std::fs::symlink_metadata(path) {
|
||||||
if meta.len() > MAX_AVATAR_FILE_SIZE {
|
Ok(meta) if meta.file_type().is_symlink() => {
|
||||||
|
log::warn!("Rejecting symlink avatar at load time: {}", path.display());
|
||||||
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(meta) if meta.len() > MAX_AVATAR_FILE_SIZE => {
|
||||||
log::debug!("Avatar file too large ({} bytes): {}", meta.len(), path.display());
|
log::debug!("Avatar file too large ({} bytes): {}", meta.len(), path.display());
|
||||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("Cannot stat avatar {}: {e}", path.display());
|
||||||
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), AVATAR_SIZE, AVATAR_SIZE, true) {
|
let Some(path_str) = path.to_str() else {
|
||||||
|
log::debug!("Non-UTF-8 avatar path, skipping: {}", path.display());
|
||||||
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match Pixbuf::from_file_at_scale(path_str, AVATAR_SIZE, AVATAR_SIZE, true) {
|
||||||
Ok(pixbuf) => {
|
Ok(pixbuf) => {
|
||||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||||
if let Some(name) = username {
|
if let Some(name) = username {
|
||||||
@@ -759,6 +825,15 @@ fn show_error(
|
|||||||
password_entry.grab_focus();
|
password_entry.grab_focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract and length-check a greetd error description from a JSON response.
|
||||||
|
fn extract_greetd_description<'a>(response: &'a serde_json::Value, fallback: &'a str) -> &'a str {
|
||||||
|
response
|
||||||
|
.get("description")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.filter(|d| !d.is_empty() && d.len() <= MAX_GREETD_ERROR_LENGTH)
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
/// Display a greetd error, using a fallback for missing or oversized descriptions.
|
/// Display a greetd error, using a fallback for missing or oversized descriptions.
|
||||||
fn show_greetd_error(
|
fn show_greetd_error(
|
||||||
error_label: >k::Label,
|
error_label: >k::Label,
|
||||||
@@ -766,15 +841,8 @@ fn show_greetd_error(
|
|||||||
response: &serde_json::Value,
|
response: &serde_json::Value,
|
||||||
fallback: &str,
|
fallback: &str,
|
||||||
) {
|
) {
|
||||||
let description = response
|
let message = extract_greetd_description(response, fallback);
|
||||||
.get("description")
|
show_error(error_label, password_entry, message);
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
if !description.is_empty() && description.len() <= MAX_GREETD_ERROR_LENGTH {
|
|
||||||
show_error(error_label, password_entry, description);
|
|
||||||
} else {
|
|
||||||
show_error(error_label, password_entry, fallback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancel any in-progress greetd session.
|
/// Cancel any in-progress greetd session.
|
||||||
@@ -870,6 +938,7 @@ fn attempt_login(
|
|||||||
let session_name = session.name.clone();
|
let session_name = session.name.clone();
|
||||||
let greetd_sock = state.borrow().greetd_sock.clone();
|
let greetd_sock = state.borrow().greetd_sock.clone();
|
||||||
let login_cancelled = state.borrow().login_cancelled.clone();
|
let login_cancelled = state.borrow().login_cancelled.clone();
|
||||||
|
let fingerprint_available = state.borrow().fingerprint_available;
|
||||||
|
|
||||||
glib::spawn_future_local(clone!(
|
glib::spawn_future_local(clone!(
|
||||||
#[weak]
|
#[weak]
|
||||||
@@ -893,6 +962,7 @@ fn attempt_login(
|
|||||||
&greetd_sock,
|
&greetd_sock,
|
||||||
&login_cancelled,
|
&login_cancelled,
|
||||||
strings,
|
strings,
|
||||||
|
fingerprint_available,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -910,6 +980,7 @@ fn attempt_login(
|
|||||||
let warning = faillock_warning(*count, strings);
|
let warning = faillock_warning(*count, strings);
|
||||||
drop(s);
|
drop(s);
|
||||||
|
|
||||||
|
set_login_sensitive(&password_entry, &session_dropdown, true);
|
||||||
show_greetd_error(
|
show_greetd_error(
|
||||||
&error_label,
|
&error_label,
|
||||||
&password_entry,
|
&password_entry,
|
||||||
@@ -920,24 +991,23 @@ fn attempt_login(
|
|||||||
let current = error_label.text().to_string();
|
let current = error_label.text().to_string();
|
||||||
error_label.set_text(&format!("{current}\n{w}"));
|
error_label.set_text(&format!("{current}\n{w}"));
|
||||||
}
|
}
|
||||||
set_login_sensitive(&password_entry, &session_dropdown, true);
|
|
||||||
}
|
}
|
||||||
Ok(Ok(LoginResult::Error { message })) => {
|
Ok(Ok(LoginResult::Error { message })) => {
|
||||||
show_error(&error_label, &password_entry, &message);
|
|
||||||
set_login_sensitive(&password_entry, &session_dropdown, true);
|
set_login_sensitive(&password_entry, &session_dropdown, true);
|
||||||
|
show_error(&error_label, &password_entry, &message);
|
||||||
}
|
}
|
||||||
Ok(Ok(LoginResult::Cancelled)) => {
|
Ok(Ok(LoginResult::Cancelled)) => {
|
||||||
set_login_sensitive(&password_entry, &session_dropdown, true);
|
set_login_sensitive(&password_entry, &session_dropdown, true);
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
log::error!("Login worker error: {e}");
|
log::error!("Login worker error: {e}");
|
||||||
show_error(&error_label, &password_entry, strings.socket_error);
|
|
||||||
set_login_sensitive(&password_entry, &session_dropdown, true);
|
set_login_sensitive(&password_entry, &session_dropdown, true);
|
||||||
|
show_error(&error_label, &password_entry, strings.socket_error);
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
log::error!("Login worker panicked");
|
log::error!("Login worker panicked");
|
||||||
show_error(&error_label, &password_entry, strings.socket_error);
|
|
||||||
set_login_sensitive(&password_entry, &session_dropdown, true);
|
set_login_sensitive(&password_entry, &session_dropdown, true);
|
||||||
|
show_error(&error_label, &password_entry, strings.socket_error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -968,6 +1038,7 @@ fn login_worker(
|
|||||||
greetd_sock: &Arc<Mutex<Option<UnixStream>>>,
|
greetd_sock: &Arc<Mutex<Option<UnixStream>>>,
|
||||||
login_cancelled: &Arc<std::sync::atomic::AtomicBool>,
|
login_cancelled: &Arc<std::sync::atomic::AtomicBool>,
|
||||||
strings: &Strings,
|
strings: &Strings,
|
||||||
|
fingerprint_available: bool,
|
||||||
) -> Result<LoginResult, String> {
|
) -> Result<LoginResult, String> {
|
||||||
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
log::debug!("Login cancelled before connect");
|
log::debug!("Login cancelled before connect");
|
||||||
@@ -976,7 +1047,9 @@ fn login_worker(
|
|||||||
|
|
||||||
log::debug!("Connecting to greetd socket: {sock_path}");
|
log::debug!("Connecting to greetd socket: {sock_path}");
|
||||||
let mut sock = UnixStream::connect(sock_path).map_err(|e| e.to_string())?;
|
let mut sock = UnixStream::connect(sock_path).map_err(|e| e.to_string())?;
|
||||||
if let Err(e) = sock.set_read_timeout(Some(std::time::Duration::from_secs(10))) {
|
// Longer timeout when fingerprint is available — pam_fprintd waits for scan
|
||||||
|
let read_timeout_secs = if fingerprint_available { 60 } else { 10 };
|
||||||
|
if let Err(e) = sock.set_read_timeout(Some(std::time::Duration::from_secs(read_timeout_secs))) {
|
||||||
log::warn!("Failed to set read timeout: {e}");
|
log::warn!("Failed to set read timeout: {e}");
|
||||||
}
|
}
|
||||||
if let Err(e) = sock.set_write_timeout(Some(std::time::Duration::from_secs(10))) {
|
if let Err(e) = sock.set_write_timeout(Some(std::time::Duration::from_secs(10))) {
|
||||||
@@ -1003,24 +1076,45 @@ fn login_worker(
|
|||||||
return Ok(LoginResult::Cancelled);
|
return Ok(LoginResult::Cancelled);
|
||||||
}
|
}
|
||||||
if response.get("type").and_then(|v| v.as_str()) == Some("error") {
|
if response.get("type").and_then(|v| v.as_str()) == Some("error") {
|
||||||
let description = response
|
let message = extract_greetd_description(&response, strings.auth_failed).to_string();
|
||||||
.get("description")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
let message = if !description.is_empty() && description.len() <= MAX_GREETD_ERROR_LENGTH {
|
|
||||||
description.to_string()
|
|
||||||
} else {
|
|
||||||
strings.auth_failed.to_string()
|
|
||||||
};
|
|
||||||
return Ok(LoginResult::Error { message });
|
return Ok(LoginResult::Error { message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Send password if auth message received
|
// Step 2: Handle auth_message loop (supports multi-stage PAM, e.g. fprintd + password)
|
||||||
if response.get("type").and_then(|v| v.as_str()) == Some("auth_message") {
|
const MAX_AUTH_ROUNDS: u32 = 5;
|
||||||
log::debug!("Sending auth response for {username}");
|
let mut auth_round = 0;
|
||||||
response =
|
|
||||||
ipc::post_auth_response(&mut sock, Some(password)).map_err(|e| e.to_string())?;
|
while response.get("type").and_then(|v| v.as_str()) == Some("auth_message") {
|
||||||
|
auth_round += 1;
|
||||||
|
if auth_round > MAX_AUTH_ROUNDS {
|
||||||
|
log::warn!("Too many auth rounds ({auth_round}), aborting");
|
||||||
|
let _ = ipc::cancel_session(&mut sock);
|
||||||
|
return Ok(LoginResult::Error {
|
||||||
|
message: strings.auth_failed.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
return Ok(LoginResult::Cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg_type = response
|
||||||
|
.get("auth_message_type")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("secret");
|
||||||
|
|
||||||
|
if msg_type == "secret" {
|
||||||
|
log::debug!("Sending password for {username} (round {auth_round})");
|
||||||
|
response =
|
||||||
|
ipc::post_auth_response(&mut sock, Some(password)).map_err(|e| e.to_string())?;
|
||||||
|
} else {
|
||||||
|
// Non-secret prompt (e.g. fprintd "Place finger on reader")
|
||||||
|
// PAM handles the actual verification; this blocks until resolved
|
||||||
|
log::debug!("Acknowledging non-secret auth prompt (round {auth_round})");
|
||||||
|
response =
|
||||||
|
ipc::post_auth_response(&mut sock, None).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
return Ok(LoginResult::Cancelled);
|
return Ok(LoginResult::Cancelled);
|
||||||
@@ -1033,14 +1127,6 @@ fn login_worker(
|
|||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.get("type").and_then(|v| v.as_str()) == Some("auth_message") {
|
|
||||||
// Multi-stage auth is not supported
|
|
||||||
let _ = ipc::cancel_session(&mut sock);
|
|
||||||
return Ok(LoginResult::Error {
|
|
||||||
message: strings.multi_stage_unsupported.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Start session
|
// Step 3: Start session
|
||||||
@@ -1080,10 +1166,7 @@ fn login_worker(
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return Ok(LoginResult::Error {
|
return Ok(LoginResult::Error {
|
||||||
message: response
|
message: extract_greetd_description(&response, strings.session_start_failed)
|
||||||
.get("description")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or(strings.session_start_failed)
|
|
||||||
.to_string(),
|
.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1099,10 +1182,13 @@ fn execute_power_action(
|
|||||||
action_fn: fn() -> Result<(), PowerError>,
|
action_fn: fn() -> Result<(), PowerError>,
|
||||||
error_message: &'static str,
|
error_message: &'static str,
|
||||||
error_label: >k::Label,
|
error_label: >k::Label,
|
||||||
|
button: >k::Button,
|
||||||
) {
|
) {
|
||||||
glib::spawn_future_local(clone!(
|
glib::spawn_future_local(clone!(
|
||||||
#[weak]
|
#[weak]
|
||||||
error_label,
|
error_label,
|
||||||
|
#[weak]
|
||||||
|
button,
|
||||||
async move {
|
async move {
|
||||||
let result = gio::spawn_blocking(move || action_fn()).await;
|
let result = gio::spawn_blocking(move || action_fn()).await;
|
||||||
|
|
||||||
@@ -1112,11 +1198,13 @@ fn execute_power_action(
|
|||||||
log::error!("Power action failed: {e}");
|
log::error!("Power action failed: {e}");
|
||||||
error_label.set_text(error_message);
|
error_label.set_text(error_message);
|
||||||
error_label.set_visible(true);
|
error_label.set_visible(true);
|
||||||
|
button.set_sensitive(true);
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
log::error!("Power action panicked");
|
log::error!("Power action panicked");
|
||||||
error_label.set_text(error_message);
|
error_label.set_text(error_message);
|
||||||
error_label.set_visible(true);
|
error_label.set_visible(true);
|
||||||
|
button.set_sensitive(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1147,18 +1235,24 @@ fn save_last_user(username: &str) {
|
|||||||
|
|
||||||
fn save_last_user_to(path: &Path, username: &str) {
|
fn save_last_user_to(path: &Path, username: &str) {
|
||||||
log::debug!("Saving last user: {username}");
|
log::debug!("Saving last user: {username}");
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent()
|
||||||
let _ = std::fs::create_dir_all(parent);
|
&& let Err(e) = std::fs::create_dir_all(parent)
|
||||||
|
{
|
||||||
|
log::warn!("Failed to create cache dir {}: {e}", parent.display());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let _ = std::fs::OpenOptions::new()
|
if let Err(e) = std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.mode(0o600)
|
.mode(0o600)
|
||||||
.open(path)
|
.open(path)
|
||||||
.and_then(|mut f| f.write_all(username.as_bytes()));
|
.and_then(|mut f| f.write_all(username.as_bytes()))
|
||||||
|
{
|
||||||
|
log::warn!("Failed to save last user to {}: {e}", path.display());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_last_session(username: &str) -> Option<String> {
|
fn load_last_session(username: &str) -> Option<String> {
|
||||||
@@ -1203,13 +1297,16 @@ fn save_last_session_to(path: &Path, session_name: &str) {
|
|||||||
log::debug!("Saving last session: {session_name}");
|
log::debug!("Saving last session: {session_name}");
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let _ = std::fs::OpenOptions::new()
|
if let Err(e) = std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.mode(0o600)
|
.mode(0o600)
|
||||||
.open(path)
|
.open(path)
|
||||||
.and_then(|mut f| f.write_all(session_name.as_bytes()));
|
.and_then(|mut f| f.write_all(session_name.as_bytes()))
|
||||||
|
{
|
||||||
|
log::warn!("Failed to save last session to {}: {e}", path.display());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1223,6 +1320,8 @@ mod tests {
|
|||||||
assert!(is_valid_username("test-user"));
|
assert!(is_valid_username("test-user"));
|
||||||
assert!(is_valid_username("test.user"));
|
assert!(is_valid_username("test.user"));
|
||||||
assert!(is_valid_username("_admin"));
|
assert!(is_valid_username("_admin"));
|
||||||
|
assert!(is_valid_username("user@domain"));
|
||||||
|
assert!(is_valid_username(&"a".repeat(MAX_USERNAME_LENGTH)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1230,6 +1329,7 @@ mod tests {
|
|||||||
assert!(!is_valid_username(""));
|
assert!(!is_valid_username(""));
|
||||||
assert!(!is_valid_username(".hidden"));
|
assert!(!is_valid_username(".hidden"));
|
||||||
assert!(!is_valid_username("-dash"));
|
assert!(!is_valid_username("-dash"));
|
||||||
|
assert!(!is_valid_username("@domain"));
|
||||||
assert!(!is_valid_username("user/name"));
|
assert!(!is_valid_username("user/name"));
|
||||||
assert!(!is_valid_username(&"a".repeat(MAX_USERNAME_LENGTH + 1)));
|
assert!(!is_valid_username(&"a".repeat(MAX_USERNAME_LENGTH + 1)));
|
||||||
}
|
}
|
||||||
@@ -1454,7 +1554,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "wrongpass", "/usr/bin/niri",
|
"alice", "wrongpass", "/usr/bin/niri",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@@ -1496,7 +1596,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "correct", "/usr/bin/bash",
|
"alice", "correct", "/usr/bin/bash",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@@ -1505,40 +1605,104 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn login_worker_multi_stage_rejected() {
|
fn login_worker_multi_stage_fingerprint_then_password() {
|
||||||
let (sock_path, handle) = fake_greetd(|stream| {
|
let (sock_path, handle) = fake_greetd(|stream| {
|
||||||
// create_session
|
// create_session
|
||||||
let _msg = ipc::recv_message(stream).unwrap();
|
let _msg = ipc::recv_message(stream).unwrap();
|
||||||
|
ipc::send_message(stream, &serde_json::json!({
|
||||||
|
"type": "auth_message",
|
||||||
|
"auth_message_type": "visible",
|
||||||
|
"auth_message": "Place your finger on the reader",
|
||||||
|
})).unwrap();
|
||||||
|
|
||||||
|
// post_auth_response with None (fingerprint prompt acknowledged)
|
||||||
|
let msg = ipc::recv_message(stream).unwrap();
|
||||||
|
assert!(msg["response"].is_null());
|
||||||
|
|
||||||
|
// Fingerprint failed, PAM falls through to password
|
||||||
ipc::send_message(stream, &serde_json::json!({
|
ipc::send_message(stream, &serde_json::json!({
|
||||||
"type": "auth_message",
|
"type": "auth_message",
|
||||||
"auth_message_type": "secret",
|
"auth_message_type": "secret",
|
||||||
"auth_message": "Password: ",
|
"auth_message": "Password: ",
|
||||||
})).unwrap();
|
})).unwrap();
|
||||||
|
|
||||||
// post_auth_response → another auth_message (TOTP)
|
// post_auth_response with password
|
||||||
let _msg = ipc::recv_message(stream).unwrap();
|
let msg = ipc::recv_message(stream).unwrap();
|
||||||
ipc::send_message(stream, &serde_json::json!({
|
assert_eq!(msg["response"], "correctpass");
|
||||||
"type": "auth_message",
|
ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap();
|
||||||
"auth_message_type": "visible",
|
|
||||||
"auth_message": "TOTP: ",
|
|
||||||
})).unwrap();
|
|
||||||
|
|
||||||
// cancel_session
|
// start_session
|
||||||
let _msg = ipc::recv_message(stream).unwrap();
|
let _msg = ipc::recv_message(stream).unwrap();
|
||||||
ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap();
|
ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "/usr/bin/niri",
|
"alice", "correctpass", "/usr/bin/bash",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), true,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = result.unwrap();
|
||||||
|
assert!(matches!(result, LoginResult::Success { .. }));
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn login_worker_multi_stage_fingerprint_success() {
|
||||||
|
let (sock_path, handle) = fake_greetd(|stream| {
|
||||||
|
// create_session
|
||||||
|
let _msg = ipc::recv_message(stream).unwrap();
|
||||||
|
ipc::send_message(stream, &serde_json::json!({
|
||||||
|
"type": "auth_message",
|
||||||
|
"auth_message_type": "visible",
|
||||||
|
"auth_message": "Place your finger on the reader",
|
||||||
|
})).unwrap();
|
||||||
|
|
||||||
|
// post_auth_response with None → fingerprint matched via PAM
|
||||||
|
let _msg = ipc::recv_message(stream).unwrap();
|
||||||
|
ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap();
|
||||||
|
|
||||||
|
// start_session
|
||||||
|
let _msg = ipc::recv_message(stream).unwrap();
|
||||||
|
ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = login_worker(
|
||||||
|
"alice", "", "/usr/bin/bash",
|
||||||
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
|
load_strings(Some("en")), true,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = result.unwrap();
|
||||||
|
assert!(matches!(result, LoginResult::Success { .. }));
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn login_worker_max_auth_rounds_exceeded() {
|
||||||
|
let (sock_path, handle) = fake_greetd(|stream| {
|
||||||
|
// create_session
|
||||||
|
let _msg = ipc::recv_message(stream).unwrap();
|
||||||
|
|
||||||
|
// Send 6 auth_messages (exceeds MAX_AUTH_ROUNDS=5)
|
||||||
|
for _ in 0..6 {
|
||||||
|
ipc::send_message(stream, &serde_json::json!({
|
||||||
|
"type": "auth_message",
|
||||||
|
"auth_message_type": "visible",
|
||||||
|
"auth_message": "Prompt",
|
||||||
|
})).unwrap();
|
||||||
|
let _msg = ipc::recv_message(stream).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = login_worker(
|
||||||
|
"alice", "pass", "/usr/bin/bash",
|
||||||
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
assert!(matches!(result, LoginResult::Error { .. }));
|
assert!(matches!(result, LoginResult::Error { .. }));
|
||||||
if let LoginResult::Error { message } = result {
|
|
||||||
assert!(message.contains("Multi-stage"));
|
|
||||||
}
|
|
||||||
handle.join().unwrap();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1568,7 +1732,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "/usr/bin/bash",
|
"alice", "pass", "/usr/bin/bash",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@@ -1583,13 +1747,25 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "/usr/bin/niri",
|
"alice", "pass", "/usr/bin/niri",
|
||||||
"/nonexistent/sock", &default_greetd_sock(), &cancelled,
|
"/nonexistent/sock", &default_greetd_sock(), &cancelled,
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
assert!(matches!(result, LoginResult::Cancelled));
|
assert!(matches!(result, LoginResult::Cancelled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn login_worker_connect_failure() {
|
||||||
|
let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||||
|
let result = login_worker(
|
||||||
|
"alice", "pass", "/usr/bin/niri",
|
||||||
|
"/nonexistent/sock", &default_greetd_sock(), &cancelled,
|
||||||
|
load_strings(Some("en")), false,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn login_worker_invalid_exec_cmd() {
|
fn login_worker_invalid_exec_cmd() {
|
||||||
let (sock_path, handle) = fake_greetd(|stream| {
|
let (sock_path, handle) = fake_greetd(|stream| {
|
||||||
@@ -1614,7 +1790,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "../../../etc/evil",
|
"alice", "pass", "../../../etc/evil",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@@ -1646,7 +1822,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "niri-session",
|
"alice", "pass", "niri-session",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@@ -1658,7 +1834,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_background_texture_missing_file_returns_none() {
|
fn load_background_texture_missing_file_returns_none() {
|
||||||
let result = load_background_texture(Path::new("/nonexistent/wallpaper.jpg"), None);
|
let result = load_background_texture(Path::new("/nonexistent/wallpaper.jpg"));
|
||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1669,7 +1845,7 @@ mod tests {
|
|||||||
// Create a sparse file that exceeds MAX_WALLPAPER_FILE_SIZE
|
// Create a sparse file that exceeds MAX_WALLPAPER_FILE_SIZE
|
||||||
let f = std::fs::File::create(&path).unwrap();
|
let f = std::fs::File::create(&path).unwrap();
|
||||||
f.set_len(MAX_WALLPAPER_FILE_SIZE + 1).unwrap();
|
f.set_len(MAX_WALLPAPER_FILE_SIZE + 1).unwrap();
|
||||||
let result = load_background_texture(&path, None);
|
let result = load_background_texture(&path);
|
||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1680,7 +1856,32 @@ mod tests {
|
|||||||
// 0xFF is not valid UTF-8
|
// 0xFF is not valid UTF-8
|
||||||
let non_utf8 = OsStr::from_bytes(&[0xff, 0xfe, 0xfd]);
|
let non_utf8 = OsStr::from_bytes(&[0xff, 0xfe, 0xfd]);
|
||||||
let path = Path::new(non_utf8);
|
let path = Path::new(non_utf8);
|
||||||
let result = load_background_texture(path, None);
|
let result = load_background_texture(path);
|
||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_greetd_description_normal() {
|
||||||
|
let resp = serde_json::json!({"type": "error", "description": "bad password"});
|
||||||
|
assert_eq!(extract_greetd_description(&resp, "fallback"), "bad password");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_greetd_description_oversized() {
|
||||||
|
let long = "x".repeat(MAX_GREETD_ERROR_LENGTH + 1);
|
||||||
|
let resp = serde_json::json!({"type": "error", "description": long});
|
||||||
|
assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_greetd_description_empty() {
|
||||||
|
let resp = serde_json::json!({"type": "error", "description": ""});
|
||||||
|
assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_greetd_description_missing() {
|
||||||
|
let resp = serde_json::json!({"type": "error"});
|
||||||
|
assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-6
@@ -23,12 +23,11 @@ pub struct Strings {
|
|||||||
pub greetd_sock_unreachable: &'static str,
|
pub greetd_sock_unreachable: &'static str,
|
||||||
pub auth_failed: &'static str,
|
pub auth_failed: &'static str,
|
||||||
pub wrong_password: &'static str,
|
pub wrong_password: &'static str,
|
||||||
pub multi_stage_unsupported: &'static str,
|
pub fingerprint_prompt: &'static str,
|
||||||
pub invalid_session_command: &'static str,
|
pub invalid_session_command: &'static str,
|
||||||
pub session_start_failed: &'static str,
|
pub session_start_failed: &'static str,
|
||||||
pub reboot_failed: &'static str,
|
pub reboot_failed: &'static str,
|
||||||
pub shutdown_failed: &'static str,
|
pub shutdown_failed: &'static str,
|
||||||
pub connection_error: &'static str,
|
|
||||||
pub socket_error: &'static str,
|
pub socket_error: &'static str,
|
||||||
pub unexpected_greetd_response: &'static str,
|
pub unexpected_greetd_response: &'static str,
|
||||||
|
|
||||||
@@ -48,12 +47,11 @@ const STRINGS_DE: Strings = Strings {
|
|||||||
greetd_sock_unreachable: "GREETD_SOCK nicht erreichbar",
|
greetd_sock_unreachable: "GREETD_SOCK nicht erreichbar",
|
||||||
auth_failed: "Authentifizierung fehlgeschlagen",
|
auth_failed: "Authentifizierung fehlgeschlagen",
|
||||||
wrong_password: "Falsches Passwort",
|
wrong_password: "Falsches Passwort",
|
||||||
multi_stage_unsupported: "Mehrstufige Authentifizierung wird nicht unterstützt",
|
fingerprint_prompt: "Fingerabdruck auflegen oder Passwort eingeben",
|
||||||
invalid_session_command: "Ungültiger Session-Befehl",
|
invalid_session_command: "Ungültiger Session-Befehl",
|
||||||
session_start_failed: "Session konnte nicht gestartet werden",
|
session_start_failed: "Session konnte nicht gestartet werden",
|
||||||
reboot_failed: "Neustart fehlgeschlagen",
|
reboot_failed: "Neustart fehlgeschlagen",
|
||||||
shutdown_failed: "Herunterfahren fehlgeschlagen",
|
shutdown_failed: "Herunterfahren fehlgeschlagen",
|
||||||
connection_error: "Verbindungsfehler",
|
|
||||||
socket_error: "Socket-Fehler",
|
socket_error: "Socket-Fehler",
|
||||||
unexpected_greetd_response: "Unerwartete Antwort von greetd",
|
unexpected_greetd_response: "Unerwartete Antwort von greetd",
|
||||||
faillock_attempts_remaining: "Noch {n} Versuch(e) vor Kontosperrung!",
|
faillock_attempts_remaining: "Noch {n} Versuch(e) vor Kontosperrung!",
|
||||||
@@ -71,12 +69,11 @@ const STRINGS_EN: Strings = Strings {
|
|||||||
greetd_sock_unreachable: "GREETD_SOCK unreachable",
|
greetd_sock_unreachable: "GREETD_SOCK unreachable",
|
||||||
auth_failed: "Authentication failed",
|
auth_failed: "Authentication failed",
|
||||||
wrong_password: "Wrong password",
|
wrong_password: "Wrong password",
|
||||||
multi_stage_unsupported: "Multi-stage authentication is not supported",
|
fingerprint_prompt: "Place finger on reader or enter password",
|
||||||
invalid_session_command: "Invalid session command",
|
invalid_session_command: "Invalid session command",
|
||||||
session_start_failed: "Failed to start session",
|
session_start_failed: "Failed to start session",
|
||||||
reboot_failed: "Reboot failed",
|
reboot_failed: "Reboot failed",
|
||||||
shutdown_failed: "Shutdown failed",
|
shutdown_failed: "Shutdown failed",
|
||||||
connection_error: "Connection error",
|
|
||||||
socket_error: "Socket error",
|
socket_error: "Socket error",
|
||||||
unexpected_greetd_response: "Unexpected response from greetd",
|
unexpected_greetd_response: "Unexpected response from greetd",
|
||||||
faillock_attempts_remaining: "{n} attempt(s) remaining before lockout!",
|
faillock_attempts_remaining: "{n} attempt(s) remaining before lockout!",
|
||||||
@@ -285,6 +282,7 @@ mod tests {
|
|||||||
assert!(!s.greetd_sock_not_set.is_empty(), "{locale}: greetd_sock_not_set");
|
assert!(!s.greetd_sock_not_set.is_empty(), "{locale}: greetd_sock_not_set");
|
||||||
assert!(!s.auth_failed.is_empty(), "{locale}: auth_failed");
|
assert!(!s.auth_failed.is_empty(), "{locale}: auth_failed");
|
||||||
assert!(!s.wrong_password.is_empty(), "{locale}: wrong_password");
|
assert!(!s.wrong_password.is_empty(), "{locale}: wrong_password");
|
||||||
|
assert!(!s.fingerprint_prompt.is_empty(), "{locale}: fingerprint_prompt");
|
||||||
assert!(!s.reboot_failed.is_empty(), "{locale}: reboot_failed");
|
assert!(!s.reboot_failed.is_empty(), "{locale}: reboot_failed");
|
||||||
assert!(!s.shutdown_failed.is_empty(), "{locale}: shutdown_failed");
|
assert!(!s.shutdown_failed.is_empty(), "{locale}: shutdown_failed");
|
||||||
assert!(!s.faillock_attempts_remaining.is_empty(), "{locale}: faillock_attempts_remaining");
|
assert!(!s.faillock_attempts_remaining.is_empty(), "{locale}: faillock_attempts_remaining");
|
||||||
|
|||||||
+18
-14
@@ -2,6 +2,7 @@
|
|||||||
// ABOUTME: Sets up GTK Application, Layer Shell, CSS, and multi-monitor windows.
|
// ABOUTME: Sets up GTK Application, Layer Shell, CSS, and multi-monitor windows.
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod fingerprint;
|
||||||
mod greeter;
|
mod greeter;
|
||||||
mod i18n;
|
mod i18n;
|
||||||
mod ipc;
|
mod ipc;
|
||||||
@@ -19,7 +20,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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,14 +52,11 @@ fn activate(app: >k::Application) {
|
|||||||
|
|
||||||
// Load config and resolve wallpaper
|
// Load config and resolve wallpaper
|
||||||
let config = config::load_config(None);
|
let config = config::load_config(None);
|
||||||
let bg_path = config::resolve_background_path(&config);
|
let bg_texture = config::resolve_background_path(&config)
|
||||||
log::debug!("Background path: {}", bg_path.display());
|
.and_then(|path| {
|
||||||
|
log::debug!("Background path: {}", path.display());
|
||||||
// Load background texture once — shared across all windows
|
greeter::load_background_texture(&path)
|
||||||
let bg_texture = greeter::load_background_texture(&bg_path, config.background_blur);
|
});
|
||||||
if bg_texture.is_none() {
|
|
||||||
log::error!("Failed to load background texture — greeter will start without wallpaper");
|
|
||||||
}
|
|
||||||
|
|
||||||
let use_layer_shell = std::env::var("MOONGREET_NO_LAYER_SHELL").is_err();
|
let use_layer_shell = std::env::var("MOONGREET_NO_LAYER_SHELL").is_err();
|
||||||
log::debug!("Layer shell: {use_layer_shell}");
|
log::debug!("Layer shell: {use_layer_shell}");
|
||||||
@@ -81,7 +79,7 @@ fn activate(app: >k::Application) {
|
|||||||
.item(i)
|
.item(i)
|
||||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
||||||
{
|
{
|
||||||
let wallpaper = greeter::create_wallpaper_window(texture, app);
|
let wallpaper = greeter::create_wallpaper_window(texture, config.background_blur, app);
|
||||||
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom);
|
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom);
|
||||||
wallpaper.set_monitor(Some(&monitor));
|
wallpaper.set_monitor(Some(&monitor));
|
||||||
wallpaper.present();
|
wallpaper.present();
|
||||||
@@ -91,10 +89,16 @@ fn activate(app: >k::Application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create journal logger: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
let level = if std::env::var("MOONGREET_DEBUG").is_ok() {
|
let level = if std::env::var("MOONGREET_DEBUG").is_ok() {
|
||||||
log::LevelFilter::Debug
|
log::LevelFilter::Debug
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+2
-3
@@ -41,8 +41,7 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
|
|||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
log::debug!("Power action {action} completed successfully");
|
log::debug!("Power action {action} completed successfully");
|
||||||
}
|
} else {
|
||||||
if !output.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
return Err(PowerError::CommandFailed {
|
return Err(PowerError::CommandFailed {
|
||||||
action,
|
action,
|
||||||
@@ -100,7 +99,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_command_passes_args() {
|
fn run_command_passes_args() {
|
||||||
let result = run_command("test", "echo", &["hello", "world"]);
|
let result = run_command("test", "true", &["--ignored-arg"]);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const DEFAULT_XSESSION_DIRS: &[&str] = &["/usr/share/xsessions"];
|
|||||||
pub struct Session {
|
pub struct Session {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub exec_cmd: String,
|
pub exec_cmd: String,
|
||||||
|
#[allow(dead_code)] // Retained for future Wayland-only filtering
|
||||||
pub session_type: String,
|
pub session_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-9
@@ -23,9 +23,11 @@ const NOLOGIN_SHELLS: &[&str] = &[
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
#[allow(dead_code)] // Retained for debugging and future UID-based features
|
||||||
pub uid: u32,
|
pub uid: u32,
|
||||||
pub gecos: String,
|
pub gecos: String,
|
||||||
pub home: PathBuf,
|
pub home: PathBuf,
|
||||||
|
#[allow(dead_code)] // Retained for debugging and future shell-based filtering
|
||||||
pub shell: String,
|
pub shell: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,16 +57,13 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec<User> {
|
|||||||
let mut users = Vec::new();
|
let mut users = Vec::new();
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let parts: Vec<&str> = line.split(':').collect();
|
let mut fields = line.splitn(7, ':');
|
||||||
if parts.len() < 7 {
|
let (Some(username), Some(_pw), Some(uid_str), Some(_gid), Some(gecos), Some(home), Some(shell)) =
|
||||||
|
(fields.next(), fields.next(), fields.next(), fields.next(),
|
||||||
|
fields.next(), fields.next(), fields.next())
|
||||||
|
else {
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
|
|
||||||
let username = parts[0];
|
|
||||||
let uid_str = parts[2];
|
|
||||||
let gecos = parts[4];
|
|
||||||
let home = parts[5];
|
|
||||||
let shell = parts[6];
|
|
||||||
|
|
||||||
let uid = match uid_str.parse::<u32>() {
|
let uid = match uid_str.parse::<u32>() {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
|
|||||||
Reference in New Issue
Block a user