diff --git a/Cargo.toml b/Cargo.toml index ecf0689..aadb2ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,4 @@ anyhow = "1.0.98" dbus = { version = "0.9.7", features = ["vendored"] } nix = { version = "0.30.1", features = ["user"] } serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" +serde_json = "1.0.140" \ No newline at end of file diff --git a/README.md b/README.md index 516a0ec..5feec10 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # pumo-system-info A Rust-based utility for gathering and presenting system information -in JSON format. For now, it only retrieves Bluetooth-related -information. +in JSON format. ## Overview @@ -15,6 +14,7 @@ be used with my [eww](https://elkowar.github.io/eww/) configuration. ## Prerequisites - DBus - BlueZ (for retrieving Bluetooth-related information) +- UPower (for retrieving power information) - Rust (specified in `rustup-toolchain.toml`) - Nix package manager with flakes (optional, for building and managing dependencies) diff --git a/src/main.rs b/src/main.rs index 1919f2e..8664219 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,21 +3,25 @@ use dbus::blocking::Connection; use bluetooth::BluetoothInfo; use user_info::UserInfo; +use power::PowerInfo; mod bluetooth; mod user_info; +mod power; #[derive(serde::Serialize)] struct SystemInfo { bluetooth: Vec, - user: UserInfo + user: UserInfo, + power: PowerInfo } fn main() -> Result<()> { let connection = Connection::new_system()?; let bluetooth = bluetooth::get_all_bluetooth_devices(&connection); let user = UserInfo::new(&connection).unwrap_or_default(); - let info = SystemInfo { bluetooth, user }; + let power = PowerInfo::new(&connection); + let info = SystemInfo { bluetooth, user, power }; println!("{}", serde_json::to_string(&info)?); Ok(()) } diff --git a/src/power.rs b/src/power.rs new file mode 100644 index 0000000..ea9737a --- /dev/null +++ b/src/power.rs @@ -0,0 +1,172 @@ +use dbus::{arg, blocking}; +use std::fmt; + +const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(5000); +const UPOWER_DEST: &str = "org.freedesktop.UPower"; +const UPOWER_DEVICE: &str = "org.freedesktop.UPower.Device"; + +#[derive(Debug)] +pub enum BatteryState { + Unknown, + Charging, + Discharging, + Empty, + FullyCharged, + PendingCharge, + PendingDischarge, +} + +impl Default for BatteryState { + fn default() -> Self { + Self::Unknown + } +} + +impl From for BatteryState { + fn from(value: u32) -> Self { + match value { + 1 => Self::Charging, + 2 => Self::Discharging, + 3 => Self::Empty, + 4 => Self::FullyCharged, + 5 => Self::PendingCharge, + 6 => Self::PendingDischarge, + _ => Self::Unknown, + } + } +} + +impl fmt::Display for BatteryState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let str = match self { + BatteryState::Unknown => "unknown", + BatteryState::Charging => "charging", + BatteryState::Discharging => "discharging", + BatteryState::Empty => "empty", + BatteryState::FullyCharged => "fully charged", + BatteryState::PendingCharge => "pending charge", + BatteryState::PendingDischarge => "pending discharge", + }; + write!(f, "{str}") + } +} + +impl serde::Serialize for BatteryState { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +#[derive(serde::Serialize, Default)] +pub struct BatteryInfo { + pub batteries: Vec, + pub on_battery: bool, +} + +impl BatteryInfo { + fn get_batteries( + connection: &blocking::Connection, + proxy: &blocking::Proxy<'_, &blocking::Connection>, + ) -> Vec { + let (batteries,): (Vec,) = proxy + .method_call(UPOWER_DEST, "EnumerateDevices", ()) + .unwrap_or_default(); + let batteries: Vec = batteries + .iter() + .filter_map(|path| match path.clone().into_cstring().into_string() { + Err(_) => None, + Ok(path) => { + if path.contains("BAT") { + Some(path) + } else { + None + } + } + }) + .flat_map(|path| Battery::new(connection, path).ok()) + .collect(); + batteries + } + + pub fn new(connection: &blocking::Connection) -> Self { + let proxy = connection.with_proxy(UPOWER_DEST, "/org/freedesktop/UPower", TIMEOUT); + use blocking::stdintf::org_freedesktop_dbus::Properties; + let on_battery: bool = proxy.get(UPOWER_DEST, "OnBattery").unwrap_or(false); + let batteries = Self::get_batteries(connection, &proxy); + Self { + on_battery, + batteries, + } + } +} + +#[derive(serde::Serialize, Default, Debug)] +pub struct Battery { + pub level: f64, + pub icon: String, + pub time_to_empty: i64, + pub time_to_full: i64, + pub state: BatteryState, +} + +impl Battery { + pub fn new(connection: &blocking::Connection, path: String) -> anyhow::Result { + let proxy = connection.with_proxy(UPOWER_DEST, path.clone(), TIMEOUT); + use blocking::stdintf::org_freedesktop_dbus::Properties; + let state: u32 = proxy.get(UPOWER_DEVICE, "State")?; + let result = Self { + level: proxy.get(UPOWER_DEVICE, "Percentage")?, + icon: proxy.get(UPOWER_DEVICE, "IconName")?, + time_to_empty: proxy.get(UPOWER_DEVICE, "TimeToEmpty").unwrap_or_default(), + time_to_full: proxy.get(UPOWER_DEVICE, "TimeToFull").unwrap_or_default(), + state: state.into(), + }; + Ok(result) + } +} + +#[derive(serde::Serialize, Default)] +pub struct PowerProfile { + pub current: String, + pub available: Vec, +} + +impl PowerProfile { + pub fn new(connection: &blocking::Connection) -> anyhow::Result { + let proxy = connection.with_proxy( + "org.freedesktop.UPower.PowerProfiles", + "/org/freedesktop/UPower/PowerProfiles", + TIMEOUT, + ); + use blocking::stdintf::org_freedesktop_dbus::Properties; + let current: String = proxy.get("org.freedesktop.UPower.PowerProfiles", "ActiveProfile")?; + let available: Vec = + proxy.get("org.freedesktop.UPower.PowerProfiles", "Profiles")?; + let available: Vec = available + .iter() + .flat_map(|profile| profile.get("Profile")?.0.as_str()) + .map(|profile| profile.to_owned()) + .collect(); + Ok(Self { current, available }) + } +} + +#[derive(serde::Serialize)] +pub struct PowerInfo { + pub power_profile: PowerProfile, + pub batteries: BatteryInfo, +} + +impl PowerInfo { + pub fn new(connection: &blocking::Connection) -> Self { + let power_profile = PowerProfile::new(connection).unwrap_or_default(); + let batteries = BatteryInfo::new(connection); + Self { + power_profile, + batteries, + } + } +}