2023-03-19 02:16:49 +05:30
|
|
|
use color_eyre::Result;
|
|
|
|
use futures_lite::stream::StreamExt;
|
|
|
|
use gtk::{prelude::*, Button};
|
|
|
|
use gtk::{Label, Orientation};
|
|
|
|
use serde::Deserialize;
|
2023-12-17 23:51:43 +00:00
|
|
|
use tokio::sync::{broadcast, mpsc};
|
2023-03-19 02:16:49 +05:30
|
|
|
use upower_dbus::BatteryState;
|
|
|
|
use zbus;
|
2024-01-07 23:50:10 +00:00
|
|
|
use zbus::fdo::PropertiesProxy;
|
2023-03-19 02:16:49 +05:30
|
|
|
|
2023-07-16 18:57:00 +01:00
|
|
|
use crate::config::CommonConfig;
|
|
|
|
use crate::gtk_helpers::IronbarGtkExt;
|
|
|
|
use crate::image::ImageProvider;
|
|
|
|
use crate::modules::PopupButton;
|
|
|
|
use crate::modules::{
|
|
|
|
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
|
|
|
};
|
2024-01-07 23:50:10 +00:00
|
|
|
use crate::{error, glib_recv, send_async, spawn, try_send};
|
2023-07-16 18:57:00 +01:00
|
|
|
|
2023-03-19 02:16:49 +05:30
|
|
|
const DAY: i64 = 24 * 60 * 60;
|
|
|
|
const HOUR: i64 = 60 * 60;
|
|
|
|
const MINUTE: i64 = 60;
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
pub struct UpowerModule {
|
|
|
|
#[serde(default = "default_format")]
|
|
|
|
format: String,
|
|
|
|
|
2023-05-26 19:07:05 +01:00
|
|
|
#[serde(default = "default_icon_size")]
|
|
|
|
icon_size: i32,
|
|
|
|
|
2023-03-19 02:16:49 +05:30
|
|
|
#[serde(flatten)]
|
|
|
|
pub common: Option<CommonConfig>,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn default_format() -> String {
|
|
|
|
String::from("{percentage}%")
|
|
|
|
}
|
|
|
|
|
2023-05-26 19:07:05 +01:00
|
|
|
const fn default_icon_size() -> i32 {
|
|
|
|
24
|
|
|
|
}
|
|
|
|
|
2023-03-19 02:16:49 +05:30
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct UpowerProperties {
|
|
|
|
percentage: f64,
|
|
|
|
icon_name: String,
|
|
|
|
state: u32,
|
|
|
|
time_to_full: i64,
|
|
|
|
time_to_empty: i64,
|
|
|
|
}
|
|
|
|
|
2023-05-26 18:58:30 +01:00
|
|
|
impl Module<gtk::Button> for UpowerModule {
|
2023-03-19 02:16:49 +05:30
|
|
|
type SendMessage = UpowerProperties;
|
|
|
|
type ReceiveMessage = ();
|
|
|
|
|
|
|
|
fn name() -> &'static str {
|
|
|
|
"upower"
|
|
|
|
}
|
|
|
|
|
|
|
|
fn spawn_controller(
|
|
|
|
&self,
|
|
|
|
_info: &ModuleInfo,
|
2024-01-07 23:42:34 +00:00
|
|
|
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
2023-12-17 23:51:43 +00:00
|
|
|
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
2023-03-19 02:16:49 +05:30
|
|
|
) -> Result<()> {
|
2024-01-07 23:42:34 +00:00
|
|
|
let tx = context.tx.clone();
|
2024-01-07 23:50:10 +00:00
|
|
|
|
|
|
|
let display_proxy = context.client::<PropertiesProxy>();
|
|
|
|
|
2023-03-19 02:16:49 +05:30
|
|
|
spawn(async move {
|
|
|
|
let mut prop_changed_stream = display_proxy.receive_properties_changed().await?;
|
|
|
|
|
|
|
|
let device_interface_name =
|
|
|
|
zbus::names::InterfaceName::from_static_str("org.freedesktop.UPower.Device")
|
|
|
|
.expect("failed to create zbus InterfaceName");
|
|
|
|
|
|
|
|
let properties = display_proxy.get_all(device_interface_name.clone()).await?;
|
|
|
|
|
|
|
|
let percentage = *properties["Percentage"]
|
|
|
|
.downcast_ref::<f64>()
|
|
|
|
.expect("expected percentage: f64 in HashMap of all properties");
|
|
|
|
let icon_name = properties["IconName"]
|
|
|
|
.downcast_ref::<str>()
|
|
|
|
.expect("expected IconName: str in HashMap of all properties")
|
|
|
|
.to_string();
|
|
|
|
let state = *properties["State"]
|
|
|
|
.downcast_ref::<u32>()
|
|
|
|
.expect("expected State: u32 in HashMap of all properties");
|
|
|
|
let time_to_full = *properties["TimeToFull"]
|
|
|
|
.downcast_ref::<i64>()
|
|
|
|
.expect("expected TimeToFull: i64 in HashMap of all properties");
|
|
|
|
let time_to_empty = *properties["TimeToEmpty"]
|
|
|
|
.downcast_ref::<i64>()
|
|
|
|
.expect("expected TimeToEmpty: i64 in HashMap of all properties");
|
|
|
|
let mut properties = UpowerProperties {
|
|
|
|
percentage,
|
|
|
|
icon_name: icon_name.clone(),
|
|
|
|
state,
|
|
|
|
time_to_full,
|
|
|
|
time_to_empty,
|
|
|
|
};
|
|
|
|
|
|
|
|
send_async!(tx, ModuleUpdateEvent::Update(properties.clone()));
|
|
|
|
|
|
|
|
while let Some(signal) = prop_changed_stream.next().await {
|
|
|
|
let args = signal.args().expect("Invalid signal arguments");
|
|
|
|
if args.interface_name != device_interface_name {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (name, changed_value) in args.changed_properties {
|
|
|
|
match name {
|
|
|
|
"Percentage" => {
|
|
|
|
properties.percentage = changed_value
|
|
|
|
.downcast::<f64>()
|
|
|
|
.expect("expected Percentage to be f64");
|
|
|
|
}
|
|
|
|
"IconName" => {
|
|
|
|
properties.icon_name = changed_value
|
|
|
|
.downcast_ref::<str>()
|
|
|
|
.expect("expected IconName to be str")
|
|
|
|
.to_string();
|
|
|
|
}
|
|
|
|
"State" => {
|
|
|
|
properties.state = changed_value
|
|
|
|
.downcast::<u32>()
|
|
|
|
.expect("expected State to be u32");
|
|
|
|
}
|
|
|
|
"TimeToFull" => {
|
|
|
|
properties.time_to_full = changed_value
|
|
|
|
.downcast::<i64>()
|
|
|
|
.expect("expected TimeToFull to be i64");
|
|
|
|
}
|
|
|
|
"TimeToEmpty" => {
|
|
|
|
properties.time_to_empty = changed_value
|
|
|
|
.downcast::<i64>()
|
|
|
|
.expect("expected TimeToEmpty to be i64");
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
send_async!(tx, ModuleUpdateEvent::Update(properties.clone()));
|
|
|
|
}
|
|
|
|
|
|
|
|
Result::<()>::Ok(())
|
|
|
|
});
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn into_widget(
|
|
|
|
self,
|
|
|
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
|
|
|
info: &ModuleInfo,
|
2023-07-16 18:57:00 +01:00
|
|
|
) -> Result<ModuleParts<Button>> {
|
2023-03-19 02:16:49 +05:30
|
|
|
let icon_theme = info.icon_theme.clone();
|
2023-05-06 00:40:06 +01:00
|
|
|
let icon = gtk::Image::new();
|
2023-07-16 18:57:00 +01:00
|
|
|
icon.add_class("icon");
|
2023-03-19 02:16:49 +05:30
|
|
|
|
|
|
|
let label = Label::builder()
|
|
|
|
.label(&self.format)
|
|
|
|
.use_markup(true)
|
|
|
|
.build();
|
2023-07-16 18:57:00 +01:00
|
|
|
label.add_class("label");
|
2023-03-19 02:16:49 +05:30
|
|
|
|
2023-05-26 18:58:30 +01:00
|
|
|
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
2023-07-16 18:57:00 +01:00
|
|
|
container.add_class("contents");
|
2023-03-19 02:16:49 +05:30
|
|
|
|
2023-05-06 00:40:06 +01:00
|
|
|
let button = Button::new();
|
2023-07-16 18:57:00 +01:00
|
|
|
button.add_class("button");
|
2023-03-19 02:16:49 +05:30
|
|
|
|
|
|
|
container.add(&icon);
|
2023-05-26 18:58:30 +01:00
|
|
|
container.add(&label);
|
|
|
|
button.add(&container);
|
2023-03-19 02:16:49 +05:30
|
|
|
|
2023-12-17 23:51:43 +00:00
|
|
|
let tx = context.tx.clone();
|
2023-03-19 02:16:49 +05:30
|
|
|
button.connect_clicked(move |button| {
|
2023-12-17 23:51:43 +00:00
|
|
|
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
2023-03-19 02:16:49 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
label.set_angle(info.bar_position.get_angle());
|
|
|
|
let format = self.format.clone();
|
|
|
|
|
2023-12-31 00:50:03 +00:00
|
|
|
let rx = context.subscribe();
|
2023-12-17 23:51:43 +00:00
|
|
|
glib_recv!(rx, properties => {
|
|
|
|
let format = format.replace("{percentage}", &properties.percentage.to_string());
|
|
|
|
let icon_name = String::from("icon:") + &properties.icon_name;
|
|
|
|
|
|
|
|
ImageProvider::parse(&icon_name, &icon_theme, false, self.icon_size)
|
2023-05-20 14:36:04 +01:00
|
|
|
.map(|provider| provider.load_into_image(icon.clone()));
|
2023-03-19 02:16:49 +05:30
|
|
|
|
2023-12-17 23:51:43 +00:00
|
|
|
label.set_markup(format.as_ref());
|
|
|
|
});
|
|
|
|
|
|
|
|
let rx = context.subscribe();
|
2023-07-16 18:57:00 +01:00
|
|
|
let popup = self
|
2023-12-17 23:51:43 +00:00
|
|
|
.into_popup(context.controller_tx, rx, info)
|
2023-07-16 18:57:00 +01:00
|
|
|
.into_popup_parts(vec![&button]);
|
2023-03-19 02:16:49 +05:30
|
|
|
|
2023-07-16 18:57:00 +01:00
|
|
|
Ok(ModuleParts::new(button, popup))
|
2023-03-19 02:16:49 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
fn into_popup(
|
|
|
|
self,
|
2023-12-17 23:51:43 +00:00
|
|
|
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
2023-12-31 00:50:03 +00:00
|
|
|
rx: broadcast::Receiver<Self::SendMessage>,
|
2023-03-19 02:16:49 +05:30
|
|
|
_info: &ModuleInfo,
|
|
|
|
) -> Option<gtk::Box>
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
let container = gtk::Box::builder()
|
|
|
|
.orientation(Orientation::Horizontal)
|
|
|
|
.build();
|
|
|
|
|
2023-05-06 00:40:06 +01:00
|
|
|
let label = Label::new(None);
|
2023-07-16 18:57:00 +01:00
|
|
|
label.add_class("upower-details");
|
2023-05-07 16:13:32 +01:00
|
|
|
container.add(&label);
|
2023-03-19 02:16:49 +05:30
|
|
|
|
2023-12-17 23:51:43 +00:00
|
|
|
glib_recv!(rx, properties => {
|
2023-03-19 02:16:49 +05:30
|
|
|
let state = u32_to_battery_state(properties.state);
|
2023-05-07 16:13:32 +01:00
|
|
|
let format = match state {
|
2023-03-19 02:16:49 +05:30
|
|
|
Ok(BatteryState::Charging | BatteryState::PendingCharge) => {
|
|
|
|
let ttf = properties.time_to_full;
|
|
|
|
if ttf > 0 {
|
2023-05-07 16:13:32 +01:00
|
|
|
format!("Full in {}", seconds_to_string(ttf))
|
|
|
|
} else {
|
|
|
|
String::new()
|
2023-03-19 02:16:49 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(BatteryState::Discharging | BatteryState::PendingDischarge) => {
|
|
|
|
let tte = properties.time_to_empty;
|
|
|
|
if tte > 0 {
|
2023-05-07 16:13:32 +01:00
|
|
|
format!("Empty in {}", seconds_to_string(tte))
|
|
|
|
} else {
|
|
|
|
String::new()
|
2023-03-19 02:16:49 +05:30
|
|
|
}
|
|
|
|
}
|
2023-05-07 16:13:32 +01:00
|
|
|
Err(state) => {
|
|
|
|
error!("Invalid battery state: {state}");
|
|
|
|
String::new()
|
|
|
|
}
|
|
|
|
_ => String::new(),
|
|
|
|
};
|
2023-03-19 02:16:49 +05:30
|
|
|
|
2023-05-07 16:13:32 +01:00
|
|
|
label.set_markup(&format);
|
2023-03-19 02:16:49 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
container.show_all();
|
|
|
|
|
|
|
|
Some(container)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn seconds_to_string(seconds: i64) -> String {
|
|
|
|
let mut time_string = String::new();
|
|
|
|
let days = seconds / (DAY);
|
|
|
|
if days > 0 {
|
|
|
|
time_string += &format!("{days}d");
|
|
|
|
}
|
|
|
|
let hours = (seconds % DAY) / HOUR;
|
|
|
|
if hours > 0 {
|
|
|
|
time_string += &format!(" {hours}h");
|
|
|
|
}
|
|
|
|
let minutes = (seconds % HOUR) / MINUTE;
|
|
|
|
if minutes > 0 {
|
|
|
|
time_string += &format!(" {minutes}m");
|
|
|
|
}
|
|
|
|
time_string.trim_start().to_string()
|
|
|
|
}
|
|
|
|
|
|
|
|
const fn u32_to_battery_state(number: u32) -> Result<BatteryState, u32> {
|
|
|
|
if number == (BatteryState::Unknown as u32) {
|
|
|
|
Ok(BatteryState::Unknown)
|
|
|
|
} else if number == (BatteryState::Charging as u32) {
|
|
|
|
Ok(BatteryState::Charging)
|
|
|
|
} else if number == (BatteryState::Discharging as u32) {
|
|
|
|
Ok(BatteryState::Discharging)
|
|
|
|
} else if number == (BatteryState::Empty as u32) {
|
|
|
|
Ok(BatteryState::Empty)
|
|
|
|
} else if number == (BatteryState::FullyCharged as u32) {
|
|
|
|
Ok(BatteryState::FullyCharged)
|
|
|
|
} else if number == (BatteryState::PendingCharge as u32) {
|
|
|
|
Ok(BatteryState::PendingCharge)
|
|
|
|
} else if number == (BatteryState::PendingDischarge as u32) {
|
|
|
|
Ok(BatteryState::PendingDischarge)
|
|
|
|
} else {
|
|
|
|
Err(number)
|
|
|
|
}
|
|
|
|
}
|