From 40d56e6ea703ee303a770879e98a7ed68bc79181 Mon Sep 17 00:00:00 2001 From: Antoine Colombier <7086688+acolombier@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:59:43 +0100 Subject: [PATCH] feat(power): add connected devices section --- Cargo.lock | 2 +- .../src/pages/power/backend/mod.rs | 221 +++++++++++++----- cosmic-settings/src/pages/power/mod.rs | 125 ++++++++-- i18n/en/cosmic_settings.ftl | 7 +- i18n/fr/cosmic_settings.ftl | 7 +- i18n/it/cosmic_settings.ftl | 2 +- i18n/pl/cosmic_settings.ftl | 2 +- i18n/pt/cosmic_settings.ftl | 2 +- 8 files changed, 293 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f314ed..6d70fb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6934,7 +6934,7 @@ checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" [[package]] name = "upower_dbus" version = "0.3.2" -source = "git+https://github.com/pop-os/dbus-settings-bindings#7aedc25e3295b95a90eb710f443029d4ec920aa8" +source = "git+https://github.com/pop-os/dbus-settings-bindings#e0d6a04d6ebf6bcede1580721c84a7f01e5ef8bb" dependencies = [ "serde", "serde_repr", diff --git a/cosmic-settings/src/pages/power/backend/mod.rs b/cosmic-settings/src/pages/power/backend/mod.rs index 8278f13..1d55979 100644 --- a/cosmic-settings/src/pages/power/backend/mod.rs +++ b/cosmic-settings/src/pages/power/backend/mod.rs @@ -1,5 +1,7 @@ use chrono::{Duration, TimeDelta}; +use futures::future::join_all; use futures::FutureExt; +use upower_dbus::{BatteryType, DeviceProxy}; use zbus::Connection; mod ppdaemon; @@ -238,6 +240,13 @@ pub struct Battery { pub remaining_duration: Duration, } +#[derive(Default, Debug, Clone)] +pub struct ConnectedDevice { + pub model: String, + pub device_icon: &'static str, + pub battery: Battery, +} + async fn get_device_proxy<'a>() -> Result, zbus::Error> { let connection = match Connection::system().await { Ok(c) => c, @@ -253,6 +262,44 @@ async fn get_device_proxy<'a>() -> Result, zbus::Er } } +async fn enumerate_devices<'a>() -> Result>, zbus::Error> { + let connection = match Connection::system().await { + Ok(c) => c, + Err(e) => { + tracing::error!("zbus connection failed. {e}"); + return Err(e); + } + }; + + let devices = upower_dbus::UPowerProxy::new(&connection) + .await? + .enumerate_devices() + .await?; + + let devices = futures::future::join_all( + devices + .into_iter() + .map(|path| DeviceProxy::new(&connection, path)), + ) + .await; + + let errors = devices.iter().filter(|device| device.is_err()); + if errors.count() > 0 { + let mut errors: Vec = devices + .into_iter() + .filter_map(std::result::Result::err) + .collect(); + if errors.len() > 1 { + eprintln!("Multiple errors occurs when fetching connected device: {errors:?}. Only the last one will be returned."); + } + return Err(errors.pop().unwrap()); + } + Ok(devices + .into_iter() + .filter_map(std::result::Result::ok) + .collect()) +} + async fn get_on_battery_status() -> Result { let connection = match Connection::system().await { Ok(c) => c, @@ -269,73 +316,76 @@ async fn get_on_battery_status() -> Result { } impl Battery { - pub async fn update_battery() -> Self { - let proxy = get_device_proxy().await; + pub async fn from_device(proxy: DeviceProxy<'_>) -> Self { + let mut remaining_duration: Duration = Duration::default(); - if let Ok(proxy) = proxy { - let mut remaining_duration: Duration = Duration::default(); + let (is_present, percentage, on_battery) = futures::join!( + proxy.is_present().map(Result::unwrap_or_default), + proxy.percentage().map(Result::unwrap_or_default), + get_on_battery_status().map(Result::unwrap_or_default) + ); - let (is_present, percentage, on_battery) = futures::join!( - proxy.is_present().map(Result::unwrap_or_default), - proxy.percentage().map(Result::unwrap_or_default), - get_on_battery_status().map(Result::unwrap_or_default) - ); + let percent = percentage.clamp(0.0, 100.0); - let percent = percentage.clamp(0.0, 100.0); - - if on_battery { - if let Ok(time) = proxy.time_to_empty().await { - if let Ok(dur) = Duration::from_std(std::time::Duration::from_secs(time as u64)) - { - remaining_duration = dur; - } - } - } else if let Ok(time) = proxy.time_to_full().await { + if on_battery { + if let Ok(time) = proxy.time_to_empty().await { if let Ok(dur) = Duration::from_std(std::time::Duration::from_secs(time as u64)) { remaining_duration = dur; } } + } else if let Ok(time) = proxy.time_to_full().await { + if let Ok(dur) = Duration::from_std(std::time::Duration::from_secs(time as u64)) { + remaining_duration = dur; + } + } - let battery_percent = if percent > 95.0 { - 100 - } else if percent > 80.0 { - 90 - } else if percent > 65.0 { - 80 - } else if percent > 35.0 { - 50 - } else if percent > 20.0 { - 35 - } else if percent > 14.0 { - 20 - } else if percent > 9.0 { - 10 - } else if percent > 5.0 { - 5 - } else { - 0 - }; - let charging = if on_battery { "" } else { "charging-" }; + let battery_percent = if percent > 95.0 { + 100 + } else if percent > 80.0 { + 90 + } else if percent > 65.0 { + 80 + } else if percent > 35.0 { + 50 + } else if percent > 20.0 { + 35 + } else if percent > 14.0 { + 20 + } else if percent > 9.0 { + 10 + } else if percent > 5.0 { + 5 + } else { + 0 + }; + let charging = if on_battery { "" } else { "charging-" }; - let icon_name = - format!("cosmic-applet-battery-level-{battery_percent}-{charging}symbolic",); + let icon_name = + format!("cosmic-applet-battery-level-{battery_percent}-{charging}symbolic",); - return Battery { - icon_name, - is_present, - percent, - on_battery, - remaining_duration, - }; + Self { + icon_name, + is_present, + percent, + on_battery, + remaining_duration, + } + } + + pub async fn update_battery() -> Self { + let proxy = get_device_proxy().await; + + if let Ok(proxy) = proxy { + return Self::from_device(proxy).await; } Battery::default() } pub fn remaining_time(&self) -> String { if self.remaining_duration <= TimeDelta::zero() { - return String::new() + return String::new(); } - + let total_seconds = self.remaining_duration.num_seconds(); let days = total_seconds / 86400; @@ -357,8 +407,11 @@ impl Battery { let last = time.pop().unwrap(); time = vec![time.join(", "), last]; } - let time = if time.is_empty() { fl!("battery", "less-than-minute") - } else {time.join(&format!(" {} ", fl!("battery", "and"))) }; + let time = if time.is_empty() { + fl!("battery", "less-than-minute") + } else { + time.join(&format!(" {} ", fl!("battery", "and"))) + }; fl!( "battery", @@ -396,3 +449,67 @@ mod tests { } } } + +impl ConnectedDevice { + async fn from_device_maybe(proxy: DeviceProxy<'_>) -> Option { + let device_type = proxy.type_().await.unwrap_or(BatteryType::Unknown); + if matches!( + device_type, + BatteryType::Unknown | BatteryType::LinePower | BatteryType::Battery + ) { + return None; + } + let model = proxy + .model() + .await + .unwrap_or(fl!("connected-devices", "unknown")); + let battery = Battery::from_device(proxy).await; + let device_icon = match device_type { + BatteryType::Ups => "uninterruptible-power-supply-symbolic", + BatteryType::Monitor => "display-symbolic", + BatteryType::Mouse => "input-mouse-symbolic", + BatteryType::Keyboard => "input-keyboard-symbolic", + BatteryType::Pda | BatteryType::Phone => "smartphone-symbolic", + BatteryType::MediaPlayer => "multimedia-player-symbolic", + BatteryType::Tablet => "tablet-symbolic", + BatteryType::Computer => "laptop-symbolic", + BatteryType::GamingInput => "input-gaming-symbolic", + BatteryType::Pen => "input-tablet-symbolic", + BatteryType::Touchpad => "input-touchpad-symbolic", + BatteryType::Network => "network-wired-symbolic", + BatteryType::Headset => "audio-headset-symbolic", + BatteryType::Speakers => "speaker-symbolic", + BatteryType::Headphones => "audio-headphones-symbolic", + BatteryType::Video => "video-display-symbolic", + BatteryType::OtherAudio => "audio-speakers-symbolic", + BatteryType::Printer => "printer-network-symbolic", + BatteryType::Scanner => "scanner-symbolic", + BatteryType::Camera => "camera-photo-symbolic", + _ => "bluetooth-symbolic", + }; + + Some(Self { + model, + device_icon, + battery, + }) + } + + pub async fn update_connected_devices() -> Vec { + let proxy = enumerate_devices().await; + + if let Ok(devices) = proxy { + return join_all( + devices + .into_iter() + .map(|device| Self::from_device_maybe(device)), + ) + .await + .into_iter() + .flatten() + .collect(); + } + + vec![] + } +} diff --git a/cosmic-settings/src/pages/power/mod.rs b/cosmic-settings/src/pages/power/mod.rs index 249e0a2..e02e2e1 100644 --- a/cosmic-settings/src/pages/power/mod.rs +++ b/cosmic-settings/src/pages/power/mod.rs @@ -1,19 +1,24 @@ mod backend; use self::backend::{GetCurrentPowerProfile, SetPowerProfile}; -use backend::{Battery, PowerProfile}; +use backend::{Battery, ConnectedDevice, PowerProfile}; +use chrono::TimeDelta; use cosmic::iced::{Alignment, Length}; -use cosmic::iced_widget::row; -use cosmic::widget::{self, column, radio, settings, text}; +use cosmic::iced_widget::{column, row}; +use cosmic::prelude::CollectionWidget; +use cosmic::widget::{self, radio, settings, text}; use cosmic::Apply; +use cosmic::Command; use cosmic_settings_page::{self as page, section, Section}; +use itertools::Itertools; use slab::Slab; use slotmap::SlotMap; #[derive(Default)] pub struct Page { battery: Battery, + connected_devices: Vec, } impl page::Page for Page { @@ -29,6 +34,7 @@ impl page::Page for Page { ) -> Option { Some(vec![ sections.insert(battery_info()), + sections.insert(connected_devices()), sections.insert(profiles()), ]) } @@ -38,11 +44,18 @@ impl page::Page for Page { _page: cosmic_settings_page::Entity, _sender: tokio::sync::mpsc::Sender, ) -> cosmic::Command { - cosmic::command::future(async move { - let battery = Battery::update_battery().await; - Message::UpdateBattery(battery) - }) - .map(crate::pages::Message::Power) + let futures: Vec> = vec![ + cosmic::command::future(async move { + let battery = Battery::update_battery().await; + Message::UpdateBattery(battery) + }), + cosmic::command::future(async move { + let devices = ConnectedDevice::update_connected_devices().await; + Message::UpdateConnectedDevices(devices) + }), + ]; + + cosmic::command::batch(futures).map(crate::pages::Message::Power) } } @@ -50,6 +63,7 @@ impl page::Page for Page { pub enum Message { PowerProfileChange(PowerProfile), UpdateBattery(Battery), + UpdateConnectedDevices(Vec), } impl Page { @@ -65,6 +79,9 @@ impl Page { } } Message::UpdateBattery(battery) => self.battery = battery, + Message::UpdateConnectedDevices(connected_devices) => { + self.connected_devices = connected_devices; + } }; } } @@ -78,13 +95,14 @@ fn battery_info() -> Section { .show_while::(|page| page.battery.is_present) .view::(move |_binder, page, section| { let battery_icon = widget::icon::from_name(page.battery.icon_name.clone()); - let battery_label = text::body(format!( - "{}% {}", - page.battery.percent, - page.battery.remaining_time() - )); + let remaining_time = page.battery.remaining_time(); + let battery_label = text::body(if remaining_time.is_empty() { + format!("{}%", page.battery.percent) + } else { + format!("{}% ({})", page.battery.percent, remaining_time) + }); - column::with_capacity(2) + widget::column::with_capacity(2) .push(text::heading(§ion.title)) .push( row!(battery_icon, battery_label) @@ -95,6 +113,83 @@ fn battery_info() -> Section { }) } +fn connected_devices() -> Section { + let descriptions = Slab::new(); + + Section::default() + .title(fl!("connected-devices")) + .descriptions(descriptions) + .show_while::(|page| !page.connected_devices.is_empty()) + .view::(move |_binder, page, section| { + let devices: Vec> = page + .connected_devices + .iter() + .map(|connected_device| { + let battery_icon = + widget::icon::from_name(connected_device.battery.icon_name.clone()); + + let battery_percent_and_time = widget::text( + if connected_device.battery.remaining_duration > TimeDelta::zero() { + format!( + "{}% - {}", + connected_device.battery.percent, + &connected_device.battery.remaining_time() + ) + } else { + format!("{}%", connected_device.battery.percent) + }, + ); + widget::container( + row!( + widget::icon::from_name(connected_device.device_icon).size(48), + column!( + text::heading(&connected_device.model), + row!(battery_icon, battery_percent_and_time) + .spacing(4) + .align_items(Alignment::Center), + ) + .height(Length::Shrink) + ) + .align_items(Alignment::Center) + .spacing(16) + .padding([8, 16]) + .width(Length::Fill) + .height(Length::Fill), + ) + .height(64) + .style(cosmic::theme::Container::List) + .into() + }) + .collect(); + + widget::column::with_capacity(2) + .spacing(8) + .push(text::heading(§ion.title)) + .push( + widget::column() + .extend( + devices + .into_iter() + .chunks(2) + .into_iter() + .map(|mut device_row| { + row!( + device_row.next().unwrap_or( + widget::horizontal_space(Length::Fill).into() + ), + device_row.next().unwrap_or( + widget::horizontal_space(Length::Fill).into() + ), + ) + .spacing(8) + }), + ) + .spacing(8), + ) + .into() + }) +} + fn profiles() -> Section { let mut descriptions = Slab::new(); @@ -119,7 +214,7 @@ fn profiles() -> Section { .into_iter() .map(|profile| { settings::item_row(vec![radio( - column::with_capacity(2) + widget::column::with_capacity(2) .push(text::body(profile.title())) .push(text::caption(profile.description())), profile.clone(), diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 965d966..657e680 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -318,10 +318,13 @@ battery = Battery } .less-than-minute = Less than a minute .and = and - .remaining-time = ({ $time } until { $action -> + .remaining-time = { $time } until { $action -> [full] full *[other] empty - }) + } + +connected-devices = Connected Devices + .unknown = Unknown device power-mode = Power Mode .battery = Extended battery life diff --git a/i18n/fr/cosmic_settings.ftl b/i18n/fr/cosmic_settings.ftl index b12e190..fd42e5a 100644 --- a/i18n/fr/cosmic_settings.ftl +++ b/i18n/fr/cosmic_settings.ftl @@ -308,10 +308,13 @@ battery = Batterie } .less-than-minute = Moins d'une minute .and = et - .remaining-time = ({ $time } jusqu'à la { $action -> + .remaining-time = { $time } jusqu'à la { $action -> [full] charge *[other] decharge - } complète) + } complète + +connected-devices = Périphériques connectés + .unknown = Périphériques inconnu power-profiles = Modes d'énergie .battery = Économie d'énergie diff --git a/i18n/it/cosmic_settings.ftl b/i18n/it/cosmic_settings.ftl index 5115822..58c2649 100644 --- a/i18n/it/cosmic_settings.ftl +++ b/i18n/it/cosmic_settings.ftl @@ -297,7 +297,7 @@ power = Alimentazione e batteria .desc = Gestione impostazioni energetiche battery = Batteria - .remaining-time = ({ $time } rimasti) + .remaining-time = { $time } rimasti power-mode = Power Mode .battery = Estendi la vita della batteria diff --git a/i18n/pl/cosmic_settings.ftl b/i18n/pl/cosmic_settings.ftl index 966f558..9a7e48a 100644 --- a/i18n/pl/cosmic_settings.ftl +++ b/i18n/pl/cosmic_settings.ftl @@ -308,7 +308,7 @@ power = Zasilanie .desc = Zarządzaj ustawieniami zasilania battery = Bateria - .remaining-time = ({ $time } pozostało) + .remaining-time = { $time } pozostało power-mode = Profile Zasilania .performance = Tryb Wysokowydajny diff --git a/i18n/pt/cosmic_settings.ftl b/i18n/pt/cosmic_settings.ftl index 39e24a2..bc1f4f6 100644 --- a/i18n/pt/cosmic_settings.ftl +++ b/i18n/pt/cosmic_settings.ftl @@ -294,7 +294,7 @@ power = Energia & Bateria .desc = Gere as configurações da energia battery = Bateria - .remaining-time = ({ $time } restante/s) + .remaining-time = { $time } restante/s power-mode = Modo de Energia .battery = Expande a vida da bateria