Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41228605ad | |||
| b9b6f50974 | |||
| 3a1af6471f |
@@ -40,12 +40,12 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
|
|||||||
- `ipc.rs` — greetd Socket-Kommunikation (4-byte LE header + JSON)
|
- `ipc.rs` — greetd Socket-Kommunikation (4-byte LE header + JSON)
|
||||||
- `users.rs` — Benutzer aus /etc/passwd, Avatare (AccountsService + ~/.face), Symlink-Rejection
|
- `users.rs` — Benutzer aus /etc/passwd, Avatare (AccountsService + ~/.face), Symlink-Rejection
|
||||||
- `sessions.rs` — Wayland/X11 Sessions aus .desktop Files
|
- `sessions.rs` — Wayland/X11 Sessions aus .desktop Files
|
||||||
- `power.rs` — Reboot/Shutdown via loginctl
|
- `power.rs` — Reboot/Shutdown via systemctl (`--no-ask-password`)
|
||||||
- `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen
|
- `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen
|
||||||
- `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback
|
- `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback
|
||||||
- `config.rs` — TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback + Blur-Validierung (finite, clamp 0–200)
|
- `config.rs` — TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback + Blur-Validierung (finite, clamp 0–200)
|
||||||
- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC (Multi-Stage-Auth für fprintd), Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o700 Dirs, 0o600 Files)
|
- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC (Multi-Stage-Auth für fprintd), Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o700 Dirs, 0o600 Files)
|
||||||
- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor mit Hotplug via `items-changed` auf Monitor-ListModel (one greeter window per monitor, first gets keyboard), systemd-journal-logger
|
- `main.rs` — Entry Point, GTK App, Layer Shell Setup, ein Greeter-Fenster auf dem fokussierten Output (kein `set_monitor`), `KeyboardMode::Exclusive`, systemd-journal-logger
|
||||||
- `resources/style.css` — Catppuccin-inspiriertes Theme
|
- `resources/style.css` — Catppuccin-inspiriertes Theme
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|||||||
Generated
+1
-1
@@ -575,7 +575,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.8.4"
|
version = "0.8.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.8.4"
|
version = "0.8.7"
|
||||||
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"
|
||||||
|
|||||||
@@ -1,5 +1,26 @@
|
|||||||
# Decisions
|
# Decisions
|
||||||
|
|
||||||
|
## 2026-06-02 – Power buttons fixed (loginctl→systemctl) + single greeter window (v0.8.7)
|
||||||
|
|
||||||
|
- **Who**: ClaudeCode, Dom
|
||||||
|
- **Why**: At the greeter the reboot and shutdown buttons always failed with "Neustart/Herunterfahren fehlgeschlagen". Root cause: `power.rs` invoked `/usr/bin/loginctl reboot|poweroff`, but `loginctl` has no such verbs (systemd 260: `Unknown command verb 'reboot'`, exit 1) — power-management verbs belong to `systemctl`. moonlock and moonset already used `systemctl`; moongreet was the outlier (moonset carried the same bug until Mar 29). The polkit rule shipped in v0.8.3 treated the wrong layer — `CanReboot` returns `yes`, polkit was never the blocker. Separately, the multi-monitor greeter (v0.8.0/v0.8.2) gave `KeyboardMode::Exclusive` to only the first enumerated monitor's window, so on a multi-output setup the user could not type the password when focused on any other output.
|
||||||
|
- **Tradeoffs**: Dropping the per-monitor + hotplug windows leaves secondary monitors blank during login; irrelevant for a login screen (input happens on one output). Exclusive keyboard binds input to the single greeter surface regardless of pointer position — the mouse may wander to a blank output but typing always reaches the greeter (chosen over compositor-level pointer confinement). The polkit rule is kept as a harmless safety net for the agent-less greeter session; its misleading "session is inactive" comment was corrected.
|
||||||
|
- **How**: (1) `power::reboot`/`shutdown` call `/usr/bin/systemctl --no-ask-password reboot|poweroff` (matches moonlock; `--no-ask-password` fails fast instead of hanging on a missing askpass agent). (2) `main.rs` `activate()` creates one greeter window with no `set_monitor` (compositor places it on the focused output, like moonset) and `KeyboardMode::Exclusive`; the monitor loop, `connect_items_changed` hotplug handler, and the now-unused `glib::clone`/`std::rc::Rc` imports are removed. (3) The missing journal entries were investigated and are **not** a logging bug — the greeter user delivers all priorities to journald (verified live); the two button errors were lost because boot -2 was hard-cut before journald's 5-minute sync.
|
||||||
|
|
||||||
|
## 2026-04-24 – Audit LOW fixes: stdout null, utf-8 path, debug value, hidden sessions (v0.8.6)
|
||||||
|
|
||||||
|
- **Who**: ClaudeCode, Dom
|
||||||
|
- **Why**: Four LOW findings cleared in a single pass. (1) `power::run_command` piped stdout it never read — structurally fragile even though current callers stay well under the pipe buffer. (2) Relative wallpaper paths were resolved via `to_string_lossy`, silently substituting `U+FFFD` for non-UTF-8 bytes and producing a path that cannot be opened. (3) `MOONGREET_DEBUG` escalated log verbosity on mere presence, so an empty variable leaked auth metadata into the journal. (4) `Hidden=true` and `NoDisplay=true` `.desktop` entries appeared in the session dropdown even though they mark disabled or stub sessions.
|
||||||
|
- **Tradeoffs**: Gating debug on the literal value `"1"` is slightly stricter than most tools but matches the security-first posture. Filtering Hidden/NoDisplay means legitimately hidden but functional sessions are now unselectable from the greeter — acceptable, that is the convention these keys signal.
|
||||||
|
- **How**: (1) `.stdout(Stdio::null())` replaces the unused pipe. (2) `to_string_lossy().to_string()` replaced by `to_str().map(|s| s.to_string())` with a `log::warn!` fallback for non-UTF-8 paths. (3) `match std::env::var("MOONGREET_DEBUG").ok().as_deref()` → `Some("1")` selects Debug, everything else Info. (4) `parse_desktop_file` reads `Hidden=` and `NoDisplay=`, returns `None` if either is `true`.
|
||||||
|
|
||||||
|
## 2026-04-24 – Audit MEDIUM fixes: FP double-init, async avatar, symlink, FD leak (v0.8.5)
|
||||||
|
|
||||||
|
- **Who**: ClaudeCode, Dom
|
||||||
|
- **Why**: Six MEDIUM findings: (1) i18n test `all_string_fields_nonempty` missed four string fields — future locales could ship empty strings unnoticed. (2) Fast user-switch could spawn two parallel fprintd `init_async` calls because both coroutines saw `fingerprint_probe = None` before either stored its probe. (3) Synchronous avatar decode via `Pixbuf::from_file_at_scale` on the GTK main thread, stalling clicks. (4) Wallpaper `MAX_WALLPAPER_FILE_SIZE = 50 MB` bounded decode at up to ~2 s. (5) Fallback wallpaper path used `is_file()` which follows symlinks, inconsistent with the symlink-rejecting user-config path. (6) After a failed login the cloned `greetd_sock` descriptor remained in shared state until the next user switch, accumulating stale FDs across retries.
|
||||||
|
- **Tradeoffs**: The init-race guard uses a bool flag on `GreeterState` + a 25 ms polling yield — cheap and race-free, but introduces a very short latency when a second probe waits. Lowering `MAX_WALLPAPER_FILE_SIZE` to 10 MB and `MAX_AVATAR_FILE_SIZE` to 5 MB caps worst-case decode but rejects legitimately huge (4K raw) wallpapers; acceptable for a greeter. Async avatar decode shows the default icon for a frame or two on cache miss.
|
||||||
|
- **How**: (1) Four new `assert!` lines in `i18n::tests::all_string_fields_nonempty`. (2) New `fingerprint_probe_initializing: bool` on `GreeterState`, atomic check-and-set under `borrow_mut`, losing coroutines yield via `glib::timeout_future` until the winning init completes. (3) `set_avatar_from_file` uses `gio::File::read_future` + `Pixbuf::from_stream_at_scale_future` inside a `glib::spawn_future_local`, sets the default icon first, swaps on success. (4) Lower both size constants. (5) `resolve_background_path_with` now applies the same `symlink_metadata` + `!is_symlink` check to the Moonarch fallback. (6) After the login worker returns, `state.greetd_sock.lock().take()` drops the stale clone regardless of login outcome.
|
||||||
|
|
||||||
## 2026-04-24 – Audit fix: shrink password-in-memory window (v0.8.4)
|
## 2026-04-24 – Audit fix: shrink password-in-memory window (v0.8.4)
|
||||||
|
|
||||||
- **Who**: ClaudeCode, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// ABOUTME: Allow the greeter user to reboot and power off without authentication.
|
// ABOUTME: Allow the greeter user to reboot and power off without authentication.
|
||||||
// ABOUTME: Required because greetd's greeter session is inactive in logind.
|
// ABOUTME: Safety net for the agent-less greeter session — no askpass/polkit agent to answer a challenge.
|
||||||
|
|
||||||
polkit.addRule(function(action, subject) {
|
polkit.addRule(function(action, subject) {
|
||||||
if (subject.user === "greeter" &&
|
if (subject.user === "greeter" &&
|
||||||
|
|||||||
+16
-6
@@ -68,8 +68,14 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
|||||||
if bg_path.is_absolute() {
|
if bg_path.is_absolute() {
|
||||||
merged.background_path = Some(bg);
|
merged.background_path = Some(bg);
|
||||||
} else if let Some(parent) = path.parent() {
|
} else if let Some(parent) = path.parent() {
|
||||||
merged.background_path =
|
let joined = parent.join(&bg);
|
||||||
Some(parent.join(&bg).to_string_lossy().to_string());
|
match joined.to_str() {
|
||||||
|
Some(s) => merged.background_path = Some(s.to_string()),
|
||||||
|
None => log::warn!(
|
||||||
|
"Ignoring non-UTF-8 background path: {}",
|
||||||
|
joined.display()
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(blur) = appearance.background_blur {
|
if let Some(blur) = appearance.background_blur {
|
||||||
@@ -123,10 +129,14 @@ pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path)
|
|||||||
log::debug!("Wallpaper: config path {} not usable, trying fallbacks", path.display());
|
log::debug!("Wallpaper: config path {} not usable, trying fallbacks", path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moonarch ecosystem default
|
// Moonarch ecosystem default — apply the same symlink rejection as the
|
||||||
if moonarch_wallpaper.is_file() {
|
// user-configured path for defense in depth. The fallback target is a
|
||||||
log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display());
|
// system file, but the caller consumes the result via the same path.
|
||||||
return Some(moonarch_wallpaper.to_path_buf());
|
if let Ok(meta) = moonarch_wallpaper.symlink_metadata() {
|
||||||
|
if meta.is_file() && !meta.file_type().is_symlink() {
|
||||||
|
log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display());
|
||||||
|
return Some(moonarch_wallpaper.to_path_buf());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Wallpaper: no wallpaper found, using GTK background color");
|
log::debug!("Wallpaper: no wallpaper found, using GTK background color");
|
||||||
|
|||||||
+70
-26
@@ -22,8 +22,8 @@ use crate::sessions::{self, Session};
|
|||||||
use crate::users::{self, User};
|
use crate::users::{self, User};
|
||||||
|
|
||||||
const AVATAR_SIZE: i32 = 128;
|
const AVATAR_SIZE: i32 = 128;
|
||||||
const MAX_AVATAR_FILE_SIZE: u64 = 10 * 1024 * 1024;
|
const MAX_AVATAR_FILE_SIZE: u64 = 5 * 1024 * 1024;
|
||||||
const MAX_WALLPAPER_FILE_SIZE: u64 = 50 * 1024 * 1024;
|
const MAX_WALLPAPER_FILE_SIZE: u64 = 10 * 1024 * 1024;
|
||||||
const LAST_USER_PATH: &str = "/var/cache/moongreet/last-user";
|
const LAST_USER_PATH: &str = "/var/cache/moongreet/last-user";
|
||||||
const LAST_SESSION_DIR: &str = "/var/cache/moongreet/last-session";
|
const LAST_SESSION_DIR: &str = "/var/cache/moongreet/last-session";
|
||||||
const MAX_USERNAME_LENGTH: usize = 256;
|
const MAX_USERNAME_LENGTH: usize = 256;
|
||||||
@@ -189,7 +189,7 @@ fn render_blurred_texture(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a Picture widget for the wallpaper background, optionally with GPU blur.
|
/// Create a Picture widget for the wallpaper background, optionally with GPU blur.
|
||||||
/// Uses `blur_cache` to compute the blurred texture only once across all monitors.
|
/// Uses `blur_cache` to compute the blurred texture only once and reuse it.
|
||||||
fn create_background_picture(
|
fn create_background_picture(
|
||||||
texture: &gdk::Texture,
|
texture: &gdk::Texture,
|
||||||
blur_radius: Option<f32>,
|
blur_radius: Option<f32>,
|
||||||
@@ -233,6 +233,9 @@ struct GreeterState {
|
|||||||
user_switch_generation: u64,
|
user_switch_generation: u64,
|
||||||
/// Cached fprintd device proxy — initialized once on first use.
|
/// Cached fprintd device proxy — initialized once on first use.
|
||||||
fingerprint_probe: Option<crate::fingerprint::FingerprintProbe>,
|
fingerprint_probe: Option<crate::fingerprint::FingerprintProbe>,
|
||||||
|
/// True while a probe init_async() is in flight. Prevents duplicate D-Bus
|
||||||
|
/// init when two user-switch probes race (both see probe == None).
|
||||||
|
fingerprint_probe_initializing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create the main greeter window with login UI.
|
/// Create the main greeter window with login UI.
|
||||||
@@ -282,6 +285,7 @@ pub fn create_greeter_window(
|
|||||||
fingerprint_available: false,
|
fingerprint_available: false,
|
||||||
user_switch_generation: 0,
|
user_switch_generation: 0,
|
||||||
fingerprint_probe: None,
|
fingerprint_probe: None,
|
||||||
|
fingerprint_probe_initializing: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Root overlay for layering
|
// Root overlay for layering
|
||||||
@@ -720,12 +724,33 @@ fn switch_to_user(
|
|||||||
#[strong]
|
#[strong]
|
||||||
state,
|
state,
|
||||||
async move {
|
async move {
|
||||||
// Initialize probe on first use, then reuse cached device proxy
|
// Initialize probe on first use, then reuse cached device proxy.
|
||||||
let needs_init = state.borrow().fingerprint_probe.is_none();
|
// Atomic check-and-set on fingerprint_probe_initializing prevents
|
||||||
if needs_init {
|
// two concurrent probes (from a fast user switch) from both
|
||||||
|
// running init_async, which would open duplicate D-Bus connections.
|
||||||
|
let should_init = {
|
||||||
|
let mut s = state.borrow_mut();
|
||||||
|
if s.fingerprint_probe.is_some() || s.fingerprint_probe_initializing {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
s.fingerprint_probe_initializing = true;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_init {
|
||||||
let mut probe = crate::fingerprint::FingerprintProbe::new();
|
let mut probe = crate::fingerprint::FingerprintProbe::new();
|
||||||
probe.init_async().await;
|
probe.init_async().await;
|
||||||
state.borrow_mut().fingerprint_probe = Some(probe);
|
let mut s = state.borrow_mut();
|
||||||
|
s.fingerprint_probe = Some(probe);
|
||||||
|
s.fingerprint_probe_initializing = false;
|
||||||
|
} else {
|
||||||
|
// Another coroutine is initializing — yield until it publishes.
|
||||||
|
while state.borrow().fingerprint_probe.is_none()
|
||||||
|
&& state.borrow().fingerprint_probe_initializing
|
||||||
|
{
|
||||||
|
glib::timeout_future(std::time::Duration::from_millis(25)).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take probe out of state to avoid holding borrow across await
|
// Take probe out of state to avoid holding borrow across await
|
||||||
@@ -782,28 +807,40 @@ fn set_avatar_from_file(
|
|||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(path_str) = path.to_str() else {
|
// Show fallback immediately; decode asynchronously via GIO so the greeter
|
||||||
log::debug!("Non-UTF-8 avatar path, skipping: {}", path.display());
|
// stays responsive during a user-switch click.
|
||||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
match Pixbuf::from_file_at_scale(path_str, AVATAR_SIZE, AVATAR_SIZE, true) {
|
let display_path = path.to_path_buf();
|
||||||
Ok(pixbuf) => {
|
let file = gio::File::for_path(path);
|
||||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
let image_clone = image.clone();
|
||||||
if let Some(name) = username {
|
let state_clone = state.clone();
|
||||||
state
|
let username_owned = username.map(String::from);
|
||||||
.borrow_mut()
|
|
||||||
.avatar_cache
|
glib::spawn_future_local(async move {
|
||||||
.insert(name.to_string(), texture.clone());
|
let stream = match file.read_future(glib::Priority::default()).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("Failed to open avatar {}: {e}", display_path.display());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match Pixbuf::from_stream_at_scale_future(&stream, AVATAR_SIZE, AVATAR_SIZE, true).await {
|
||||||
|
Ok(pixbuf) => {
|
||||||
|
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||||
|
if let Some(ref name) = username_owned {
|
||||||
|
state_clone
|
||||||
|
.borrow_mut()
|
||||||
|
.avatar_cache
|
||||||
|
.insert(name.clone(), texture.clone());
|
||||||
|
}
|
||||||
|
image_clone.set_paintable(Some(&texture));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("Failed to decode avatar {}: {e}", display_path.display());
|
||||||
}
|
}
|
||||||
image.set_paintable(Some(&texture));
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
});
|
||||||
log::debug!("Failed to load avatar {}: {e}", path.display());
|
|
||||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the default avatar SVG from GResources, tinted with the foreground color.
|
/// Load the default avatar SVG from GResources, tinted with the foreground color.
|
||||||
@@ -1036,6 +1073,13 @@ fn attempt_login(
|
|||||||
glib::timeout_future(min_response - elapsed).await;
|
glib::timeout_future(min_response - elapsed).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The login_worker's own socket is already dropped by now; drop the
|
||||||
|
// shared clone too so repeated failed attempts do not accumulate
|
||||||
|
// stale file descriptors in state.greetd_sock.
|
||||||
|
if let Ok(mut g) = state.borrow().greetd_sock.lock() {
|
||||||
|
g.take();
|
||||||
|
}
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(LoginResult::Success { username })) => {
|
Ok(Ok(LoginResult::Success { username })) => {
|
||||||
save_last_user(&username);
|
save_last_user(&username);
|
||||||
|
|||||||
@@ -286,6 +286,10 @@ mod tests {
|
|||||||
assert!(!s.faillock_attempts_remaining.is_empty(), "{locale}: faillock_attempts_remaining");
|
assert!(!s.faillock_attempts_remaining.is_empty(), "{locale}: faillock_attempts_remaining");
|
||||||
assert!(!s.faillock_locked.is_empty(), "{locale}: faillock_locked");
|
assert!(!s.faillock_locked.is_empty(), "{locale}: faillock_locked");
|
||||||
assert!(!s.unexpected_greetd_response.is_empty(), "{locale}: unexpected_greetd_response");
|
assert!(!s.unexpected_greetd_response.is_empty(), "{locale}: unexpected_greetd_response");
|
||||||
|
assert!(!s.greetd_sock_not_absolute.is_empty(), "{locale}: greetd_sock_not_absolute");
|
||||||
|
assert!(!s.invalid_session_command.is_empty(), "{locale}: invalid_session_command");
|
||||||
|
assert!(!s.session_start_failed.is_empty(), "{locale}: session_start_failed");
|
||||||
|
assert!(!s.socket_error.is_empty(), "{locale}: socket_error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-54
@@ -1,5 +1,5 @@
|
|||||||
// ABOUTME: Entry point for Moongreet — greetd greeter for Wayland.
|
// ABOUTME: Entry point for Moongreet — greetd greeter for Wayland.
|
||||||
// ABOUTME: Sets up GTK Application, Layer Shell, CSS, and multi-monitor windows.
|
// ABOUTME: Sets up GTK Application, Layer Shell, CSS, and a single greeter window.
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod fingerprint;
|
mod fingerprint;
|
||||||
@@ -11,11 +11,9 @@ mod sessions;
|
|||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
use gdk4 as gdk;
|
use gdk4 as gdk;
|
||||||
use glib::clone;
|
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use gtk4::{self as gtk, gio};
|
use gtk4::{self as gtk, gio};
|
||||||
use gtk4_layer_shell::LayerShell;
|
use gtk4_layer_shell::LayerShell;
|
||||||
use std::rc::Rc;
|
|
||||||
fn load_css(display: &gdk::Display) {
|
fn load_css(display: &gdk::Display) {
|
||||||
let css_provider = gtk::CssProvider::new();
|
let css_provider = gtk::CssProvider::new();
|
||||||
css_provider.load_from_resource("/dev/moonarch/moongreet/style.css");
|
css_provider.load_from_resource("/dev/moonarch/moongreet/style.css");
|
||||||
@@ -26,13 +24,11 @@ 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::Exclusive);
|
||||||
window.set_keyboard_mode(gtk4_layer_shell::KeyboardMode::Exclusive);
|
|
||||||
}
|
|
||||||
// Anchor to all edges for fullscreen
|
// Anchor to all edges for fullscreen
|
||||||
window.set_anchor(gtk4_layer_shell::Edge::Top, true);
|
window.set_anchor(gtk4_layer_shell::Edge::Top, true);
|
||||||
window.set_anchor(gtk4_layer_shell::Edge::Bottom, true);
|
window.set_anchor(gtk4_layer_shell::Edge::Bottom, true);
|
||||||
@@ -66,49 +62,15 @@ fn activate(app: >k::Application) {
|
|||||||
log::debug!("Layer shell: {use_layer_shell}");
|
log::debug!("Layer shell: {use_layer_shell}");
|
||||||
|
|
||||||
if use_layer_shell {
|
if use_layer_shell {
|
||||||
// One greeter window per monitor — only the first gets keyboard input
|
// Single greeter window. No set_monitor — the compositor places it on the
|
||||||
let monitors = display.monitors();
|
// focused output (same as moonset). Exclusive keyboard binds input to this
|
||||||
log::debug!("Monitor count: {}", monitors.n_items());
|
// surface regardless of pointer position; the mouse may wander to other
|
||||||
let mut first = true;
|
// outputs but typing always reaches the greeter. The previous per-monitor
|
||||||
for i in 0..monitors.n_items() {
|
// approach gave keyboard only to the first monitor's window, so a user on
|
||||||
if let Some(monitor) = monitors
|
// any other output could not type the password.
|
||||||
.item(i)
|
let window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
|
||||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
setup_layer_shell(&window, gtk4_layer_shell::Layer::Top);
|
||||||
{
|
window.present();
|
||||||
let window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
|
|
||||||
setup_layer_shell(&window, first, gtk4_layer_shell::Layer::Top);
|
|
||||||
window.set_monitor(Some(&monitor));
|
|
||||||
window.present();
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle monitor hotplug — create greeter windows for newly added monitors
|
|
||||||
// (without keyboard, since the primary monitor already has it)
|
|
||||||
let bg_texture = Rc::new(bg_texture);
|
|
||||||
let config = Rc::new(config);
|
|
||||||
monitors.connect_items_changed(clone!(
|
|
||||||
#[weak]
|
|
||||||
app,
|
|
||||||
#[strong]
|
|
||||||
blur_cache,
|
|
||||||
move |list, position, _removed, added| {
|
|
||||||
for i in position..position + added {
|
|
||||||
if let Some(monitor) = list
|
|
||||||
.item(i)
|
|
||||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
|
||||||
{
|
|
||||||
log::debug!("Monitor hotplug: creating greeter window");
|
|
||||||
let window = greeter::create_greeter_window(
|
|
||||||
bg_texture.as_ref().as_ref(), &config, &blur_cache, &app,
|
|
||||||
);
|
|
||||||
setup_layer_shell(&window, false, gtk4_layer_shell::Layer::Top);
|
|
||||||
window.set_monitor(Some(&monitor));
|
|
||||||
window.present();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
// No layer shell — single window for development
|
// No layer shell — single window for development
|
||||||
let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
|
let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
|
||||||
@@ -127,10 +89,12 @@ fn setup_logging() {
|
|||||||
eprintln!("Failed to create journal logger: {e}");
|
eprintln!("Failed to create journal logger: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let level = if std::env::var("MOONGREET_DEBUG").is_ok() {
|
// Require MOONGREET_DEBUG=1 to raise verbosity. Mere presence (e.g. an
|
||||||
log::LevelFilter::Debug
|
// empty value in a session-setup script) must not escalate the journal
|
||||||
} else {
|
// to Debug, which leaks socket paths, usernames, and auth round counts.
|
||||||
log::LevelFilter::Info
|
let level = match std::env::var("MOONGREET_DEBUG").ok().as_deref() {
|
||||||
|
Some("1") => log::LevelFilter::Debug,
|
||||||
|
_ => log::LevelFilter::Info,
|
||||||
};
|
};
|
||||||
log::set_max_level(level);
|
log::set_max_level(level);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-6
@@ -1,4 +1,4 @@
|
|||||||
// ABOUTME: Power actions — reboot and shutdown via loginctl.
|
// ABOUTME: Power actions — reboot and shutdown via systemctl.
|
||||||
// ABOUTME: Wrappers around system commands for the greeter UI.
|
// ABOUTME: Wrappers around system commands for the greeter UI.
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -40,7 +40,9 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
|
|||||||
log::debug!("Power action: {action} ({program} {args:?})");
|
log::debug!("Power action: {action} ({program} {args:?})");
|
||||||
let mut child = Command::new(program)
|
let mut child = Command::new(program)
|
||||||
.args(args)
|
.args(args)
|
||||||
.stdout(Stdio::piped())
|
// stdout is never read; piping without draining would deadlock on any
|
||||||
|
// command that writes more than one OS pipe buffer before wait() returns.
|
||||||
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| PowerError::CommandFailed {
|
.map_err(|e| PowerError::CommandFailed {
|
||||||
@@ -97,14 +99,21 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reboot the system via loginctl.
|
/// Reboot the system via systemctl.
|
||||||
|
///
|
||||||
|
/// `--no-ask-password` keeps systemctl from spawning an interactive askpass
|
||||||
|
/// agent — the greeter session has none, so without it a denied authorization
|
||||||
|
/// would hang instead of failing fast.
|
||||||
pub fn reboot() -> Result<(), PowerError> {
|
pub fn reboot() -> Result<(), PowerError> {
|
||||||
run_command("reboot", "/usr/bin/loginctl", &["reboot"])
|
run_command("reboot", "/usr/bin/systemctl", &["--no-ask-password", "reboot"])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shut down the system via loginctl.
|
/// Shut down the system via systemctl.
|
||||||
|
///
|
||||||
|
/// `--no-ask-password` for the same reason as [`reboot`] — the agent-less
|
||||||
|
/// greeter session has nothing to answer an authorization challenge.
|
||||||
pub fn shutdown() -> Result<(), PowerError> {
|
pub fn shutdown() -> Result<(), PowerError> {
|
||||||
run_command("shutdown", "/usr/bin/loginctl", &["poweroff"])
|
run_command("shutdown", "/usr/bin/systemctl", &["--no-ask-password", "poweroff"])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
|
|||||||
let mut in_section = false;
|
let mut in_section = false;
|
||||||
let mut name: Option<String> = None;
|
let mut name: Option<String> = None;
|
||||||
let mut exec_cmd: Option<String> = None;
|
let mut exec_cmd: Option<String> = None;
|
||||||
|
let mut hidden = false;
|
||||||
|
let mut no_display = false;
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
@@ -44,9 +46,18 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
|
|||||||
&& exec_cmd.is_none()
|
&& exec_cmd.is_none()
|
||||||
{
|
{
|
||||||
exec_cmd = Some(value.to_string());
|
exec_cmd = Some(value.to_string());
|
||||||
|
} else if let Some(value) = line.strip_prefix("Hidden=") {
|
||||||
|
hidden = value.eq_ignore_ascii_case("true");
|
||||||
|
} else if let Some(value) = line.strip_prefix("NoDisplay=") {
|
||||||
|
no_display = value.eq_ignore_ascii_case("true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hidden || no_display {
|
||||||
|
log::debug!("Skipping {}: Hidden/NoDisplay entry", path.display());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let name = name.filter(|s| !s.is_empty());
|
let name = name.filter(|s| !s.is_empty());
|
||||||
let exec_cmd = exec_cmd.filter(|s| !s.is_empty());
|
let exec_cmd = exec_cmd.filter(|s| !s.is_empty());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user