From 3c34b4ec2558c91d6bcb328b28c1a31b4d4d3275 Mon Sep 17 00:00:00 2001 From: nevaforget Date: Tue, 2 Jun 2026 13:39:12 +0200 Subject: [PATCH] feat: confirm dialog before power actions (v0.10.0) Reboot/shutdown buttons triggered immediately on click. Now show an inline confirmation prompt, mirroring moonlock's show_power_confirm. - i18n: reboot_confirm, shutdown_confirm, confirm_yes, confirm_no (DE/EN) - greeter: confirm_area in login_box, handlers route through show_power_confirm/dismiss_power_confirm; execute_power_action drops the now-redundant button-disable guard - style: .confirm-label/-yes/-no classes --- CLAUDE.md | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- DECISIONS.md | 7 +++ resources/style.css | 32 ++++++++++++ src/greeter.rs | 123 +++++++++++++++++++++++++++++++++++++++----- src/i18n.rs | 18 +++++++ 7 files changed, 171 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bffb03c..488231b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git--x86_64.pkg.tar.z - `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 - `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, Power-Confirm (Inline-Bestätigung vor Reboot/Shutdown, wie moonlock), Avatar-Cache, Last-User/Last-Session Persistence (0o700 Dirs, 0o600 Files) - `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 diff --git a/Cargo.lock b/Cargo.lock index d5a22f4..b151485 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -575,7 +575,7 @@ dependencies = [ [[package]] name = "moongreet" -version = "0.9.0" +version = "0.10.0" dependencies = [ "gdk-pixbuf", "gdk4", diff --git a/Cargo.toml b/Cargo.toml index ae1556a..f9ed760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moongreet" -version = "0.9.0" +version = "0.10.0" edition = "2024" description = "A greetd greeter for Wayland with GTK4 and Layer Shell" license = "MIT" diff --git a/DECISIONS.md b/DECISIONS.md index 8baf1a7..fc74d32 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,12 @@ # Decisions +## 2026-06-02 – Inline power confirmation before reboot/shutdown (v0.10.0) + +- **Who**: ClaudeCode, Dom +- **Why**: Reboot/Shutdown buttons triggered the action immediately on click — one misclick rebooted the machine from the greeter. moonlock already guards power actions with an inline confirm; moongreet should match. +- **Tradeoffs**: Ported moonlock's `show_power_confirm`/`dismiss_power_confirm` verbatim instead of inventing a new widget — keeps the two codebases symmetric (i18n, CSS classes, focus-on-Cancel behaviour all identical). Dropped the `button` parameter from `execute_power_action`: the old per-button `set_sensitive(false)` double-click guard is now redundant because the confirm box itself blocks accidental re-trigger, and after "Yes" there is no button left to re-enable. +- **How**: Inline confirm box appended to the central `login_box` (mirrors moonlock placement). Reboot/Shutdown handlers call `show_power_confirm`; "Yes" dismisses and runs the action, "Cancel" (focused by default) just dismisses. New i18n strings (`reboot_confirm`, `shutdown_confirm`, `confirm_yes`, `confirm_no`) and `.confirm-*` CSS classes ported from moonlock; `.confirm-no` background adapted to moongreet's `alpha(@theme_fg_color, …)` idiom. + ## 2026-06-02 – Cursor theme via GtkSettings, salvaged from unpushed work (v0.9.0) - **Who**: ClaudeCode, Dom diff --git a/resources/style.css b/resources/style.css index e28d272..b7209bf 100644 --- a/resources/style.css +++ b/resources/style.css @@ -82,6 +82,38 @@ window.wallpaper { background-color: alpha(@theme_fg_color, 0.2); } +/* Power confirmation prompt */ +.confirm-label { + font-size: 16px; + color: @theme_fg_color; + margin-bottom: 4px; +} + +.confirm-yes { + padding: 8px 24px; + border-radius: 8px; + background-color: @error_color; + color: @theme_bg_color; + border: none; + font-weight: bold; +} + +.confirm-yes:hover { + background-color: lighter(@error_color); +} + +.confirm-no { + padding: 8px 24px; + border-radius: 8px; + background-color: alpha(@theme_fg_color, 0.15); + color: @theme_fg_color; + border: none; +} + +.confirm-no:hover { + background-color: alpha(@theme_fg_color, 0.25); +} + /* Power buttons on the bottom right */ .power-button { min-width: 48px; diff --git a/src/greeter.rs b/src/greeter.rs index c953db1..5fadc16 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -373,6 +373,12 @@ pub fn create_greeter_window( error_label.set_visible(false); login_box.append(&error_label); + // Confirm box area (for power confirm) + let confirm_area = gtk::Box::new(gtk::Orientation::Vertical, 0); + confirm_area.set_halign(gtk::Align::Center); + login_box.append(&confirm_area); + let confirm_box: Rc>> = Rc::new(RefCell::new(None)); + // Fingerprint label (hidden until probe confirms availability) let fp_label = gtk::Label::new(None); fp_label.add_css_class("fingerprint-label"); @@ -428,7 +434,12 @@ pub fn create_greeter_window( state, #[strong] sessions_rc, + #[weak] + confirm_area, + #[strong] + confirm_box, move |_| { + dismiss_power_confirm(&confirm_area, &confirm_box); cancel_pending_session(&state); switch_to_user( &user_clone, @@ -466,11 +477,22 @@ pub fn create_greeter_window( reboot_btn.add_css_class("power-button"); reboot_btn.set_tooltip_text(Some(strings.reboot_tooltip)); reboot_btn.connect_clicked(clone!( + #[weak] + confirm_area, + #[strong] + confirm_box, #[weak] error_label, - move |btn| { - btn.set_sensitive(false); - execute_power_action(power::reboot, strings.reboot_failed, &error_label, btn); + move |_| { + show_power_confirm( + strings.reboot_confirm, + power::reboot, + strings.reboot_failed, + strings, + &confirm_area, + &confirm_box, + &error_label, + ); } )); power_box.append(&reboot_btn); @@ -480,11 +502,22 @@ pub fn create_greeter_window( shutdown_btn.add_css_class("power-button"); shutdown_btn.set_tooltip_text(Some(strings.shutdown_tooltip)); shutdown_btn.connect_clicked(clone!( + #[weak] + confirm_area, + #[strong] + confirm_box, #[weak] error_label, - move |btn| { - btn.set_sensitive(false); - execute_power_action(power::shutdown, strings.shutdown_failed, &error_label, btn); + move |_| { + show_power_confirm( + strings.shutdown_confirm, + power::shutdown, + strings.shutdown_failed, + strings, + &confirm_area, + &confirm_box, + &error_label, + ); } )); power_box.append(&shutdown_btn); @@ -539,17 +572,22 @@ pub fn create_greeter_window( } )); - // Keyboard handling — Escape clears password and error + // Keyboard handling — Escape clears password, error, and any open power confirm let key_controller = gtk::EventControllerKey::new(); key_controller.connect_key_pressed(clone!( #[weak] password_entry, #[weak] error_label, + #[weak] + confirm_area, + #[strong] + confirm_box, #[upgrade_or] glib::Propagation::Proceed, move |_, keyval, _, _| { if keyval == gdk::Key::Escape { + dismiss_power_confirm(&confirm_area, &confirm_box); password_entry.set_text(""); error_label.set_visible(false); glib::Propagation::Stop @@ -1314,18 +1352,81 @@ fn login_worker( }) } +/// Show an inline confirmation prompt before executing a power action. +fn show_power_confirm( + message: &'static str, + action_fn: fn() -> Result<(), PowerError>, + error_message: &'static str, + strings: &'static Strings, + confirm_area: >k::Box, + confirm_box: &Rc>>, + error_label: >k::Label, +) { + dismiss_power_confirm(confirm_area, confirm_box); + error_label.set_visible(false); + + let new_box = gtk::Box::new(gtk::Orientation::Vertical, 8); + new_box.set_halign(gtk::Align::Center); + new_box.set_margin_top(16); + + let confirm_label = gtk::Label::new(Some(message)); + confirm_label.add_css_class("confirm-label"); + new_box.append(&confirm_label); + + let button_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); + button_row.set_halign(gtk::Align::Center); + + let yes_btn = gtk::Button::with_label(strings.confirm_yes); + yes_btn.add_css_class("confirm-yes"); + yes_btn.connect_clicked(clone!( + #[weak] + confirm_area, + #[strong] + confirm_box, + #[weak] + error_label, + move |_| { + dismiss_power_confirm(&confirm_area, &confirm_box); + execute_power_action(action_fn, error_message, &error_label); + } + )); + button_row.append(&yes_btn); + + let no_btn = gtk::Button::with_label(strings.confirm_no); + no_btn.add_css_class("confirm-no"); + no_btn.connect_clicked(clone!( + #[weak] + confirm_area, + #[strong] + confirm_box, + move |_| { + dismiss_power_confirm(&confirm_area, &confirm_box); + } + )); + button_row.append(&no_btn); + + new_box.append(&button_row); + confirm_area.append(&new_box); + *confirm_box.borrow_mut() = Some(new_box); + no_btn.grab_focus(); +} + +/// Remove the power confirmation prompt. +fn dismiss_power_confirm(confirm_area: >k::Box, confirm_box: &Rc>>) { + if let Some(box_widget) = confirm_box.borrow_mut().take() { + confirm_area.remove(&box_widget); + } +} + /// Execute a power action in a background thread. fn execute_power_action( action_fn: fn() -> Result<(), PowerError>, error_message: &'static str, error_label: >k::Label, - button: >k::Button, ) { glib::spawn_future_local(clone!( #[weak] error_label, - #[weak] - button, async move { let result = gio::spawn_blocking(action_fn).await; @@ -1335,13 +1436,11 @@ fn execute_power_action( log::error!("Power action failed: {e}"); error_label.set_text(error_message); error_label.set_visible(true); - button.set_sensitive(true); } Err(_) => { log::error!("Power action panicked"); error_label.set_text(error_message); error_label.set_visible(true); - button.set_sensitive(true); } } } diff --git a/src/i18n.rs b/src/i18n.rs index 16719cb..c29a8a7 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -16,6 +16,12 @@ pub struct Strings { pub reboot_tooltip: &'static str, pub shutdown_tooltip: &'static str, + // Power confirmation prompts + pub reboot_confirm: &'static str, + pub shutdown_confirm: &'static str, + pub confirm_yes: &'static str, + pub confirm_no: &'static str, + // Error messages pub no_session_selected: &'static str, pub greetd_sock_not_set: &'static str, @@ -39,6 +45,10 @@ const STRINGS_DE: Strings = Strings { password_placeholder: "Passwort", reboot_tooltip: "Neustart", shutdown_tooltip: "Herunterfahren", + reboot_confirm: "Wirklich neu starten?", + shutdown_confirm: "Wirklich herunterfahren?", + confirm_yes: "Ja", + confirm_no: "Abbrechen", no_session_selected: "Keine Session ausgewählt", greetd_sock_not_set: "GREETD_SOCK nicht gesetzt", greetd_sock_not_absolute: "GREETD_SOCK ist kein absoluter Pfad", @@ -59,6 +69,10 @@ const STRINGS_EN: Strings = Strings { password_placeholder: "Password", reboot_tooltip: "Reboot", shutdown_tooltip: "Shut down", + reboot_confirm: "Really reboot?", + shutdown_confirm: "Really shut down?", + confirm_yes: "Yes", + confirm_no: "Cancel", no_session_selected: "No session selected", greetd_sock_not_set: "GREETD_SOCK not set", greetd_sock_not_absolute: "GREETD_SOCK is not an absolute path", @@ -276,6 +290,10 @@ mod tests { assert!(!s.password_placeholder.is_empty(), "{locale}: password_placeholder"); assert!(!s.reboot_tooltip.is_empty(), "{locale}: reboot_tooltip"); assert!(!s.shutdown_tooltip.is_empty(), "{locale}: shutdown_tooltip"); + assert!(!s.reboot_confirm.is_empty(), "{locale}: reboot_confirm"); + assert!(!s.shutdown_confirm.is_empty(), "{locale}: shutdown_confirm"); + assert!(!s.confirm_yes.is_empty(), "{locale}: confirm_yes"); + assert!(!s.confirm_no.is_empty(), "{locale}: confirm_no"); assert!(!s.no_session_selected.is_empty(), "{locale}: no_session_selected"); assert!(!s.greetd_sock_not_set.is_empty(), "{locale}: greetd_sock_not_set"); assert!(!s.auth_failed.is_empty(), "{locale}: auth_failed");