Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64470f99c3 | |||
| 293bba32a6 | |||
| 14d6476e5a | |||
| 4c9b436978 | |||
| 96c94f030a | |||
| b91e8d47d1 | |||
| 5db23937ea | |||
| 0d4a1b035a |
@@ -19,6 +19,7 @@ Teil des Moonarch-Ökosystems.
|
|||||||
- `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, wallpaper.jpg, 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`)
|
||||||
|
|
||||||
## Kommandos
|
## Kommandos
|
||||||
|
|
||||||
@@ -29,8 +30,11 @@ cargo test
|
|||||||
# Release-Build
|
# Release-Build
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
# Greeter starten (nur zum Testen, braucht normalerweise greetd)
|
# Greeter im Fenster starten (ohne greetd/Layer Shell)
|
||||||
LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moongreet
|
MOONGREET_NO_LAYER_SHELL=1 ./target/release/moongreet
|
||||||
|
|
||||||
|
# Paket bauen und installieren
|
||||||
|
cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.zst
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
@@ -39,10 +43,10 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moongreet
|
|||||||
- `users.rs` — Benutzer aus /etc/passwd, Avatare (AccountsService + ~/.face), Symlink-Rejection
|
- `users.rs` — Benutzer aus /etc/passwd, Avatare (AccountsService + ~/.face), Symlink-Rejection
|
||||||
- `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)
|
- `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
|
- `config.rs` — TOML-Config ([appearance] background, gtk-theme) + Wallpaper-Fallback
|
||||||
- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC, Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence
|
- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC, Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o600 Permissions)
|
||||||
- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor
|
- `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
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
@@ -53,3 +57,11 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moongreet
|
|||||||
- **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)
|
- **Symmetrie mit moonset**: Gleiche Patterns (i18n, config, users, power, GResource)
|
||||||
|
- **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
|
||||||
|
- **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var
|
||||||
|
- **File Permissions**: Cache-Dateien 0o600
|
||||||
|
- **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
|
||||||
|
- **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
|
||||||
|
|||||||
Generated
+131
-164
@@ -3,63 +3,10 @@
|
|||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "adler2"
|
||||||
version = "1.1.4"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstream"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
|
||||||
dependencies = [
|
|
||||||
"anstyle",
|
|
||||||
"anstyle-parse",
|
|
||||||
"anstyle-query",
|
|
||||||
"anstyle-wincon",
|
|
||||||
"colorchoice",
|
|
||||||
"is_terminal_polyfill",
|
|
||||||
"utf8parse",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle"
|
|
||||||
version = "1.0.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-parse"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
|
||||||
dependencies = [
|
|
||||||
"utf8parse",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-query"
|
|
||||||
version = "1.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-wincon"
|
|
||||||
version = "3.0.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
|
||||||
dependencies = [
|
|
||||||
"anstyle",
|
|
||||||
"once_cell_polyfill",
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
@@ -79,6 +26,18 @@ version = "2.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder-lite"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cairo-rs"
|
name = "cairo-rs"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@@ -119,32 +78,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "crc32fast"
|
||||||
version = "1.0.5"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "env_filter"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"cfg-if",
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "env_logger"
|
|
||||||
version = "0.11.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
|
|
||||||
dependencies = [
|
|
||||||
"anstream",
|
|
||||||
"anstyle",
|
|
||||||
"env_filter",
|
|
||||||
"jiff",
|
|
||||||
"log",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -169,6 +108,15 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fdeflate"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||||
|
dependencies = [
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "field-offset"
|
name = "field-offset"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
@@ -179,6 +127,16 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -592,6 +550,21 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image"
|
||||||
|
version = "0.25.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"byteorder-lite",
|
||||||
|
"moxcms",
|
||||||
|
"num-traits",
|
||||||
|
"png",
|
||||||
|
"zune-core",
|
||||||
|
"zune-jpeg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@@ -604,42 +577,12 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "is_terminal_polyfill"
|
|
||||||
version = "1.70.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiff"
|
|
||||||
version = "0.2.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
|
|
||||||
dependencies = [
|
|
||||||
"jiff-static",
|
|
||||||
"log",
|
|
||||||
"portable-atomic",
|
|
||||||
"portable-atomic-util",
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiff-static"
|
|
||||||
version = "0.2.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "khronos_api"
|
name = "khronos_api"
|
||||||
version = "3.1.0"
|
version = "3.1.0"
|
||||||
@@ -686,10 +629,19 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moongreet"
|
name = "miniz_oxide"
|
||||||
version = "0.3.0"
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "moongreet"
|
||||||
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"env_logger",
|
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
"gio",
|
"gio",
|
||||||
@@ -697,25 +649,40 @@ dependencies = [
|
|||||||
"glib-build-tools",
|
"glib-build-tools",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-layer-shell",
|
"gtk4-layer-shell",
|
||||||
|
"image",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"systemd-journal-logger",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"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"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell_polyfill"
|
|
||||||
version = "1.70.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@@ -753,18 +720,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "png"
|
||||||
version = "1.13.1"
|
version = "0.18.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "portable-atomic-util"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"portable-atomic",
|
"bitflags",
|
||||||
|
"crc32fast",
|
||||||
|
"fdeflate",
|
||||||
|
"flate2",
|
||||||
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -795,6 +760,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pxfm"
|
||||||
|
version = "0.1.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
@@ -810,35 +781,6 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex"
|
|
||||||
version = "1.12.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-automata",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-automata"
|
|
||||||
version = "0.4.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.8.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -928,6 +870,12 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@@ -964,6 +912,16 @@ dependencies = [
|
|||||||
"version-compare",
|
"version-compare",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "systemd-journal-logger"
|
||||||
|
version = "2.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7266304d24ca5a4b230545fc558c80e18bd3e1d2eb1be149b6bcd04398d3e79c"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.13.3"
|
version = "0.13.3"
|
||||||
@@ -1096,12 +1054,6 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8parse"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -1292,3 +1244,18 @@ name = "zmij"
|
|||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zune-core"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zune-jpeg"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
||||||
|
dependencies = [
|
||||||
|
"zune-core",
|
||||||
|
]
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.3.1"
|
version = "0.4.0"
|
||||||
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,8 +15,9 @@ 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"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
systemd-journal-logger = "2.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Decisions
|
||||||
|
|
||||||
|
## 2026-03-28 – Optional background blur via `image` crate
|
||||||
|
|
||||||
|
- **Who**: Selene, Dom
|
||||||
|
- **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens
|
||||||
|
- **Tradeoffs**: Adds `image` crate dependency (~15 transitive crates); CPU-side Gaussian blur at load time adds startup latency proportional to image size and sigma. Acceptable because blur runs once and the texture is shared across monitors.
|
||||||
|
- **How**: `load_background_texture(bg_path, blur_radius)` loads texture, optionally applies `imageops::blur()`, returns blurred `gdk::Texture`. Config option `background-blur: Option<f32>` in `[appearance]` TOML section.
|
||||||
|
|
||||||
|
## 2026-03-28 – Audit fixes for shared wallpaper texture (v0.4.1)
|
||||||
|
- **Who**: Selene, Dominik
|
||||||
|
- **Why**: Quality, performance, and security audits flagged issues in `load_background_texture()`, debug logging, and greetd error handling
|
||||||
|
- **Tradeoffs**: GResource path now requires UTF-8 (returns `None` for non-UTF-8 instead of aborting); 50 MB wallpaper limit is generous but prevents OOM; debug logging off by default trades observability for security
|
||||||
|
- **How**: GResource branch via `resources_lookup_data()` + `from_bytes()` (no abort), file size limit, error details only at debug level, `MOONGREET_DEBUG` env var for log level, greetd retry path truncation matching `show_greetd_error()`
|
||||||
@@ -5,4 +5,4 @@
|
|||||||
# Absolute path to wallpaper image
|
# Absolute path to wallpaper image
|
||||||
background = "/usr/share/backgrounds/wallpaper.jpg"
|
background = "/usr/share/backgrounds/wallpaper.jpg"
|
||||||
# GTK theme for the greeter UI
|
# GTK theme for the greeter UI
|
||||||
gtk-theme = "catppuccin-mocha-lavender-standard+default"
|
gtk-theme = "Colloid-Catppuccin"
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# Maintainer: Dominik Kressler
|
# Maintainer: Dominik Kressler
|
||||||
|
|
||||||
pkgname=moongreet-git
|
pkgname=moongreet-git
|
||||||
pkgver=0.3.0.r0.g0000000
|
pkgver=0.3.1.r5.g4c9b436
|
||||||
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')
|
||||||
|
|||||||
+50
-16
@@ -22,6 +22,8 @@ struct TomlConfig {
|
|||||||
#[derive(Debug, Clone, Default, Deserialize)]
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
struct Appearance {
|
struct Appearance {
|
||||||
background: Option<String>,
|
background: Option<String>,
|
||||||
|
#[serde(rename = "background-blur")]
|
||||||
|
background_blur: Option<f32>,
|
||||||
#[serde(rename = "gtk-theme")]
|
#[serde(rename = "gtk-theme")]
|
||||||
gtk_theme: Option<String>,
|
gtk_theme: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -30,6 +32,7 @@ struct Appearance {
|
|||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub background_path: Option<String>,
|
pub background_path: Option<String>,
|
||||||
|
pub background_blur: Option<f32>,
|
||||||
pub gtk_theme: Option<String>,
|
pub gtk_theme: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,27 +43,42 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
|||||||
|
|
||||||
let mut merged = Config::default();
|
let mut merged = Config::default();
|
||||||
for path in paths {
|
for path in paths {
|
||||||
if let Ok(content) = fs::read_to_string(path) {
|
match fs::read_to_string(path) {
|
||||||
if let Ok(parsed) = toml::from_str::<TomlConfig>(&content) {
|
Ok(content) => {
|
||||||
if let Some(appearance) = parsed.appearance {
|
match toml::from_str::<TomlConfig>(&content) {
|
||||||
if let Some(bg) = appearance.background {
|
Ok(parsed) => {
|
||||||
// Resolve relative paths against config file directory
|
log::debug!("Config loaded: {}", path.display());
|
||||||
let bg_path = PathBuf::from(&bg);
|
if let Some(appearance) = parsed.appearance {
|
||||||
if bg_path.is_absolute() {
|
if let Some(bg) = appearance.background {
|
||||||
merged.background_path = Some(bg);
|
// Resolve relative paths against config file directory
|
||||||
} else if let Some(parent) = path.parent() {
|
let bg_path = PathBuf::from(&bg);
|
||||||
merged.background_path =
|
if bg_path.is_absolute() {
|
||||||
Some(parent.join(&bg).to_string_lossy().to_string());
|
merged.background_path = Some(bg);
|
||||||
|
} else if let Some(parent) = path.parent() {
|
||||||
|
merged.background_path =
|
||||||
|
Some(parent.join(&bg).to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if appearance.background_blur.is_some() {
|
||||||
|
merged.background_blur = appearance.background_blur;
|
||||||
|
}
|
||||||
|
if appearance.gtk_theme.is_some() {
|
||||||
|
merged.gtk_theme = appearance.gtk_theme;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if appearance.gtk_theme.is_some() {
|
Err(e) => {
|
||||||
merged.gtk_theme = appearance.gtk_theme;
|
log::warn!("Config parse error in {}: {e}", path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::debug!("Config not found: {}", path.display());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}", merged.background_path, merged.background_blur, merged.gtk_theme);
|
||||||
merged
|
merged
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,16 +95,20 @@ pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &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());
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
log::debug!("Wallpaper: config path {} not found, trying fallbacks", path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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());
|
||||||
return moonarch_wallpaper.to_path_buf();
|
return moonarch_wallpaper.to_path_buf();
|
||||||
}
|
}
|
||||||
|
|
||||||
// GResource fallback path (loaded from compiled resources at runtime)
|
// GResource fallback path (loaded from compiled resources at runtime)
|
||||||
|
log::debug!("Wallpaper: using GResource fallback");
|
||||||
PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg"))
|
PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +120,7 @@ mod tests {
|
|||||||
fn default_config_has_none_fields() {
|
fn default_config_has_none_fields() {
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
assert!(config.background_path.is_none());
|
assert!(config.background_path.is_none());
|
||||||
|
assert!(config.background_blur.is_none());
|
||||||
assert!(config.gtk_theme.is_none());
|
assert!(config.gtk_theme.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +138,7 @@ mod tests {
|
|||||||
let conf = dir.path().join("moongreet.toml");
|
let conf = dir.path().join("moongreet.toml");
|
||||||
fs::write(
|
fs::write(
|
||||||
&conf,
|
&conf,
|
||||||
"[appearance]\nbackground = \"/custom/wallpaper.jpg\"\ngtk-theme = \"catppuccin\"\n",
|
"[appearance]\nbackground = \"/custom/wallpaper.jpg\"\nbackground-blur = 20.0\ngtk-theme = \"catppuccin\"\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let paths = vec![conf];
|
let paths = vec![conf];
|
||||||
@@ -124,9 +147,20 @@ mod tests {
|
|||||||
config.background_path.as_deref(),
|
config.background_path.as_deref(),
|
||||||
Some("/custom/wallpaper.jpg")
|
Some("/custom/wallpaper.jpg")
|
||||||
);
|
);
|
||||||
|
assert_eq!(config.background_blur, Some(20.0));
|
||||||
assert_eq!(config.gtk_theme.as_deref(), Some("catppuccin"));
|
assert_eq!(config.gtk_theme.as_deref(), Some("catppuccin"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_blur_optional() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let conf = dir.path().join("moongreet.toml");
|
||||||
|
fs::write(&conf, "[appearance]\nbackground = \"/bg.jpg\"\n").unwrap();
|
||||||
|
let paths = vec![conf];
|
||||||
|
let config = load_config(Some(&paths));
|
||||||
|
assert!(config.background_blur.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_config_resolves_relative_background() {
|
fn load_config_resolves_relative_background() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -180,7 +214,7 @@ mod tests {
|
|||||||
fs::write(&wallpaper, "fake").unwrap();
|
fs::write(&wallpaper, "fake").unwrap();
|
||||||
let config = Config {
|
let config = Config {
|
||||||
background_path: Some(wallpaper.to_str().unwrap().to_string()),
|
background_path: Some(wallpaper.to_str().unwrap().to_string()),
|
||||||
gtk_theme: None,
|
..Config::default()
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_background_path_with(&config, Path::new("/nonexistent")),
|
resolve_background_path_with(&config, Path::new("/nonexistent")),
|
||||||
@@ -192,7 +226,7 @@ mod tests {
|
|||||||
fn resolve_ignores_config_path_when_file_missing() {
|
fn resolve_ignores_config_path_when_file_missing() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
background_path: Some("/nonexistent/wallpaper.jpg".to_string()),
|
background_path: Some("/nonexistent/wallpaper.jpg".to_string()),
|
||||||
gtk_theme: None,
|
..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.to_str().unwrap().contains("moongreet"));
|
||||||
|
|||||||
+318
-55
@@ -6,6 +6,7 @@ use gdk_pixbuf::Pixbuf;
|
|||||||
use glib::clone;
|
use glib::clone;
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use gtk4::{self as gtk, gio};
|
use gtk4::{self as gtk, gio};
|
||||||
|
use image::imageops;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
@@ -22,6 +23,7 @@ use crate::users::{self, User};
|
|||||||
|
|
||||||
const AVATAR_SIZE: i32 = 128;
|
const AVATAR_SIZE: i32 = 128;
|
||||||
const MAX_AVATAR_FILE_SIZE: u64 = 10 * 1024 * 1024;
|
const MAX_AVATAR_FILE_SIZE: u64 = 10 * 1024 * 1024;
|
||||||
|
const MAX_WALLPAPER_FILE_SIZE: u64 = 50 * 1024 * 1024;
|
||||||
const LAST_USER_PATH: &str = "/var/cache/moongreet/last-user";
|
const LAST_USER_PATH: &str = "/var/cache/moongreet/last-user";
|
||||||
const LAST_SESSION_DIR: &str = "/var/cache/moongreet/last-session";
|
const LAST_SESSION_DIR: &str = "/var/cache/moongreet/last-session";
|
||||||
const MAX_USERNAME_LENGTH: usize = 256;
|
const MAX_USERNAME_LENGTH: usize = 256;
|
||||||
@@ -94,9 +96,78 @@ fn is_valid_username(name: &str) -> bool {
|
|||||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load the background image as a shared texture (decode once, reuse everywhere).
|
||||||
|
/// When `blur_radius` is `Some(sigma)` with sigma > 0, a Gaussian blur is applied.
|
||||||
|
pub fn load_background_texture(bg_path: &Path, blur_radius: Option<f32>) -> Option<gdk::Texture> {
|
||||||
|
let path_str = bg_path.to_str()?;
|
||||||
|
let texture = if bg_path.starts_with("/dev/moonarch/moongreet") {
|
||||||
|
match gio::resources_lookup_data(path_str, gio::ResourceLookupFlags::NONE) {
|
||||||
|
Ok(bytes) => match gdk::Texture::from_bytes(&bytes) {
|
||||||
|
Ok(texture) => Some(texture),
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("GResource texture decode error: {e}");
|
||||||
|
log::warn!("Failed to decode background texture from GResource {path_str}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("GResource lookup error: {e}");
|
||||||
|
log::warn!("Failed to load background texture from GResource {path_str}");
|
||||||
|
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.
|
||||||
|
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)
|
||||||
|
.expect("pixel buffer size matches texture dimensions");
|
||||||
|
let blurred = imageops::blur(&image::DynamicImage::ImageRgba8(img), sigma);
|
||||||
|
|
||||||
|
let bytes = glib::Bytes::from(blurred.as_raw());
|
||||||
|
let mem_texture = gdk::MemoryTexture::new(
|
||||||
|
width as i32,
|
||||||
|
height as i32,
|
||||||
|
gdk::MemoryFormat::B8g8r8a8Premultiplied,
|
||||||
|
&bytes,
|
||||||
|
stride,
|
||||||
|
);
|
||||||
|
mem_texture.upcast()
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a wallpaper-only window for secondary monitors.
|
/// Create a wallpaper-only window for secondary monitors.
|
||||||
pub fn create_wallpaper_window(
|
pub fn create_wallpaper_window(
|
||||||
bg_path: &Path,
|
texture: &gdk::Texture,
|
||||||
app: >k::Application,
|
app: >k::Application,
|
||||||
) -> gtk::ApplicationWindow {
|
) -> gtk::ApplicationWindow {
|
||||||
let window = gtk::ApplicationWindow::builder()
|
let window = gtk::ApplicationWindow::builder()
|
||||||
@@ -104,19 +175,15 @@ pub fn create_wallpaper_window(
|
|||||||
.build();
|
.build();
|
||||||
window.add_css_class("wallpaper");
|
window.add_css_class("wallpaper");
|
||||||
|
|
||||||
let background = create_background_picture(bg_path);
|
let background = create_background_picture(texture);
|
||||||
window.set_child(Some(&background));
|
window.set_child(Some(&background));
|
||||||
|
|
||||||
window
|
window
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a Picture widget for the wallpaper background.
|
/// Create a Picture widget for the wallpaper background from a pre-loaded texture.
|
||||||
fn create_background_picture(bg_path: &Path) -> gtk::Picture {
|
fn create_background_picture(texture: &gdk::Texture) -> gtk::Picture {
|
||||||
let background = if bg_path.starts_with("/dev/moonarch/moongreet") {
|
let background = gtk::Picture::for_paintable(texture);
|
||||||
gtk::Picture::for_resource(bg_path.to_str().unwrap_or(""))
|
|
||||||
} else {
|
|
||||||
gtk::Picture::for_filename(bg_path.to_str().unwrap_or(""))
|
|
||||||
};
|
|
||||||
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);
|
||||||
@@ -135,7 +202,7 @@ struct GreeterState {
|
|||||||
|
|
||||||
/// Create the main greeter window with login UI.
|
/// Create the main greeter window with login UI.
|
||||||
pub fn create_greeter_window(
|
pub fn create_greeter_window(
|
||||||
bg_path: &Path,
|
texture: Option<&gdk::Texture>,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
app: >k::Application,
|
app: >k::Application,
|
||||||
) -> gtk::ApplicationWindow {
|
) -> gtk::ApplicationWindow {
|
||||||
@@ -147,14 +214,26 @@ pub fn create_greeter_window(
|
|||||||
|
|
||||||
// Apply GTK theme from config
|
// Apply GTK theme from config
|
||||||
if let Some(ref theme_name) = config.gtk_theme {
|
if let Some(ref theme_name) = config.gtk_theme {
|
||||||
if let Some(settings) = gtk::Settings::default() {
|
if !theme_name.is_empty()
|
||||||
settings.set_gtk_theme_name(Some(theme_name));
|
&& theme_name
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '+' | '.'))
|
||||||
|
{
|
||||||
|
if let Some(settings) = gtk::Settings::default() {
|
||||||
|
settings.set_gtk_theme_name(Some(theme_name));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("Ignoring invalid GTK theme name: {theme_name}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let strings = load_strings(None);
|
let strings = load_strings(None);
|
||||||
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());
|
||||||
|
if let Some(ref theme) = config.gtk_theme {
|
||||||
|
log::debug!("GTK theme: {theme}");
|
||||||
|
}
|
||||||
|
|
||||||
let state = Rc::new(RefCell::new(GreeterState {
|
let state = Rc::new(RefCell::new(GreeterState {
|
||||||
selected_user: None,
|
selected_user: None,
|
||||||
@@ -170,7 +249,9 @@ pub fn create_greeter_window(
|
|||||||
window.set_child(Some(&overlay));
|
window.set_child(Some(&overlay));
|
||||||
|
|
||||||
// Background wallpaper
|
// Background wallpaper
|
||||||
overlay.set_child(Some(&create_background_picture(bg_path)));
|
if let Some(texture) = texture {
|
||||||
|
overlay.set_child(Some(&create_background_picture(texture)));
|
||||||
|
}
|
||||||
|
|
||||||
// Main layout: 3 rows (top spacer, center login, bottom bar)
|
// Main layout: 3 rows (top spacer, center login, bottom bar)
|
||||||
let main_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
let main_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
@@ -481,6 +562,7 @@ fn select_initial_user(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|name| users.iter().find(|u| &u.username == name))
|
.and_then(|name| users.iter().find(|u| &u.username == name))
|
||||||
.unwrap_or(&users[0]);
|
.unwrap_or(&users[0]);
|
||||||
|
log::debug!("Initial user: {} (last_user={:?})", target.username, last_username);
|
||||||
|
|
||||||
switch_to_user(
|
switch_to_user(
|
||||||
target,
|
target,
|
||||||
@@ -507,6 +589,7 @@ fn switch_to_user(
|
|||||||
sessions: &[Session],
|
sessions: &[Session],
|
||||||
window: >k::ApplicationWindow,
|
window: >k::ApplicationWindow,
|
||||||
) {
|
) {
|
||||||
|
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());
|
||||||
@@ -523,8 +606,10 @@ fn switch_to_user(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(texture) = cached {
|
if let Some(texture) = cached {
|
||||||
|
log::debug!("Avatar cache hit for {}", user.username);
|
||||||
avatar_image.set_paintable(Some(&texture));
|
avatar_image.set_paintable(Some(&texture));
|
||||||
} else {
|
} else {
|
||||||
|
log::debug!("Avatar cache miss for {}", user.username);
|
||||||
let avatar_path = users::get_avatar_path(&user.username, &user.home);
|
let avatar_path = users::get_avatar_path(&user.username, &user.home);
|
||||||
if let Some(path) = avatar_path {
|
if let Some(path) = avatar_path {
|
||||||
// get_avatar_path already checks existence — go straight to loading
|
// get_avatar_path already checks existence — go straight to loading
|
||||||
@@ -550,6 +635,7 @@ fn set_avatar_from_file(
|
|||||||
// Reject oversized files
|
// Reject oversized files
|
||||||
if let Ok(meta) = std::fs::metadata(path) {
|
if let Ok(meta) = std::fs::metadata(path) {
|
||||||
if meta.len() > MAX_AVATAR_FILE_SIZE {
|
if meta.len() > MAX_AVATAR_FILE_SIZE {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -566,7 +652,8 @@ fn set_avatar_from_file(
|
|||||||
}
|
}
|
||||||
image.set_paintable(Some(&texture));
|
image.set_paintable(Some(&texture));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
|
log::debug!("Failed to load avatar {}: {e}", path.display());
|
||||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -582,10 +669,12 @@ fn set_default_avatar(
|
|||||||
{
|
{
|
||||||
let s = state.borrow();
|
let s = state.borrow();
|
||||||
if let Some(ref texture) = s.default_avatar_texture {
|
if let Some(ref texture) = s.default_avatar_texture {
|
||||||
|
log::debug!("Default avatar: using cached texture");
|
||||||
image.set_paintable(Some(texture));
|
image.set_paintable(Some(texture));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log::debug!("Default avatar: tinting SVG from GResource");
|
||||||
|
|
||||||
let resource_path = users::get_default_avatar_path();
|
let resource_path = users::get_default_avatar_path();
|
||||||
if let Ok(bytes) =
|
if let Ok(bytes) =
|
||||||
@@ -606,7 +695,9 @@ fn set_default_avatar(
|
|||||||
if let Ok(loader) = gdk_pixbuf::PixbufLoader::with_type("svg") {
|
if let Ok(loader) = gdk_pixbuf::PixbufLoader::with_type("svg") {
|
||||||
loader.set_size(AVATAR_SIZE, AVATAR_SIZE);
|
loader.set_size(AVATAR_SIZE, AVATAR_SIZE);
|
||||||
if loader.write(svg_bytes).is_ok() {
|
if loader.write(svg_bytes).is_ok() {
|
||||||
let _ = loader.close();
|
if let Err(e) = loader.close() {
|
||||||
|
log::warn!("Failed to close SVG loader: {e}");
|
||||||
|
}
|
||||||
if let Some(pixbuf) = loader.pixbuf() {
|
if let Some(pixbuf) = loader.pixbuf() {
|
||||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||||
state.borrow_mut().default_avatar_texture = Some(texture.clone());
|
state.borrow_mut().default_avatar_texture = Some(texture.clone());
|
||||||
@@ -629,8 +720,11 @@ fn get_selected_session(
|
|||||||
if sessions.is_empty() {
|
if sessions.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let idx = dropdown.selected() as usize;
|
let idx = dropdown.selected();
|
||||||
sessions.get(idx).cloned()
|
if idx == gtk::INVALID_LIST_POSITION {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
sessions.get(idx as usize).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pre-select the last used session for a user in the dropdown.
|
/// Pre-select the last used session for a user in the dropdown.
|
||||||
@@ -685,6 +779,7 @@ fn show_greetd_error(
|
|||||||
|
|
||||||
/// Cancel any in-progress greetd session.
|
/// Cancel any in-progress greetd session.
|
||||||
fn cancel_pending_session(state: &Rc<RefCell<GreeterState>>) {
|
fn cancel_pending_session(state: &Rc<RefCell<GreeterState>>) {
|
||||||
|
log::debug!("Cancelling pending greetd session");
|
||||||
let s = state.borrow();
|
let s = state.borrow();
|
||||||
s.login_cancelled
|
s.login_cancelled
|
||||||
.store(true, std::sync::atomic::Ordering::SeqCst);
|
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
@@ -718,6 +813,7 @@ fn attempt_login(
|
|||||||
password_entry: >k::PasswordEntry,
|
password_entry: >k::PasswordEntry,
|
||||||
session_dropdown: >k::DropDown,
|
session_dropdown: >k::DropDown,
|
||||||
) {
|
) {
|
||||||
|
log::debug!("Login attempt for user: {}", user.username);
|
||||||
let sock_path = match std::env::var("GREETD_SOCK") {
|
let sock_path = match std::env::var("GREETD_SOCK") {
|
||||||
Ok(p) if !p.is_empty() => p,
|
Ok(p) if !p.is_empty() => p,
|
||||||
_ => {
|
_ => {
|
||||||
@@ -727,6 +823,7 @@ fn attempt_login(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Validate socket path
|
// Validate socket path
|
||||||
|
log::debug!("GREETD_SOCK: {sock_path}");
|
||||||
let sock_pathbuf = PathBuf::from(&sock_path);
|
let sock_pathbuf = PathBuf::from(&sock_path);
|
||||||
if !sock_pathbuf.is_absolute() {
|
if !sock_pathbuf.is_absolute() {
|
||||||
show_error(
|
show_error(
|
||||||
@@ -795,6 +892,7 @@ fn attempt_login(
|
|||||||
&sock_path,
|
&sock_path,
|
||||||
&greetd_sock,
|
&greetd_sock,
|
||||||
&login_cancelled,
|
&login_cancelled,
|
||||||
|
strings,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -869,20 +967,28 @@ fn login_worker(
|
|||||||
sock_path: &str,
|
sock_path: &str,
|
||||||
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,
|
||||||
) -> 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");
|
||||||
return Ok(LoginResult::Cancelled);
|
return Ok(LoginResult::Cancelled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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())?;
|
||||||
sock.set_read_timeout(Some(std::time::Duration::from_secs(10)))
|
if let Err(e) = sock.set_read_timeout(Some(std::time::Duration::from_secs(10))) {
|
||||||
.ok();
|
log::warn!("Failed to set read timeout: {e}");
|
||||||
|
}
|
||||||
|
if let Err(e) = sock.set_write_timeout(Some(std::time::Duration::from_secs(10))) {
|
||||||
|
log::warn!("Failed to set write timeout: {e}");
|
||||||
|
}
|
||||||
{
|
{
|
||||||
let mut guard = greetd_sock.lock().map_err(|e| e.to_string())?;
|
let mut guard = greetd_sock.lock().map_err(|e| e.to_string())?;
|
||||||
*guard = Some(sock.try_clone().map_err(|e| e.to_string())?);
|
*guard = Some(sock.try_clone().map_err(|e| e.to_string())?);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Create session — if a stale session exists, cancel it and retry
|
// Step 1: Create session — if a stale session exists, cancel it and retry
|
||||||
|
log::debug!("Creating greetd session for {username}");
|
||||||
let mut response = ipc::create_session(&mut sock, username).map_err(|e| e.to_string())?;
|
let mut response = ipc::create_session(&mut sock, username).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
@@ -890,24 +996,29 @@ fn login_worker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if response.get("type").and_then(|v| v.as_str()) == Some("error") {
|
if response.get("type").and_then(|v| v.as_str()) == Some("error") {
|
||||||
|
log::debug!("Stale session detected, cancelling and retrying");
|
||||||
let _ = ipc::cancel_session(&mut sock);
|
let _ = ipc::cancel_session(&mut sock);
|
||||||
response = ipc::create_session(&mut sock, username).map_err(|e| e.to_string())?;
|
response = ipc::create_session(&mut sock, username).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);
|
||||||
}
|
}
|
||||||
if response.get("type").and_then(|v| v.as_str()) == Some("error") {
|
if response.get("type").and_then(|v| v.as_str()) == Some("error") {
|
||||||
return Ok(LoginResult::Error {
|
let description = response
|
||||||
message: response
|
.get("description")
|
||||||
.get("description")
|
.and_then(|v| v.as_str())
|
||||||
.and_then(|v| v.as_str())
|
.unwrap_or("");
|
||||||
.unwrap_or("Authentication failed")
|
let message = if !description.is_empty() && description.len() <= MAX_GREETD_ERROR_LENGTH {
|
||||||
.to_string(),
|
description.to_string()
|
||||||
});
|
} else {
|
||||||
|
strings.auth_failed.to_string()
|
||||||
|
};
|
||||||
|
return Ok(LoginResult::Error { message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Send password if auth message received
|
// Step 2: Send password if auth message received
|
||||||
if response.get("type").and_then(|v| v.as_str()) == Some("auth_message") {
|
if response.get("type").and_then(|v| v.as_str()) == Some("auth_message") {
|
||||||
|
log::debug!("Sending auth response for {username}");
|
||||||
response =
|
response =
|
||||||
ipc::post_auth_response(&mut sock, Some(password)).map_err(|e| e.to_string())?;
|
ipc::post_auth_response(&mut sock, Some(password)).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
@@ -927,29 +1038,31 @@ fn login_worker(
|
|||||||
// Multi-stage auth is not supported
|
// Multi-stage auth is not supported
|
||||||
let _ = ipc::cancel_session(&mut sock);
|
let _ = ipc::cancel_session(&mut sock);
|
||||||
return Ok(LoginResult::Error {
|
return Ok(LoginResult::Error {
|
||||||
message: "Multi-stage authentication is not supported".to_string(),
|
message: strings.multi_stage_unsupported.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Start session
|
// Step 3: Start session
|
||||||
if response.get("type").and_then(|v| v.as_str()) == Some("success") {
|
if response.get("type").and_then(|v| v.as_str()) == Some("success") {
|
||||||
|
log::debug!("Auth successful, starting session: {exec_cmd}");
|
||||||
let cmd = match split_shell_words(exec_cmd) {
|
let cmd = match split_shell_words(exec_cmd) {
|
||||||
Some(words) if !words.is_empty() => words,
|
Some(words) if !words.is_empty() => words,
|
||||||
_ => {
|
_ => {
|
||||||
let _ = ipc::cancel_session(&mut sock);
|
let _ = ipc::cancel_session(&mut sock);
|
||||||
return Ok(LoginResult::Error {
|
return Ok(LoginResult::Error {
|
||||||
message: "Invalid session command".to_string(),
|
message: strings.invalid_session_command.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate: first token must be an absolute path to an existing file
|
// Validate: reject obviously invalid commands (empty, null bytes, path traversal)
|
||||||
let binary = std::path::Path::new(&cmd[0]);
|
// greetd resolves PATH for relative commands like "niri-session"
|
||||||
if !binary.is_absolute() || !binary.is_file() {
|
let first = &cmd[0];
|
||||||
|
if first.is_empty() || first.contains('\0') || first.contains("..") {
|
||||||
let _ = ipc::cancel_session(&mut sock);
|
let _ = ipc::cancel_session(&mut sock);
|
||||||
return Ok(LoginResult::Error {
|
return Ok(LoginResult::Error {
|
||||||
message: "Invalid session command".to_string(),
|
message: strings.invalid_session_command.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,6 +1074,7 @@ fn login_worker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if response.get("type").and_then(|v| v.as_str()) == Some("success") {
|
if response.get("type").and_then(|v| v.as_str()) == Some("success") {
|
||||||
|
log::info!("Login successful for {username}");
|
||||||
return Ok(LoginResult::Success {
|
return Ok(LoginResult::Success {
|
||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
});
|
});
|
||||||
@@ -969,14 +1083,14 @@ fn login_worker(
|
|||||||
message: response
|
message: response
|
||||||
.get("description")
|
.get("description")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("Failed to start session")
|
.unwrap_or(strings.session_start_failed)
|
||||||
.to_string(),
|
.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(LoginResult::Error {
|
Ok(LoginResult::Error {
|
||||||
message: "Unexpected response from greetd".to_string(),
|
message: strings.unexpected_greetd_response.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1012,30 +1126,53 @@ fn execute_power_action(
|
|||||||
// -- Last user/session persistence --
|
// -- Last user/session persistence --
|
||||||
|
|
||||||
fn load_last_user() -> Option<String> {
|
fn load_last_user() -> Option<String> {
|
||||||
let content = std::fs::read_to_string(LAST_USER_PATH).ok()?;
|
load_last_user_from(Path::new(LAST_USER_PATH))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_last_user_from(path: &Path) -> Option<String> {
|
||||||
|
let content = std::fs::read_to_string(path).ok()?;
|
||||||
let username = content.trim();
|
let username = content.trim();
|
||||||
if is_valid_username(username) {
|
if is_valid_username(username) {
|
||||||
|
log::debug!("Loaded last user: {username}");
|
||||||
Some(username.to_string())
|
Some(username.to_string())
|
||||||
} else {
|
} else {
|
||||||
|
log::debug!("Invalid last user in {}", path.display());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_last_user(username: &str) {
|
fn save_last_user(username: &str) {
|
||||||
let path = Path::new(LAST_USER_PATH);
|
save_last_user_to(Path::new(LAST_USER_PATH), username);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_last_user_to(path: &Path, username: &str) {
|
||||||
|
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 _ = std::fs::create_dir_all(parent);
|
||||||
}
|
}
|
||||||
let _ = std::fs::write(path, username);
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
use std::io::Write;
|
||||||
|
let _ = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.mode(0o600)
|
||||||
|
.open(path)
|
||||||
|
.and_then(|mut f| f.write_all(username.as_bytes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_last_session(username: &str) -> Option<String> {
|
fn load_last_session(username: &str) -> Option<String> {
|
||||||
let path = Path::new(LAST_SESSION_DIR).join(username);
|
load_last_session_from(&Path::new(LAST_SESSION_DIR).join(username))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_last_session_from(path: &Path) -> Option<String> {
|
||||||
let content = std::fs::read_to_string(path).ok()?;
|
let content = std::fs::read_to_string(path).ok()?;
|
||||||
let name = content.trim();
|
let name = content.trim();
|
||||||
if is_valid_session_name(name) {
|
if is_valid_session_name(name) {
|
||||||
|
log::debug!("Loaded last session: {name}");
|
||||||
Some(name.to_string())
|
Some(name.to_string())
|
||||||
} else {
|
} else {
|
||||||
|
log::debug!("Invalid last session in {}", path.display());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1059,7 +1196,20 @@ fn save_last_session(username: &str, session_name: &str) {
|
|||||||
}
|
}
|
||||||
let dir = Path::new(LAST_SESSION_DIR);
|
let dir = Path::new(LAST_SESSION_DIR);
|
||||||
let _ = std::fs::create_dir_all(dir);
|
let _ = std::fs::create_dir_all(dir);
|
||||||
let _ = std::fs::write(dir.join(username), session_name);
|
save_last_session_to(&dir.join(username), session_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_last_session_to(path: &Path, session_name: &str) {
|
||||||
|
log::debug!("Saving last session: {session_name}");
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
use std::io::Write;
|
||||||
|
let _ = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.mode(0o600)
|
||||||
|
.open(path)
|
||||||
|
.and_then(|mut f| f.write_all(session_name.as_bytes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1103,26 +1253,71 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn last_user_persistence() {
|
fn last_user_roundtrip() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("last-user");
|
let path = dir.path().join("last-user");
|
||||||
std::fs::write(&path, "alice").unwrap();
|
save_last_user_to(&path, "alice");
|
||||||
|
let loaded = load_last_user_from(&path);
|
||||||
let content = std::fs::read_to_string(&path).unwrap();
|
assert_eq!(loaded, Some("alice".to_string()));
|
||||||
let username = content.trim();
|
|
||||||
assert!(is_valid_username(username));
|
|
||||||
assert_eq!(username, "alice");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn last_session_persistence() {
|
fn last_user_rejects_invalid() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let session_dir = dir.path().join("last-session");
|
let path = dir.path().join("last-user");
|
||||||
std::fs::create_dir_all(&session_dir).unwrap();
|
save_last_user_to(&path, "../evil");
|
||||||
std::fs::write(session_dir.join("alice"), "Niri").unwrap();
|
let loaded = load_last_user_from(&path);
|
||||||
|
assert_eq!(loaded, None);
|
||||||
|
}
|
||||||
|
|
||||||
let content = std::fs::read_to_string(session_dir.join("alice")).unwrap();
|
#[test]
|
||||||
assert_eq!(content.trim(), "Niri");
|
fn last_user_missing_file() {
|
||||||
|
let loaded = load_last_user_from(Path::new("/nonexistent/last-user"));
|
||||||
|
assert_eq!(loaded, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn last_session_roundtrip() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("alice");
|
||||||
|
save_last_session_to(&path, "Niri");
|
||||||
|
let loaded = load_last_session_from(&path);
|
||||||
|
assert_eq!(loaded, Some("Niri".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn last_session_rejects_invalid() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("alice");
|
||||||
|
save_last_session_to(&path, "../../../etc/evil");
|
||||||
|
let loaded = load_last_session_from(&path);
|
||||||
|
assert_eq!(loaded, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn last_session_missing_file() {
|
||||||
|
let loaded = load_last_session_from(Path::new("/nonexistent/session"));
|
||||||
|
assert_eq!(loaded, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn last_user_file_permissions() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("last-user");
|
||||||
|
save_last_user_to(&path, "alice");
|
||||||
|
let meta = std::fs::metadata(&path).unwrap();
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
assert_eq!(meta.permissions().mode() & 0o777, 0o600);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn last_session_file_permissions() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("session");
|
||||||
|
save_last_session_to(&path, "Niri");
|
||||||
|
let meta = std::fs::metadata(&path).unwrap();
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
assert_eq!(meta.permissions().mode() & 0o777, 0o600);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- split_shell_words tests --
|
// -- split_shell_words tests --
|
||||||
@@ -1259,6 +1454,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")),
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@@ -1300,6 +1496,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")),
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@@ -1334,6 +1531,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "/usr/bin/niri",
|
"alice", "pass", "/usr/bin/niri",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
|
load_strings(Some("en")),
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@@ -1370,6 +1568,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")),
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@@ -1384,6 +1583,7 @@ 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")),
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@@ -1405,19 +1605,82 @@ mod tests {
|
|||||||
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();
|
||||||
|
|
||||||
// cancel_session (from invalid exec_cmd path)
|
// cancel_session (from invalid exec_cmd with path traversal)
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Non-absolute exec_cmd
|
// exec_cmd with path traversal
|
||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "relative-binary",
|
"alice", "pass", "../../../etc/evil",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
|
load_strings(Some("en")),
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
assert!(matches!(result, LoginResult::Error { .. }));
|
assert!(matches!(result, LoginResult::Error { .. }));
|
||||||
handle.join().unwrap();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn login_worker_relative_exec_cmd_allowed() {
|
||||||
|
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": "secret",
|
||||||
|
"auth_message": "Password: ",
|
||||||
|
})).unwrap();
|
||||||
|
|
||||||
|
// post_auth_response → success
|
||||||
|
let _msg = ipc::recv_message(stream).unwrap();
|
||||||
|
ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap();
|
||||||
|
|
||||||
|
// start_session with relative command (e.g. niri-session)
|
||||||
|
let _msg = ipc::recv_message(stream).unwrap();
|
||||||
|
ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Relative exec_cmd like "niri-session" should be allowed
|
||||||
|
let result = login_worker(
|
||||||
|
"alice", "pass", "niri-session",
|
||||||
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
|
load_strings(Some("en")),
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = result.unwrap();
|
||||||
|
assert!(matches!(result, LoginResult::Success { .. }));
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- load_background_texture tests --
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_background_texture_missing_file_returns_none() {
|
||||||
|
let result = load_background_texture(Path::new("/nonexistent/wallpaper.jpg"), None);
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_background_texture_oversized_file_returns_none() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("huge.jpg");
|
||||||
|
// Create a sparse file that exceeds MAX_WALLPAPER_FILE_SIZE
|
||||||
|
let f = std::fs::File::create(&path).unwrap();
|
||||||
|
f.set_len(MAX_WALLPAPER_FILE_SIZE + 1).unwrap();
|
||||||
|
let result = load_background_texture(&path, None);
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_background_texture_non_utf8_path_returns_none() {
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
// 0xFF is not valid UTF-8
|
||||||
|
let non_utf8 = OsStr::from_bytes(&[0xff, 0xfe, 0xfd]);
|
||||||
|
let path = Path::new(non_utf8);
|
||||||
|
let result = load_background_texture(path, None);
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -30,6 +30,7 @@ pub struct Strings {
|
|||||||
pub shutdown_failed: &'static str,
|
pub shutdown_failed: &'static str,
|
||||||
pub connection_error: &'static str,
|
pub connection_error: &'static str,
|
||||||
pub socket_error: &'static str,
|
pub socket_error: &'static str,
|
||||||
|
pub unexpected_greetd_response: &'static str,
|
||||||
|
|
||||||
// Templates (use .replace("{n}", &count.to_string()))
|
// Templates (use .replace("{n}", &count.to_string()))
|
||||||
pub faillock_attempts_remaining: &'static str,
|
pub faillock_attempts_remaining: &'static str,
|
||||||
@@ -54,6 +55,7 @@ const STRINGS_DE: Strings = Strings {
|
|||||||
shutdown_failed: "Herunterfahren fehlgeschlagen",
|
shutdown_failed: "Herunterfahren fehlgeschlagen",
|
||||||
connection_error: "Verbindungsfehler",
|
connection_error: "Verbindungsfehler",
|
||||||
socket_error: "Socket-Fehler",
|
socket_error: "Socket-Fehler",
|
||||||
|
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!",
|
||||||
faillock_locked: "Konto ist möglicherweise gesperrt",
|
faillock_locked: "Konto ist möglicherweise gesperrt",
|
||||||
};
|
};
|
||||||
@@ -76,6 +78,7 @@ const STRINGS_EN: Strings = Strings {
|
|||||||
shutdown_failed: "Shutdown failed",
|
shutdown_failed: "Shutdown failed",
|
||||||
connection_error: "Connection error",
|
connection_error: "Connection error",
|
||||||
socket_error: "Socket error",
|
socket_error: "Socket error",
|
||||||
|
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!",
|
||||||
faillock_locked: "Account may be locked",
|
faillock_locked: "Account may be locked",
|
||||||
};
|
};
|
||||||
@@ -124,10 +127,15 @@ pub fn detect_locale() -> String {
|
|||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.or_else(|| read_lang_from_conf(Path::new(DEFAULT_LOCALE_CONF)));
|
.or_else(|| read_lang_from_conf(Path::new(DEFAULT_LOCALE_CONF)));
|
||||||
|
|
||||||
match lang {
|
let result = match lang {
|
||||||
Some(l) => parse_lang_prefix(&l),
|
Some(ref l) => parse_lang_prefix(l),
|
||||||
None => "en".to_string(),
|
None => "en".to_string(),
|
||||||
}
|
};
|
||||||
|
log::debug!("Detected locale: {result} (source: {})", match lang {
|
||||||
|
Some(_) => "LANG env or locale.conf",
|
||||||
|
None => "default",
|
||||||
|
});
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the string table for the given locale, defaulting to English.
|
/// Return the string table for the given locale, defaulting to English.
|
||||||
@@ -281,6 +289,7 @@ mod tests {
|
|||||||
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");
|
||||||
assert!(!s.faillock_locked.is_empty(), "{locale}: faillock_locked");
|
assert!(!s.faillock_locked.is_empty(), "{locale}: faillock_locked");
|
||||||
|
assert!(!s.unexpected_greetd_response.is_empty(), "{locale}: unexpected_greetd_response");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -75,7 +75,6 @@ fn recv_payload(stream: &mut UnixStream, n: usize) -> Result<Vec<u8>, IpcError>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send a length-prefixed JSON message to the greetd socket.
|
/// Send a length-prefixed JSON message to the greetd socket.
|
||||||
/// Header and payload are sent in a single write for atomicity.
|
|
||||||
pub fn send_message(
|
pub fn send_message(
|
||||||
stream: &mut UnixStream,
|
stream: &mut UnixStream,
|
||||||
msg: &serde_json::Value,
|
msg: &serde_json::Value,
|
||||||
@@ -85,11 +84,12 @@ pub fn send_message(
|
|||||||
return Err(IpcError::PayloadTooLarge(payload.len()));
|
return Err(IpcError::PayloadTooLarge(payload.len()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let msg_type = msg.get("type").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||||
|
log::debug!("IPC send: type={msg_type}, size={} bytes", payload.len());
|
||||||
|
|
||||||
let header = (payload.len() as u32).to_le_bytes();
|
let header = (payload.len() as u32).to_le_bytes();
|
||||||
let mut buf = Vec::with_capacity(4 + payload.len());
|
stream.write_all(&header)?;
|
||||||
buf.extend_from_slice(&header);
|
stream.write_all(&payload)?;
|
||||||
buf.extend_from_slice(&payload);
|
|
||||||
stream.write_all(&buf)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +104,8 @@ pub fn recv_message(stream: &mut UnixStream) -> Result<serde_json::Value, IpcErr
|
|||||||
|
|
||||||
let payload = recv_payload(stream, length)?;
|
let payload = recv_payload(stream, length)?;
|
||||||
let value: serde_json::Value = serde_json::from_slice(&payload)?;
|
let value: serde_json::Value = serde_json::from_slice(&payload)?;
|
||||||
|
let msg_type = value.get("type").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||||
|
log::debug!("IPC recv: type={msg_type}, size={length} bytes");
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-26
@@ -13,8 +13,6 @@ use gdk4 as gdk;
|
|||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use gtk4::{self as gtk, gio};
|
use gtk4::{self as gtk, gio};
|
||||||
use gtk4_layer_shell::LayerShell;
|
use gtk4_layer_shell::LayerShell;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
fn load_css(display: &gdk::Display) {
|
fn load_css(display: &gdk::Display) {
|
||||||
let css_provider = gtk::CssProvider::new();
|
let css_provider = gtk::CssProvider::new();
|
||||||
css_provider.load_from_resource("/dev/moonarch/moongreet/style.css");
|
css_provider.load_from_resource("/dev/moonarch/moongreet/style.css");
|
||||||
@@ -25,9 +23,9 @@ fn load_css(display: &gdk::Display) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_layer_shell(window: >k::ApplicationWindow, keyboard: bool) {
|
fn setup_layer_shell(window: >k::ApplicationWindow, keyboard: bool, layer: gtk4_layer_shell::Layer) {
|
||||||
window.init_layer_shell();
|
window.init_layer_shell();
|
||||||
window.set_layer(gtk4_layer_shell::Layer::Top);
|
window.set_layer(layer);
|
||||||
window.set_exclusive_zone(-1);
|
window.set_exclusive_zone(-1);
|
||||||
if keyboard {
|
if keyboard {
|
||||||
window.set_keyboard_mode(gtk4_layer_shell::KeyboardMode::Exclusive);
|
window.set_keyboard_mode(gtk4_layer_shell::KeyboardMode::Exclusive);
|
||||||
@@ -48,31 +46,43 @@ fn activate(app: >k::Application) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
log::debug!("Display: {:?}", display);
|
||||||
load_css(&display);
|
load_css(&display);
|
||||||
|
|
||||||
// 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_path = config::resolve_background_path(&config);
|
||||||
|
log::debug!("Background path: {}", bg_path.display());
|
||||||
|
|
||||||
|
// Load background texture once — shared across all windows
|
||||||
|
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}");
|
||||||
|
|
||||||
// Main greeter window (login UI) — compositor picks focused monitor
|
// Main greeter window (login UI) — compositor picks focused monitor
|
||||||
let greeter_window = greeter::create_greeter_window(&bg_path, &config, app);
|
let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, app);
|
||||||
if use_layer_shell {
|
if use_layer_shell {
|
||||||
setup_layer_shell(&greeter_window, true);
|
setup_layer_shell(&greeter_window, true, gtk4_layer_shell::Layer::Top);
|
||||||
}
|
}
|
||||||
greeter_window.present();
|
greeter_window.present();
|
||||||
|
|
||||||
// Wallpaper-only windows on all monitors (only with layer shell)
|
// Wallpaper-only windows on all monitors (only with layer shell)
|
||||||
if use_layer_shell {
|
if use_layer_shell
|
||||||
|
&& let Some(ref texture) = bg_texture
|
||||||
|
{
|
||||||
let monitors = display.monitors();
|
let monitors = display.monitors();
|
||||||
|
log::debug!("Monitor count: {}", monitors.n_items());
|
||||||
for i in 0..monitors.n_items() {
|
for i in 0..monitors.n_items() {
|
||||||
if let Some(monitor) = monitors
|
if let Some(monitor) = monitors
|
||||||
.item(i)
|
.item(i)
|
||||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
||||||
{
|
{
|
||||||
let wallpaper = greeter::create_wallpaper_window(&bg_path, app);
|
let wallpaper = greeter::create_wallpaper_window(texture, app);
|
||||||
setup_layer_shell(&wallpaper, false);
|
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom);
|
||||||
wallpaper.set_monitor(Some(&monitor));
|
wallpaper.set_monitor(Some(&monitor));
|
||||||
wallpaper.present();
|
wallpaper.present();
|
||||||
}
|
}
|
||||||
@@ -81,23 +91,16 @@ fn activate(app: >k::Application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn setup_logging() {
|
fn setup_logging() {
|
||||||
let mut builder = env_logger::Builder::from_default_env();
|
systemd_journal_logger::JournalLog::new()
|
||||||
builder.filter_level(log::LevelFilter::Info);
|
.unwrap()
|
||||||
|
.install()
|
||||||
// Try file logging to /var/cache/moongreet/ — fall back to stderr
|
.unwrap();
|
||||||
let log_dir = PathBuf::from("/var/cache/moongreet");
|
let level = if std::env::var("MOONGREET_DEBUG").is_ok() {
|
||||||
if log_dir.is_dir() {
|
log::LevelFilter::Debug
|
||||||
let log_file = log_dir.join("moongreet.log");
|
} else {
|
||||||
if let Ok(file) = std::fs::OpenOptions::new()
|
log::LevelFilter::Info
|
||||||
.create(true)
|
};
|
||||||
.append(true)
|
log::set_max_level(level);
|
||||||
.open(&log_file)
|
|
||||||
{
|
|
||||||
builder.target(env_logger::Target::Pipe(Box::new(file)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
+4
-10
@@ -7,7 +7,6 @@ use std::process::Command;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PowerError {
|
pub enum PowerError {
|
||||||
CommandFailed { action: &'static str, message: String },
|
CommandFailed { action: &'static str, message: String },
|
||||||
Timeout { action: &'static str },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for PowerError {
|
impl fmt::Display for PowerError {
|
||||||
@@ -16,9 +15,6 @@ impl fmt::Display for PowerError {
|
|||||||
PowerError::CommandFailed { action, message } => {
|
PowerError::CommandFailed { action, message } => {
|
||||||
write!(f, "{action} failed: {message}")
|
write!(f, "{action} failed: {message}")
|
||||||
}
|
}
|
||||||
PowerError::Timeout { action } => {
|
|
||||||
write!(f, "{action} timed out")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +23,7 @@ impl std::error::Error for PowerError {}
|
|||||||
|
|
||||||
/// Run a command and return a PowerError on failure.
|
/// Run a command and return a PowerError on failure.
|
||||||
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
|
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
|
||||||
|
log::debug!("Power action: {action} ({program} {args:?})");
|
||||||
let child = Command::new(program)
|
let child = Command::new(program)
|
||||||
.args(args)
|
.args(args)
|
||||||
.spawn()
|
.spawn()
|
||||||
@@ -42,6 +39,9 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
|
|||||||
message: e.to_string(),
|
message: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
log::debug!("Power action {action} completed successfully");
|
||||||
|
}
|
||||||
if !output.status.success() {
|
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 {
|
||||||
@@ -76,12 +76,6 @@ mod tests {
|
|||||||
assert_eq!(err.to_string(), "reboot failed: No such file or directory");
|
assert_eq!(err.to_string(), "reboot failed: No such file or directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn power_error_timeout_display() {
|
|
||||||
let err = PowerError::Timeout { action: "shutdown" };
|
|
||||||
assert_eq!(err.to_string(), "shutdown timed out");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_command_returns_error_for_missing_binary() {
|
fn run_command_returns_error_for_missing_binary() {
|
||||||
let result = run_command("test", "nonexistent-binary-xyz", &[]);
|
let result = run_command("test", "nonexistent-binary-xyz", &[]);
|
||||||
|
|||||||
+16
-3
@@ -46,8 +46,17 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = name.filter(|s| !s.is_empty())?;
|
let name = name.filter(|s| !s.is_empty());
|
||||||
let exec_cmd = exec_cmd.filter(|s| !s.is_empty())?;
|
let exec_cmd = exec_cmd.filter(|s| !s.is_empty());
|
||||||
|
|
||||||
|
if name.is_none() || exec_cmd.is_none() {
|
||||||
|
log::debug!("Skipping {}: missing Name={} Exec={}", path.display(),
|
||||||
|
name.is_some(), exec_cmd.is_some());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = name?;
|
||||||
|
let exec_cmd = exec_cmd?;
|
||||||
|
|
||||||
Some(Session {
|
Some(Session {
|
||||||
name,
|
name,
|
||||||
@@ -74,7 +83,10 @@ pub fn get_sessions(
|
|||||||
for (dirs, session_type) in [(wayland, "wayland"), (xsession, "x11")] {
|
for (dirs, session_type) in [(wayland, "wayland"), (xsession, "x11")] {
|
||||||
for directory in dirs {
|
for directory in dirs {
|
||||||
let entries = match fs::read_dir(directory) {
|
let entries = match fs::read_dir(directory) {
|
||||||
Ok(e) => e,
|
Ok(e) => {
|
||||||
|
log::debug!("Scanning session directory: {}", directory.display());
|
||||||
|
e
|
||||||
|
}
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,6 +105,7 @@ pub fn get_sessions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("Found {} session(s)", sessions.len());
|
||||||
sessions
|
sessions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-5
@@ -46,7 +46,10 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec<User> {
|
|||||||
|
|
||||||
let content = match fs::read_to_string(path) {
|
let content = match fs::read_to_string(path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return Vec::new(),
|
Err(e) => {
|
||||||
|
log::warn!("Failed to read passwd file {}: {e}", path.display());
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut users = Vec::new();
|
let mut users = Vec::new();
|
||||||
@@ -88,6 +91,7 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec<User> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("Found {} login user(s)", users.len());
|
||||||
users
|
users
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,17 +110,28 @@ pub fn get_avatar_path_with(
|
|||||||
// AccountsService icon takes priority
|
// AccountsService icon takes priority
|
||||||
if accountsservice_dir.exists() {
|
if accountsservice_dir.exists() {
|
||||||
let icon = accountsservice_dir.join(username);
|
let icon = accountsservice_dir.join(username);
|
||||||
if icon.exists() && !icon.is_symlink() {
|
if let Ok(meta) = icon.symlink_metadata() {
|
||||||
return Some(icon);
|
if meta.file_type().is_symlink() {
|
||||||
|
log::warn!("Rejecting symlink avatar for {username}: {}", icon.display());
|
||||||
|
} else {
|
||||||
|
log::debug!("Avatar for {username}: AccountsService {}", icon.display());
|
||||||
|
return Some(icon);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ~/.face fallback
|
// ~/.face fallback
|
||||||
let face = home.join(".face");
|
let face = home.join(".face");
|
||||||
if face.exists() && !face.is_symlink() {
|
if let Ok(meta) = face.symlink_metadata() {
|
||||||
return Some(face);
|
if meta.file_type().is_symlink() {
|
||||||
|
log::warn!("Rejecting symlink avatar for {username}: {}", face.display());
|
||||||
|
} else {
|
||||||
|
log::debug!("Avatar for {username}: ~/.face {}", face.display());
|
||||||
|
return Some(face);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("No avatar found for {username}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user