feat(power): add connected devices section

This commit is contained in:
Antoine Colombier 2024-09-05 14:59:43 +01:00 committed by GitHub
parent 97bcbc64ec
commit 40d56e6ea7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 293 additions and 75 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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<upower_dbus::DeviceProxy<'a>, zbus::Error> {
let connection = match Connection::system().await {
Ok(c) => c,
@ -253,6 +262,44 @@ async fn get_device_proxy<'a>() -> Result<upower_dbus::DeviceProxy<'a>, zbus::Er
}
}
async fn enumerate_devices<'a>() -> Result<Vec<upower_dbus::DeviceProxy<'a>>, 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<zbus::Error> = 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<bool, zbus::Error> {
let connection = match Connection::system().await {
Ok(c) => c,
@ -269,73 +316,76 @@ async fn get_on_battery_status() -> Result<bool, zbus::Error> {
}
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<Self> {
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<Self> {
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![]
}
}

View file

@ -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<ConnectedDevice>,
}
impl page::Page<crate::pages::Message> for Page {
@ -29,6 +34,7 @@ impl page::Page<crate::pages::Message> for Page {
) -> Option<page::Content> {
Some(vec![
sections.insert(battery_info()),
sections.insert(connected_devices()),
sections.insert(profiles()),
])
}
@ -38,11 +44,18 @@ impl page::Page<crate::pages::Message> for Page {
_page: cosmic_settings_page::Entity,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> cosmic::Command<crate::pages::Message> {
cosmic::command::future(async move {
let battery = Battery::update_battery().await;
Message::UpdateBattery(battery)
})
.map(crate::pages::Message::Power)
let futures: Vec<Command<Message>> = 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<crate::pages::Message> for Page {
pub enum Message {
PowerProfileChange(PowerProfile),
UpdateBattery(Battery),
UpdateConnectedDevices(Vec<ConnectedDevice>),
}
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<crate::pages::Message> {
.show_while::<Page>(|page| page.battery.is_present)
.view::<Page>(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(&section.title))
.push(
row!(battery_icon, battery_label)
@ -95,6 +113,83 @@ fn battery_info() -> Section<crate::pages::Message> {
})
}
fn connected_devices() -> Section<crate::pages::Message> {
let descriptions = Slab::new();
Section::default()
.title(fl!("connected-devices"))
.descriptions(descriptions)
.show_while::<Page>(|page| !page.connected_devices.is_empty())
.view::<Page>(move |_binder, page, section| {
let devices: Vec<cosmic::Element<'_, _>> = 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(&section.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<crate::pages::Message> {
let mut descriptions = Slab::new();
@ -119,7 +214,7 @@ fn profiles() -> Section<crate::pages::Message> {
.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(),

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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