feat(power): add connected devices section
This commit is contained in:
parent
97bcbc64ec
commit
40d56e6ea7
8 changed files with 293 additions and 75 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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![]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(§ion.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(§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<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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue