Compare commits

..

15 Commits

Author SHA1 Message Date
nevaforget ce9f2196ca refactor: single greeter window, drop per-output keyboard grab (v0.10.0)
Update PKGBUILD version / update-pkgver (push) Successful in 2s
The v0.8.0 → v0.8.5 multi-monitor story (login widget on primary only,
wallpaper-only on secondaries, KeyboardMode::Exclusive on the primary
surface, hotplug handler) was a pile of workarounds that kept breaking
on real hardware — after switching the greeter compositor to niri, the
pointer could not cross onto the primary output and keyboard tab did
not reach the UI. Niri in a normal user session has none of these
issues; the bug was moongreet's own output-scoped policy.

Back to basics: one layer-shell window on the built-in display,
KeyboardMode::OnDemand. Secondary outputs stay under compositor
control. No hotplug callbacks, no wallpaper-only windows, no DisplayLink
phantom workarounds.

-81 / +26 lines.
2026-04-24 11:27:48 +02:00
nevaforget 29ce185886 feat: apply cursor theme via GtkSettings instead of XCURSOR_THEME env (v0.9.0)
Update PKGBUILD version / update-pkgver (push) Successful in 2s
GTK4 under greetd does not honour XCURSOR_THEME reliably — the env-prefix
hack in /etc/greetd/config.toml only reached the wlroots pointer in cage,
while GTK widgets kept using the default fallback cursor. Mirror the
existing gtk-theme handling: new cursor-theme + cursor-size fields in the
[appearance] section, applied via gtk::Settings::set_gtk_cursor_theme_*.
Keeps the fix scoped to the greeter, no system-wide GTK config changes.
2026-04-24 08:56:41 +02:00
nevaforget 91b4289748 fix: wallpaper-only windows on secondary monitors (v0.8.5)
Update PKGBUILD version / update-pkgver (push) Successful in 3s
The v0.8.4 keyboard fix only half-worked: keys were still dropped
until the pointer moved to the built-in panel. Niri scopes layer-shell
keyboard routing per active output, so a single Exclusive surface is
not enough when another output is active.

Revert 2026-04-08 partially: only the built-in panel shows the login
widget, other monitors get a wallpaper-only window with
KeyboardMode::None. Hotplugged monitors also get wallpaper-only.
Compositor-agnostic — no Niri IPC.
2026-04-23 14:06:12 +02:00
nevaforget 97165d94f8 fix: keyboard focus on built-in display to avoid evdi phantom grab (v0.8.4)
Update PKGBUILD version / update-pkgver (push) Successful in 3s
DisplayLink/evdi virtual displays enumerate as DVI-I-* before eDP-1 and
were stealing the KeyboardMode::Exclusive grab on the first enumerated
monitor, leaving the visible greeter surfaces without keyboard input.

Introduce pick_primary_monitor_index() that prefers eDP/LVDS/DSI
connectors for the keyboard grab and falls back to index 0 when no
built-in panel is present. Pure, unit-tested; hotplug path unchanged.
2026-04-23 11:02:24 +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
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
11 changed files with 307 additions and 74 deletions
+5 -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.
@@ -45,9 +43,9 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
- `power.rs` — Reboot/Shutdown via loginctl - `power.rs` — Reboot/Shutdown via loginctl
- `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen - `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen
- `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 einziges Greeter-Fenster, verankert am Built-in-Display (via `pick_primary_monitor_index`), `KeyboardMode::OnDemand` — moongreet ist ein normaler layer-shell-client, keine output-scoped policies. Sekundäre Monitore bleiben unter Compositor-Kontrolle. Systemd-journal-logger.
- `resources/style.css` — Catppuccin-inspiriertes Theme - `resources/style.css` — Catppuccin-inspiriertes Theme
## Design Decisions ## Design Decisions
@@ -62,9 +60,11 @@ 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
- **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor - **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor
- **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None` - **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None`
- **Error-Detail-Filterung**: GDK/greetd-Fehlerdetails nur auf `debug!`-Level, `warn!` ohne interne Details — verhindert Systeminfo-Leak ins Journal - **Error-Detail-Filterung**: GDK/greetd-Fehlerdetails nur auf `debug!`-Level, `warn!` ohne interne Details — verhindert Systeminfo-Leak ins Journal
- **Single Greeter Window, keine Output-Policies**: Ein einziges layer-shell-fenster auf dem Built-in-Display, `KeyboardMode::OnDemand`. Sekundäre Outputs bleiben unter Compositor-Kontrolle. Grund: Die output-scoped policies aus v0.8.0v0.8.5 (Exclusive-Keyboard auf Primary, Wallpaper-Only auf Secondaries, Hotplug-Callbacks) haben den Greeter bei realen Multi-Monitor-Setups wiederholt kaputt gemacht (Pointer kommt nicht zum Primary, Keyboard tabt nicht zur UI). Im User-Session-Niri gibt es diese Probleme nicht — moongreet verhält sich jetzt wie jeder normale layer-shell-client.
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.1" version = "0.10.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"
+70 -7
View File
@@ -1,49 +1,112 @@
# Decisions # Decisions
## 2026-04-24 Single greeter window, no per-output keyboard grab (v0.10.0)
- **Who**: ClaudeCode, Dom
- **Why**: The v0.8.0 → v0.8.4 → v0.8.5 sequence accumulated multi-monitor logic (login widget on primary, wallpaper-only on secondaries, `KeyboardMode::Exclusive` on the primary surface) to work around keyboard-routing on specific hardware setups. After the moonarch greeter switched to niri, the symptoms returned: the UI was on eDP-1 but the pointer could not cross onto it, and keyboard tab did not reach the login widget. Niri in a normal user session never behaves like this — the issue was our greeter's self-imposed per-output scope, not the compositor. Every earlier "fix" made it more bespoke instead of making moongreet a well-behaved layer-shell client.
- **Tradeoffs**: Reverts the multi-output story entirely. Secondary monitors get nothing from moongreet — the compositor decides what renders there (black, its own wallpaper, whatever). The "wallpaper on every screen" look is gone. In exchange, cursor and keyboard follow normal niri focus rules, nothing is grabbed, no hotplug callbacks, no DisplayLink phantom workarounds.
- **How**: `main.rs::activate` builds **one** greeter window, anchors it to the built-in display picked by `pick_primary_monitor_index`, and calls `setup_layer_shell` with `KeyboardMode::OnDemand`. The hotplug `connect_items_changed` handler is gone. `create_wallpaper_window` is removed. `setup_layer_shell` no longer takes a `keyboard: bool` — there is only one policy.
## 2026-04-24 Cursor theme via config instead of env (v0.9.0)
- **Who**: ClaudeCode, Dom
- **Why**: Cursor theme in the greeter was the default fallback even with `XCURSOR_THEME=Sweet-cursors` in `/etc/greetd/config.toml`'s `env` prefix. Cage forwards the env, but GTK4 does not honour `XCURSOR_THEME` reliably under greetd — it picks up the theme from `gtk-cursor-theme-name` on `GtkSettings`, and without a session-level settings.ini or GSettings override in the greeter user's home, that property stays at the GTK default. Adding an env-var hack worked for the wlroots pointer rendered by cage, but GTK widgets (button hover, text input) used their own wrong cursor.
- **Tradeoffs**: Adds two config fields (`cursor-theme`, `cursor-size`) — symmetric with the existing `gtk-theme` field and justified by the same cause (GTK4 under greetd ignores the usual discovery paths). Alternative would have been a system-wide `/etc/gtk-4.0/settings.ini` with `gtk-cursor-theme-name=`, but that couples moongreet's appearance to the host system's GTK config and affects every GTK4 app running as any user.
- **How**: `config.rs` gains `cursor_theme: Option<String>` and `cursor_size: Option<i32>` (range-validated 1256). `greeter.rs::create_greeter_window` applies them via `gtk::Settings::set_gtk_cursor_theme_name()` and `set_gtk_cursor_theme_size()` directly after the existing `gtk-theme` handling, reusing `is_valid_gtk_theme()` for name validation. Moonarch's deployed config gains `cursor-theme = "Sweet-cursors"` + `cursor-size = 24`. The env-prefix hack in `/etc/greetd/config.toml` is now redundant.
## 2026-04-23 Wallpaper-only windows on secondary monitors (v0.8.5)
- **Who**: ClaudeCode, Dom
- **Why**: The v0.8.4 fix (keyboard grab on the built-in panel) only half-worked. The greeter still rejected keystrokes until the user moved the mouse to eDP-1 — Niri scopes layer-shell keyboard routing by active output, so even though the primary window was the sole `KeyboardMode::Exclusive` surface, keys went nowhere when another output was active. Hardcoding a compositor focus call (e.g. `niri msg action focus-monitor`) would tie moongreet to a specific compositor.
- **Tradeoffs**: Reverts part of 2026-04-08: only the built-in panel shows the full greeter UI, other monitors go back to wallpaper-only. Users with multiple monitors lose the symmetric "login widget on every screen" look, but gain a reliable keyboard path regardless of which output the compositor considers active at startup. Compositor-agnostic — no Niri-specific IPC.
- **How**: New `create_wallpaper_window()` in `greeter.rs` builds a minimal `ApplicationWindow` with the shared background `Picture` (same `blur_cache` as the primary) and no login widgets. `main.rs` uses `create_greeter_window()` for the index returned by `pick_primary_monitor_index()` and `create_wallpaper_window()` for the rest. Hotplugged monitors also get wallpaper-only windows. Both variants use `Layer::Top`; only the primary sets `KeyboardMode::Exclusive`.
## 2026-04-23 Keyboard focus on built-in display, not first enumerated monitor (v0.8.4)
- **Who**: ClaudeCode, Dom
- **Why**: With a DisplayLink dock attached, the greeter showed its UI on all monitors but the password entry accepted no input. `display.monitors()` enumerated evdi phantom connectors (`DVI-I-*`) before the laptop panel (`eDP-1`); the v0.8.0 logic gave `KeyboardMode::Exclusive` to index 0, so the keyboard grab landed on an invisible surface. Symptom showed up on 2026-04-23 after kernel 6.19.11 → 6.19.12 + moongreet 0.8.0 → 0.8.2 changed evdi enumeration timing — previous Thursdays with the same dock worked.
- **Tradeoffs**: Prefers built-in displays by connector-name pattern (`eDP*`/`LVDS*`/`DSI*`) rather than a generic "primary monitor" concept — Wayland has no portable primary signal, and gdk4's `primary_monitor()` was removed. Pattern-matching covers every current Linux laptop, at the cost of a tiny list to maintain if a new form factor ships a new connector type. Fallback is still index 0, so behavior on desktops without a built-in panel is unchanged.
- **How**: New pure function `pick_primary_monitor_index()` in `main.rs` scans connector names and returns the built-in index (or 0). Used during initial enumeration to decide which window gets `KeyboardMode::Exclusive`. Hotplug branch unchanged — new monitors still get keyboard=false so focus never migrates off the panel. Unit-tested against evdi/eDP/LVDS/DSI/HDMI/DP mixes.
## 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
- **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 ## 2026-03-31 Fourth audit: power timeout, timing mitigation, release profile, GREETD_SOCK caching
- **Who**: Ragnar, Dom - **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. - **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. - **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. - **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).
## 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.
## 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.
## 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** — Login UI on the built-in display, wallpaper-only on other monitors; hotplugged monitors get wallpaper 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
+8 -1
View File
@@ -4,5 +4,12 @@
[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 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" 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: 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;
}
});
+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);
} }
+61 -1
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,
} }
} }
@@ -82,6 +90,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 +116,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
} }
@@ -321,6 +347,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();
+29 -18
View File
@@ -188,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(
@@ -277,6 +259,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);
@@ -553,6 +552,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]
+95 -23
View File
@@ -24,20 +24,37 @@ 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::OnDemand);
window.set_keyboard_mode(gtk4_layer_shell::KeyboardMode::Exclusive);
}
// 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);
window.set_anchor(gtk4_layer_shell::Edge::Left, true); window.set_anchor(gtk4_layer_shell::Edge::Left, true);
window.set_anchor(gtk4_layer_shell::Edge::Right, true); window.set_anchor(gtk4_layer_shell::Edge::Right, true);
} }
/// Pick the index of the built-in display among the given connector names.
///
/// Prefers `eDP*` / `LVDS*` / `DSI*` over anything else — otherwise keyboard
/// focus can land on DisplayLink/evdi phantom displays (connector `DVI-I-*`)
/// that are enumerated before the laptop panel. Falls back to index 0 when
/// no built-in connector is present.
fn pick_primary_monitor_index<'a, I>(connectors: I) -> usize
where
I: IntoIterator<Item = Option<&'a str>>,
{
for (i, conn) in connectors.into_iter().enumerate() {
if let Some(c) = conn
&& (c.starts_with("eDP") || c.starts_with("LVDS") || c.starts_with("DSI"))
{
return i;
}
}
0
}
fn activate(app: &gtk::Application) { fn activate(app: &gtk::Application) {
let display = match gdk::Display::default() { let display = match gdk::Display::default() {
Some(d) => d, Some(d) => d,
@@ -63,30 +80,44 @@ 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 anchored to the built-in display. Other
} // outputs stay under compositor control — the greeter is just a
greeter_window.present(); // normal layer-shell client, no per-output keyboard grabs.
// 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()); let count = monitors.n_items();
for i in 0..monitors.n_items() { log::debug!("Monitor count: {count}");
if let Some(monitor) = monitors
let connectors: Vec<Option<String>> = (0..count)
.map(|i| {
monitors
.item(i) .item(i)
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok()) .and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
.and_then(|m| m.connector())
.map(|gs| gs.to_string())
})
.collect();
let primary_idx = pick_primary_monitor_index(connectors.iter().map(|o| o.as_deref()));
log::debug!(
"Primary monitor: idx={primary_idx} connector={:?}",
connectors.get(primary_idx).and_then(|o| o.as_deref())
);
if let Some(monitor) = monitors
.item(primary_idx as u32)
.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, gtk4_layer_shell::Layer::Top);
wallpaper.set_monitor(Some(&monitor)); window.set_monitor(Some(&monitor));
wallpaper.present(); window.present();
} } else {
log::error!("Primary monitor {primary_idx} not available — greeter will not be shown");
} }
} else {
let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
greeter_window.present();
} }
} }
@@ -109,6 +140,47 @@ fn setup_logging() {
log::set_max_level(level); log::set_max_level(level);
} }
#[cfg(test)]
mod tests {
use super::pick_primary_monitor_index;
#[test]
fn prefers_edp_over_phantom_dvi() {
assert_eq!(
pick_primary_monitor_index([Some("DVI-I-1"), Some("eDP-1"), Some("DVI-I-2")]),
1,
);
}
#[test]
fn prefers_lvds() {
assert_eq!(pick_primary_monitor_index([Some("HDMI-A-1"), Some("LVDS-1")]), 1);
}
#[test]
fn prefers_dsi() {
assert_eq!(pick_primary_monitor_index([Some("DP-1"), Some("DSI-1")]), 1);
}
#[test]
fn falls_back_to_first_without_builtin() {
assert_eq!(
pick_primary_monitor_index([Some("DVI-I-1"), Some("HDMI-A-1")]),
0,
);
}
#[test]
fn skips_missing_connector() {
assert_eq!(pick_primary_monitor_index([None, Some("eDP-1")]), 1);
}
#[test]
fn empty_returns_zero() {
assert_eq!(pick_primary_monitor_index(std::iter::empty::<Option<&str>>()), 0);
}
}
fn main() { fn main() {
setup_logging(); setup_logging();
log::info!("Moongreet starting"); log::info!("Moongreet starting");