diff --git a/Cargo.lock b/Cargo.lock index e74511f..c437204 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -712,6 +712,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -1032,6 +1043,8 @@ dependencies = [ "local-ip-address", "log", "maxminddb", + "num-derive", + "num-traits", "pretty_env_logger", "reqwest", "serde", diff --git a/Cargo.toml b/Cargo.toml index aac876c..9271199 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ futures = { version = "0.3", default-features = false } local-ip-address = "0.5" log = "0.4" maxminddb = "0.23" +num-derive = "0.4" +num-traits = "0.2" pretty_env_logger = "0.5" reqwest = { version = "0.11", features = ["rustls-tls"], default-features = false } serde = { version = "1.0", features = ["derive"] } diff --git a/build.rs b/build.rs index 91c345a..647cb64 100644 --- a/build.rs +++ b/build.rs @@ -16,6 +16,7 @@ fn main() { "org.freedesktop.NetworkManager.AccessPoint.xml", "org.freedesktop.NetworkManager.Connection.Active.xml", "org.freedesktop.NetworkManager.xml", + "org.freedesktop.UPower.Device.xml", "org.freedesktop.hostname1.xml", ]); let required_interfaces = required_files diff --git a/flake.nix b/flake.nix index cc93507..c7d60fe 100644 --- a/flake.nix +++ b/flake.nix @@ -13,7 +13,7 @@ pkgs = import nixpkgs { inherit system; }; inherit (pkgs) lib; dbusPaths = lib.makeSearchPathOutput "out" "share/dbus-1/interfaces" - (with pkgs; [ systemd networkmanager ]); + (with pkgs; [ systemd networkmanager upower ]); in rec { packages.rustybar = with pkgs; rustPlatform.buildRustPackage { diff --git a/src/config.rs b/src/config.rs index 3871dec..fd5548c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -226,7 +226,7 @@ pub fn launch_tile( ) -> JoinHandle<()> { let output_chan = OutputChannel::with_random_uuid(sender); match tile.clone() { - TileConfig::Battery(c) => spawn(tiles::battery(c, output_chan), tile_id), + TileConfig::Battery(c) => spawn(tiles::battery(c, output_chan, dbus_conn.clone()), tile_id), TileConfig::Hostname(c) => { spawn(tiles::hostname(c, output_chan, dbus_conn.clone()), tile_id) } diff --git a/src/formatting.rs b/src/formatting.rs index 5ccf7aa..0dc99e8 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -26,7 +26,7 @@ static BATTERY_CHARGING_SYMBOLS: &[char] = &[ '\u{f0085}', // nf-md-battery_charging_100 (󰂅) ]; -pub fn charging_symbol(percentage: f64, charging: bool) -> char { +pub fn battery_symbol(percentage: f64, charging: bool) -> char { let index = (percentage.clamp(0.0, 100.0) / 10.0).round() as usize; if charging { BATTERY_CHARGING_SYMBOLS[index] diff --git a/src/main.rs b/src/main.rs index 2c750d4..7a76a4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ async fn main() -> eyre::Result<()> { let err = resource.await; panic!("Lost connection to D-Bus: {}", err); }); + dbus_conn.set_signal_match_mode(true); let mut stream_map = StreamMap::new(); diff --git a/src/tiles/battery.rs b/src/tiles/battery.rs index 8fd2cc9..86bc508 100644 --- a/src/tiles/battery.rs +++ b/src/tiles/battery.rs @@ -1,57 +1,156 @@ +#![allow(clippy::option_map_unit_fn)] use crate::config::BatteryConfig; -use crate::formatting::charging_symbol; +use crate::formatting::battery_symbol; +use crate::generated::OrgFreedesktopUPowerDevice; use crate::output::OutputChannel; use crate::tile::Block; -use std::convert::Infallible; -use std::path::Path; -use std::str::FromStr; -use tokio::fs::try_exists; +use dbus::arg::RefArg; +use dbus::arg::Variant; +use dbus::message::SignalArgs; +use dbus::nonblock::stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged; +use dbus::nonblock::{Proxy, SyncConnection}; +use dbus::strings::BusName; +use eyre::OptionExt; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use std::convert::TryInto; +use std::sync::Arc; +use std::{convert::Infallible, time::Duration}; +use tokio::sync::watch; use tokio::try_join; -pub async fn battery(config: BatteryConfig, output: OutputChannel) -> eyre::Result { - let base_dir = Path::new("/sys/class/power_supply").join(&*config.battery); - let file_prefix = if try_exists(base_dir.join("energy_now")).await? { - "energy_" - } else { - "charge_" - }; - let now_path = base_dir.join(file_prefix.to_owned() + "now"); - let full_path = base_dir.join(file_prefix.to_owned() + "full"); - let status_path = base_dir.join("status"); +#[derive(FromPrimitive, Debug, PartialEq, Eq)] +enum DeviceState { + Unknown = 0, + Charging = 1, + Discharging = 2, + Empty = 3, + FullyCharged = 4, + PendingCharge = 5, +} - let mut interval = tokio::time::interval(config.update); - loop { - interval.tick().await; +#[derive(Debug)] +struct BatteryInfo { + pub percentage: f64, + pub state: DeviceState, + pub time_to_empty: Duration, + pub time_to_full: Duration, +} - async fn read_and_parse(path: &Path) -> eyre::Result - where - ::Err: Send + Sync + std::error::Error + 'static, - { - Ok(tokio::fs::read_to_string(path).await?.trim_end().parse()?) +fn format_duration(dur: Duration) -> String { + let hours = dur.as_secs() / 60 / 60; + let minutes = (dur.as_secs() / 60) % 60; + format!(" ({}:{:02})", hours, minutes) +} + +pub async fn battery( + config: BatteryConfig, + output: OutputChannel, + dbus_conn: Arc, +) -> eyre::Result { + let dest = Box::leak(Box::new(BusName::new("org.freedesktop.UPower").unwrap())); + let path = Box::leak(Box::new( + dbus::Path::new("/org/freedesktop/UPower/devices/DisplayDevice").unwrap(), + )); + let proxy = Proxy::new( + dest.clone(), + path.clone(), + Duration::from_secs(5), + dbus_conn.as_ref(), + ); + + let init_info = { + let (percentage, raw_state, time_to_empty, time_to_full) = try_join!( + proxy.percentage(), + proxy.state(), + proxy.time_to_empty(), + proxy.time_to_full() + )?; + BatteryInfo { + percentage, + state: DeviceState::from_u32(raw_state).ok_or_eyre("Invalid state")?, + time_to_empty: Duration::from_secs(time_to_empty.clamp(0, i64::MAX).try_into()?), + time_to_full: Duration::from_secs(time_to_full.clamp(0, i64::MAX).try_into()?), } + }; - let charge_now = read_and_parse::(&now_path); - let charge_total = read_and_parse::(&full_path); - let status = read_and_parse::(&status_path); - let (charge_now, charge_total, status) = try_join!(charge_now, charge_total, status)?; + eprintln!("{:#?}", init_info); - let percentage = charge_now * 100.0 / charge_total; - let is_charging = status != "Discharging"; - let block = if config.use_symbols { - let symbol = charging_symbol(percentage, is_charging); - Block { - full_text: format!("{} {:.0}%", symbol, percentage).into(), - short_text: symbol.to_string().into_boxed_str().into(), - ..Default::default() - } - } else { - Block { - full_text: format!("{:.0}% {}", percentage, status).into(), - short_text: format!("{:.0}%", percentage).into_boxed_str().into(), - ..Default::default() + let (tx, mut rx) = watch::channel(init_info); + + let _reciever = dbus_conn + .add_match(PropertiesPropertiesChanged::match_rule( + Some(dest), + Some(path), + )) + .await? + .cb(move |_, changed: PropertiesPropertiesChanged| { + tx.send_modify(|info| { + let props = changed.changed_properties; + props + .get("Percentage") + .and_then(Variant::as_f64) + .map(|val| info.percentage = val); + + props + .get("State") + .and_then(Variant::as_u64) + .and_then(DeviceState::from_u64) + .map(|val| info.state = val); + + props + .get("TimeToEmpty") + .and_then(Variant::as_i64) + .map(|val| val.clamp(0, i64::MAX).try_into().unwrap()) + .map(Duration::from_secs) + .map(|val| info.time_to_empty = val); + + props + .get("TimeToFull") + .and_then(Variant::as_i64) + .map(|val| val.clamp(0, i64::MAX).try_into().unwrap()) + .map(Duration::from_secs) + .map(|val| info.time_to_full = val); + }); + true + }); + + loop { + let block = { + let info = rx.borrow(); + let time_str = match info.state { + DeviceState::Discharging => format_duration(info.time_to_empty), + DeviceState::Charging => format_duration(info.time_to_full), + _ => "".to_string(), + }; + if config.use_symbols { + let symbol = match info.state { + DeviceState::Charging | DeviceState::FullyCharged => { + battery_symbol(info.percentage, true) + } + DeviceState::Discharging | DeviceState::Empty => { + battery_symbol(info.percentage, false) + } + DeviceState::Unknown => '\u{f0091}', + DeviceState::PendingCharge => '\u{f1211}', + }; + Block { + full_text: format!("{} {:.0}%{}", symbol, info.percentage, time_str).into(), + short_text: symbol.to_string().into_boxed_str().into(), + ..Default::default() + } + } else { + Block { + full_text: format!("{:.0}% {:?}{}", info.percentage, info.state, time_str) + .into(), + short_text: format!("{:.0}%", info.percentage).into_boxed_str().into(), + ..Default::default() + } } }; output.send(block).await?; + + rx.changed().await?; } }