Compare commits
9 Commits
v0.7.3
...
ce9f2196ca
| Author | SHA1 | Date | |
|---|---|---|---|
| ce9f2196ca | |||
| 29ce185886 | |||
| 91b4289748 | |||
| 97165d94f8 | |||
| 48d363bb18 | |||
| 448e4212e3 | |||
| cd42df1095 | |||
| f7e258d402 | |||
| de97d6658e |
@@ -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 0–200)
|
- `config.rs` — TOML-Config ([appearance] background, gtk-theme, cursor-theme, cursor-size, fingerprint-enabled) + Wallpaper-Fallback + Blur-Validierung (finite, clamp 0–200) + Cursor-Size-Validierung (range 1–256)
|
||||||
- `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.0–v0.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
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.7.3"
|
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"
|
||||||
|
|||||||
+52
-3
@@ -1,5 +1,54 @@
|
|||||||
# 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 1–256). `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
|
## 2026-04-06 – Restore explicit gtk-theme in moongreet config
|
||||||
|
|
||||||
- **Who**: ClaudeCode, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
@@ -37,7 +86,7 @@
|
|||||||
|
|
||||||
## 2026-03-28 – Remove embedded wallpaper from binary
|
## 2026-03-28 – Remove embedded wallpaper from binary
|
||||||
|
|
||||||
- **Who**: Selene, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg. Embedding a 374K JPEG in the binary is redundant. GTK background color (Catppuccin Mocha base) is a clean fallback.
|
- **Why**: Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg. Embedding a 374K JPEG in the binary is redundant. GTK background color (Catppuccin Mocha base) is a clean fallback.
|
||||||
- **Tradeoffs**: Without moonarch installed AND without config, greeter shows plain dark background instead of wallpaper. Acceptable — that's the expected minimal state.
|
- **Tradeoffs**: Without moonarch installed AND without config, greeter shows plain dark background instead of wallpaper. Acceptable — that's the expected minimal state.
|
||||||
- **How**: Remove wallpaper.jpg from GResources, return None from resolve_background_path when no file found, skip wallpaper window creation and background picture when no path available.
|
- **How**: Remove wallpaper.jpg from GResources, return None from resolve_background_path when no file found, skip wallpaper window creation and background picture when no path available.
|
||||||
@@ -51,13 +100,13 @@
|
|||||||
|
|
||||||
## 2026-03-28 – Optional background blur via `image` crate (superseded)
|
## 2026-03-28 – Optional background blur via `image` crate (superseded)
|
||||||
|
|
||||||
- **Who**: Selene, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens
|
- **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens
|
||||||
- **Tradeoffs**: Adds `image` crate dependency (~15 transitive crates); CPU-side Gaussian blur at load time adds startup latency proportional to image size and sigma. Acceptable because blur runs once and the texture is shared across monitors.
|
- **Tradeoffs**: Adds `image` crate dependency (~15 transitive crates); CPU-side Gaussian blur at load time adds startup latency proportional to image size and sigma. Acceptable because blur runs once and the texture is shared across monitors.
|
||||||
- **How**: `load_background_texture(bg_path, blur_radius)` loads texture, optionally applies `imageops::blur()`, returns blurred `gdk::Texture`. Config option `background-blur: Option<f32>` in `[appearance]` TOML section.
|
- **How**: `load_background_texture(bg_path, blur_radius)` loads texture, optionally applies `imageops::blur()`, returns blurred `gdk::Texture`. Config option `background-blur: Option<f32>` in `[appearance]` TOML section.
|
||||||
|
|
||||||
## 2026-03-28 – Audit fixes for shared wallpaper texture (v0.4.1)
|
## 2026-03-28 – Audit fixes for shared wallpaper texture (v0.4.1)
|
||||||
- **Who**: Selene, Dominik
|
- **Who**: ClaudeCode, Dominik
|
||||||
- **Why**: Quality, performance, and security audits flagged issues in `load_background_texture()`, debug logging, and greetd error handling
|
- **Why**: Quality, performance, and security audits flagged issues in `load_background_texture()`, debug logging, and greetd error handling
|
||||||
- **Tradeoffs**: GResource path now requires UTF-8 (returns `None` for non-UTF-8 instead of aborting); 50 MB wallpaper limit is generous but prevents OOM; debug logging off by default trades observability for security
|
- **Tradeoffs**: GResource path now requires UTF-8 (returns `None` for non-UTF-8 instead of aborting); 50 MB wallpaper limit is generous but prevents OOM; debug logging off by default trades observability for security
|
||||||
- **How**: GResource branch via `resources_lookup_data()` + `from_bytes()` (no abort), file size limit, error details only at debug level, `MOONGREET_DEBUG` env var for log level, greetd retry path truncation matching `show_greetd_error()`
|
- **How**: GResource branch via `resources_lookup_data()` + `from_bytes()` (no abort), file size limit, error details only at debug level, `MOONGREET_DEBUG` env var for log level, greetd retry path truncation matching `show_greetd_error()`
|
||||||
|
|||||||
@@ -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,3 +8,8 @@ background = "/usr/share/backgrounds/wallpaper.jpg"
|
|||||||
# GTK theme name — must match a directory in /usr/share/themes/
|
# GTK theme name — must match a directory in /usr/share/themes/
|
||||||
# Required because GTK4 under greetd does not reliably read settings.ini
|
# Required because GTK4 under greetd does not reliably read settings.ini
|
||||||
gtk-theme = "Colloid-Grey-Dark-Catppuccin"
|
gtk-theme = "Colloid-Grey-Dark-Catppuccin"
|
||||||
|
|
||||||
|
# Cursor theme name — must match a directory in /usr/share/icons/
|
||||||
|
# GTK4 under greetd does not honour XCURSOR_THEME, so set it here.
|
||||||
|
cursor-theme = "Sweet-cursors"
|
||||||
|
cursor-size = 24
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
+61
-1
@@ -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 (1–256): {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
@@ -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: >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.
|
/// 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
@@ -24,20 +24,37 @@ fn load_css(display: &gdk::Display) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_layer_shell(window: >k::ApplicationWindow, keyboard: bool, layer: gtk4_layer_shell::Layer) {
|
fn setup_layer_shell(window: >k::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: >k::Application) {
|
fn activate(app: >k::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: >k::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");
|
||||||
|
|||||||
Reference in New Issue
Block a user