From a2718ed61c9d1276e8d077797e0a8a1996ba2cb0 Mon Sep 17 00:00:00 2001 From: Graham Boylan Date: Mon, 25 May 2026 23:04:10 -0400 Subject: [PATCH] feat(power): add controls for power button behavior Adds a section to the power submenu allowing the user to select from the following power button behaviors: - Confirm Shutdown (default, existing behavior) - cosmic-osd shutdown - Confirm Log Out - cosmic-osd logout - Confirm Restart - cosmic-osd restart - Shutdown - systemctl poweroff - Log Out - loginctl terminate-user $USER - Restart - systemctl reboot - Suspend - systemctl suspend - Hibernate - systemctl hibernate The default option is 'Confirm Shutdown', unchanged from prior to this commit. If the value at ~/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/system_actions has been customized to some command outside of this list, that value will not be overwritten unless the user selects an item in the list again. Closes #1664 --- cosmic-settings/Cargo.toml | 2 +- .../src/pages/power/backend/mod.rs | 90 +++++++++++++++++++ cosmic-settings/src/pages/power/mod.rs | 54 ++++++++++- i18n/en/cosmic_settings.ftl | 20 +++++ 4 files changed, 164 insertions(+), 2 deletions(-) diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index afee51c32..4440e9b20 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -173,7 +173,7 @@ page-networking = [ "dep:nm-secret-agent-manager", "dep:zbus", ] -page-power = ["dep:upower_dbus", "dep:zbus"] +page-power = ["dep:cosmic-settings-config", "dep:upower_dbus", "dep:zbus"] page-region = [ "gettext", "dep:locales-rs", diff --git a/cosmic-settings/src/pages/power/backend/mod.rs b/cosmic-settings/src/pages/power/backend/mod.rs index afff0031f..400dc6fb9 100644 --- a/cosmic-settings/src/pages/power/backend/mod.rs +++ b/cosmic-settings/src/pages/power/backend/mod.rs @@ -1,3 +1,7 @@ +use std::collections::BTreeMap; + +use cosmic_config::{Config, ConfigGet}; +use cosmic_settings_config::shortcuts::action::System; use futures::future::join_all; use futures::{FutureExt, Stream, StreamExt}; use jiff::{Span, SpanRelativeTo, SpanRound, ToSpan, Unit}; @@ -97,6 +101,92 @@ pub fn get_power_profiles() -> Vec { ] } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PowerButtonBehavior { + ConfirmShutdown, + ConfirmLogOut, + ConfirmRestart, + Shutdown, + LogOut, + Restart, + Suspend, + Hibernate, +} + +impl PowerButtonBehavior { + pub fn from_config() -> Option { + match Config::new("com.system76.CosmicSettings.Shortcuts", 1) + .and_then(|cfg| cfg.get::>("system_actions")) { + Ok(system_actions) => match system_actions.get(&System::PowerOff) { + None => Some(PowerButtonBehavior::ConfirmShutdown), + Some(cmd) if cmd == "cosmic-osd shutdown" => Some(Self::ConfirmShutdown), + Some(cmd) if cmd == "cosmic-osd log-out" => Some(Self::ConfirmLogOut), + Some(cmd) if cmd == "cosmic-osd restart" => Some(Self::ConfirmRestart), + Some(cmd) if cmd == "systemctl poweroff" => Some(Self::Shutdown), + Some(cmd) if cmd == "loginctl terminate-user $USER" => Some(Self::LogOut), + Some(cmd) if cmd == "systemctl reboot" => Some(Self::Restart), + Some(cmd) if cmd == "systemctl suspend" => Some(Self::Suspend), + Some(cmd) if cmd == "systemctl hibernate" => Some(Self::Hibernate), + // Only return None if the user has customized the command + _ => None, + }, + Err(_) => Some(Self::ConfirmShutdown), + } + } + + pub fn title(&self) -> String { + match self { + Self::ConfirmShutdown => fl!("power-button-behavior", "confirm-shutdown"), + Self::ConfirmLogOut => fl!("power-button-behavior", "confirm-log-out"), + Self::ConfirmRestart => fl!("power-button-behavior", "confirm-restart"), + Self::Shutdown => fl!("power-button-behavior", "shutdown"), + Self::LogOut => fl!("power-button-behavior", "log-out"), + Self::Restart => fl!("power-button-behavior", "restart"), + Self::Suspend => fl!("power-button-behavior", "suspend"), + Self::Hibernate => fl!("power-button-behavior", "hibernate"), + } + } + + pub fn description(&self) -> String { + match self { + Self::ConfirmShutdown => fl!("power-button-behavior", "confirm-shutdown-desc"), + Self::ConfirmLogOut => fl!("power-button-behavior", "confirm-log-out-desc"), + Self::ConfirmRestart => fl!("power-button-behavior", "confirm-restart-desc"), + Self::Shutdown => fl!("power-button-behavior", "shutdown-desc"), + Self::LogOut => fl!("power-button-behavior", "log-out-desc"), + Self::Restart => fl!("power-button-behavior", "restart-desc"), + Self::Suspend => fl!("power-button-behavior", "suspend-desc"), + Self::Hibernate => fl!("power-button-behavior", "hibernate-desc"), + } + } + + pub fn command(&self) -> String { + match self { + Self::ConfirmShutdown => "cosmic-osd shutdown".to_string(), + Self::ConfirmLogOut => "cosmic-osd log-out".to_string(), + Self::ConfirmRestart => "cosmic-osd restart".to_string(), + Self::Shutdown => "systemctl poweroff".to_string(), + Self::LogOut => "loginctl terminate-user $USER".to_string(), + Self::Restart => "systemctl reboot".to_string(), + Self::Suspend => "systemctl suspend".to_string(), + Self::Hibernate => "systemctl hibernate".to_string(), + } + } +} + +pub fn get_power_button_behaviors() -> Vec { + vec![ + PowerButtonBehavior::ConfirmShutdown, + PowerButtonBehavior::ConfirmLogOut, + PowerButtonBehavior::ConfirmRestart, + PowerButtonBehavior::Shutdown, + PowerButtonBehavior::LogOut, + PowerButtonBehavior::Restart, + PowerButtonBehavior::Suspend, + PowerButtonBehavior::Hibernate, + ] +} + #[derive(Debug, Clone)] pub struct S76Backend {} diff --git a/cosmic-settings/src/pages/power/mod.rs b/cosmic-settings/src/pages/power/mod.rs index 33e75d657..5fbc56583 100644 --- a/cosmic-settings/src/pages/power/mod.rs +++ b/cosmic-settings/src/pages/power/mod.rs @@ -1,5 +1,9 @@ +use cosmic_config::ConfigGet; mod backend; +use crate::pages::power::backend::PowerButtonBehavior; +use std::collections::BTreeMap; + use self::backend::{GetCurrentPowerProfile, SetPowerProfile}; use backend::{Battery, ConnectedDevice, PowerProfile}; @@ -8,7 +12,7 @@ use cosmic::iced::widget::{column, row}; use cosmic::iced::{self, Alignment, Length, stream}; use cosmic::widget::{self, settings, space, text}; use cosmic::{Apply, Task, surface}; -use cosmic_config::{Config, CosmicConfigEntry}; +use cosmic_config::{Config, ConfigSet, CosmicConfigEntry}; use cosmic_idle_config::CosmicIdleConfig; use cosmic_settings_page::{self as page, Section, section}; use futures::{SinkExt, StreamExt}; @@ -18,6 +22,7 @@ use slotmap::SlotMap; use std::hash::Hash; use std::iter; use std::time::Duration; +use cosmic_settings_config::shortcuts::action::System; use upower_dbus::DeviceProxy; static SCREEN_OFF_TIMES: &[Duration] = &[ @@ -61,12 +66,14 @@ pub struct Page { idle_conf: CosmicIdleConfig, backend: Option, current_power_profile: Option, + power_button_behavior: Option, } impl Default for Page { fn default() -> Self { let idle_config = Config::new("com.system76.CosmicIdle", 1).unwrap(); let idle_conf = CosmicIdleConfig::get_entry(&idle_config).unwrap_or_else(|(_, conf)| conf); + let power_button_behavior = PowerButtonBehavior::from_config(); Self { entity: Default::default(), @@ -89,6 +96,7 @@ impl Default for Page { idle_conf, backend: None, current_power_profile: None, + power_button_behavior, } } } @@ -113,6 +121,7 @@ impl page::Page for Page { sections.insert(connected_devices()), sections.insert(profiles()), sections.insert(power_saving()), + sections.insert(power_button()) ]) } @@ -300,6 +309,7 @@ impl page::Page for Page { #[derive(Clone, Debug)] pub enum Message { PowerProfileChange(PowerProfile), + PowerButtonBehaviorChange(PowerButtonBehavior), /// Update the system battery UpdateBattery(Battery), /// Update the battery of a connected device @@ -340,6 +350,21 @@ impl Page { }); } } + Message::PowerButtonBehaviorChange(behavior) => { + match Config::new("com.system76.CosmicSettings.Shortcuts", 1) { + Ok(config) => { + let mut system_actions: BTreeMap = config + .get("system_actions") + .unwrap_or_default(); + system_actions.insert(System::PowerOff, behavior.command()); + if let Err(err) = config.set("system_actions", system_actions) { + tracing::error!("Failed to set power button action: {}", err); + } + } + Err(err) => tracing::error!("Failed to open shortcuts config: {}", err), + } + self.power_button_behavior = Some(behavior); + } Message::UpdateBattery(battery) => self.battery = battery, Message::UpdateDeviceBattery(path, battery) => { for device in &mut self.connected_devices { @@ -555,6 +580,33 @@ fn profiles() -> Section { }) } +fn power_button() -> Section { + crate::slab!(descriptions { + _power_button_desc = fl!("power-button-behavior", "desc"); + }); + Section::default() + .title(fl!("power-button-behavior")) + .descriptions(descriptions) + .view::(move |_binder, page, section| { + let mut section = settings::section().title(§ion.title); + + if page.backend.is_some() { + let behaviors = backend::get_power_button_behaviors(); + section = behaviors + .into_iter() + .map(|behavior| { + settings::item::builder(behavior.title()) + .description(behavior.description()) + .radio(behavior, page.power_button_behavior, Message::PowerButtonBehaviorChange) + }).fold(section, settings::Section::add); + } + + section + .apply(cosmic::Element::from) + .map(crate::pages::Message::Power) + }) +} + fn power_saving_row<'a>( label: &'a str, labels: &'a [String], diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 535477b5f..3a0785e20 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -661,6 +661,26 @@ power-saving = Power saving options .auto-suspend-ac = Automatic suspend when plugged in .auto-suspend-battery = Automatic suspend on battery power +power-button-behavior = Power button + .desc = Sets how the power button behaves when pressed + .confirm-shutdown = Confirm Shutdown + .confirm-shutdown-desc = Shut the system down after the timer ends (default) + .confirm-log-out = Confirm Log Out + .confirm-log-out-desc = Log out of the system after the timer ends + .confirm-restart = Confirm Restart + .confirm-restart-desc = Restart the system after the timer ends + + .shutdown = Shutdown + .shutdown-desc = Shut the system down immediately + .log-out = Log Out + .log-out-desc = Log out of the system immediately + .restart = Restart + .restart-desc = Restart the system immediately + .suspend = Suspend + .suspend-desc = Suspend the system immediately + .hibernate = Hibernate + .hibernate-desc = Hibernate the system immediately + ## Input acceleration-desc = Automatically adjusts tracking sensitivity based on speed