Compare commits

...

10 Commits

Author SHA1 Message Date
nevaforget 63d6c656bd feat: apply cursor theme via GtkSettings (v0.9.0)
Update PKGBUILD version / update-pkgver (push) Successful in 4s
GTK4 under greetd does not honour XCURSOR_THEME reliably, so the greeter
showed the wrong cursor over GTK widgets on some machines. Set
gtk-cursor-theme-name / -size on GtkSettings from new [appearance] config
fields (cursor-theme, cursor-size), mirroring the gtk-theme handling and
reusing is_valid_gtk_theme validation.

Salvaged from commit 29ce185 — tagged v0.9.0 on 2026-04-24 but never
pushed. The orphan branch's keyboard refactor (v0.10.0) is discarded,
superseded by the v0.8.7 single-window fix.
2026-06-02 12:58:59 +02:00
nevaforget 41228605ad fix: power buttons via systemctl, single greeter window (v0.8.7)
Update PKGBUILD version / update-pkgver (push) Successful in 6s
Reboot/shutdown buttons always failed: power.rs called `loginctl
reboot|poweroff`, but loginctl has no such verbs (systemd 260) — those
belong to systemctl. moonlock/moonset already used systemctl; moongreet
was the outlier. Switch to `systemctl --no-ask-password reboot|poweroff`.

The multi-monitor greeter gave Exclusive keyboard only to the first
monitor's window, so a user focused on any other output could not type
the password. Drop the per-monitor loop + hotplug; create one window on
the focused output (no set_monitor) with Exclusive keyboard.

Polkit rule kept as a harmless safety net (it was never the blocker;
CanReboot returns yes). The missing journal errors were not a logging
bug — they were lost to a hard power-cut before journald synced.
2026-06-02 12:46:13 +02:00
nevaforget b9b6f50974 fix: audit LOW fixes — stdout null, utf-8 path, debug value, hidden sessions (v0.8.6)
Update PKGBUILD version / update-pkgver (push) Successful in 2s
- power::run_command: .stdout(Stdio::null()) — the pipe was never drained,
  structurally fragile even if no current caller hits it.
- config: replace to_string_lossy() on relative wallpaper paths with
  to_str() + log::warn, so non-UTF-8 paths are dropped cleanly instead
  of being mangled into unopenable U+FFFD strings.
- main: require MOONGREET_DEBUG=1 to raise verbosity. Mere presence of
  the var must not leak socket paths, usernames, and auth round counts
  into the journal.
- sessions: parse Hidden= and NoDisplay= keys, skip entries marked true.
  Keeps disabled or stub .desktop files out of the session dropdown.
2026-04-24 14:08:35 +02:00
nevaforget 3a1af6471f fix: audit MEDIUM fixes — FP race, async avatar, symlink, FD leak (v0.8.5)
- i18n test: assert four previously-missing string fields so future locales
  cannot ship empty strings unnoticed.
- greeter: atomic check-and-set fingerprint_probe_initializing to keep a
  fast user switch from spawning two parallel fprintd D-Bus inits.
- greeter: set_avatar_from_file decodes via gio::File::read_future +
  Pixbuf::from_stream_at_scale_future inside glib::spawn_future_local;
  shows default icon first, swaps on completion.
- greeter: cap MAX_WALLPAPER_FILE_SIZE at 10 MB and MAX_AVATAR_FILE_SIZE
  at 5 MB to bound worst-case decode latency.
- config: apply the same symlink-rejection check to the Moonarch
  wallpaper fallback that the user-configured path already uses.
- greeter: after login_worker returns, drop the cloned greetd socket
  held in shared state so repeated failed logins do not leak FDs.
2026-04-24 13:26:52 +02:00
nevaforget 35f1a17cdf fix: audit fix — reduce password copies in memory (v0.8.4)
- attempt_login takes Zeroizing<String> by value, eliminating the redundant
  Zeroizing::new(password.to_string()) that doubled the Rust-owned copy.
- Clear password_entry's internal buffer immediately after extracting the
  password, shortening the window during which the GTK GString persists in
  non-zeroizable libc memory.
2026-04-24 12:52:59 +02:00
nevaforget 48d363bb18 fix: ship polkit rule so greeter user can reboot/power off (v0.8.3)
Update PKGBUILD version / update-pkgver (push) Successful in 3s
The rule that grants the greeter user authorization for
org.freedesktop.login1.{reboot,power-off} lived only in the moonarch
repo and was never installed by any PKGBUILD. On a fresh install the
reboot/shutdown buttons silently failed because greetd's greeter
session is inactive in logind and polkit denies inactive sessions by
default.

Move the rule into the moongreet source tree where it belongs and
ship it via moongreet-git.
2026-04-21 09:11:59 +02:00
nevaforget 448e4212e3 docs: drop Selene persona, unify attribution on ClaudeCode
Remove the Selene persona block from CLAUDE.md and rewrite prior
DECISIONS entries from Selene to ClaudeCode for consistency with
the rest of the Moonarch ecosystem.
2026-04-21 09:03:21 +02:00
nevaforget cd42df1095 fix: handle monitor hotplug for greeter windows (v0.8.2)
Update PKGBUILD version / update-pkgver (push) Successful in 2s
Greeter windows were only created at startup. Hotplugged monitors (e.g.
HDMI reconnect) would show no UI. Connect to the monitor ListModel's
items-changed signal to create greeter windows for newly added monitors.

Aligned with moonlock's hotplug fix using the same pattern adapted for
gtk4-layer-shell (ListModel) instead of session-lock (connect_monitor).
2026-04-09 15:06:29 +02:00
nevaforget f7e258d402 feat: show greeter UI on all monitors, not just one (v0.8.0)
Update PKGBUILD version / update-pkgver (push) Successful in 3s
Wayland surfaces belong to exactly one output — mirroring is not an option.
Create one full greeter window per monitor via set_monitor(), with only the
first receiving KeyboardMode::Exclusive. Removes the old wallpaper-only
secondary windows. Matches moonlock's per-monitor pattern.
2026-04-08 08:48:04 +02:00
nevaforget de97d6658e fix: grab keyboard focus on map instead of realize (v0.7.4)
Update PKGBUILD version / update-pkgver (push) Successful in 3s
Layer-shell keyboard grab is only confirmed by the compositor at map
time. The previous realize-time grab_focus() could fire before the
compositor assigned keyboard input, causing intermittent input loss.
2026-04-06 22:29:37 +02:00
13 changed files with 325 additions and 103 deletions
+4 -5
View File
@@ -1,7 +1,5 @@
# Moongreet # Moongreet
**Name**: Selene (Mondgöttin — passend zu Moon-greet)
## Projekt ## Projekt
Moongreet ist ein greetd-Greeter für Wayland, gebaut mit Rust + gtk4-rs + gtk4-layer-shell. Moongreet ist ein greetd-Greeter für Wayland, gebaut mit Rust + gtk4-rs + gtk4-layer-shell.
@@ -42,12 +40,12 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
- `ipc.rs` — greetd Socket-Kommunikation (4-byte LE header + JSON) - `ipc.rs` — greetd Socket-Kommunikation (4-byte LE header + JSON)
- `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 systemctl (`--no-ask-password`)
- `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
- `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback - `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback
- `config.rs` — TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback + Blur-Validierung (finite, clamp 0200) - `config.rs` — TOML-Config ([appearance] background, gtk-theme, cursor-theme, cursor-size, fingerprint-enabled) + Wallpaper-Fallback + Blur-Validierung (finite, clamp 0200) + Cursor-Size-Validierung (range 1256)
- `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 (0o700 Dirs, 0o600 Files) - `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 (0o700 Dirs, 0o600 Files)
- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, systemd-journal-logger - `main.rs` — Entry Point, GTK App, Layer Shell Setup, ein Greeter-Fenster auf dem fokussierten Output (kein `set_monitor`), `KeyboardMode::Exclusive`, systemd-journal-logger
- `resources/style.css` — Catppuccin-inspiriertes Theme - `resources/style.css` — Catppuccin-inspiriertes Theme
## Design Decisions ## Design Decisions
@@ -62,6 +60,7 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
- **Symmetrie mit moonlock/moonset**: Gleiche Patterns (i18n, config, users, power, GResource, GPU-Blur) - **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
- **Cursor-Theme via GtkSettings**: GTK4 unter greetd liest `XCURSOR_THEME` env nicht zuverlässig — Cursor wird via `gtk::Settings::set_gtk_cursor_theme_name()` gesetzt, analog zu `gtk-theme`. Gleiche Validierung (`is_valid_gtk_theme`) gegen Path-Traversal.
- **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-Verzeichnisse 0o700 via `DirBuilder::mode()`, Cache-Dateien 0o600 - **File Permissions**: Cache-Verzeichnisse 0o700 via `DirBuilder::mode()`, 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
Generated
+1 -1
View File
@@ -575,7 +575,7 @@ dependencies = [
[[package]] [[package]]
name = "moongreet" name = "moongreet"
version = "0.7.1" version = "0.9.0"
dependencies = [ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "moongreet" name = "moongreet"
version = "0.7.3" version = "0.9.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"
+59 -3
View File
@@ -1,5 +1,61 @@
# Decisions # Decisions
## 2026-06-02 Cursor theme via GtkSettings, salvaged from unpushed work (v0.9.0)
- **Who**: ClaudeCode, Dom
- **Why**: On some machines the greeter showed the wrong (GTK-default) cursor. GTK4 under greetd does not honour `XCURSOR_THEME` reliably — niri renders its own pointer from the kdl `cursor` block, but GTK widgets (button hover, text-input I-beam) read `gtk-cursor-theme-name` on `GtkSettings`, which without a session settings.ini stays at the GTK default. This fix was written and tagged v0.9.0 on 2026-04-24 but never pushed — it sat in a local-only branch while the bug kept shipping. Salvaged onto main now (cherry-picked from commit 29ce185).
- **Tradeoffs**: Adds two `[appearance]` config fields (`cursor-theme`, `cursor-size`), symmetric with the existing `gtk-theme` field. Alternative — a system-wide `/etc/gtk-4.0/settings.ini` with `gtk-cursor-theme-name=` — would couple moongreet to host GTK config and affect every GTK4 app; rejected for the same reason as `gtk-theme`.
- **How**: `config.rs` gains `cursor_theme: Option<String>` and `cursor_size: Option<i32>` (range-validated 1256). `greeter::create_greeter_window` applies them via `gtk::Settings::set_gtk_cursor_theme_name()` / `set_gtk_cursor_theme_size()` after the existing gtk-theme handling, reusing `is_valid_gtk_theme()`. Deployed `moongreet.toml` gains `cursor-theme = "Sweet-cursors"` + `cursor-size = 24`. The orphaned April branch (v0.9.0/v0.10.0) is otherwise discarded; its keyboard refactor is superseded by the v0.8.7 single-window fix.
## 2026-06-02 Power buttons fixed (loginctl→systemctl) + single greeter window (v0.8.7)
- **Who**: ClaudeCode, Dom
- **Why**: At the greeter the reboot and shutdown buttons always failed with "Neustart/Herunterfahren fehlgeschlagen". Root cause: `power.rs` invoked `/usr/bin/loginctl reboot|poweroff`, but `loginctl` has no such verbs (systemd 260: `Unknown command verb 'reboot'`, exit 1) — power-management verbs belong to `systemctl`. moonlock and moonset already used `systemctl`; moongreet was the outlier (moonset carried the same bug until Mar 29). The polkit rule shipped in v0.8.3 treated the wrong layer — `CanReboot` returns `yes`, polkit was never the blocker. Separately, the multi-monitor greeter (v0.8.0/v0.8.2) gave `KeyboardMode::Exclusive` to only the first enumerated monitor's window, so on a multi-output setup the user could not type the password when focused on any other output.
- **Tradeoffs**: Dropping the per-monitor + hotplug windows leaves secondary monitors blank during login; irrelevant for a login screen (input happens on one output). Exclusive keyboard binds input to the single greeter surface regardless of pointer position — the mouse may wander to a blank output but typing always reaches the greeter (chosen over compositor-level pointer confinement). The polkit rule is kept as a harmless safety net for the agent-less greeter session; its misleading "session is inactive" comment was corrected.
- **How**: (1) `power::reboot`/`shutdown` call `/usr/bin/systemctl --no-ask-password reboot|poweroff` (matches moonlock; `--no-ask-password` fails fast instead of hanging on a missing askpass agent). (2) `main.rs` `activate()` creates one greeter window with no `set_monitor` (compositor places it on the focused output, like moonset) and `KeyboardMode::Exclusive`; the monitor loop, `connect_items_changed` hotplug handler, and the now-unused `glib::clone`/`std::rc::Rc` imports are removed. (3) The missing journal entries were investigated and are **not** a logging bug — the greeter user delivers all priorities to journald (verified live); the two button errors were lost because boot -2 was hard-cut before journald's 5-minute sync.
## 2026-04-24 Audit LOW fixes: stdout null, utf-8 path, debug value, hidden sessions (v0.8.6)
- **Who**: ClaudeCode, Dom
- **Why**: Four LOW findings cleared in a single pass. (1) `power::run_command` piped stdout it never read — structurally fragile even though current callers stay well under the pipe buffer. (2) Relative wallpaper paths were resolved via `to_string_lossy`, silently substituting `U+FFFD` for non-UTF-8 bytes and producing a path that cannot be opened. (3) `MOONGREET_DEBUG` escalated log verbosity on mere presence, so an empty variable leaked auth metadata into the journal. (4) `Hidden=true` and `NoDisplay=true` `.desktop` entries appeared in the session dropdown even though they mark disabled or stub sessions.
- **Tradeoffs**: Gating debug on the literal value `"1"` is slightly stricter than most tools but matches the security-first posture. Filtering Hidden/NoDisplay means legitimately hidden but functional sessions are now unselectable from the greeter — acceptable, that is the convention these keys signal.
- **How**: (1) `.stdout(Stdio::null())` replaces the unused pipe. (2) `to_string_lossy().to_string()` replaced by `to_str().map(|s| s.to_string())` with a `log::warn!` fallback for non-UTF-8 paths. (3) `match std::env::var("MOONGREET_DEBUG").ok().as_deref()``Some("1")` selects Debug, everything else Info. (4) `parse_desktop_file` reads `Hidden=` and `NoDisplay=`, returns `None` if either is `true`.
## 2026-04-24 Audit MEDIUM fixes: FP double-init, async avatar, symlink, FD leak (v0.8.5)
- **Who**: ClaudeCode, Dom
- **Why**: Six MEDIUM findings: (1) i18n test `all_string_fields_nonempty` missed four string fields — future locales could ship empty strings unnoticed. (2) Fast user-switch could spawn two parallel fprintd `init_async` calls because both coroutines saw `fingerprint_probe = None` before either stored its probe. (3) Synchronous avatar decode via `Pixbuf::from_file_at_scale` on the GTK main thread, stalling clicks. (4) Wallpaper `MAX_WALLPAPER_FILE_SIZE = 50 MB` bounded decode at up to ~2 s. (5) Fallback wallpaper path used `is_file()` which follows symlinks, inconsistent with the symlink-rejecting user-config path. (6) After a failed login the cloned `greetd_sock` descriptor remained in shared state until the next user switch, accumulating stale FDs across retries.
- **Tradeoffs**: The init-race guard uses a bool flag on `GreeterState` + a 25 ms polling yield — cheap and race-free, but introduces a very short latency when a second probe waits. Lowering `MAX_WALLPAPER_FILE_SIZE` to 10 MB and `MAX_AVATAR_FILE_SIZE` to 5 MB caps worst-case decode but rejects legitimately huge (4K raw) wallpapers; acceptable for a greeter. Async avatar decode shows the default icon for a frame or two on cache miss.
- **How**: (1) Four new `assert!` lines in `i18n::tests::all_string_fields_nonempty`. (2) New `fingerprint_probe_initializing: bool` on `GreeterState`, atomic check-and-set under `borrow_mut`, losing coroutines yield via `glib::timeout_future` until the winning init completes. (3) `set_avatar_from_file` uses `gio::File::read_future` + `Pixbuf::from_stream_at_scale_future` inside a `glib::spawn_future_local`, sets the default icon first, swaps on success. (4) Lower both size constants. (5) `resolve_background_path_with` now applies the same `symlink_metadata` + `!is_symlink` check to the Moonarch fallback. (6) After the login worker returns, `state.greetd_sock.lock().take()` drops the stale clone regardless of login outcome.
## 2026-04-24 Audit fix: shrink password-in-memory window (v0.8.4)
- **Who**: ClaudeCode, Dom
- **Why**: Security audit flagged the GTK password path as holding more copies of the plaintext password in memory than necessary. `attempt_login` wrapped the already-`Zeroizing<String>` caller value into a second `Zeroizing<String>` (`password.to_string()`), and the GTK `GString` backing `entry.text()` persisted in libc malloc'd memory until the allocator reused the page.
- **Tradeoffs**: The GTK `GString` and the libc `strdup` copy on the PAM FFI boundary remain non-zeroizable — this is an inherent GTK/libc limitation, already documented in CLAUDE.md. This change reduces the Rust-owned copies to one and clears the `PasswordEntry` text field immediately after extraction to shorten the GTK-side window.
- **How**: (1) `attempt_login` now takes `password: Zeroizing<String>` by value instead of `&str`, moving ownership into the `spawn_blocking` closure. (2) The redundant `Zeroizing::new(password.to_string())` inside `attempt_login` is removed. (3) `password_entry.set_text("")` is called right after the password is extracted from the activate handler, shortening the lifetime of the GTK-internal buffer.
## 2026-04-21 Ship polkit rule in moongreet instead of moonarch (v0.8.3)
- **Who**: ClaudeCode, Dom
- **Why**: Reboot/shutdown from the greeter silently failed on a fresh install. The polkit rule that grants the `greeter` user `org.freedesktop.login1.{reboot,power-off}` lived in the moonarch repo but was never installed by any PKGBUILD. The laptop worked only because the rule had been hand-deployed once.
- **Tradeoffs**: Rule ownership moves from moonarch (system defaults) to moongreet (greeter-specific auth). Cleaner boundary — moonarch no longer needs to know about the greeter's auth requirements — but it means moongreet is now responsible for a system polkit rule that ties it to a fixed username (`greeter`).
- **How**: Source file moved to `moongreet/config/polkit/50-moongreet-power.rules`, installed to `/etc/polkit-1/rules.d/` by `moongreet-git/PKGBUILD`. Old file removed from the moonarch repo.
## 2026-04-09 Monitor hotplug via ListModel items-changed
- **Who**: ClaudeCode, Dom
- **Why**: Greeter windows were only created at startup. If a monitor was hotplugged (e.g. HDMI reconnect), it would show no greeter UI. Aligned with moonlock's hotplug fix (same day).
- **Tradeoffs**: Hotplugged monitors get greeter windows without keyboard input (keyboard stays on the primary monitor). Acceptable — user can still interact on the primary screen.
- **How**: Connect to `display.monitors().connect_items_changed()` and create new greeter windows for added monitors. Shared state (config, texture, blur_cache) moved to Rc for the closure.
## 2026-04-08 Show greeter UI on all monitors instead of just one
- **Who**: ClaudeCode, Dom
- **Why**: moonlock showed its UI on all monitors via ext-session-lock-v1, but moongreet only showed the login UI on one monitor (compositor-picked) with wallpaper-only windows on the rest. Inconsistent UX across the ecosystem.
- **Tradeoffs**: Each monitor gets its own full greeter widget tree (slightly more memory), but the UI is lightweight. Screen mirroring (e.g., wl-mirror/screencopy) was considered and rejected — it requires an external process, compositor screencopy support, adds latency, and fights Wayland's per-output model. One-window-per-monitor is the established Wayland pattern (swaylock, hyprlock, moonlock all do this).
- **How**: Create one `create_greeter_window()` per monitor with `set_monitor()`, only the first gets `KeyboardMode::Exclusive`. Removed `create_wallpaper_window()` (no longer needed). No layer shell fallback keeps single-window mode for development.
## 2026-04-06 Restore explicit gtk-theme in moongreet config ## 2026-04-06 Restore explicit gtk-theme in moongreet config
- **Who**: ClaudeCode, Dom - **Who**: ClaudeCode, Dom
@@ -37,7 +93,7 @@
## 2026-03-28 Remove embedded wallpaper from binary ## 2026-03-28 Remove embedded wallpaper from binary
- **Who**: Selene, Dom - **Who**: ClaudeCode, 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. - **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. - **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. - **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.
@@ -51,13 +107,13 @@
## 2026-03-28 Optional background blur via `image` crate (superseded) ## 2026-03-28 Optional background blur via `image` crate (superseded)
- **Who**: Selene, Dom - **Who**: ClaudeCode, 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
- **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. - **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. - **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) ## 2026-03-28 Audit fixes for shared wallpaper texture (v0.4.1)
- **Who**: Selene, Dominik - **Who**: ClaudeCode, Dominik
- **Why**: Quality, performance, and security audits flagged issues in `load_background_texture()`, debug logging, and greetd error handling - **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 - **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()` - **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()`
+9 -1
View File
@@ -12,7 +12,7 @@ Part of the Moonarch ecosystem.
- **Last user/session** — Remembered in `/var/cache/moongreet/` - **Last user/session** — Remembered in `/var/cache/moongreet/`
- **Power actions** — Reboot / Shutdown via `loginctl` - **Power actions** — Reboot / Shutdown via `loginctl`
- **Layer Shell** — Fullscreen via gtk4-layer-shell (TOP layer) - **Layer Shell** — Fullscreen via gtk4-layer-shell (TOP layer)
- **Multi-monitor** — Greeter on primary, wallpaper on all monitors - **Multi-monitor + hotplug** — Full greeter UI on all monitors (keyboard input on first), hotplugged monitors get windows automatically
- **GPU blur** — Background blur via GskBlurNode (shared cache across monitors) - **GPU blur** — Background blur via GskBlurNode (shared cache across 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
@@ -60,6 +60,14 @@ sudo cp config/moongreet.toml /etc/moongreet/moongreet.toml
user = "greeter" user = "greeter"
``` ```
4. Install the polkit rule so the greeter user can reboot / power off:
```bash
sudo install -Dm644 config/polkit/50-moongreet-power.rules \
/etc/polkit-1/rules.d/50-moongreet-power.rules
```
Without this rule, `loginctl reboot` / `loginctl poweroff` fail because
greetd's greeter session is inactive in logind.
## Development ## Development
```bash ```bash
+5
View File
@@ -8,3 +8,8 @@ background = "/usr/share/backgrounds/wallpaper.jpg"
# GTK theme name — must match a directory in /usr/share/themes/ # GTK theme name — must match a directory in /usr/share/themes/
# Required because GTK4 under greetd does not reliably read settings.ini # Required because GTK4 under greetd does not reliably read settings.ini
gtk-theme = "Colloid-Grey-Dark-Catppuccin" gtk-theme = "Colloid-Grey-Dark-Catppuccin"
# Cursor theme name — must match a directory in /usr/share/icons/
# GTK4 under greetd does not honour XCURSOR_THEME, so set it here.
cursor-theme = "Sweet-cursors"
cursor-size = 24
+12
View File
@@ -0,0 +1,12 @@
// ABOUTME: Allow the greeter user to reboot and power off without authentication.
// ABOUTME: Safety net for the agent-less greeter session — no askpass/polkit agent to answer a challenge.
polkit.addRule(function(action, subject) {
if (subject.user === "greeter" &&
(action.id === "org.freedesktop.login1.reboot" ||
action.id === "org.freedesktop.login1.reboot-multiple-sessions" ||
action.id === "org.freedesktop.login1.power-off" ||
action.id === "org.freedesktop.login1.power-off-multiple-sessions")) {
return polkit.Result.YES;
}
});
+77 -7
View File
@@ -25,6 +25,10 @@ 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 = "cursor-theme")]
cursor_theme: Option<String>,
#[serde(rename = "cursor-size")]
cursor_size: Option<i32>,
#[serde(rename = "fingerprint-enabled")] #[serde(rename = "fingerprint-enabled")]
fingerprint_enabled: Option<bool>, fingerprint_enabled: Option<bool>,
} }
@@ -35,6 +39,8 @@ 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 cursor_theme: Option<String>,
pub cursor_size: Option<i32>,
pub fingerprint_enabled: bool, pub fingerprint_enabled: bool,
} }
@@ -44,6 +50,8 @@ impl Default for Config {
background_path: None, background_path: None,
background_blur: None, background_blur: None,
gtk_theme: None, gtk_theme: None,
cursor_theme: None,
cursor_size: None,
fingerprint_enabled: true, fingerprint_enabled: true,
} }
} }
@@ -68,8 +76,14 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
if bg_path.is_absolute() { if bg_path.is_absolute() {
merged.background_path = Some(bg); merged.background_path = Some(bg);
} else if let Some(parent) = path.parent() { } else if let Some(parent) = path.parent() {
merged.background_path = let joined = parent.join(&bg);
Some(parent.join(&bg).to_string_lossy().to_string()); match joined.to_str() {
Some(s) => merged.background_path = Some(s.to_string()),
None => log::warn!(
"Ignoring non-UTF-8 background path: {}",
joined.display()
),
}
} }
} }
if let Some(blur) = appearance.background_blur { if let Some(blur) = appearance.background_blur {
@@ -82,6 +96,16 @@ 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 appearance.cursor_theme.is_some() {
merged.cursor_theme = appearance.cursor_theme;
}
if let Some(size) = appearance.cursor_size {
if (1..=256).contains(&size) {
merged.cursor_size = Some(size);
} else {
log::warn!("Ignoring cursor-size out of range (1256): {size}");
}
}
if let Some(fp) = appearance.fingerprint_enabled { if let Some(fp) = appearance.fingerprint_enabled {
merged.fingerprint_enabled = fp; merged.fingerprint_enabled = fp;
} }
@@ -98,7 +122,15 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
} }
} }
log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}, fingerprint={}", merged.background_path, merged.background_blur, merged.gtk_theme, merged.fingerprint_enabled); log::debug!(
"Config result: background={:?}, blur={:?}, gtk_theme={:?}, cursor_theme={:?}, cursor_size={:?}, fingerprint={}",
merged.background_path,
merged.background_blur,
merged.gtk_theme,
merged.cursor_theme,
merged.cursor_size,
merged.fingerprint_enabled
);
merged merged
} }
@@ -123,10 +155,14 @@ pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path)
log::debug!("Wallpaper: config path {} not usable, trying fallbacks", path.display()); log::debug!("Wallpaper: config path {} not usable, trying fallbacks", path.display());
} }
// Moonarch ecosystem default // Moonarch ecosystem default — apply the same symlink rejection as the
if moonarch_wallpaper.is_file() { // user-configured path for defense in depth. The fallback target is a
log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display()); // system file, but the caller consumes the result via the same path.
return Some(moonarch_wallpaper.to_path_buf()); if let Ok(meta) = moonarch_wallpaper.symlink_metadata() {
if meta.is_file() && !meta.file_type().is_symlink() {
log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display());
return Some(moonarch_wallpaper.to_path_buf());
}
} }
log::debug!("Wallpaper: no wallpaper found, using GTK background color"); log::debug!("Wallpaper: no wallpaper found, using GTK background color");
@@ -321,6 +357,40 @@ mod tests {
assert!(config.background_blur.is_none()); assert!(config.background_blur.is_none());
} }
// -- Cursor theme tests --
#[test]
fn load_config_cursor_theme_and_size() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moongreet.toml");
fs::write(
&conf,
"[appearance]\ncursor-theme = \"Sweet-cursors\"\ncursor-size = 32\n",
)
.unwrap();
let config = load_config(Some(&[conf]));
assert_eq!(config.cursor_theme.as_deref(), Some("Sweet-cursors"));
assert_eq!(config.cursor_size, Some(32));
}
#[test]
fn load_config_cursor_size_out_of_range_rejected() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moongreet.toml");
fs::write(&conf, "[appearance]\ncursor-size = 9999\n").unwrap();
let config = load_config(Some(&[conf]));
assert!(config.cursor_size.is_none());
}
#[test]
fn load_config_cursor_size_zero_rejected() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moongreet.toml");
fs::write(&conf, "[appearance]\ncursor-size = 0\n").unwrap();
let config = load_config(Some(&[conf]));
assert!(config.cursor_size.is_none());
}
#[test] #[test]
fn load_config_blur_inf_rejected() { fn load_config_blur_inf_rejected() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
+105 -47
View File
@@ -22,8 +22,8 @@ use crate::sessions::{self, Session};
use crate::users::{self, User}; 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 = 5 * 1024 * 1024;
const MAX_WALLPAPER_FILE_SIZE: u64 = 50 * 1024 * 1024; const MAX_WALLPAPER_FILE_SIZE: u64 = 10 * 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;
@@ -188,26 +188,8 @@ fn render_blurred_texture(
Some(renderer.render_texture(&node, Some(&viewport))) Some(renderer.render_texture(&node, Some(&viewport)))
} }
/// Create a wallpaper-only window for secondary monitors.
pub fn create_wallpaper_window(
texture: &gdk::Texture,
blur_radius: Option<f32>,
blur_cache: &Rc<RefCell<Option<gdk::Texture>>>,
app: &gtk::Application,
) -> gtk::ApplicationWindow {
let window = gtk::ApplicationWindow::builder()
.application(app)
.build();
window.add_css_class("wallpaper");
let background = create_background_picture(texture, blur_radius, blur_cache);
window.set_child(Some(&background));
window
}
/// Create a Picture widget for the wallpaper background, optionally with GPU blur. /// Create a Picture widget for the wallpaper background, optionally with GPU blur.
/// Uses `blur_cache` to compute the blurred texture only once across all monitors. /// Uses `blur_cache` to compute the blurred texture only once and reuse it.
fn create_background_picture( fn create_background_picture(
texture: &gdk::Texture, texture: &gdk::Texture,
blur_radius: Option<f32>, blur_radius: Option<f32>,
@@ -251,6 +233,9 @@ struct GreeterState {
user_switch_generation: u64, user_switch_generation: u64,
/// Cached fprintd device proxy — initialized once on first use. /// Cached fprintd device proxy — initialized once on first use.
fingerprint_probe: Option<crate::fingerprint::FingerprintProbe>, fingerprint_probe: Option<crate::fingerprint::FingerprintProbe>,
/// True while a probe init_async() is in flight. Prevents duplicate D-Bus
/// init when two user-switch probes race (both see probe == None).
fingerprint_probe_initializing: bool,
} }
/// Create the main greeter window with login UI. /// Create the main greeter window with login UI.
@@ -277,6 +262,23 @@ pub fn create_greeter_window(
} }
} }
// Apply cursor theme from config — GTK4 under greetd does not read XCURSOR_THEME
// reliably, so set the gtk-cursor-theme-name property directly.
if let Some(ref cursor) = config.cursor_theme {
if is_valid_gtk_theme(cursor) {
if let Some(settings) = gtk::Settings::default() {
settings.set_gtk_cursor_theme_name(Some(cursor));
}
} else {
log::warn!("Ignoring invalid cursor theme name: {cursor}");
}
}
if let Some(size) = config.cursor_size {
if let Some(settings) = gtk::Settings::default() {
settings.set_gtk_cursor_theme_size(size);
}
}
let strings = load_strings(None); let strings = load_strings(None);
let fingerprint_enabled = config.fingerprint_enabled; let fingerprint_enabled = config.fingerprint_enabled;
let all_users = users::get_users(None); let all_users = users::get_users(None);
@@ -300,6 +302,7 @@ pub fn create_greeter_window(
fingerprint_available: false, fingerprint_available: false,
user_switch_generation: 0, user_switch_generation: 0,
fingerprint_probe: None, fingerprint_probe: None,
fingerprint_probe_initializing: false,
})); }));
// Root overlay for layering // Root overlay for layering
@@ -511,6 +514,10 @@ pub fn create_greeter_window(
let Some(user) = user else { return }; let Some(user) = user else { return };
let password = Zeroizing::new(entry.text().to_string()); let password = Zeroizing::new(entry.text().to_string());
// Clear the GTK entry's internal buffer as early as possible. GTK allocates
// the backing `GString` via libc malloc, which `zeroize` cannot reach — the
// best we can do is shorten the window during which it resides in memory.
entry.set_text("");
let session = get_selected_session(&session_dropdown, &sessions_rc); let session = get_selected_session(&session_dropdown, &sessions_rc);
let Some(session) = session else { let Some(session) = session else {
@@ -520,7 +527,7 @@ pub fn create_greeter_window(
attempt_login( attempt_login(
&user, &user,
&password, password,
&session, &session,
strings, strings,
&state, &state,
@@ -553,6 +560,18 @@ pub fn create_greeter_window(
)); ));
window.add_controller(key_controller); window.add_controller(key_controller);
// Grab keyboard focus after map — layer-shell keyboard grab is only
// confirmed by the compositor at map time, not at realize time.
window.connect_map(clone!(
#[weak]
password_entry,
move |_| {
glib::idle_add_local_once(move || {
password_entry.grab_focus();
});
}
));
// Defer initial user selection until realized (for correct theme colors) // Defer initial user selection until realized (for correct theme colors)
window.connect_realize(clone!( window.connect_realize(clone!(
#[strong] #[strong]
@@ -722,12 +741,33 @@ fn switch_to_user(
#[strong] #[strong]
state, state,
async move { async move {
// Initialize probe on first use, then reuse cached device proxy // Initialize probe on first use, then reuse cached device proxy.
let needs_init = state.borrow().fingerprint_probe.is_none(); // Atomic check-and-set on fingerprint_probe_initializing prevents
if needs_init { // two concurrent probes (from a fast user switch) from both
// running init_async, which would open duplicate D-Bus connections.
let should_init = {
let mut s = state.borrow_mut();
if s.fingerprint_probe.is_some() || s.fingerprint_probe_initializing {
false
} else {
s.fingerprint_probe_initializing = true;
true
}
};
if should_init {
let mut probe = crate::fingerprint::FingerprintProbe::new(); let mut probe = crate::fingerprint::FingerprintProbe::new();
probe.init_async().await; probe.init_async().await;
state.borrow_mut().fingerprint_probe = Some(probe); let mut s = state.borrow_mut();
s.fingerprint_probe = Some(probe);
s.fingerprint_probe_initializing = false;
} else {
// Another coroutine is initializing — yield until it publishes.
while state.borrow().fingerprint_probe.is_none()
&& state.borrow().fingerprint_probe_initializing
{
glib::timeout_future(std::time::Duration::from_millis(25)).await;
}
} }
// Take probe out of state to avoid holding borrow across await // Take probe out of state to avoid holding borrow across await
@@ -784,28 +824,40 @@ fn set_avatar_from_file(
Ok(_) => {} Ok(_) => {}
} }
let Some(path_str) = path.to_str() else { // Show fallback immediately; decode asynchronously via GIO so the greeter
log::debug!("Non-UTF-8 avatar path, skipping: {}", path.display()); // stays responsive during a user-switch click.
image.set_icon_name(Some("avatar-default-symbolic")); image.set_icon_name(Some("avatar-default-symbolic"));
return;
};
match Pixbuf::from_file_at_scale(path_str, AVATAR_SIZE, AVATAR_SIZE, true) { let display_path = path.to_path_buf();
Ok(pixbuf) => { let file = gio::File::for_path(path);
let texture = gdk::Texture::for_pixbuf(&pixbuf); let image_clone = image.clone();
if let Some(name) = username { let state_clone = state.clone();
state let username_owned = username.map(String::from);
.borrow_mut()
.avatar_cache glib::spawn_future_local(async move {
.insert(name.to_string(), texture.clone()); let stream = match file.read_future(glib::Priority::default()).await {
Ok(s) => s,
Err(e) => {
log::debug!("Failed to open avatar {}: {e}", display_path.display());
return;
}
};
match Pixbuf::from_stream_at_scale_future(&stream, AVATAR_SIZE, AVATAR_SIZE, true).await {
Ok(pixbuf) => {
let texture = gdk::Texture::for_pixbuf(&pixbuf);
if let Some(ref name) = username_owned {
state_clone
.borrow_mut()
.avatar_cache
.insert(name.clone(), texture.clone());
}
image_clone.set_paintable(Some(&texture));
}
Err(e) => {
log::debug!("Failed to decode avatar {}: {e}", display_path.display());
} }
image.set_paintable(Some(&texture));
} }
Err(e) => { });
log::debug!("Failed to load avatar {}: {e}", path.display());
image.set_icon_name(Some("avatar-default-symbolic"));
}
}
} }
/// Load the default avatar SVG from GResources, tinted with the foreground color. /// Load the default avatar SVG from GResources, tinted with the foreground color.
@@ -959,7 +1011,7 @@ fn set_login_sensitive(
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn attempt_login( fn attempt_login(
user: &User, user: &User,
password: &str, password: Zeroizing<String>,
session: &Session, session: &Session,
strings: &'static Strings, strings: &'static Strings,
state: &Rc<RefCell<GreeterState>>, state: &Rc<RefCell<GreeterState>>,
@@ -998,7 +1050,6 @@ fn attempt_login(
set_login_sensitive(password_entry, session_dropdown, false); set_login_sensitive(password_entry, session_dropdown, false);
let username = user.username.clone(); let username = user.username.clone();
let password = Zeroizing::new(password.to_string());
let exec_cmd = session.exec_cmd.clone(); let exec_cmd = session.exec_cmd.clone();
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();
@@ -1039,6 +1090,13 @@ fn attempt_login(
glib::timeout_future(min_response - elapsed).await; glib::timeout_future(min_response - elapsed).await;
} }
// The login_worker's own socket is already dropped by now; drop the
// shared clone too so repeated failed attempts do not accumulate
// stale file descriptors in state.greetd_sock.
if let Ok(mut g) = state.borrow().greetd_sock.lock() {
g.take();
}
match result { match result {
Ok(Ok(LoginResult::Success { username })) => { Ok(Ok(LoginResult::Success { username })) => {
save_last_user(&username); save_last_user(&username);
+4
View File
@@ -286,6 +286,10 @@ mod tests {
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"); assert!(!s.unexpected_greetd_response.is_empty(), "{locale}: unexpected_greetd_response");
assert!(!s.greetd_sock_not_absolute.is_empty(), "{locale}: greetd_sock_not_absolute");
assert!(!s.invalid_session_command.is_empty(), "{locale}: invalid_session_command");
assert!(!s.session_start_failed.is_empty(), "{locale}: session_start_failed");
assert!(!s.socket_error.is_empty(), "{locale}: socket_error");
} }
} }
+22 -32
View File
@@ -1,5 +1,5 @@
// ABOUTME: Entry point for Moongreet — greetd greeter for Wayland. // ABOUTME: Entry point for Moongreet — greetd greeter for Wayland.
// ABOUTME: Sets up GTK Application, Layer Shell, CSS, and multi-monitor windows. // ABOUTME: Sets up GTK Application, Layer Shell, CSS, and a single greeter window.
mod config; mod config;
mod fingerprint; mod fingerprint;
@@ -24,13 +24,11 @@ fn load_css(display: &gdk::Display) {
); );
} }
fn setup_layer_shell(window: &gtk::ApplicationWindow, keyboard: bool, layer: gtk4_layer_shell::Layer) { fn setup_layer_shell(window: &gtk::ApplicationWindow, layer: gtk4_layer_shell::Layer) {
window.init_layer_shell(); window.init_layer_shell();
window.set_layer(layer); window.set_layer(layer);
window.set_exclusive_zone(-1); window.set_exclusive_zone(-1);
if keyboard { window.set_keyboard_mode(gtk4_layer_shell::KeyboardMode::Exclusive);
window.set_keyboard_mode(gtk4_layer_shell::KeyboardMode::Exclusive);
}
// Anchor to all edges for fullscreen // Anchor to all edges for fullscreen
window.set_anchor(gtk4_layer_shell::Edge::Top, true); window.set_anchor(gtk4_layer_shell::Edge::Top, true);
window.set_anchor(gtk4_layer_shell::Edge::Bottom, true); window.set_anchor(gtk4_layer_shell::Edge::Bottom, true);
@@ -63,30 +61,20 @@ fn activate(app: &gtk::Application) {
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}");
// Main greeter window (login UI) — compositor picks focused monitor
let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
if use_layer_shell { if use_layer_shell {
setup_layer_shell(&greeter_window, true, gtk4_layer_shell::Layer::Top); // Single greeter window. No set_monitor — the compositor places it on the
} // focused output (same as moonset). Exclusive keyboard binds input to this
greeter_window.present(); // surface regardless of pointer position; the mouse may wander to other
// outputs but typing always reaches the greeter. The previous per-monitor
// Wallpaper-only windows on all monitors (only with layer shell) // approach gave keyboard only to the first monitor's window, so a user on
if use_layer_shell // any other output could not type the password.
&& let Some(ref texture) = bg_texture let window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
{ setup_layer_shell(&window, gtk4_layer_shell::Layer::Top);
let monitors = display.monitors(); window.present();
log::debug!("Monitor count: {}", monitors.n_items()); } else {
for i in 0..monitors.n_items() { // No layer shell — single window for development
if let Some(monitor) = monitors let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
.item(i) greeter_window.present();
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
{
let wallpaper = greeter::create_wallpaper_window(texture, config.background_blur, &blur_cache, app);
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom);
wallpaper.set_monitor(Some(&monitor));
wallpaper.present();
}
}
} }
} }
@@ -101,10 +89,12 @@ fn setup_logging() {
eprintln!("Failed to create journal logger: {e}"); eprintln!("Failed to create journal logger: {e}");
} }
} }
let level = if std::env::var("MOONGREET_DEBUG").is_ok() { // Require MOONGREET_DEBUG=1 to raise verbosity. Mere presence (e.g. an
log::LevelFilter::Debug // empty value in a session-setup script) must not escalate the journal
} else { // to Debug, which leaks socket paths, usernames, and auth round counts.
log::LevelFilter::Info let level = match std::env::var("MOONGREET_DEBUG").ok().as_deref() {
Some("1") => log::LevelFilter::Debug,
_ => log::LevelFilter::Info,
}; };
log::set_max_level(level); log::set_max_level(level);
} }
+15 -6
View File
@@ -1,4 +1,4 @@
// ABOUTME: Power actions — reboot and shutdown via loginctl. // ABOUTME: Power actions — reboot and shutdown via systemctl.
// ABOUTME: Wrappers around system commands for the greeter UI. // ABOUTME: Wrappers around system commands for the greeter UI.
use std::fmt; use std::fmt;
@@ -40,7 +40,9 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
log::debug!("Power action: {action} ({program} {args:?})"); log::debug!("Power action: {action} ({program} {args:?})");
let mut child = Command::new(program) let mut child = Command::new(program)
.args(args) .args(args)
.stdout(Stdio::piped()) // stdout is never read; piping without draining would deadlock on any
// command that writes more than one OS pipe buffer before wait() returns.
.stdout(Stdio::null())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .spawn()
.map_err(|e| PowerError::CommandFailed { .map_err(|e| PowerError::CommandFailed {
@@ -97,14 +99,21 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
} }
} }
/// Reboot the system via loginctl. /// Reboot the system via systemctl.
///
/// `--no-ask-password` keeps systemctl from spawning an interactive askpass
/// agent — the greeter session has none, so without it a denied authorization
/// would hang instead of failing fast.
pub fn reboot() -> Result<(), PowerError> { pub fn reboot() -> Result<(), PowerError> {
run_command("reboot", "/usr/bin/loginctl", &["reboot"]) run_command("reboot", "/usr/bin/systemctl", &["--no-ask-password", "reboot"])
} }
/// Shut down the system via loginctl. /// Shut down the system via systemctl.
///
/// `--no-ask-password` for the same reason as [`reboot`] — the agent-less
/// greeter session has nothing to answer an authorization challenge.
pub fn shutdown() -> Result<(), PowerError> { pub fn shutdown() -> Result<(), PowerError> {
run_command("shutdown", "/usr/bin/loginctl", &["poweroff"]) run_command("shutdown", "/usr/bin/systemctl", &["--no-ask-password", "poweroff"])
} }
#[cfg(test)] #[cfg(test)]
+11
View File
@@ -23,6 +23,8 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
let mut in_section = false; let mut in_section = false;
let mut name: Option<String> = None; let mut name: Option<String> = None;
let mut exec_cmd: Option<String> = None; let mut exec_cmd: Option<String> = None;
let mut hidden = false;
let mut no_display = false;
for line in content.lines() { for line in content.lines() {
let line = line.trim(); let line = line.trim();
@@ -44,9 +46,18 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
&& exec_cmd.is_none() && exec_cmd.is_none()
{ {
exec_cmd = Some(value.to_string()); exec_cmd = Some(value.to_string());
} else if let Some(value) = line.strip_prefix("Hidden=") {
hidden = value.eq_ignore_ascii_case("true");
} else if let Some(value) = line.strip_prefix("NoDisplay=") {
no_display = value.eq_ignore_ascii_case("true");
} }
} }
if hidden || no_display {
log::debug!("Skipping {}: Hidden/NoDisplay entry", path.display());
return None;
}
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());