mirror of
https://github.com/JakeStanger/ironbar.git
synced 2024-11-22 23:16:46 +03:00
feat: implement upower module
This commit is contained in:
parent
1e1d65ae49
commit
ad3c171eca
1161
Cargo.lock
generated
1161
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -14,10 +14,11 @@ default = [
|
||||
"music+all",
|
||||
"sys_info",
|
||||
"tray",
|
||||
"upower",
|
||||
"workspaces+all"
|
||||
]
|
||||
|
||||
http = ["dep:reqwest"]
|
||||
upower = ["upower_dbus", "zbus", "futures-lite"]
|
||||
|
||||
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn"]
|
||||
"config+json" = ["universal-config/json"]
|
||||
@ -88,6 +89,11 @@ sysinfo = { version = "0.28.4", optional = true }
|
||||
# tray
|
||||
stray = { version = "0.1.3", optional = true }
|
||||
|
||||
# upower
|
||||
upower_dbus = { version = "0.3.2", optional = true }
|
||||
futures-lite = { version = "1.12.0", optional = true }
|
||||
zbus = { version = "3.11.0", optional = true }
|
||||
|
||||
# workspaces
|
||||
swayipc-async = { version = "2.0.1", optional = true }
|
||||
hyprland = { version = "0.3.1", optional = true }
|
||||
|
80
docs/modules/Upower.md
Normal file
80
docs/modules/Upower.md
Normal file
@ -0,0 +1,80 @@
|
||||
Displays system power information such as the battery percentage, and estimated time to empty.
|
||||
|
||||
`TODO: ADD SCREENSHOT`
|
||||
|
||||
[//]: # (![Screenshot](https://user-images.githubusercontent.com/5057870/184540521-2278bdec-9742-46f0-9ac2-58a7b6f6ea1d.png))
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `upower`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|----------|----------|-----------------|---------------------------------------------------|
|
||||
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "upower",
|
||||
"format": "{percentage}%"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "upower"
|
||||
format = "{percentage}%"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "upower"
|
||||
format: "{percentage}%"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "upower"
|
||||
format = "{percentage}%"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|---------------------------------|-----------------------------|
|
||||
| `#upower` | Upower widget container. |
|
||||
| `#upower #icon` | Upower widget battery icon. |
|
||||
| `#upower #button` | Upower widget button. |
|
||||
| `#upower #button #label` | Upower widget button label. |
|
||||
| `#popup-upower` | Clock popup box. |
|
||||
| `#popup-upower #upower-details` | Label inside the popup. |
|
@ -223,6 +223,8 @@ fn add_modules(
|
||||
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "tray")]
|
||||
ModuleConfig::Tray(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "upower")]
|
||||
ModuleConfig::Upower(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "workspaces")]
|
||||
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
|
||||
}
|
||||
|
@ -6,4 +6,6 @@ pub mod compositor;
|
||||
pub mod music;
|
||||
#[cfg(feature = "tray")]
|
||||
pub mod system_tray;
|
||||
#[cfg(feature = "upower")]
|
||||
pub mod upower;
|
||||
pub mod wayland;
|
||||
|
40
src/clients/upower.rs
Normal file
40
src/clients/upower.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use async_once::AsyncOnce;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use upower_dbus::UPowerProxy;
|
||||
use zbus::fdo::PropertiesProxy;
|
||||
|
||||
lazy_static! {
|
||||
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
|
||||
let dbus = zbus::Connection::system()
|
||||
.await
|
||||
.expect("failed to create connection to system bus");
|
||||
|
||||
let device_proxy = UPowerProxy::new(&dbus)
|
||||
.await
|
||||
.expect("failed to create upower proxy");
|
||||
|
||||
let display_device = device_proxy
|
||||
.get_display_device()
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("failed to get display device for {device_proxy:?}"));
|
||||
|
||||
let path = display_device.path().to_owned();
|
||||
|
||||
let proxy = PropertiesProxy::builder(&dbus)
|
||||
.destination("org.freedesktop.UPower")
|
||||
.expect("failed to set proxy destination address")
|
||||
.path(path)
|
||||
.expect("failed to set proxy path")
|
||||
.cache_properties(zbus::CacheProperties::No)
|
||||
.build()
|
||||
.await
|
||||
.expect("failed to build proxy");
|
||||
|
||||
Arc::new(proxy)
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get_display_proxy() -> &'static PropertiesProxy<'static> {
|
||||
DISPLAY_PROXY.get().await
|
||||
}
|
@ -16,6 +16,8 @@ use crate::modules::script::ScriptModule;
|
||||
use crate::modules::sysinfo::SysInfoModule;
|
||||
#[cfg(feature = "tray")]
|
||||
use crate::modules::tray::TrayModule;
|
||||
#[cfg(feature = "upower")]
|
||||
use crate::modules::upower::UpowerModule;
|
||||
#[cfg(feature = "workspaces")]
|
||||
use crate::modules::workspaces::WorkspacesModule;
|
||||
use crate::script::ScriptInput;
|
||||
@ -57,6 +59,8 @@ pub enum ModuleConfig {
|
||||
SysInfo(Box<SysInfoModule>),
|
||||
#[cfg(feature = "tray")]
|
||||
Tray(Box<TrayModule>),
|
||||
#[cfg(feature = "upower")]
|
||||
Upower(Box<UpowerModule>),
|
||||
#[cfg(feature = "workspaces")]
|
||||
Workspaces(Box<WorkspacesModule>),
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ pub mod script;
|
||||
pub mod sysinfo;
|
||||
#[cfg(feature = "tray")]
|
||||
pub mod tray;
|
||||
#[cfg(feature = "upower")]
|
||||
pub mod upower;
|
||||
#[cfg(feature = "workspaces")]
|
||||
pub mod workspaces;
|
||||
|
||||
|
281
src/modules/upower.rs
Normal file
281
src/modules/upower.rs
Normal file
@ -0,0 +1,281 @@
|
||||
use crate::clients::upower::get_display_proxy;
|
||||
use crate::config::CommonConfig;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::Popup;
|
||||
use crate::{await_sync, error, send_async, try_send};
|
||||
use color_eyre::Result;
|
||||
use futures_lite::stream::StreamExt;
|
||||
use gtk::{prelude::*, Button};
|
||||
use gtk::{Label, Orientation};
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use upower_dbus::BatteryState;
|
||||
use zbus;
|
||||
|
||||
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,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
fn default_format() -> String {
|
||||
String::from("{percentage}%")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UpowerProperties {
|
||||
percentage: f64,
|
||||
icon_name: String,
|
||||
state: u32,
|
||||
time_to_full: i64,
|
||||
time_to_empty: i64,
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for UpowerModule {
|
||||
type SendMessage = UpowerProperties;
|
||||
type ReceiveMessage = ();
|
||||
|
||||
fn name() -> &'static str {
|
||||
"upower"
|
||||
}
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
_rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
spawn(async move {
|
||||
// await_sync due to strange "higher-ranked lifetime error"
|
||||
let display_proxy = await_sync(async move { get_display_proxy().await });
|
||||
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,
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
let icon_theme = info.icon_theme.clone();
|
||||
let icon = gtk::Image::builder().name("icon").build();
|
||||
|
||||
let label = Label::builder()
|
||||
.label(&self.format)
|
||||
.use_markup(true)
|
||||
.name("label")
|
||||
.build();
|
||||
|
||||
let container = gtk::Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.name("upower")
|
||||
.build();
|
||||
|
||||
let button = Button::builder().name("button").build();
|
||||
|
||||
button.add(&label);
|
||||
container.add(&button);
|
||||
container.add(&icon);
|
||||
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
context.tx,
|
||||
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
|
||||
);
|
||||
});
|
||||
|
||||
label.set_angle(info.bar_position.get_angle());
|
||||
let format = self.format.clone();
|
||||
|
||||
context
|
||||
.widget_rx
|
||||
.attach(None, move |properties: UpowerProperties| {
|
||||
let format = format.replace("{percentage}", &properties.percentage.to_string());
|
||||
let icon_name = String::from("icon:") + &properties.icon_name;
|
||||
if let Err(err) = ImageProvider::parse(&icon_name, &icon_theme, 32)
|
||||
.and_then(|provider| provider.load_into_image(icon.clone()))
|
||||
{
|
||||
error!("{err:?}");
|
||||
}
|
||||
label.set_markup(format.as_ref());
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: container,
|
||||
popup,
|
||||
})
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
_tx: Sender<Self::ReceiveMessage>,
|
||||
rx: glib::Receiver<Self::SendMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let container = gtk::Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.name("popup-upower")
|
||||
.build();
|
||||
|
||||
let label = Label::builder().name("upower-details").build();
|
||||
container.add(&label);
|
||||
|
||||
rx.attach(None, move |properties| {
|
||||
let mut format = String::new();
|
||||
let state = u32_to_battery_state(properties.state);
|
||||
match state {
|
||||
Ok(BatteryState::Charging | BatteryState::PendingCharge) => {
|
||||
let ttf = properties.time_to_full;
|
||||
if ttf > 0 {
|
||||
format = format!("Full in {}", seconds_to_string(ttf));
|
||||
}
|
||||
}
|
||||
Ok(BatteryState::Discharging | BatteryState::PendingDischarge) => {
|
||||
let tte = properties.time_to_empty;
|
||||
if tte > 0 {
|
||||
format = format!("Empty in {}", seconds_to_string(tte));
|
||||
}
|
||||
}
|
||||
Err(state) => error!("Invalid battery state: {state}"),
|
||||
_ => {}
|
||||
}
|
||||
label.set_markup(&format);
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user