Compare commits
No commits in common. "main" and "v0.7.2" have entirely different histories.
@ -1,5 +1,7 @@
|
||||
# Moongreet
|
||||
|
||||
**Name**: Selene (Mondgöttin — passend zu Moon-greet)
|
||||
|
||||
## Projekt
|
||||
|
||||
Moongreet ist ein greetd-Greeter für Wayland, gebaut mit Rust + gtk4-rs + gtk4-layer-shell.
|
||||
@ -45,7 +47,7 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
|
||||
- `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 0–200)
|
||||
- `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 mit Hotplug via `items-changed` auf Monitor-ListModel (one greeter window per monitor, first gets keyboard), systemd-journal-logger
|
||||
- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, systemd-journal-logger
|
||||
- `resources/style.css` — Catppuccin-inspiriertes Theme
|
||||
|
||||
## Design Decisions
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -575,7 +575,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "moongreet"
|
||||
version = "0.8.6"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"gdk-pixbuf",
|
||||
"gdk4",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "moongreet"
|
||||
version = "0.8.6"
|
||||
version = "0.7.2"
|
||||
edition = "2024"
|
||||
description = "A greetd greeter for Wayland with GTK4 and Layer Shell"
|
||||
license = "MIT"
|
||||
|
||||
65
DECISIONS.md
65
DECISIONS.md
@ -1,105 +1,56 @@
|
||||
# Decisions
|
||||
|
||||
## 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
|
||||
|
||||
- **Who**: ClaudeCode, Dom
|
||||
- **Why**: GTK4 under greetd does not reliably read `/etc/xdg/gtk-4.0/settings.ini` — likely requires a settings daemon that doesn't run in the greeter session. moongreet fell back to Adwaita/Colloid-default (blue accent) instead of Colloid-Grey-Dark-Catppuccin.
|
||||
- **Tradeoffs**: Reverts `094878f` ("Remove gtk-theme from app config, use system-wide GTK settings instead"). Duplicates the theme name between settings.ini and moongreet.toml, but the explicit set via `set_gtk_theme_name()` is the only reliable path in a greetd context.
|
||||
- **How**: Added `gtk-theme = "Colloid-Grey-Dark-Catppuccin"` to example config and deployed `/etc/moongreet/moongreet.toml`.
|
||||
|
||||
## 2026-04-02 – Replace hardcoded CSS colors with GTK theme variables
|
||||
|
||||
- **Who**: ClaudeCode, Dom
|
||||
- **Who**: Ragnar, Dom
|
||||
- **Why**: moongreet used hardcoded colors (#1a1a2e, white, #ff6b6b) while moonset already used @theme_bg_color, @theme_fg_color, @error_color etc. Inconsistent across the ecosystem and broke theme flexibility.
|
||||
- **Tradeoffs**: Depends on the active GTK theme defining standard color variables. Catppuccin Colloid provides all needed vars (@theme_bg_color, @theme_fg_color, @error_color, @success_color, @theme_selected_bg_color). Fallback behavior if a theme lacks vars is GTK's default colors — acceptable.
|
||||
- **How**: Replaced all hardcoded hex/named colors with GTK theme variables. Coordinated change across moongreet, moonlock, and moonset (all three now use identical pattern).
|
||||
|
||||
## 2026-03-31 – Fourth audit: power timeout, timing mitigation, release profile, GREETD_SOCK caching
|
||||
|
||||
- **Who**: ClaudeCode, Dom
|
||||
- **Who**: Ragnar, Dom
|
||||
- **Why**: Fourth triple audit found moongreet power.rs had no timeout on loginctl (greeter could freeze), username enumeration via timing differential, GREETD_SOCK re-read on every login, missing release profile, and missing GResource compression.
|
||||
- **Tradeoffs**: 500ms minimum login response time adds slight delay on fast auth but prevents timing-based username enumeration. Power timeout (30s + SIGKILL) matches moonset pattern — aggressive but prevents greeter freeze.
|
||||
- **How**: (1) power.rs adapted from moonset with 30s timeout + SIGKILL (nix dependency added). (2) 500ms min response floor in attempt_login via Instant + glib::timeout_future. (3) GREETD_SOCK cached in GreeterState at startup. (4) `[profile.release]` with LTO, codegen-units=1, strip. (5) `compressed="true"` on GResource entries. (6) SYNC comments on duplicated blur/background functions.
|
||||
|
||||
## 2026-03-30 – Full audit fix: security, quality, performance (v0.6.2)
|
||||
|
||||
- **Who**: ClaudeCode, Dom
|
||||
- **Who**: Ragnar, Dom
|
||||
- **Why**: Three parallel audits (security, code quality, performance) identified 10 actionable findings across the codebase — from world-readable cache dirs to a GPU blur geometry bug to a race condition in fingerprint probing.
|
||||
- **Tradeoffs**: `too_many_arguments` Clippy warnings suppressed with `#[allow]` rather than introducing a `UiWidgets` struct — GTK's `clone!` macro with `#[weak]` refs requires individual widget parameters, a struct would fight the idiom. Async avatar loading skipped because `Pixbuf` is `!Send`; cache already prevents repeat loads. TOCTOU socket pre-check removed entirely — `connect()` in login_worker already handles errors, the `metadata()` check gave false security guarantees.
|
||||
- **How**: Cache dirs use `DirBuilder::mode(0o700)` instead of `create_dir_all`. Blur config clamped to `0.0..=200.0` with `is_finite()` guard. Blur texture cached in `Rc<RefCell<Option<gdk::Texture>>>` across monitors. FingerprintProbe device proxy cached in `GreeterState` with generation counter to prevent stale async writes. GPU blur geometry fixed (`-pad` origin shift instead of texture stretching). `is_valid_gtk_theme` extracted as testable function. 9 new tests.
|
||||
|
||||
## 2026-03-29 – Fingerprint authentication via greetd multi-stage PAM
|
||||
|
||||
- **Who**: ClaudeCode, Dom
|
||||
- **Who**: Ragnar, Dom
|
||||
- **Why**: moonlock supports fprintd but moongreet rejected multi-stage auth. Users with enrolled fingerprints couldn't use them at the login screen.
|
||||
- **Tradeoffs**: Direct fprintd D-Bus verification (like moonlock) can't start a greetd session — greetd controls session creation via PAM. Using greetd multi-stage means PAM decides the auth order (fingerprint first, then password fallback), not truly parallel. Acceptable — matches standard pam_fprintd behavior.
|
||||
- **How**: Replace single-pass auth with a loop over auth_message rounds. Secret prompts get the password, non-secret prompts (fprintd) get None and block until PAM resolves. fprintd D-Bus probe (gio::DBusProxy) only for UI — detecting device availability and enrolled fingers. 60s socket timeout when fingerprint available. Config option `fingerprint-enabled` (default true).
|
||||
|
||||
## 2026-03-28 – Remove embedded wallpaper from binary
|
||||
|
||||
- **Who**: ClaudeCode, Dom
|
||||
- **Who**: Selene, Dom
|
||||
- **Why**: Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg. Embedding a 374K JPEG in the binary is redundant. GTK background color (Catppuccin Mocha base) is a clean fallback.
|
||||
- **Tradeoffs**: Without moonarch installed AND without config, greeter shows plain dark background instead of wallpaper. Acceptable — that's the expected minimal state.
|
||||
- **How**: Remove wallpaper.jpg from GResources, return None from resolve_background_path when no file found, skip wallpaper window creation and background picture when no path available.
|
||||
|
||||
## 2026-03-28 – GPU blur via GskBlurNode replaces CPU blur
|
||||
|
||||
- **Who**: ClaudeCode, Dom
|
||||
- **Who**: Ragnar, Dom
|
||||
- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms–2s on 4K wallpapers at cold cache. Disk cache and async orchestration added significant complexity.
|
||||
- **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper backgrounds. Removes `image` crate dependency entirely (~15 transitive crates eliminated). No disk cache needed.
|
||||
- **How**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` on `connect_realize`. Blur happens once on the GPU when the widget gets its renderer, producing a concrete `gdk::Texture`. Zero startup latency. Symmetric with moonlock and moonset.
|
||||
|
||||
## 2026-03-28 – Optional background blur via `image` crate (superseded)
|
||||
|
||||
- **Who**: ClaudeCode, Dom
|
||||
- **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**: ClaudeCode, Dominik
|
||||
- **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()`
|
||||
|
||||
10
README.md
10
README.md
@ -12,7 +12,7 @@ Part of the Moonarch ecosystem.
|
||||
- **Last user/session** — Remembered in `/var/cache/moongreet/`
|
||||
- **Power actions** — Reboot / Shutdown via `loginctl`
|
||||
- **Layer Shell** — Fullscreen via gtk4-layer-shell (TOP layer)
|
||||
- **Multi-monitor + hotplug** — Full greeter UI on all monitors (keyboard input on first), hotplugged monitors get windows automatically
|
||||
- **Multi-monitor** — Greeter on primary, wallpaper on all monitors
|
||||
- **GPU blur** — Background blur via GskBlurNode (shared cache across monitors)
|
||||
- **i18n** — German and English (auto-detected from system locale)
|
||||
- **Faillock warning** — Warns after 2 failed attempts, locked message after 3
|
||||
@ -60,14 +60,6 @@ sudo cp config/moongreet.toml /etc/moongreet/moongreet.toml
|
||||
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
|
||||
|
||||
```bash
|
||||
|
||||
@ -4,7 +4,3 @@
|
||||
[appearance]
|
||||
# Absolute path to wallpaper image
|
||||
background = "/usr/share/backgrounds/wallpaper.jpg"
|
||||
|
||||
# GTK theme name — must match a directory in /usr/share/themes/
|
||||
# Required because GTK4 under greetd does not reliably read settings.ini
|
||||
gtk-theme = "Colloid-Grey-Dark-Catppuccin"
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
// ABOUTME: Allow the greeter user to reboot and power off without authentication.
|
||||
// ABOUTME: Required because greetd's greeter session is inactive in logind.
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
@ -68,14 +68,8 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
||||
if bg_path.is_absolute() {
|
||||
merged.background_path = Some(bg);
|
||||
} else if let Some(parent) = path.parent() {
|
||||
let joined = parent.join(&bg);
|
||||
match joined.to_str() {
|
||||
Some(s) => merged.background_path = Some(s.to_string()),
|
||||
None => log::warn!(
|
||||
"Ignoring non-UTF-8 background path: {}",
|
||||
joined.display()
|
||||
),
|
||||
}
|
||||
merged.background_path =
|
||||
Some(parent.join(&bg).to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
if let Some(blur) = appearance.background_blur {
|
||||
@ -129,15 +123,11 @@ pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path)
|
||||
log::debug!("Wallpaper: config path {} not usable, trying fallbacks", path.display());
|
||||
}
|
||||
|
||||
// Moonarch ecosystem default — apply the same symlink rejection as the
|
||||
// user-configured path for defense in depth. The fallback target is a
|
||||
// system file, but the caller consumes the result via the same path.
|
||||
if let Ok(meta) = moonarch_wallpaper.symlink_metadata() {
|
||||
if meta.is_file() && !meta.file_type().is_symlink() {
|
||||
// Moonarch ecosystem default
|
||||
if moonarch_wallpaper.is_file() {
|
||||
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");
|
||||
None
|
||||
|
||||
115
src/greeter.rs
115
src/greeter.rs
@ -22,8 +22,8 @@ use crate::sessions::{self, Session};
|
||||
use crate::users::{self, User};
|
||||
|
||||
const AVATAR_SIZE: i32 = 128;
|
||||
const MAX_AVATAR_FILE_SIZE: u64 = 5 * 1024 * 1024;
|
||||
const MAX_WALLPAPER_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_SESSION_DIR: &str = "/var/cache/moongreet/last-session";
|
||||
const MAX_USERNAME_LENGTH: usize = 256;
|
||||
@ -188,6 +188,24 @@ fn render_blurred_texture(
|
||||
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: >k::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.
|
||||
/// Uses `blur_cache` to compute the blurred texture only once across all monitors.
|
||||
fn create_background_picture(
|
||||
@ -233,9 +251,6 @@ struct GreeterState {
|
||||
user_switch_generation: u64,
|
||||
/// Cached fprintd device proxy — initialized once on first use.
|
||||
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.
|
||||
@ -285,7 +300,6 @@ pub fn create_greeter_window(
|
||||
fingerprint_available: false,
|
||||
user_switch_generation: 0,
|
||||
fingerprint_probe: None,
|
||||
fingerprint_probe_initializing: false,
|
||||
}));
|
||||
|
||||
// Root overlay for layering
|
||||
@ -497,10 +511,6 @@ pub fn create_greeter_window(
|
||||
let Some(user) = user else { return };
|
||||
|
||||
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 Some(session) = session else {
|
||||
@ -510,7 +520,7 @@ pub fn create_greeter_window(
|
||||
|
||||
attempt_login(
|
||||
&user,
|
||||
password,
|
||||
&password,
|
||||
&session,
|
||||
strings,
|
||||
&state,
|
||||
@ -543,18 +553,6 @@ pub fn create_greeter_window(
|
||||
));
|
||||
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)
|
||||
window.connect_realize(clone!(
|
||||
#[strong]
|
||||
@ -724,33 +722,12 @@ fn switch_to_user(
|
||||
#[strong]
|
||||
state,
|
||||
async move {
|
||||
// Initialize probe on first use, then reuse cached device proxy.
|
||||
// Atomic check-and-set on fingerprint_probe_initializing prevents
|
||||
// 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 {
|
||||
// Initialize probe on first use, then reuse cached device proxy
|
||||
let needs_init = state.borrow().fingerprint_probe.is_none();
|
||||
if needs_init {
|
||||
let mut probe = crate::fingerprint::FingerprintProbe::new();
|
||||
probe.init_async().await;
|
||||
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;
|
||||
}
|
||||
state.borrow_mut().fingerprint_probe = Some(probe);
|
||||
}
|
||||
|
||||
// Take probe out of state to avoid holding borrow across await
|
||||
@ -807,40 +784,28 @@ fn set_avatar_from_file(
|
||||
Ok(_) => {}
|
||||
}
|
||||
|
||||
// Show fallback immediately; decode asynchronously via GIO so the greeter
|
||||
// stays responsive during a user-switch click.
|
||||
let Some(path_str) = path.to_str() else {
|
||||
log::debug!("Non-UTF-8 avatar path, skipping: {}", path.display());
|
||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||
|
||||
let display_path = path.to_path_buf();
|
||||
let file = gio::File::for_path(path);
|
||||
let image_clone = image.clone();
|
||||
let state_clone = state.clone();
|
||||
let username_owned = username.map(String::from);
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
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 {
|
||||
|
||||
match Pixbuf::from_file_at_scale(path_str, AVATAR_SIZE, AVATAR_SIZE, true) {
|
||||
Ok(pixbuf) => {
|
||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||
if let Some(ref name) = username_owned {
|
||||
state_clone
|
||||
if let Some(name) = username {
|
||||
state
|
||||
.borrow_mut()
|
||||
.avatar_cache
|
||||
.insert(name.clone(), texture.clone());
|
||||
.insert(name.to_string(), texture.clone());
|
||||
}
|
||||
image_clone.set_paintable(Some(&texture));
|
||||
image.set_paintable(Some(&texture));
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Failed to decode avatar {}: {e}", display_path.display());
|
||||
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.
|
||||
@ -994,7 +959,7 @@ fn set_login_sensitive(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn attempt_login(
|
||||
user: &User,
|
||||
password: Zeroizing<String>,
|
||||
password: &str,
|
||||
session: &Session,
|
||||
strings: &'static Strings,
|
||||
state: &Rc<RefCell<GreeterState>>,
|
||||
@ -1033,6 +998,7 @@ fn attempt_login(
|
||||
set_login_sensitive(password_entry, session_dropdown, false);
|
||||
|
||||
let username = user.username.clone();
|
||||
let password = Zeroizing::new(password.to_string());
|
||||
let exec_cmd = session.exec_cmd.clone();
|
||||
let session_name = session.name.clone();
|
||||
let greetd_sock = state.borrow().greetd_sock.clone();
|
||||
@ -1073,13 +1039,6 @@ fn attempt_login(
|
||||
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 {
|
||||
Ok(Ok(LoginResult::Success { username })) => {
|
||||
save_last_user(&username);
|
||||
|
||||
@ -286,10 +286,6 @@ mod tests {
|
||||
assert!(!s.faillock_attempts_remaining.is_empty(), "{locale}: faillock_attempts_remaining");
|
||||
assert!(!s.faillock_locked.is_empty(), "{locale}: faillock_locked");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
64
src/main.rs
64
src/main.rs
@ -11,11 +11,9 @@ mod sessions;
|
||||
mod users;
|
||||
|
||||
use gdk4 as gdk;
|
||||
use glib::clone;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{self as gtk, gio};
|
||||
use gtk4_layer_shell::LayerShell;
|
||||
use std::rc::Rc;
|
||||
fn load_css(display: &gdk::Display) {
|
||||
let css_provider = gtk::CssProvider::new();
|
||||
css_provider.load_from_resource("/dev/moonarch/moongreet/style.css");
|
||||
@ -65,54 +63,30 @@ fn activate(app: >k::Application) {
|
||||
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
|
||||
let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
|
||||
if use_layer_shell {
|
||||
// One greeter window per monitor — only the first gets keyboard input
|
||||
setup_layer_shell(&greeter_window, true, gtk4_layer_shell::Layer::Top);
|
||||
}
|
||||
greeter_window.present();
|
||||
|
||||
// Wallpaper-only windows on all monitors (only with layer shell)
|
||||
if use_layer_shell
|
||||
&& let Some(ref texture) = bg_texture
|
||||
{
|
||||
let monitors = display.monitors();
|
||||
log::debug!("Monitor count: {}", monitors.n_items());
|
||||
let mut first = true;
|
||||
for i in 0..monitors.n_items() {
|
||||
if let Some(monitor) = monitors
|
||||
.item(i)
|
||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
||||
{
|
||||
let window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
|
||||
setup_layer_shell(&window, first, gtk4_layer_shell::Layer::Top);
|
||||
window.set_monitor(Some(&monitor));
|
||||
window.present();
|
||||
first = false;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle monitor hotplug — create greeter windows for newly added monitors
|
||||
// (without keyboard, since the primary monitor already has it)
|
||||
let bg_texture = Rc::new(bg_texture);
|
||||
let config = Rc::new(config);
|
||||
monitors.connect_items_changed(clone!(
|
||||
#[weak]
|
||||
app,
|
||||
#[strong]
|
||||
blur_cache,
|
||||
move |list, position, _removed, added| {
|
||||
for i in position..position + added {
|
||||
if let Some(monitor) = list
|
||||
.item(i)
|
||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
||||
{
|
||||
log::debug!("Monitor hotplug: creating greeter window");
|
||||
let window = greeter::create_greeter_window(
|
||||
bg_texture.as_ref().as_ref(), &config, &blur_cache, &app,
|
||||
);
|
||||
setup_layer_shell(&window, false, gtk4_layer_shell::Layer::Top);
|
||||
window.set_monitor(Some(&monitor));
|
||||
window.present();
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
} else {
|
||||
// No layer shell — single window for development
|
||||
let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
|
||||
greeter_window.present();
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,12 +101,10 @@ fn setup_logging() {
|
||||
eprintln!("Failed to create journal logger: {e}");
|
||||
}
|
||||
}
|
||||
// Require MOONGREET_DEBUG=1 to raise verbosity. Mere presence (e.g. an
|
||||
// empty value in a session-setup script) must not escalate the journal
|
||||
// to Debug, which leaks socket paths, usernames, and auth round counts.
|
||||
let level = match std::env::var("MOONGREET_DEBUG").ok().as_deref() {
|
||||
Some("1") => log::LevelFilter::Debug,
|
||||
_ => log::LevelFilter::Info,
|
||||
let level = if std::env::var("MOONGREET_DEBUG").is_ok() {
|
||||
log::LevelFilter::Debug
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
};
|
||||
log::set_max_level(level);
|
||||
}
|
||||
|
||||
@ -40,9 +40,7 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
|
||||
log::debug!("Power action: {action} ({program} {args:?})");
|
||||
let mut child = Command::new(program)
|
||||
.args(args)
|
||||
// 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())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| PowerError::CommandFailed {
|
||||
|
||||
@ -23,8 +23,6 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
|
||||
let mut in_section = false;
|
||||
let mut name: Option<String> = None;
|
||||
let mut exec_cmd: Option<String> = None;
|
||||
let mut hidden = false;
|
||||
let mut no_display = false;
|
||||
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
@ -46,18 +44,9 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
|
||||
&& exec_cmd.is_none()
|
||||
{
|
||||
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 exec_cmd = exec_cmd.filter(|s| !s.is_empty());
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user