diff --git a/README.md b/README.md index 12ae6e7..9865551 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ This will build ashell from source, but you can also use `pkgs.ashell` from nixp - OS Updates indicator - Hyprland Active Window - Hyprland Workspaces -- System Information (CPU, RAM, Temperature) +- System Information (CPU, RAM, Temperature, Peripheral battery levels) - Hyprland Keyboard Layout - Hyprland Keyboard Submap - Tray @@ -219,6 +219,7 @@ enable_workspace_filling = false # - IpAddress # - DownloadSpeed # - UploadSpeed +# - Peripherals # optional, the following is the default configuration # If for example you want to dispay the usage of the root and home partition # you can use the following configuration diff --git a/src/app.rs b/src/app.rs index e34e34e..23c18f8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,7 +23,10 @@ use crate::{ }, outputs::{HasOutput, Outputs}, position_button::ButtonUIRef, - services::{Service, ServiceEvent, brightness::BrightnessCommand, tray::TrayEvent}, + services::{ + ReadOnlyService, Service, ServiceEvent, brightness::BrightnessCommand, tray::TrayEvent, + upower::UPowerService, + }, style::{ashell_theme, backdrop_color, darken_color}, utils, }; @@ -59,6 +62,7 @@ pub struct App { pub privacy: Privacy, pub settings: Settings, pub media_player: MediaPlayer, + pub upower: Option, } #[derive(Debug, Clone)] @@ -80,6 +84,7 @@ pub enum Message { Privacy(modules::privacy::PrivacyMessage), Settings(modules::settings::Message), MediaPlayer(modules::media_player::Message), + UPowerEvent(ServiceEvent), OutputEvent((OutputEvent, WlOutput)), } @@ -105,6 +110,7 @@ impl App { privacy: Privacy::default(), settings: Settings::default(), media_player: MediaPlayer::default(), + upower: None, config, }, task, @@ -249,6 +255,20 @@ impl App { self.settings .update(message, &self.config.settings, &mut self.outputs) } + Message::MediaPlayer(msg) => self.media_player.update(msg), + Message::UPowerEvent(event) => match event { + ServiceEvent::Init(service) => { + self.upower = Some(service); + Task::none() + } + ServiceEvent::Update(data) => { + if let Some(upower) = self.upower.as_mut() { + upower.update(data); + } + Task::none() + } + ServiceEvent::Error(_) => Task::none(), + }, Message::OutputEvent((event, wl_output)) => match event { iced::event::wayland::OutputEvent::Created(info) => { info!("Output created: {:?}", info); @@ -275,7 +295,6 @@ impl App { } _ => Task::none(), }, - Message::MediaPlayer(msg) => self.media_player.update(msg), } } @@ -441,7 +460,9 @@ impl App { ), Some((MenuType::SystemInfo, button_ui_ref)) => menu_wrapper( id, - self.system_info.menu_view().map(Message::SystemInfo), + self.system_info + .menu_view(&self.upower) + .map(Message::SystemInfo), MenuSize::Large, *button_ui_ref, self.config.position, @@ -460,6 +481,7 @@ impl App { Subscription::batch(self.modules_subscriptions(&self.config.modules.left)), Subscription::batch(self.modules_subscriptions(&self.config.modules.center)), Subscription::batch(self.modules_subscriptions(&self.config.modules.right)), + UPowerService::subscribe().map(Message::UPowerEvent), config::subscription(), listen_with(|evt, _, _| match evt { iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( diff --git a/src/components/icons.rs b/src/components/icons.rs index 7d78502..5aa51c5 100644 --- a/src/components/icons.rs +++ b/src/components/icons.rs @@ -74,6 +74,8 @@ pub enum Icons { UploadSpeed, Copy, RightChevron, + Mouse, + Keyboard, } impl From for &'static str { @@ -147,6 +149,8 @@ impl From for &'static str { Icons::UploadSpeed => "󰛶", Icons::Copy => "󰆏", Icons::RightChevron => "󰅂", + Icons::Mouse => "󰍽", + Icons::Keyboard => "", } } } diff --git a/src/config.rs b/src/config.rs index 0e3be3f..f4d2765 100644 --- a/src/config.rs +++ b/src/config.rs @@ -120,6 +120,7 @@ pub enum SystemIndicator { IpAddress, DownloadSpeed, UploadSpeed, + Peripherals, } #[derive(Deserialize, Clone, Debug)] @@ -141,6 +142,7 @@ fn default_system_indicators() -> Vec { SystemIndicator::Cpu, SystemIndicator::Memory, SystemIndicator::Temperature, + SystemIndicator::Peripherals, ] } diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 86612f8..a735d65 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -241,7 +241,7 @@ impl App { self.config.appearance.special_workspace_colors.as_deref(), )), ModuleName::WindowTitle => self.window_title.view(()), - ModuleName::SystemInfo => self.system_info.view(&self.config.system), + ModuleName::SystemInfo => self.system_info.view((&self.config.system, &self.upower)), ModuleName::KeyboardLayout => self.keyboard_layout.view(&self.config.keyboard_layout), ModuleName::KeyboardSubmap => self.keyboard_submap.view(()), ModuleName::Tray => self.tray.view((id, opacity)), diff --git a/src/modules/settings/mod.rs b/src/modules/settings/mod.rs index c1d5d41..c84dd91 100644 --- a/src/modules/settings/mod.rs +++ b/src/modules/settings/mod.rs @@ -453,7 +453,7 @@ impl Settings { let battery_data = self .upower .as_ref() - .and_then(|upower| upower.battery) + .and_then(|upower| upower.system_battery) .map(|battery| battery.settings_indicator()); let right_buttons = Row::new() .push_maybe(config.lock_cmd.as_ref().map(|_| { @@ -641,7 +641,7 @@ impl Module for Settings { .push_maybe( self.upower .as_ref() - .and_then(|upower| upower.battery) + .and_then(|upower| upower.system_battery) .map(|battery| battery.indicator()), ) .spacing(8) diff --git a/src/modules/system_info.rs b/src/modules/system_info.rs index 80d3f63..acb9ff0 100644 --- a/src/modules/system_info.rs +++ b/src/modules/system_info.rs @@ -3,6 +3,7 @@ use crate::{ components::icons::{Icons, icon}, config::{SystemIndicator, SystemModuleConfig}, menu::MenuType, + services::upower::{PeripheralDeviceKind, UPowerService}, }; use iced::{ Alignment, Element, Length, Subscription, Task, Theme, @@ -220,7 +221,7 @@ impl SystemInfo { } } - pub fn menu_view(&self) -> Element { + pub fn menu_view(&self, upower: &Option) -> Element { column!( column!(text("System Info").size(20), horizontal_rule(1)).spacing(4), Column::new() @@ -289,6 +290,28 @@ impl SystemInfo { ), ]) })) + .push_maybe( + upower + .as_ref() + .and_then(|upower| { + (!upower.peripheral.is_empty()).then_some(&upower.peripheral) + }) + .map(|peripherals| { + let devices = peripherals.iter().map(|peripheral| { + let icon = match peripheral.kind { + PeripheralDeviceKind::Keyboard => Icons::Keyboard, + PeripheralDeviceKind::Mouse => Icons::Mouse, + }; + Self::info_element( + icon, + peripheral.name.clone(), + format!("{}%", peripheral.data.capacity), + ) + }); + + Column::with_children(devices) + }), + ) .spacing(4) .padding([0, 8]) ) @@ -298,12 +321,12 @@ impl SystemInfo { } impl Module for SystemInfo { - type ViewData<'a> = &'a SystemModuleConfig; + type ViewData<'a> = (&'a SystemModuleConfig, &'a Option); type SubscriptionData<'a> = (); fn view( &self, - config: Self::ViewData<'_>, + (config, upower): Self::ViewData<'_>, ) -> Option<(Element, Option)> { let indicators = config.indicators.iter().filter_map(|i| match i { SystemIndicator::Cpu => Some(Self::indicator_info_element( @@ -397,6 +420,29 @@ impl Module for SystemInfo { None, ) }), + SystemIndicator::Peripherals => upower + .as_ref() + .and_then(|upower| (!upower.peripheral.is_empty()).then_some(&upower.peripheral)) + .map(|peripheral| { + let devices = peripheral.iter().map(|peripheral| { + let icon = match peripheral.kind { + PeripheralDeviceKind::Keyboard => Icons::Keyboard, + PeripheralDeviceKind::Mouse => Icons::Mouse, + }; + Self::indicator_info_element( + icon, + peripheral.data.capacity, + "%", + None, + None, + ) + }); + + Row::with_children(devices) + .align_y(Alignment::Center) + .spacing(4) + .into() + }), }); Some(( @@ -409,6 +455,10 @@ impl Module for SystemInfo { } fn subscription(&self, _: Self::SubscriptionData<'_>) -> Option> { - Some(every(Duration::from_secs(5)).map(|_| app::Message::SystemInfo(Message::Update))) + Some( + every(Duration::from_secs(5)) + .map(|_| Message::Update) + .map(app::Message::SystemInfo), + ) } } diff --git a/src/services/upower/dbus.rs b/src/services/upower/dbus.rs index d9e9932..26fbca3 100644 --- a/src/services/upower/dbus.rs +++ b/src/services/upower/dbus.rs @@ -1,6 +1,6 @@ use std::ops::Deref; use zbus::{ - Result, proxy, + proxy, zvariant::{ObjectPath, OwnedObjectPath}, }; @@ -14,9 +14,10 @@ impl<'a> Deref for UPowerDbus<'a> { } } -pub struct Battery(Vec>); +#[derive(Default)] +pub struct SystemBattery(Vec>); -impl Battery { +impl SystemBattery { pub async fn state(&self) -> i32 { let mut charging = false; let mut discharging = false; @@ -90,6 +91,101 @@ impl Battery { } } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum UpDeviceKind { + #[default] + Unknown = 0, + LinePower = 1, + Battery = 2, + Ups = 3, + Monitor = 4, + Mouse = 5, + Keyboard = 6, + Pda = 7, + Phone = 8, + MediaPlayer = 9, + Tablet = 10, + Computer = 11, + GamingInput = 12, +} + +impl UpDeviceKind { + /// Convert from u32 to UpDeviceKind + pub fn from_u32(value: u32) -> Option { + match value { + 0 => Some(Self::Unknown), + 1 => Some(Self::LinePower), + 2 => Some(Self::Battery), + 3 => Some(Self::Ups), + 4 => Some(Self::Monitor), + 5 => Some(Self::Mouse), + 6 => Some(Self::Keyboard), + 7 => Some(Self::Pda), + 8 => Some(Self::Phone), + 9 => Some(Self::MediaPlayer), + 10 => Some(Self::Tablet), + 11 => Some(Self::Computer), + 12 => Some(Self::GamingInput), + _ => None, + } + } + + /// Convert to u32 + pub fn to_u32(self) -> u32 { + self as u32 + } + + /// Check if this device type is a peripheral input device + pub fn is_peripheral(&self) -> bool { + matches!(self, Self::Mouse | Self::Keyboard | Self::GamingInput) + } + + /// Check if this device type is a system power source + pub fn is_power_source(&self) -> bool { + matches!(self, Self::Battery | Self::Ups | Self::LinePower) + } + + /// Get a human-readable description + pub fn description(&self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::LinePower => "Line Power", + Self::Battery => "Battery", + Self::Ups => "UPS", + Self::Monitor => "Monitor", + Self::Mouse => "Mouse", + Self::Keyboard => "Keyboard", + Self::Pda => "PDA", + Self::Phone => "Phone", + Self::MediaPlayer => "Media Player", + Self::Tablet => "Tablet", + Self::Computer => "Computer", + Self::GamingInput => "Gaming Input", + } + } +} + +impl std::fmt::Display for UpDeviceKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.description()) + } +} + +impl From for u32 { + fn from(kind: UpDeviceKind) -> Self { + kind.to_u32() + } +} + +impl TryFrom for UpDeviceKind { + type Error = (); + + fn try_from(value: u32) -> Result { + Self::from_u32(value).ok_or(()) + } +} + impl UPowerDbus<'_> { pub async fn new(conn: &zbus::Connection) -> anyhow::Result { let nm = UPowerProxy::new(conn).await?; @@ -97,7 +193,43 @@ impl UPowerDbus<'_> { Ok(Self(nm)) } - pub async fn get_battery_devices(&self) -> anyhow::Result> { + pub async fn get_system_batteries(&self) -> anyhow::Result> { + self.get_battery_devices(|device_type, power_supply| { + device_type.is_power_source() && power_supply + }) + .await + .map(|devices| { + if !devices.is_empty() { + Some(SystemBattery(devices)) + } else { + None + } + }) + } + + pub async fn get_peripheral_batteries(&self) -> anyhow::Result>> { + self.get_battery_devices(|device_type, power_supply| { + device_type.is_peripheral() && !power_supply + }) + .await + } + + pub async fn get_device( + &self, + path: &ObjectPath<'static>, + ) -> anyhow::Result> { + let device = DeviceProxy::builder(self.inner().connection()) + .path(path)? + .build() + .await?; + + Ok(device) + } + + async fn get_battery_devices( + &self, + f: fn(UpDeviceKind, bool) -> bool, + ) -> anyhow::Result>> { let devices = self.enumerate_devices().await?; let mut res = Vec::new(); @@ -108,31 +240,19 @@ impl UPowerDbus<'_> { .build() .await?; - let device_type = device.device_type().await?; + let device_type = device + .device_type() + .await? + .try_into() + .unwrap_or(UpDeviceKind::Unknown); let power_supply = device.power_supply().await?; - if device_type == 2 && power_supply { + if f(device_type, power_supply) { res.push(device); } } - if !res.is_empty() { - Ok(Some(Battery(res))) - } else { - Ok(None) - } - } - - pub async fn get_device( - &self, - path: &ObjectPath<'static>, - ) -> anyhow::Result> { - let device = DeviceProxy::builder(self.inner().connection()) - .path(path)? - .build() - .await?; - - Ok(device) + Ok(res) } } @@ -142,10 +262,13 @@ impl UPowerDbus<'_> { default_path = "/org/freedesktop/UPower" )] pub trait UPower { - fn enumerate_devices(&self) -> Result>; + fn enumerate_devices(&self) -> zbus::Result>; #[zbus(signal)] - fn device_added(&self) -> Result; + fn device_added(&self) -> zbus::Result; + + #[zbus(signal)] + fn device_removed(&self) -> zbus::Result; } #[proxy( @@ -155,22 +278,25 @@ pub trait UPower { )] pub trait Device { #[zbus(property, name = "Type")] - fn device_type(&self) -> Result; + fn device_type(&self) -> zbus::Result; #[zbus(property)] - fn power_supply(&self) -> Result; + fn power_supply(&self) -> zbus::Result; #[zbus(property)] - fn time_to_empty(&self) -> Result; + fn time_to_empty(&self) -> zbus::Result; #[zbus(property)] - fn time_to_full(&self) -> Result; + fn time_to_full(&self) -> zbus::Result; #[zbus(property)] - fn percentage(&self) -> Result; + fn percentage(&self) -> zbus::Result; #[zbus(property)] - fn state(&self) -> Result; + fn state(&self) -> zbus::Result; + + #[zbus(property, name = "Model")] + fn model(&self) -> zbus::Result; } #[proxy( @@ -180,8 +306,8 @@ pub trait Device { )] pub trait PowerProfiles { #[zbus(property)] - fn active_profile(&self) -> Result; + fn active_profile(&self) -> zbus::Result; #[zbus(property)] - fn set_active_profile(&self, profile: &str) -> Result<()>; + fn set_active_profile(&self, profile: &str) -> zbus::Result<()>; } diff --git a/src/services/upower/mod.rs b/src/services/upower/mod.rs index 04b2fc9..f6a3eee 100644 --- a/src/services/upower/mod.rs +++ b/src/services/upower/mod.rs @@ -1,6 +1,6 @@ use super::{ReadOnlyService, Service, ServiceEvent}; use crate::{components::icons::Icons, utils::IndicatorState}; -use dbus::{Battery, PowerProfilesProxy, UPowerDbus}; +use dbus::{DeviceProxy, PowerProfilesProxy, SystemBattery, UPowerDbus, UPowerProxy, UpDeviceKind}; use iced::{ Subscription, futures::{ @@ -12,7 +12,7 @@ use iced::{ stream::channel, }; use log::{error, warn}; -use std::{any::TypeId, time::Duration}; +use std::{any::TypeId, fmt, time::Duration}; use zbus::zvariant::ObjectPath; mod dbus; @@ -65,9 +65,33 @@ impl BatteryData { } } +#[derive(Debug, Clone)] +pub struct Peripheral { + pub name: String, + pub kind: PeripheralDeviceKind, + pub data: BatteryData, + pub device: DeviceProxy<'static>, +} + +#[derive(Debug, Clone, Copy)] +pub enum PeripheralDeviceKind { + Keyboard, + Mouse, +} + +impl fmt::Display for PeripheralDeviceKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PeripheralDeviceKind::Keyboard => write!(f, "Keyboard"), + PeripheralDeviceKind::Mouse => write!(f, "Mouse"), + } + } +} + #[derive(Debug, Clone)] pub enum UPowerEvent { - UpdateBattery(BatteryData), + UpdateSystemBattery(BatteryData), + UpdatePeripheral(Vec), NoBattery, UpdatePowerProfile(PowerProfile), } @@ -112,14 +136,19 @@ impl From for Icons { #[derive(Debug, Clone)] pub struct UPowerService { - pub battery: Option, + pub system_battery: Option, + pub peripheral: Vec, pub power_profile: PowerProfile, conn: zbus::Connection, } enum State { Init, - Active(zbus::Connection, Option>>), + Active( + zbus::Connection, + Option>>, + Vec>, + ), Error, } @@ -129,11 +158,14 @@ impl ReadOnlyService for UPowerService { fn update(&mut self, event: Self::UpdateEvent) { match event { - UPowerEvent::UpdateBattery(data) => { - self.battery.replace(data); + UPowerEvent::UpdateSystemBattery(data) => { + self.system_battery.replace(data); + } + UPowerEvent::UpdatePeripheral(data) => { + self.peripheral = data; } UPowerEvent::NoBattery => { - self.battery = None; + self.system_battery = None; } UPowerEvent::UpdatePowerProfile(profile) => { self.power_profile = profile; @@ -162,14 +194,23 @@ impl UPowerService { conn: &zbus::Connection, ) -> anyhow::Result<( Option<(BatteryData, Vec>)>, + Vec, + Vec>, PowerProfile, )> { - let battery = UPowerService::initialize_battery_data(conn).await?; + let system_battery = UPowerService::initialize_system_battery_data(conn).await?; + let peripherals = UPowerService::initialize_peripheral_data(conn).await?; + let peripheral_paths = peripherals + .iter() + .map(|device| device.device.inner().path().to_owned()) + .collect(); let power_profile = UPowerService::initialize_power_profile_data(conn).await; - match (battery, power_profile) { + match (system_battery, power_profile) { (Some(battery), Ok(power_profile)) => Ok(( Some((battery.0, battery.1.get_devices_path())), + peripherals, + peripheral_paths, power_profile, )), (Some(battery), Err(err)) => { @@ -177,14 +218,16 @@ impl UPowerService { Ok(( Some((battery.0, battery.1.get_devices_path())), + peripherals, + peripheral_paths, PowerProfile::Unknown, )) } - (None, Ok(power_profile)) => Ok((None, power_profile)), + (None, Ok(power_profile)) => Ok((None, peripherals, peripheral_paths, power_profile)), (None, Err(err)) => { warn!("Failed to get power profile: {}", err); - Ok((None, PowerProfile::Unknown)) + Ok((None, peripherals, peripheral_paths, PowerProfile::Unknown)) } } } @@ -202,11 +245,11 @@ impl UPowerService { Ok(profile) } - async fn initialize_battery_data( + async fn initialize_system_battery_data( conn: &zbus::Connection, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let upower = UPowerDbus::new(conn).await?; - let battery = upower.get_battery_devices().await?; + let battery = upower.get_system_batteries().await?; match battery { Some(battery) => { @@ -235,11 +278,88 @@ impl UPowerService { } } + async fn initialize_peripheral_data( + conn: &zbus::Connection, + ) -> anyhow::Result> { + let upower = UPowerDbus::new(conn).await?; + let devices = upower.get_peripheral_batteries().await?; + + let mut peripherals = Vec::with_capacity(devices.len()); + + for device in devices { + let Ok(device_type) = device.device_type().await else { + warn!( + "Failed to read device's type for device '{}'", + device.inner().path().as_str() + ); + continue; + }; + let device_kind = match UpDeviceKind::from_u32(device_type).unwrap_or_default() { + UpDeviceKind::Mouse => PeripheralDeviceKind::Mouse, + UpDeviceKind::Keyboard => PeripheralDeviceKind::Keyboard, + _ => continue, + }; + + let name = match device.model().await { + Ok(model) => model, + Err(_) => device_kind.to_string(), + }; + + let Ok(state) = device.state().await else { + continue; + }; + let state = match state { + 1 => { + let Ok(time_to_full) = device.time_to_full().await else { + warn!( + "Failed to read device's time_to_full for device '{}'", + device.inner().path().as_str() + ); + continue; + }; + BatteryStatus::Charging(Duration::from_secs(time_to_full as u64)) + } + 2 => { + let Ok(time_to_empty) = device.time_to_empty().await else { + warn!( + "Failed to read device's time_to_empty for device '{}'", + device.inner().path().as_str() + ); + continue; + }; + BatteryStatus::Discharging(Duration::from_secs(time_to_empty as u64)) + } + 4 => BatteryStatus::Full, + _ => BatteryStatus::Discharging(Duration::from_secs(0)), + }; + let Ok(percentage) = device.percentage().await else { + warn!( + "Failed to read device's percentage for device '{}'", + device.inner().path().as_str() + ); + continue; + }; + + peripherals.push(Peripheral { + name, + kind: device_kind, + data: BatteryData { + capacity: percentage as i64, + status: state, + }, + device, + }); + } + + Ok(peripherals) + } + async fn events( conn: &zbus::Connection, - battery_devices: &Option>>, + system_battery_devices: &Option>>, + peripheral_paths: &[ObjectPath<'static>], ) -> anyhow::Result + use<>> { - let battery_event = if let Some(battery_devices) = battery_devices { + let system_battery_event = if let Some(battery_devices) = system_battery_devices { let upower = UPowerDbus::new(conn).await?; let mut events = Vec::new(); @@ -259,10 +379,12 @@ impl UPowerService { move |_| { let conn = conn.clone(); async move { - if let Some((data, _)) = - Self::initialize_battery_data(&conn).await.ok().flatten() + if let Some((data, _)) = Self::initialize_system_battery_data(&conn) + .await + .ok() + .flatten() { - Some(UPowerEvent::UpdateBattery(data)) + Some(UPowerEvent::UpdateSystemBattery(data)) } else { None } @@ -278,6 +400,77 @@ impl UPowerService { once(async {}).map(|_| UPowerEvent::NoBattery).boxed() }; + let peripheral_event = if !peripheral_paths.is_empty() { + let upower = UPowerDbus::new(conn).await?; + + let mut events = Vec::new(); + + for device_path in peripheral_paths { + let device = upower.get_device(device_path).await?; + + events.push( + stream_select!( + device.receive_state_changed().await.map(|_| ()), + device.receive_percentage_changed().await.map(|_| ()), + device.receive_time_to_full_changed().await.map(|_| ()), + device.receive_time_to_empty_changed().await.map(|_| ()), + ) + .filter_map({ + let conn = conn.clone(); + move |_| { + let conn = conn.clone(); + async move { + Self::initialize_peripheral_data(&conn) + .await + .ok() + .map(UPowerEvent::UpdatePeripheral) + } + } + }) + .boxed(), + ); + } + + select_all(events).boxed() + } else { + pending().boxed() + }; + + let upower_proxy = UPowerProxy::new(conn).await?; + let device_added_event = upower_proxy + .receive_device_added() + .await? + .filter_map({ + let conn = conn.clone(); + move |_added_device| { + let conn = conn.clone(); + async move { + Self::initialize_peripheral_data(&conn) + .await + .ok() + .map(UPowerEvent::UpdatePeripheral) + } + } + }) + .boxed(); + + let device_removed_event = upower_proxy + .receive_device_removed() + .await? + .filter_map({ + let conn = conn.clone(); + move |_removed_device| { + let conn = conn.clone(); + async move { + Self::initialize_peripheral_data(&conn) + .await + .ok() + .map(UPowerEvent::UpdatePeripheral) + } + } + }) + .boxed(); + let powerprofiles = PowerProfilesProxy::new(conn).await?; let power_profile_event = powerprofiles @@ -292,48 +485,71 @@ impl UPowerService { ) }); - Ok(stream_select!(battery_event, power_profile_event)) + Ok(stream_select!( + system_battery_event, + peripheral_event, + device_added_event, + device_removed_event, + power_profile_event + )) } async fn start_listening(state: State, output: &mut Sender>) -> State { match state { State::Init => match zbus::Connection::system().await { Ok(conn) => { - let (battery, battery_path, power_profile) = - match UPowerService::initialize_data(&conn).await { - Ok((Some((battery_data, battery_path)), power_profile)) => { - (Some(battery_data), Some(battery_path), power_profile) - } - Ok((None, power_profile)) => (None, None, power_profile), - Err(err) => { - error!("Failed to initialize upower service: {}", err); + let ( + system_battery, + peripherals, + system_battery_paths, + peripheral_paths, + power_profile, + ) = match UPowerService::initialize_data(&conn).await { + Ok(( + Some((battery_data, battery_path)), + peripherals, + peripheral_paths, + power_profile, + )) => ( + Some(battery_data), + peripherals, + Some(battery_path), + peripheral_paths, + power_profile, + ), + Ok((None, peripherals, peripheral_paths, power_profile)) => { + (None, peripherals, None, peripheral_paths, power_profile) + } + Err(err) => { + error!("Failed to initialize upower service: {}", err); - return State::Error; - } - }; + return State::Error; + } + }; let service = UPowerService { - battery, + system_battery, + peripheral: peripherals, power_profile, conn: conn.clone(), }; let _ = output.send(ServiceEvent::Init(service)).await; - State::Active(conn, battery_path) + State::Active(conn, system_battery_paths, peripheral_paths) } Err(err) => { error!("Failed to connect to system bus for upower: {}", err); State::Error } }, - State::Active(conn, battery_devices) => { - match UPowerService::events(&conn, &battery_devices).await { + State::Active(conn, system_battery_paths, peripheral_paths) => { + match UPowerService::events(&conn, &system_battery_paths, &peripheral_paths).await { Ok(mut events) => { while let Some(event) = events.next().await { let _ = output.send(ServiceEvent::Update(event)).await; } - State::Active(conn, battery_devices) + State::Active(conn, system_battery_paths, peripheral_paths) } Err(err) => { error!("Failed to listen for upower events: {}", err);