basic battery module/service

This commit is contained in:
2026-09-16 11:06:12 +01:00
parent 120eaf5855
commit 63e1c0ee4c
10 changed files with 639 additions and 1 deletions
Generated
+360
View File
@@ -145,6 +145,120 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"pin-project-lite",
"slab",
]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix 1.1.4",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener",
"futures-lite",
"rustix 1.1.4",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "async-signal"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix 1.1.4",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.6" version = "0.3.6"
@@ -167,6 +281,23 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.5",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -256,6 +387,19 @@ dependencies = [
"objc2 0.5.2", "objc2 0.5.2",
] ]
[[package]]
name = "blocking"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]] [[package]]
name = "borsh" name = "borsh"
version = "1.8.1" version = "1.8.1"
@@ -854,6 +998,12 @@ dependencies = [
"simdutf8", "simdutf8",
] ]
[[package]]
name = "endi"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]] [[package]]
name = "enumflags2" name = "enumflags2"
version = "0.7.12" version = "0.7.12"
@@ -861,6 +1011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [ dependencies = [
"enumflags2_derive", "enumflags2_derive",
"serde",
] ]
[[package]] [[package]]
@@ -915,6 +1066,26 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "event-listener"
version = "5.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
dependencies = [
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.5.0" version = "2.5.0"
@@ -1082,7 +1253,10 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [ dependencies = [
"fastrand",
"futures-core", "futures-core",
"futures-io",
"parking",
"pin-project-lite", "pin-project-lite",
] ]
@@ -1354,6 +1528,12 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hexf-parse" name = "hexf-parse"
version = "0.2.1" version = "0.2.1"
@@ -2200,6 +2380,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "metal" name = "metal"
version = "0.32.0" version = "0.32.0"
@@ -2748,6 +2937,16 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]] [[package]]
name = "owned_ttf_parser" name = "owned_ttf_parser"
version = "0.25.1" version = "0.25.1"
@@ -2757,6 +2956,12 @@ dependencies = [
"ttf-parser", "ttf-parser",
] ]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@@ -2830,6 +3035,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]] [[package]]
name = "pipewire-native" name = "pipewire-native"
version = "0.1.4" version = "0.1.4"
@@ -3529,6 +3745,7 @@ dependencies = [
"serde_json", "serde_json",
"tokio", "tokio",
"tracing", "tracing",
"zbus",
] ]
[[package]] [[package]]
@@ -3546,6 +3763,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]] [[package]]
name = "simd_cesu8" name = "simd_cesu8"
version = "1.2.0" version = "1.2.0"
@@ -4204,6 +4431,17 @@ dependencies = [
"core_maths", "core_maths",
] ]
[[package]]
name = "uds_windows"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.18" version = "0.3.18"
@@ -4282,6 +4520,17 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
dependencies = [
"js-sys",
"serde_core",
"wasm-bindgen",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.1" version = "0.1.1"
@@ -5164,6 +5413,76 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zbus"
version = "5.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
dependencies = [
"async-broadcast",
"async-executor",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"libc",
"ordered-stream",
"rustix 1.1.4",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.5",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
dependencies = [
"serde",
"winnow",
"zvariant",
]
[[package]]
name = "zcheapstr"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "zeno" name = "zeno"
version = "0.3.3" version = "0.3.3"
@@ -5255,3 +5574,44 @@ name = "zmij"
version = "1.0.23" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]]
name = "zvariant"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow",
"zcheapstr",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.5",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 3.0.5",
"winnow",
]
+2
View File
@@ -17,6 +17,7 @@ pub struct BarbarConfig {
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Modules { pub enum Modules {
Audio, Audio,
Battery,
Clock, Clock,
Weather, Weather,
Workspaces, Workspaces,
@@ -26,6 +27,7 @@ impl Modules {
pub fn to_string(&self) -> &str { pub fn to_string(&self) -> &str {
match self { match self {
Modules::Audio => "audio", Modules::Audio => "audio",
Modules::Battery => "battery",
Modules::Clock => "clock", Modules::Clock => "clock",
Modules::Weather => "weather", Modules::Weather => "weather",
Modules::Workspaces => "workspaces", Modules::Workspaces => "workspaces",
@@ -0,0 +1,44 @@
//! Battery service messages.
use crate::Service;
/// Route key a module subscribes under to receive battery events. One
/// stream carries every event; the [`BatteryEvent`] payload tags which one,
/// so modules filter on it.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum BatteryMsg {
Events,
}
impl BatteryMsg {
/// Route key a module subscribes under to receive this service's events.
pub fn key(self) -> Service {
Service(match self {
Self::Events => "battery.events",
})
}
}
/// Charge state of the first battery, without depending on D-Bus types
/// outside the service itself.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub enum BatteryStatus {
#[default]
Unknown,
Charging,
Discharging,
Empty,
Full,
}
/// A battery event, published to subscribers of the events key.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum BatteryEvent {
/// Charge percentage changed (0..=100).
PercentageChanged(i32),
/// Charging state changed.
StatusChanged(BatteryStatus),
/// Seconds until full (charging) or empty (discharging) changed;
/// `None` when unknown or not applicable.
EtaChanged(Option<u64>),
}
@@ -1,10 +1,12 @@
//! Messages owned by services. //! Messages owned by services.
mod battery;
mod datetime; mod datetime;
mod hyprland; mod hyprland;
mod pipewire; mod pipewire;
mod weather; mod weather;
pub use battery::*;
pub use datetime::*; pub use datetime::*;
pub use hyprland::*; pub use hyprland::*;
pub use pipewire::*; pub use pipewire::*;
+41
View File
@@ -0,0 +1,41 @@
//! Battery module: prints the charge percentage from the battery service.
use iced::widget::text;
use iced::{Element, Task};
use common::{BarModule, BatteryEvent, BatteryMsg, ModuleEffect, Service, Wire};
pub struct Battery {
percent: Option<i32>,
}
impl Battery {
pub fn new() -> Self {
Self { percent: None }
}
}
impl BarModule for Battery {
fn id(&self) -> &'static str {
"battery"
}
fn view(&self, _window_id: Option<iced::window::Id>) -> Element<'_, Wire> {
match self.percent {
Some(percent) => text(format!("{percent}%")).into(),
None => text("--").into(),
}
}
fn update(&mut self, msg: Wire) -> Task<ModuleEffect> {
if let Some(BatteryEvent::PercentageChanged(percent)) = msg.downcast::<BatteryEvent>()
{
self.percent = Some(*percent);
}
Task::none()
}
fn services(&self) -> Vec<Service> {
vec![BatteryMsg::Events.key()]
}
}
+2
View File
@@ -2,6 +2,7 @@
//! a new file under `src/` plus one entry in `all()` — core never changes. //! a new file under `src/` plus one entry in `all()` — core never changes.
mod audio; mod audio;
mod battery;
mod clock; mod clock;
mod weather; mod weather;
mod workspaces; mod workspaces;
@@ -15,6 +16,7 @@ pub use weather::*;
pub fn all(module_config: Table) -> Vec<Box<dyn BarModule>> { pub fn all(module_config: Table) -> Vec<Box<dyn BarModule>> {
vec![ vec![
Box::new(audio::Audio::new()), Box::new(audio::Audio::new()),
Box::new(battery::Battery::new()),
Box::new(clock::Clock::new(Some(module_config.clone()))), Box::new(clock::Clock::new(Some(module_config.clone()))),
Box::new(weather::WeatherModule::default()), Box::new(weather::WeatherModule::default()),
Box::new(workspaces::Workspaces::new(Some(module_config))), Box::new(workspaces::Workspaces::new(Some(module_config))),
+1
View File
@@ -9,6 +9,7 @@ path = "src/lib.rs"
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
zbus = "5"
iced = { workspace = true } iced = { workspace = true }
tokio = { workspace = true, features = ["net", "io-util"] } tokio = { workspace = true, features = ["net", "io-util"] }
tracing = { workspace = true } tracing = { workspace = true }
+184
View File
@@ -0,0 +1,184 @@
//! Battery service: follows UPower's display device over D-Bus and
//! republishes each aspect whenever UPower pushes a change — no polling.
//!
//! Machines without a battery (or without UPower) simply never publish;
//! the service keeps retrying in case UPower shows up later.
use std::time::Duration;
use common::{BatteryEvent, BatteryStatus, Endpoint, Inbox, Wire};
use iced::futures::{channel::mpsc, SinkExt, StreamExt};
use iced::Subscription;
/// Route-key namespace owned by this service; keys are `"battery.<kind>"`.
pub const NAMESPACE: &str = "battery.";
/// How long to wait before retrying when UPower is unreachable.
const RETRY: Duration = Duration::from_secs(30);
/// UPower device states (`org.freedesktop.UPower.Device.State`).
const CHARGING: u32 = 1;
const DISCHARGING: u32 = 2;
const EMPTY: u32 = 3;
const FULLY_CHARGED: u32 = 4;
pub struct BatteryService;
impl BatteryService {
pub fn run(inbox: Inbox) -> Subscription<Wire> {
Subscription::run_with(inbox, |inbox| {
let key = inbox.key();
// We dont need to rx any msgs from modules
let mut _rx = inbox.take().expect("inbox receiver is taken once");
iced::stream::channel(0, move |mut sender: mpsc::Sender<Wire>| async move {
tracing::info!(key, "battery service started");
let mut last: Option<(i32, BatteryStatus, i64, i64)> = None;
loop {
match serve(&mut sender, key, &mut last).await {
// Subscriber gone: stop the service.
Ok(false) => return,
Ok(true) => tracing::debug!(key, "upower stream ended; reconnecting"),
Err(err) => tracing::debug!(key, %err, "upower unavailable; retrying"),
}
tokio::time::sleep(RETRY).await;
}
})
})
}
}
/// Follows the display device until UPower goes away. Returns whether the
/// subscriber is still alive (`false` stops the service, `true` reconnects).
async fn serve(
sender: &mut mpsc::Sender<Wire>,
key: &'static str,
last: &mut Option<(i32, BatteryStatus, i64, i64)>,
) -> Result<bool, zbus::Error> {
let conn = zbus::Connection::system().await?;
let display = UPowerProxy::new(&conn).await?.get_display_device().await?;
let device = DeviceProxy::builder(&conn).path(display)?.build().await?;
if !emit(sender, key, last, read(&device).await?).await {
return Ok(false);
}
let mut percentage = device.receive_percentage_changed().await;
let mut state = device.receive_state_changed().await;
let mut time_to_empty = device.receive_time_to_empty_changed().await;
let mut time_to_full = device.receive_time_to_full_changed().await;
loop {
// `last` is always `Some` here: the initial read published first.
let (percent, status, to_full, to_empty) = last.unwrap_or_default();
let current = tokio::select! {
Some(change) = percentage.next() => (change.get().await?.round() as i32, status, to_full, to_empty),
Some(change) = state.next() => (percent, map_state(change.get().await?), to_full, to_empty),
Some(change) = time_to_empty.next() => (percent, status, to_full, change.get().await?),
Some(change) = time_to_full.next() => (percent, status, change.get().await?, to_empty),
else => return Ok(true),
};
if !emit(sender, key, last, current).await {
return Ok(false);
}
}
}
/// Sends the aspects that differ from `last`; `false` when the subscriber
/// is gone and the service should stop.
async fn emit(
sender: &mut mpsc::Sender<Wire>,
key: &'static str,
last: &mut Option<(i32, BatteryStatus, i64, i64)>,
(percent, status, to_full, to_empty): (i32, BatteryStatus, i64, i64),
) -> bool {
let mut events = Vec::new();
if last.map(|(p, _, _, _)| p) != Some(percent) {
tracing::debug!(key, percent, "publishing battery percentage");
events.push(BatteryEvent::PercentageChanged(percent));
}
if last.map(|(_, s, _, _)| s) != Some(status) {
tracing::debug!(key, ?status, "publishing battery status");
events.push(BatteryEvent::StatusChanged(status));
}
let old_eta = last.map(|(_, s, f, e)| eta(s, f, e)).unwrap_or_default();
let new_eta = eta(status, to_full, to_empty);
if old_eta != new_eta {
tracing::debug!(key, ?new_eta, "publishing battery eta");
events.push(BatteryEvent::EtaChanged(new_eta));
}
*last = Some((percent, status, to_full, to_empty));
for event in events {
let wire = Wire::topic(Endpoint::service(key), key, event);
if sender.send(wire).await.is_err() {
tracing::warn!(key, "battery send failed; stopping service");
return false;
}
}
true
}
/// Current percentage, status, and raw UPower time estimates of the display device.
async fn read(device: &DeviceProxy<'_>) -> Result<(i32, BatteryStatus, i64, i64), zbus::Error> {
Ok((
device.percentage().await?.round() as i32,
map_state(device.state().await?),
device.time_to_full().await?,
device.time_to_empty().await?,
))
}
/// Seconds until full while charging, or until empty while discharging.
/// UPower reports 0 when there is no estimate, and neither state applies
/// once full, empty, or unknown — all of those surface as `None`.
fn eta(status: BatteryStatus, to_full: i64, to_empty: i64) -> Option<u64> {
let secs = match status {
BatteryStatus::Charging => to_full,
BatteryStatus::Discharging => to_empty,
_ => return None,
};
(secs > 0).then_some(secs as u64)
}
/// UPower reports pending/unknown states our protocol has no variant for;
/// those surface as [`BatteryStatus::Unknown`].
fn map_state(state: u32) -> BatteryStatus {
match state {
CHARGING => BatteryStatus::Charging,
DISCHARGING => BatteryStatus::Discharging,
EMPTY => BatteryStatus::Empty,
FULLY_CHARGED => BatteryStatus::Full,
_ => BatteryStatus::Unknown,
}
}
#[zbus::proxy(
interface = "org.freedesktop.UPower",
default_service = "org.freedesktop.UPower",
default_path = "/org/freedesktop/UPower"
)]
trait UPower {
/// Path of the composite display device aggregating all batteries.
fn get_display_device(&self) -> zbus::Result<zbus::zvariant::OwnedObjectPath>;
}
#[zbus::proxy(
interface = "org.freedesktop.UPower.Device",
default_service = "org.freedesktop.UPower"
)]
trait Device {
/// Charge percentage (0..=100).
#[zbus(property)]
fn percentage(&self) -> zbus::Result<f64>;
/// Device state (1 = charging, 2 = discharging, 3 = empty, 4 = full).
#[zbus(property)]
fn state(&self) -> zbus::Result<u32>;
/// Seconds until empty (0 when unknown).
#[zbus(property)]
fn time_to_empty(&self) -> zbus::Result<i64>;
/// Seconds until full (0 when unknown).
#[zbus(property)]
fn time_to_full(&self) -> zbus::Result<i64>;
}
+2
View File
@@ -6,6 +6,7 @@ use iced::Subscription;
use common::{Inbox, Service, Wire}; use common::{Inbox, Service, Wire};
pub mod battery;
pub mod compositors; pub mod compositors;
pub mod datetime; pub mod datetime;
pub mod pipewire; pub mod pipewire;
@@ -24,6 +25,7 @@ impl IntoSubscription for Service {
fn into_subscription(self, inbox: Inbox) -> Subscription<Wire> { fn into_subscription(self, inbox: Inbox) -> Subscription<Wire> {
let key = self.0; let key = self.0;
match key { match key {
k if k.starts_with(battery::NAMESPACE) => battery::BatteryService::run(inbox),
k if k.starts_with(datetime::NAMESPACE) => datetime::ClockTicker::run(inbox), k if k.starts_with(datetime::NAMESPACE) => datetime::ClockTicker::run(inbox),
k if k.starts_with(compositors::hyprland::NAMESPACE) => { k if k.starts_with(compositors::hyprland::NAMESPACE) => {
compositors::hyprland::HyprlandService::run(inbox) compositors::hyprland::HyprlandService::run(inbox)
+1 -1
View File
@@ -16,7 +16,7 @@ theme = 'example_base16.yaml'
[order] [order]
left = ["workspaces", "weather"] left = ["workspaces", "weather"]
middle = [] middle = []
right = ["audio", "clock"] right = ["battery", "audio", "clock"]
[modules.clock] [modules.clock]
show_seconds = false show_seconds = false