Compare commits

...

10 Commits

Author SHA1 Message Date
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
nevaforget 9c1e00d0ef fix: restore explicit gtk-theme in config for greetd session (v0.7.3)
GTK4 does not reliably read /etc/xdg/gtk-4.0/settings.ini under greetd
without a settings daemon, falling back to default blue accent instead
of Colloid-Grey-Dark-Catppuccin.
2026-04-06 22:24:06 +02:00
nevaforget 874888391e docs: rename Ragnar to ClaudeCode in DECISIONS.md
Update PKGBUILD version / update-pkgver (push) Successful in 2s
2026-04-02 10:13:34 +02:00
nevaforget 51157ecb23 fix: replace hardcoded CSS colors with GTK theme variables (v0.7.2)
Greeter used hardcoded colors (#1a1a2e, white, #ff6b6b) instead of
GTK theme variables, breaking theme consistency across the ecosystem.
Now uses @theme_bg_color, @theme_fg_color, @error_color etc. —
matching moonlock and moonset.
2026-04-02 10:12:01 +02:00
nevaforget 183e10c1cc Remove unnecessary pacman git install from CI workflow
Update PKGBUILD version / update-pkgver (push) Successful in 2s
Git is already available in the runner image.
2026-04-02 08:28:06 +02:00
nevaforget 094878fc2e Remove gtk-theme from app config, use system-wide GTK settings instead
The GTK theme is now set globally via /etc/xdg/gtk-4.0/settings.ini
rather than per-application config.
2026-04-02 08:27:54 +02:00
nevaforget cf18105887 Revert CI workaround: remove pacman install step
Update PKGBUILD version / update-pkgver (push) Failing after 0s
The act_runner now uses a custom Arch-based image with git
pre-installed, so per-workflow installs are no longer needed.
2026-04-01 16:17:47 +02:00
nevaforget f6f33a13ab fix: audit fixes — power timeout, timing mitigation, release profile, GREETD_SOCK cache (v0.7.1)
Update PKGBUILD version / update-pkgver (push) Successful in 2s
- Add 30s timeout with SIGKILL to power actions (adapted from moonset)
- Add 500ms minimum login response time against timing enumeration
- Cache GREETD_SOCK in GreeterState at startup
- Add [profile.release] with LTO, codegen-units=1, strip
- Add compressed="true" to GResource CSS/SVG entries
- Add SYNC comments to duplicated blur/background functions
- Add nix dependency for signal handling in power timeout
2026-03-31 11:08:40 +02:00
nevaforget 60d294fa37 docs: update README, fix build.rs comment, correct gtk-theme in config
README: replace LD_PRELOAD with MOONGREET_NO_LAYER_SHELL env var,
add missing features (GPU blur, journal logging, password wiping).
build.rs: remove wallpaper.jpg reference.
moongreet.toml: correct gtk-theme to Colloid-Grey-Dark-Catppuccin.
2026-03-31 09:36:19 +02:00
12 changed files with 197 additions and 81 deletions
+1 -1
View File
@@ -47,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 - `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, fingerprint-enabled) + Wallpaper-Fallback + Blur-Validierung (finite, clamp 0200)
- `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, Multi-Monitor (one greeter window per monitor, first gets keyboard), systemd-journal-logger
- `resources/style.css` — Catppuccin-inspiriertes Theme - `resources/style.css` — Catppuccin-inspiriertes Theme
## Design Decisions ## Design Decisions
Generated
+20 -1
View File
@@ -59,6 +59,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@@ -569,7 +575,7 @@ dependencies = [
[[package]] [[package]]
name = "moongreet" name = "moongreet"
version = "0.7.0" version = "0.7.4"
dependencies = [ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
@@ -580,6 +586,7 @@ dependencies = [
"gtk4", "gtk4",
"gtk4-layer-shell", "gtk4-layer-shell",
"log", "log",
"nix",
"serde", "serde",
"serde_json", "serde_json",
"systemd-journal-logger", "systemd-journal-logger",
@@ -588,6 +595,18 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
+7 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "moongreet" name = "moongreet"
version = "0.7.0" version = "0.8.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"
@@ -16,6 +16,7 @@ toml = "0.8"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
graphene-rs = { version = "0.22", package = "graphene-rs" } graphene-rs = { version = "0.22", package = "graphene-rs" }
nix = { version = "0.29", features = ["signal"] }
zeroize = { version = "1", features = ["std"] } zeroize = { version = "1", features = ["std"] }
log = "0.4" log = "0.4"
systemd-journal-logger = "2.2" systemd-journal-logger = "2.2"
@@ -23,5 +24,10 @@ systemd-journal-logger = "2.2"
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"
[profile.release]
lto = "thin"
codegen-units = 1
strip = true
[build-dependencies] [build-dependencies]
glib-build-tools = "0.22" glib-build-tools = "0.22"
+31 -3
View File
@@ -1,15 +1,43 @@
# Decisions # Decisions
## 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
- **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
- **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) ## 2026-03-30 Full audit fix: security, quality, performance (v0.6.2)
- **Who**: Ragnar, Dom - **Who**: ClaudeCode, 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. - **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. - **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. - **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 ## 2026-03-29 Fingerprint authentication via greetd multi-stage PAM
- **Who**: Ragnar, Dom - **Who**: ClaudeCode, Dom
- **Why**: moonlock supports fprintd but moongreet rejected multi-stage auth. Users with enrolled fingerprints couldn't use them at the login screen. - **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. - **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). - **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).
@@ -23,7 +51,7 @@
## 2026-03-28 GPU blur via GskBlurNode replaces CPU blur ## 2026-03-28 GPU blur via GskBlurNode replaces CPU blur
- **Who**: Ragnar, Dom - **Who**: ClaudeCode, Dom
- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms2s on 4K wallpapers at cold cache. Disk cache and async orchestration added significant complexity. - **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms2s 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. - **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. - **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.
+6 -3
View File
@@ -12,10 +12,13 @@ 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** — Full greeter UI on all monitors (keyboard input on first)
- **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
- **Fingerprint** — fprintd support via greetd multi-stage PAM (configurable) - **Fingerprint** — fprintd support via greetd multi-stage PAM (configurable)
- **Journal logging** — `journalctl -t moongreet`, debug level via `MOONGREET_DEBUG` env var
- **Password wiping** — Zeroize on drop
## Requirements ## Requirements
@@ -66,8 +69,8 @@ cargo test
# Build release # Build release
cargo build --release cargo build --release
# Run locally (without greetd, needs LD_PRELOAD for layer-shell) # Run locally (without greetd, disables layer-shell)
LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moongreet MOONGREET_NO_LAYER_SHELL=1 ./target/release/moongreet
``` ```
## License ## License
+1 -1
View File
@@ -1,5 +1,5 @@
// ABOUTME: Build script for compiling GResource bundle. // ABOUTME: Build script for compiling GResource bundle.
// ABOUTME: Bundles style.css, wallpaper.jpg, and default-avatar.svg into the binary. // ABOUTME: Bundles style.css and default-avatar.svg into the binary.
fn main() { fn main() {
glib_build_tools::compile_resources( glib_build_tools::compile_resources(
+4 -2
View File
@@ -4,5 +4,7 @@
[appearance] [appearance]
# 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 = "Colloid-Catppuccin" # 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"
+2 -2
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<gresources> <gresources>
<gresource prefix="/dev/moonarch/moongreet"> <gresource prefix="/dev/moonarch/moongreet">
<file>style.css</file> <file compressed="true">style.css</file>
<file>default-avatar.svg</file> <file compressed="true">default-avatar.svg</file>
</gresource> </gresource>
</gresources> </gresources>
+13 -13
View File
@@ -1,16 +1,16 @@
/* ABOUTME: GTK4 CSS stylesheet for the Moongreet greeter. */ /* ABOUTME: GTK4 CSS stylesheet for the Moongreet greeter. */
/* ABOUTME: Defines styling for the login screen layout. */ /* ABOUTME: Uses GTK theme colors for consistency with the active desktop theme. */
/* Main window background */ /* Main window background */
window.greeter { window.greeter {
background-color: #1a1a2e; background-color: @theme_bg_color;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
} }
/* Wallpaper-only window for secondary monitors */ /* Wallpaper-only window for secondary monitors */
window.wallpaper { window.wallpaper {
background-color: #1a1a2e; background-color: @theme_bg_color;
} }
/* Central login area */ /* Central login area */
@@ -26,14 +26,14 @@ window.wallpaper {
min-width: 128px; min-width: 128px;
min-height: 128px; min-height: 128px;
background-color: @theme_selected_bg_color; background-color: @theme_selected_bg_color;
border: 3px solid alpha(white, 0.3); border: 3px solid alpha(@theme_fg_color, 0.3);
} }
/* Username label */ /* Username label */
.username-label { .username-label {
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
color: white; color: @theme_fg_color;
margin-top: 12px; margin-top: 12px;
margin-bottom: 40px; margin-bottom: 40px;
} }
@@ -50,13 +50,13 @@ window.wallpaper {
/* Error message label */ /* Error message label */
.error-label { .error-label {
color: #ff6b6b; color: @error_color;
font-size: 14px; font-size: 14px;
} }
/* Fingerprint prompt label */ /* Fingerprint prompt label */
.fingerprint-label { .fingerprint-label {
color: alpha(white, 0.6); color: alpha(@theme_fg_color, 0.6);
font-size: 13px; font-size: 13px;
margin-top: 8px; margin-top: 8px;
} }
@@ -70,16 +70,16 @@ window.wallpaper {
.user-list-item { .user-list-item {
padding: 8px 16px; padding: 8px 16px;
border-radius: 8px; border-radius: 8px;
color: white; color: @theme_fg_color;
font-size: 14px; font-size: 14px;
} }
.user-list-item:hover { .user-list-item:hover {
background-color: alpha(white, 0.15); background-color: alpha(@theme_fg_color, 0.15);
} }
.user-list-item:selected { .user-list-item:selected {
background-color: alpha(white, 0.2); background-color: alpha(@theme_fg_color, 0.2);
} }
/* Power buttons on the bottom right */ /* Power buttons on the bottom right */
@@ -88,12 +88,12 @@ window.wallpaper {
min-height: 48px; min-height: 48px;
padding: 0px; padding: 0px;
border-radius: 24px; border-radius: 24px;
background-color: alpha(white, 0.1); background-color: alpha(@theme_fg_color, 0.1);
color: white; color: @theme_fg_color;
border: none; border: none;
margin: 4px; margin: 4px;
} }
.power-button:hover { .power-button:hover {
background-color: alpha(white, 0.25); background-color: alpha(@theme_fg_color, 0.25);
} }
+31 -21
View File
@@ -133,6 +133,10 @@ pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
// -- GPU blur via GskBlurNode ------------------------------------------------- // -- GPU blur via GskBlurNode -------------------------------------------------
// SYNC: MAX_BLUR_DIMENSION, render_blurred_texture, and create_background_picture
// are duplicated in moonlock/src/lockscreen.rs and moonset/src/panel.rs.
// Changes here must be mirrored to the other two projects.
/// Maximum texture dimension before downscaling for blur. /// Maximum texture dimension before downscaling for blur.
/// Keeps GPU work reasonable on 4K+ displays. /// Keeps GPU work reasonable on 4K+ displays.
const MAX_BLUR_DIMENSION: f32 = 1920.0; const MAX_BLUR_DIMENSION: f32 = 1920.0;
@@ -184,24 +188,6 @@ 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 across all monitors.
fn create_background_picture( fn create_background_picture(
@@ -240,6 +226,7 @@ struct GreeterState {
default_avatar_texture: Option<gdk::Texture>, default_avatar_texture: Option<gdk::Texture>,
failed_attempts: HashMap<String, u32>, failed_attempts: HashMap<String, u32>,
greetd_sock: Arc<Mutex<Option<UnixStream>>>, greetd_sock: Arc<Mutex<Option<UnixStream>>>,
greetd_sock_path: Option<String>,
login_cancelled: Arc<std::sync::atomic::AtomicBool>, login_cancelled: Arc<std::sync::atomic::AtomicBool>,
fingerprint_available: bool, fingerprint_available: bool,
/// Incremented on each user switch to discard stale async results. /// Incremented on each user switch to discard stale async results.
@@ -281,12 +268,16 @@ pub fn create_greeter_window(
log::debug!("GTK theme: {theme}"); log::debug!("GTK theme: {theme}");
} }
// Cache GREETD_SOCK at startup — it never changes during runtime
let greetd_sock_path = std::env::var("GREETD_SOCK").ok().filter(|p| !p.is_empty());
let state = Rc::new(RefCell::new(GreeterState { let state = Rc::new(RefCell::new(GreeterState {
selected_user: None, selected_user: None,
avatar_cache: HashMap::new(), avatar_cache: HashMap::new(),
default_avatar_texture: None, default_avatar_texture: None,
failed_attempts: HashMap::new(), failed_attempts: HashMap::new(),
greetd_sock: Arc::new(Mutex::new(None)), greetd_sock: Arc::new(Mutex::new(None)),
greetd_sock_path,
login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)), login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
fingerprint_available: false, fingerprint_available: false,
user_switch_generation: 0, user_switch_generation: 0,
@@ -544,6 +535,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]
@@ -960,9 +963,9 @@ fn attempt_login(
session_dropdown: &gtk::DropDown, session_dropdown: &gtk::DropDown,
) { ) {
log::debug!("Login attempt for user: {}", user.username); log::debug!("Login attempt for user: {}", user.username);
let sock_path = match std::env::var("GREETD_SOCK") { let sock_path = match state.borrow().greetd_sock_path.clone() {
Ok(p) if !p.is_empty() => p, Some(p) => p,
_ => { None => {
show_error(error_label, password_entry, strings.greetd_sock_not_set); show_error(error_label, password_entry, strings.greetd_sock_not_set);
return; return;
} }
@@ -1009,6 +1012,8 @@ fn attempt_login(
state, state,
async move { async move {
let session_name_clone = session_name.clone(); let session_name_clone = session_name.clone();
// Minimum response time to prevent username enumeration via timing
let login_start = std::time::Instant::now();
let result = gio::spawn_blocking(move || { let result = gio::spawn_blocking(move || {
login_worker( login_worker(
&username, &username,
@@ -1022,6 +1027,11 @@ fn attempt_login(
) )
}) })
.await; .await;
let elapsed = login_start.elapsed();
let min_response = std::time::Duration::from_millis(500);
if elapsed < min_response {
glib::timeout_future(min_response - elapsed).await;
}
match result { match result {
Ok(Ok(LoginResult::Success { username })) => { Ok(Ok(LoginResult::Success { username })) => {
+11 -14
View File
@@ -63,30 +63,27 @@ 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); // One greeter window per monitor — only the first gets keyboard input
}
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(); let monitors = display.monitors();
log::debug!("Monitor count: {}", monitors.n_items()); log::debug!("Monitor count: {}", monitors.n_items());
let mut first = true;
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(texture, config.background_blur, &blur_cache, app); let window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom); setup_layer_shell(&window, first, gtk4_layer_shell::Layer::Top);
wallpaper.set_monitor(Some(&monitor)); window.set_monitor(Some(&monitor));
wallpaper.present(); window.present();
first = false;
} }
} }
} 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();
} }
} }
+70 -19
View File
@@ -2,11 +2,18 @@
// ABOUTME: Wrappers around system commands for the greeter UI. // ABOUTME: Wrappers around system commands for the greeter UI.
use std::fmt; use std::fmt;
use std::process::Command; use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
const POWER_TIMEOUT: Duration = Duration::from_secs(30);
#[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 {
@@ -15,41 +22,79 @@ 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")
}
} }
} }
} }
impl std::error::Error for PowerError {} impl std::error::Error for PowerError {}
/// Run a command and return a PowerError on failure. /// Run a command with timeout and return a PowerError on failure.
///
/// Uses blocking `child.wait()` with a separate timeout thread that sends
/// SIGKILL after POWER_TIMEOUT. This runs inside `gio::spawn_blocking`,
/// so blocking is expected.
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:?})"); log::debug!("Power action: {action} ({program} {args:?})");
let child = Command::new(program) let mut child = Command::new(program)
.args(args) .args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn() .spawn()
.map_err(|e| PowerError::CommandFailed { .map_err(|e| PowerError::CommandFailed {
action, action,
message: e.to_string(), message: e.to_string(),
})?; })?;
let output = child let child_pid = nix::unistd::Pid::from_raw(child.id() as i32);
.wait_with_output() let done = Arc::new(AtomicBool::new(false));
.map_err(|e| PowerError::CommandFailed { let done_clone = done.clone();
action,
message: e.to_string(),
})?;
if output.status.success() { let timeout_thread = std::thread::spawn(move || {
log::debug!("Power action {action} completed successfully"); let interval = Duration::from_millis(100);
let mut elapsed = Duration::ZERO;
while elapsed < POWER_TIMEOUT {
std::thread::sleep(interval);
if done_clone.load(Ordering::Relaxed) {
return;
}
elapsed += interval;
}
// ESRCH if the process already exited — harmless
let _ = nix::sys::signal::kill(child_pid, nix::sys::signal::Signal::SIGKILL);
});
let status = child.wait().map_err(|e| PowerError::CommandFailed {
action,
message: e.to_string(),
})?;
done.store(true, Ordering::Relaxed);
let _ = timeout_thread.join();
if status.success() {
log::debug!("Power action {action} completed");
Ok(())
} else { } else {
let stderr = String::from_utf8_lossy(&output.stderr); #[cfg(unix)]
return Err(PowerError::CommandFailed { {
action, use std::os::unix::process::ExitStatusExt;
message: format!("exit code {}: {}", output.status, stderr.trim()), if status.signal() == Some(9) {
}); return Err(PowerError::Timeout { action });
} }
}
Ok(()) let mut stderr_buf = String::new();
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_string(&mut stderr_buf);
}
Err(PowerError::CommandFailed {
action,
message: format!("exit code {}: {}", status, stderr_buf.trim()),
})
}
} }
/// Reboot the system via loginctl. /// Reboot the system via loginctl.
@@ -75,6 +120,12 @@ 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", &[]);
@@ -99,7 +150,7 @@ mod tests {
#[test] #[test]
fn run_command_passes_args() { fn run_command_passes_args() {
let result = run_command("test", "true", &["--ignored-arg"]); let result = run_command("test", "echo", &["hello", "world"]);
assert!(result.is_ok()); assert!(result.is_ok());
} }
} }