// Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only use std::cell::LazyCell; use cosmic::{ app, applet::{menu_button, padded_control}, cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity, cosmic_theme::Spacing, iced::{ self, platform_specific::{ runtime::wayland::subsurface, shell::commands::popup::{destroy_popup, get_popup}, }, widget::{self, column, row}, window, Alignment, Length, }, iced_runtime::core::layout::Limits, surface_message::{MessageWrapper, SurfaceMessage}, theme, widget::{autosize, button, divider, icon, layer_container::layer_container, text, Space}, Element, Task, }; use once_cell::sync::Lazy; use logind_zbus::{ manager::ManagerProxy, session::{SessionClass, SessionProxy, SessionType}, user::UserProxy, }; use rustix::process::getuid; use tokio::process; use zbus::Connection; pub mod cosmic_session; mod localize; pub mod session_manager; use crate::{cosmic_session::CosmicSessionProxy, session_manager::SessionManagerProxy}; static SUBSURFACE_ID: Lazy = Lazy::new(|| cosmic::widget::Id::new("subsurface")); pub fn run() -> cosmic::iced::Result { localize::localize(); cosmic::applet::run::(()) } struct Power { core: cosmic::app::Core, icon_name: String, popup: Option, subsurface_id: window::Id, } #[derive(Debug, Clone, Copy)] enum PowerAction { Lock, LogOut, Suspend, Restart, Shutdown, } impl PowerAction { fn perform(self) -> iced::Task> { let msg = |m| cosmic::app::message::app(Message::Zbus(m)); match self { PowerAction::Lock => iced::Task::perform(lock(), msg), PowerAction::LogOut => iced::Task::perform(log_out(), msg), PowerAction::Suspend => iced::Task::perform(suspend(), msg), PowerAction::Restart => iced::Task::perform(restart(), msg), PowerAction::Shutdown => iced::Task::perform(shutdown(), msg), } } } impl From for MessageWrapper { fn from(value: Message) -> Self { match value { Message::Surface(s) => MessageWrapper::Surface(s), m => MessageWrapper::Message(m), } } } impl From for Message { fn from(value: SurfaceMessage) -> Self { Message::Surface(value) } } #[derive(Debug, Clone)] enum Message { Action(PowerAction), TogglePopup, Settings, Zbus(Result<(), zbus::Error>), Closed(window::Id), Surface(SurfaceMessage), } impl cosmic::Application for Power { type Executor = cosmic::SingleThreadExecutor; type Flags = (); type Message = Message; const APP_ID: &'static str = "com.system76.CosmicAppletPower"; fn core(&self) -> &cosmic::app::Core { &self.core } fn core_mut(&mut self) -> &mut cosmic::app::Core { &mut self.core } fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task) { ( Self { core, icon_name: "system-shutdown-symbolic".to_string(), subsurface_id: window::Id::unique(), popup: Default::default(), }, Task::none(), ) } fn on_close_requested(&self, id: window::Id) -> Option { Some(Message::Closed(id)) } fn update(&mut self, message: Message) -> app::Task { match message { Message::TogglePopup => { if let Some(p) = self.popup.take() { destroy_popup(p) } else { let new_id = window::Id::unique(); self.popup.replace(new_id); let mut popup_settings = self.core.applet.get_popup_settings( self.core.main_window_id().unwrap(), new_id, Some((500, 500)), None, None, ); popup_settings.positioner.size_limits = Limits::NONE .min_width(100.0) .min_height(100.0) .max_height(400.0) .max_width(500.0); get_popup(popup_settings) } } Message::Settings => { let _ = process::Command::new("cosmic-settings").spawn(); Task::none() } Message::Action(action) => { match action { PowerAction::LogOut => { if let Err(err) = process::Command::new("cosmic-osd").arg("log-out").spawn() { tracing::error!("Failed to spawn cosmic-osd. {err:?}"); return PowerAction::LogOut.perform(); } } PowerAction::Restart => { if let Err(err) = process::Command::new("cosmic-osd").arg("restart").spawn() { tracing::error!("Failed to spawn cosmic-osd. {err:?}"); return PowerAction::Restart.perform(); } } PowerAction::Shutdown => { if let Err(err) = process::Command::new("cosmic-osd").arg("shutdown").spawn() { tracing::error!("Failed to spawn cosmic-osd. {err:?}"); return PowerAction::Shutdown.perform(); } } a => return a.perform(), }; Task::none() } Message::Zbus(result) => { if let Err(e) = result { eprintln!("cosmic-applet-power ERROR: '{}'", e); } Task::none() } Message::Closed(id) => { if self.popup == Some(id) { self.popup = None; } Task::none() } Message::Surface(surface_message) => unimplemented!(), } } fn view(&self) -> Element { self.core .applet .applet_tooltip( self.core .applet .icon_button(&self.icon_name) .on_press_down(Message::TogglePopup), "power", self.popup.is_some(), ) .into() } fn view_window(&self, id: window::Id) -> Element { let Spacing { space_xxs, space_s, space_m, .. } = theme::active().cosmic().spacing; if matches!(self.popup, Some(p) if p == id) { let settings = menu_button(text::body(fl!("settings"))).on_press(Message::Settings); let session = column![ menu_button( row![ text_icon("system-lock-screen-symbolic", 24), text::body(fl!("lock-screen")), Space::with_width(Length::Fill), text::body(fl!("lock-screen-shortcut")), ] .align_y(Alignment::Center) .spacing(space_xxs) ) .on_press(Message::Action(PowerAction::Lock)), menu_button( row![ text_icon("system-log-out-symbolic", 24), text::body(fl!("log-out")), Space::with_width(Length::Fill), text::body(fl!("log-out-shortcut")), ] .align_y(Alignment::Center) .spacing(space_xxs) ) .on_press(Message::Action(PowerAction::LogOut)), ]; let power = row![ power_buttons("system-suspend-symbolic", fl!("suspend")) .on_press(Message::Action(PowerAction::Suspend)), power_buttons("system-reboot-symbolic", fl!("restart")) .on_press(Message::Action(PowerAction::Restart)), power_buttons("system-shutdown-symbolic", fl!("shutdown")) .on_press(Message::Action(PowerAction::Shutdown)), ] .spacing(space_m) .padding([0, space_m]); let content = column![ settings, padded_control(divider::horizontal::default()).padding([space_xxs, space_s]), session, padded_control(divider::horizontal::default()).padding([space_xxs, space_s]), power ] .align_x(Alignment::Start) .padding([8, 0]); self.core .applet .popup_container(content) .max_height(400.) .max_width(500.) .into() } else { //panic!("no view for window {}", id.0) widget::text("").into() } } fn style(&self) -> Option { Some(cosmic::applet::style()) } } fn power_buttons(name: &str, msg: String) -> cosmic::widget::Button { button::custom( column![text_icon(name, 40), text::body(msg)] .spacing(4) .align_x(Alignment::Center) .width(Length::Fill), ) .width(Length::Fill) .height(Length::Fixed(76.0)) .class(theme::Button::Text) } fn text_icon(name: &str, size: u16) -> cosmic::widget::Icon { icon::from_name(name).size(size).symbolic(true).icon() } // ### System helpers async fn restart() -> zbus::Result<()> { let connection = Connection::system().await?; let manager_proxy = ManagerProxy::new(&connection).await?; manager_proxy.reboot(true).await } async fn shutdown() -> zbus::Result<()> { let connection = Connection::system().await?; let manager_proxy = ManagerProxy::new(&connection).await?; manager_proxy.power_off(true).await } async fn suspend() -> zbus::Result<()> { let connection = Connection::system().await?; let manager_proxy = ManagerProxy::new(&connection).await?; manager_proxy.suspend(true).await } async fn lock() -> zbus::Result<()> { let connection = Connection::system().await?; let manager_proxy = ManagerProxy::new(&connection).await?; // Get the session this current process is running in let our_uid = getuid().as_raw() as u32; let user_path = manager_proxy.get_user(our_uid).await?; let user = UserProxy::builder(&connection) .path(user_path)? .build() .await?; // Lock all non-TTY sessions of this user let sessions = user.sessions().await?; let mut locked_successfully = false; for (_, session_path) in sessions { let Ok(session) = SessionProxy::builder(&connection) .path(session_path)? .build() .await else { continue; }; if session.class().await == Ok(SessionClass::User) && session.type_().await? != SessionType::TTY && session.lock().await.is_ok() { locked_successfully = true; } } if locked_successfully { Ok(()) } else { Err(zbus::Error::Failure("locking session failed".to_string())) } } async fn log_out() -> zbus::Result<()> { let session_type = std::env::var("XDG_CURRENT_DESKTOP").ok(); let connection = Connection::session().await?; match session_type.as_ref().map(|s| s.trim()) { Some("pop:GNOME") => { let manager_proxy = SessionManagerProxy::new(&connection).await?; manager_proxy.logout(0).await?; } // By default assume COSMIC _ => { let cosmic_session = CosmicSessionProxy::new(&connection).await?; cosmic_session.exit().await?; } } Ok(()) }