diff --git a/Cargo.lock b/Cargo.lock index f9525ff6..81befae2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -736,12 +736,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "displaydoc" version = "0.2.5" @@ -1018,22 +1012,6 @@ dependencies = [ "syn 2.0.99", ] -[[package]] -name = "futures-signals" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70abe9c40a0dccd69bf7c59ba58714ebeb6c15a88143a10c6be7130e895f1696" -dependencies = [ - "discard", - "futures-channel", - "futures-core", - "futures-util", - "gensym", - "log", - "pin-project", - "serde", -] - [[package]] name = "futures-sink" version = "0.3.31" @@ -1145,18 +1123,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "gensym" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913dce4c5f06c2ea40fc178c06f777ac89fc6b1383e90c254fafb1abe4ba3c82" -dependencies = [ - "proc-macro2", - "quote 1.0.39", - "syn 2.0.99", - "uuid", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -1845,7 +1811,6 @@ dependencies = [ "dirs", "evdev-rs", "futures-lite", - "futures-signals", "glib", "gtk", "gtk-layer-shell", @@ -1869,6 +1834,7 @@ dependencies = [ "sysinfo", "system-tray", "tokio", + "tokio-stream", "tracing", "tracing-appender", "tracing-error", @@ -2541,26 +2507,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote 1.0.39", - "syn 2.0.99", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3557,6 +3503,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -3865,15 +3822,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" -dependencies = [ - "getrandom 0.3.1", -] - [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 87108672..366d3b38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ music = ["dep:regex"] "music+mpris" = ["music", "mpris"] "music+mpd" = ["music", "mpd-utils"] -network_manager = ["futures-lite", "futures-signals", "zbus"] +network_manager = ["futures-lite", "tokio-stream", "zbus"] notifications = ["zbus"] @@ -171,7 +171,7 @@ regex = { version = "1.11.1", default-features = false, features = [ ], optional = true } # network_manager -futures-signals = { version = "0.3.34", optional = true } +tokio-stream = { version = "0.1.17", optional = true } # sys_info sysinfo = { version = "0.36.1", optional = true } diff --git a/src/clients/mod.rs b/src/clients/mod.rs index c4fce0dc..cbacaa32 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -190,7 +190,7 @@ impl Clients { if let Some(client) = &self.network_manager { Ok(client.clone()) } else { - let client = await_sync(async move { networkmanager::create_client().await })?; + let client = networkmanager::create_client()?; self.network_manager = Some(client.clone()); Ok(client) } diff --git a/src/clients/networkmanager.rs b/src/clients/networkmanager.rs deleted file mode 100644 index f81ca454..00000000 --- a/src/clients/networkmanager.rs +++ /dev/null @@ -1,171 +0,0 @@ -use std::sync::Arc; - -use crate::{register_fallible_client, spawn}; -use color_eyre::Result; -use futures_signals::signal::{Mutable, MutableSignalCloned}; -use tracing::error; -use zbus::export::ordered_stream::OrderedStreamExt; -use zbus::fdo::PropertiesProxy; -use zbus::{ - Connection, - names::InterfaceName, - proxy, - zvariant::{ObjectPath, Str}, -}; - -const DBUS_BUS: &str = "org.freedesktop.NetworkManager"; -const DBUS_PATH: &str = "/org/freedesktop/NetworkManager"; -const DBUS_INTERFACE: &str = "org.freedesktop.NetworkManager"; - -#[derive(Debug)] -pub struct Client { - client_state: Mutable, - interface_name: InterfaceName<'static>, - dbus_connection: Connection, - props_proxy: PropertiesProxy<'static>, -} - -#[derive(Clone, Debug)] -pub enum ClientState { - WiredConnected, - WifiConnected, - CellularConnected, - VpnConnected, - WifiDisconnected, - Offline, - Unknown, -} - -#[proxy( - default_service = "org.freedesktop.NetworkManager", - interface = "org.freedesktop.NetworkManager", - default_path = "/org/freedesktop/NetworkManager" -)] -trait NetworkManagerDbus { - #[zbus(property)] - fn active_connections(&self) -> Result>; - - #[zbus(property)] - fn devices(&self) -> Result>; - - #[zbus(property)] - fn networking_enabled(&self) -> Result; - - #[zbus(property)] - fn primary_connection(&self) -> Result; - - #[zbus(property)] - fn primary_connection_type(&self) -> Result; - - #[zbus(property)] - fn wireless_enabled(&self) -> Result; -} - -impl Client { - async fn new() -> Result { - let client_state = Mutable::new(ClientState::Unknown); - let dbus_connection = Connection::system().await?; - let interface_name = InterfaceName::from_static_str(DBUS_INTERFACE)?; - let props_proxy = PropertiesProxy::builder(&dbus_connection) - .destination(DBUS_BUS)? - .path(DBUS_PATH)? - .build() - .await?; - - Ok(Self { - client_state, - interface_name, - dbus_connection, - props_proxy, - }) - } - - async fn run(&self) -> Result<()> { - let proxy = NetworkManagerDbusProxy::new(&self.dbus_connection).await?; - - let mut primary_connection = proxy.primary_connection().await?; - let mut primary_connection_type = proxy.primary_connection_type().await?; - let mut wireless_enabled = proxy.wireless_enabled().await?; - - self.client_state.set(determine_state( - &primary_connection, - &primary_connection_type, - wireless_enabled, - )); - - let mut stream = self.props_proxy.receive_properties_changed().await?; - while let Some(change) = stream.next().await { - let args = change.args()?; - if args.interface_name != self.interface_name { - continue; - } - - let changed_props = args.changed_properties; - let mut relevant_prop_changed = false; - - if changed_props.contains_key("PrimaryConnection") { - primary_connection = proxy.primary_connection().await?; - relevant_prop_changed = true; - } - if changed_props.contains_key("PrimaryConnectionType") { - primary_connection_type = proxy.primary_connection_type().await?; - relevant_prop_changed = true; - } - if changed_props.contains_key("WirelessEnabled") { - wireless_enabled = proxy.wireless_enabled().await?; - relevant_prop_changed = true; - } - - if relevant_prop_changed { - self.client_state.set(determine_state( - &primary_connection, - &primary_connection_type, - wireless_enabled, - )); - } - } - - Ok(()) - } - - pub fn subscribe(&self) -> MutableSignalCloned { - self.client_state.signal_cloned() - } -} - -pub async fn create_client() -> Result> { - let client = Arc::new(Client::new().await?); - { - let client = client.clone(); - spawn(async move { - if let Err(error) = client.run().await { - error!("{}", error); - } - }); - } - Ok(client) -} - -fn determine_state( - primary_connection: &str, - primary_connection_type: &str, - wireless_enabled: bool, -) -> ClientState { - if primary_connection == "/" { - if wireless_enabled { - ClientState::WifiDisconnected - } else { - ClientState::Offline - } - } else { - match primary_connection_type { - "802-3-ethernet" | "adsl" | "pppoe" => ClientState::WiredConnected, - "802-11-olpc-mesh" | "802-11-wireless" | "wifi-p2p" => ClientState::WifiConnected, - "cdma" | "gsm" | "wimax" => ClientState::CellularConnected, - "vpn" | "wireguard" => ClientState::VpnConnected, - _ => ClientState::Unknown, - } - } -} - -register_fallible_client!(Client, network_manager); diff --git a/src/clients/networkmanager/dbus.rs b/src/clients/networkmanager/dbus.rs new file mode 100644 index 00000000..0f1e36a2 --- /dev/null +++ b/src/clients/networkmanager/dbus.rs @@ -0,0 +1,84 @@ +use color_eyre::Result; +use zbus::proxy; +use zbus::zvariant::{ObjectPath, OwnedValue, Str}; + +#[proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager", + default_path = "/org/freedesktop/NetworkManager" +)] +pub(super) trait Dbus { + #[zbus(property)] + fn all_devices(&self) -> Result>>; +} + +#[proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager.Device" +)] +pub(super) trait DeviceDbus { + #[zbus(property)] + fn device_type(&self) -> Result; + + #[zbus(property)] + fn interface(&self) -> Result>; + + #[zbus(property)] + fn state(&self) -> Result; +} + +// For reference: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/e1a7d5ac062f4f23ce3a6b33c62e856056161ad8/src/libnm-core-public/nm-dbus-interface.h#L212-L253 +#[derive(Clone, Debug, Eq, Hash, OwnedValue, PartialEq)] +#[repr(u32)] +pub enum DeviceType { + Unknown = 0, + Ethernet = 1, + Wifi = 2, + Bluetooth = 5, + OlpcMesh = 6, + Wimax = 7, + Modem = 8, + Infiniband = 9, + Bond = 10, + Vlan = 11, + Adsl = 12, + Bridge = 13, + Team = 15, + Tun = 16, + IpTunnel = 17, + Macvlan = 18, + Vxlan = 19, + Veth = 20, + Macsec = 21, + Dummy = 22, + Ppp = 23, + OvsInterface = 24, + OvsPort = 25, + OvsBridge = 26, + Wpan = 27, + Lowpan = 28, + Wireguard = 29, + WifiP2p = 30, + Vrf = 31, + Loopback = 32, + Hsr = 33, +} + +// For reference: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/e1a7d5ac062f4f23ce3a6b33c62e856056161ad8/src/libnm-core-public/nm-dbus-interface.h#L501-L538 +#[derive(Clone, Debug, OwnedValue, PartialEq)] +#[repr(u32)] +pub enum DeviceState { + Unknown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IpConfig = 70, + IpCheck = 80, + Secondaries = 90, + Activated = 100, + Deactivating = 110, + Failed = 120, +} diff --git a/src/clients/networkmanager/event.rs b/src/clients/networkmanager/event.rs new file mode 100644 index 00000000..2fdca55a --- /dev/null +++ b/src/clients/networkmanager/event.rs @@ -0,0 +1,18 @@ +use crate::clients::networkmanager::dbus::{DeviceState, DeviceType}; + +#[derive(Debug, Clone)] +pub enum ClientToModuleEvent { + DeviceChanged { + number: u32, + r#type: DeviceType, + new_state: DeviceState, + }, + DeviceRemoved { + number: u32, + }, +} + +#[derive(Debug, Clone)] +pub enum ModuleToClientEvent { + NewController, +} diff --git a/src/clients/networkmanager/mod.rs b/src/clients/networkmanager/mod.rs new file mode 100644 index 00000000..74595318 --- /dev/null +++ b/src/clients/networkmanager/mod.rs @@ -0,0 +1,239 @@ +use color_eyre::Result; +use color_eyre::eyre::Ok; +use futures_lite::StreamExt; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use tokio::sync::{RwLock, broadcast}; +use tokio::task::JoinHandle; +use tracing::debug; +use zbus::Connection; +use zbus::zvariant::ObjectPath; + +use crate::clients::ClientResult; +use crate::clients::networkmanager::dbus::{DbusProxy, DeviceDbusProxy}; +use crate::clients::networkmanager::event::{ClientToModuleEvent, ModuleToClientEvent}; +use crate::{register_fallible_client, spawn}; + +pub mod dbus; +pub mod event; + +#[derive(Debug)] +pub struct Client { + inner: &'static ClientInner, +} + +impl Client { + fn new() -> Client { + let inner = Box::leak(Box::new(ClientInner::new())); + Client { inner } + } + + fn run(&self) -> Result<()> { + self.inner.run() + } + + pub fn subscribe(&self) -> broadcast::Receiver { + self.inner.subscribe() + } + + pub fn get_sender(&self) -> broadcast::Sender { + self.inner.get_sender() + } +} + +#[derive(Debug)] +struct ClientInner { + controller_sender: broadcast::Sender, + sender: broadcast::Sender, + device_watchers: RwLock, DeviceWatcher>>, + dbus_connection: RwLock>, +} + +#[derive(Clone, Debug)] +struct DeviceWatcher { + state_watcher: Arc>>, +} + +impl ClientInner { + fn new() -> ClientInner { + let (controller_sender, _) = broadcast::channel(64); + let (sender, _) = broadcast::channel(8); + let device_watchers = RwLock::new(HashMap::new()); + let dbus_connection = RwLock::new(None); + ClientInner { + controller_sender, + sender, + device_watchers, + dbus_connection, + } + } + + fn run(&'static self) -> Result<()> { + debug!("Client running"); + + spawn(self.watch_devices_list()); + + let receiver = self.sender.subscribe(); + spawn(self.handle_received_events(receiver)); + + Ok(()) + } + + fn subscribe(&self) -> broadcast::Receiver { + self.controller_sender.subscribe() + } + + fn get_sender(&self) -> broadcast::Sender { + self.sender.clone() + } + + async fn watch_devices_list(&'static self) -> Result<()> { + debug!("D-Bus devices list watcher starting"); + + let root = DbusProxy::new(&self.dbus_connection().await?).await?; + + let mut devices_changes = root.receive_all_devices_changed().await; + while let Some(devices_change) = devices_changes.next().await { + // The new list of devices from dbus, not to be confused with the added devices below + let new_device_paths = devices_change + .get() + .await? + .iter() + .map(ObjectPath::to_owned) + .collect::>(); + + let mut watchers = self.device_watchers.write().await; + let device_paths = watchers.keys().cloned().collect::>(); + + let added_device_paths = new_device_paths.difference(&device_paths); + for added_device_path in added_device_paths { + debug_assert!(!watchers.contains_key(added_device_path)); + + let watcher = self.watch_device(added_device_path.clone()).await?; + watchers.insert(added_device_path.clone(), watcher); + } + + let removed_device_paths = device_paths.difference(&new_device_paths); + for removed_device_path in removed_device_paths { + let watcher = watchers + .get(removed_device_path) + .expect("Device to be removed should be present in watchers"); + watcher.state_watcher.abort(); + watchers.remove(removed_device_path); + + let number = get_number_from_dbus_path(removed_device_path); + self.controller_sender + .send(ClientToModuleEvent::DeviceRemoved { number })?; + + debug!("D-bus device watchers for {} stopped", removed_device_path); + } + } + + Ok(()) + } + + async fn handle_received_events( + &'static self, + mut receiver: broadcast::Receiver, + ) -> Result<()> { + while let Result::Ok(event) = receiver.recv().await { + match event { + ModuleToClientEvent::NewController => { + debug!("Client received NewController event"); + + for device_path in self.device_watchers.read().await.keys() { + let dbus_connection = &self.dbus_connection().await?; + let device = DeviceDbusProxy::new(dbus_connection, device_path).await?; + + let number = get_number_from_dbus_path(device_path); + let r#type = device.device_type().await?; + let new_state = device.state().await?; + self.controller_sender + .send(ClientToModuleEvent::DeviceChanged { + number, + r#type, + new_state, + })?; + } + } + } + } + + Ok(()) + } + + async fn watch_device(&'static self, path: ObjectPath<'_>) -> Result { + let dbus_connection = &self.dbus_connection().await?; + let proxy = DeviceDbusProxy::new(dbus_connection, path.to_owned()).await?; + + let number = get_number_from_dbus_path(&path); + let r#type = proxy.device_type().await?; + let new_state = proxy.state().await?; + + // Notify modules that the device exists even if its properties don't change + self.controller_sender + .send(ClientToModuleEvent::DeviceChanged { + number, + r#type: r#type.clone(), + new_state, + })?; + + let state_watcher = Arc::new(spawn(self.watch_device_state(proxy))); + + Ok(DeviceWatcher { state_watcher }) + } + + async fn watch_device_state(&'static self, proxy: DeviceDbusProxy<'_>) -> Result<()> { + let path = proxy.inner().path(); + + debug!("D-Bus device state watcher for {} starting", path); + + let number = get_number_from_dbus_path(path); + let r#type = proxy.device_type().await?; + + let mut changes = proxy.receive_state_changed().await; + while let Some(change) = changes.next().await { + let new_state = change.get().await?; + self.controller_sender + .send(ClientToModuleEvent::DeviceChanged { + number, + r#type: r#type.clone(), + new_state, + })?; + } + + Ok(()) + } + + async fn dbus_connection(&self) -> Result { + let dbus_connection_guard = self.dbus_connection.read().await; + if let Some(dbus_connection) = &*dbus_connection_guard { + Ok(dbus_connection.clone()) + } else { + // Yes it's a bit awkward to first obtain a read lock and then a write lock but it + // needs to happen only once, and after that all read lock acquisitions will be + // instant + drop(dbus_connection_guard); + let dbus_connection = Connection::system().await?; + *self.dbus_connection.write().await = Some(dbus_connection.clone()); + Ok(dbus_connection) + } + } +} + +pub fn create_client() -> ClientResult { + let client = Arc::new(Client::new()); + client.run()?; + Ok(client) +} + +fn get_number_from_dbus_path(path: &ObjectPath) -> u32 { + let (_, number_str) = path + .rsplit_once('/') + .expect("Path must have at least two segments to contain an object number"); + number_str + .parse() + .expect("Last segment was not a positive integer") +} + +register_fallible_client!(Client, network_manager); diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index adb4ff4e..62518e4c 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -1,16 +1,19 @@ -use crate::channels::{AsyncSenderExt, BroadcastReceiverExt}; -use crate::clients::networkmanager::{Client, ClientState}; +use crate::clients::networkmanager::Client; +use crate::clients::networkmanager::dbus::{DeviceState, DeviceType}; +use crate::clients::networkmanager::event::{ClientToModuleEvent, ModuleToClientEvent}; use crate::config::CommonConfig; use crate::gtk_helpers::IronbarGtkExt; +use crate::image::Provider; use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext}; use crate::{module_impl, spawn}; -use color_eyre::Result; -use futures_lite::StreamExt; -use futures_signals::signal::SignalExt; -use gtk::prelude::ContainerExt; -use gtk::{Box as GtkBox, Image}; +use color_eyre::{Result, eyre::Ok}; +use glib::spawn_future_local; +use gtk::prelude::{ContainerExt, WidgetExt}; +use gtk::{Image, Orientation}; use serde::Deserialize; -use tokio::sync::mpsc::Receiver; +use std::collections::HashMap; +use tokio::sync::{broadcast, mpsc}; +use tracing::debug; #[derive(Debug, Deserialize, Clone)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -26,26 +29,34 @@ const fn default_icon_size() -> i32 { 24 } -impl Module for NetworkManagerModule { - type SendMessage = ClientState; +impl Module for NetworkManagerModule { + type SendMessage = ClientToModuleEvent; type ReceiveMessage = (); module_impl!("network_manager"); fn spawn_controller( &self, - _: &ModuleInfo, - context: &WidgetContext, - _: Receiver<()>, + _info: &ModuleInfo, + context: &WidgetContext, + _widget_receiver: mpsc::Receiver<()>, ) -> Result<()> { let client = context.try_client::()?; - let mut client_signal = client.subscribe().to_stream(); - let tx = context.tx.clone(); + // Should we be using context.tx with ModuleUpdateEvent::Update instead? + let widget_sender = context.update_tx.clone(); + + // Must be done here otherwise we miss the response to our `NewController` event + let mut client_receiver = client.subscribe(); + + client + .get_sender() + .send(ModuleToClientEvent::NewController)?; spawn(async move { - while let Some(state) = client_signal.next().await { - tx.send_update(state).await; + while let Result::Ok(event) = client_receiver.recv().await { + widget_sender.send(event)?; } + Ok(()) }); Ok(()) @@ -53,50 +64,122 @@ impl Module for NetworkManagerModule { fn into_widget( self, - context: WidgetContext, - info: &ModuleInfo, - ) -> Result> { - const INITIAL_ICON_NAME: &str = "content-loading-symbolic"; - - let container = GtkBox::new(info.bar_position.orientation(), 0); - let icon = Image::new(); - icon.add_class("icon"); - container.add(&icon); - - let image_provider = context.ironbar.image_provider(); - - glib::spawn_future_local({ - let image_provider = image_provider.clone(); - let icon = icon.clone(); - - async move { - image_provider - .load_into_image_silent(INITIAL_ICON_NAME, self.icon_size, false, &icon) - .await; - } - }); + context: WidgetContext, + _info: &ModuleInfo, + ) -> Result> { + // Must be done here otherwise we miss the response to our `NewController` event + let receiver = context.subscribe(); + + let container = gtk::Box::new(Orientation::Horizontal, 0); + + // We cannot use recv_glib_async here because the lifetimes don't work out + spawn_future_local(handle_update_events( + receiver, + container.clone(), + self.icon_size, + context.ironbar.image_provider(), + )); + + Ok(ModuleParts::new(container, None)) + } +} - context.subscribe().recv_glib_async((), move |(), state| { - let image_provider = image_provider.clone(); - let icon = icon.clone(); - - let icon_name = match state { - ClientState::WiredConnected => "network-wired-symbolic", - ClientState::WifiConnected => "network-wireless-symbolic", - ClientState::CellularConnected => "network-cellular-symbolic", - ClientState::VpnConnected => "network-vpn-symbolic", - ClientState::WifiDisconnected => "network-wireless-acquiring-symbolic", - ClientState::Offline => "network-wireless-disabled-symbolic", - ClientState::Unknown => "dialog-question-symbolic", - }; - - async move { - image_provider - .load_into_image_silent(icon_name, self.icon_size, false, &icon) - .await; +async fn handle_update_events( + mut widget_receiver: broadcast::Receiver, + container: gtk::Box, + icon_size: i32, + image_provider: Provider, +) -> Result<()> { + // TODO: Ensure the visible icons are always in the same order + let mut icons = HashMap::::new(); + + while let Result::Ok(event) = widget_receiver.recv().await { + match event { + ClientToModuleEvent::DeviceChanged { + number, + r#type, + new_state, + } => { + debug!( + "Module widget received DeviceChanged event for number {}", + number + ); + + let icon: &_ = icons.entry(number).or_insert_with(|| { + debug!("Adding icon for device {}", number); + + let icon = Image::new(); + icon.add_class("icon"); + container.add(&icon); + icon + }); + + // TODO: Make this configurable at runtime + let icon_name = get_icon_for_device_state(&r#type, &new_state); + match icon_name { + Some(icon_name) => { + image_provider + .load_into_image_silent(icon_name, icon_size, false, icon) + .await; + icon.show(); + } + None => { + icon.hide(); + } + } } - }); + ClientToModuleEvent::DeviceRemoved { number } => { + debug!( + "Module widget received DeviceRemoved event for number {}", + number + ); + + let icon = icons + .get(&number) + .expect("The icon for {} was about to be removed but was not present"); + container.remove(icon); + icons.remove(&number); + } + } + } - Ok(ModuleParts::new(container, None)) + Ok(()) +} + +fn get_icon_for_device_state(r#type: &DeviceType, state: &DeviceState) -> Option<&'static str> { + match r#type { + DeviceType::Ethernet => match state { + DeviceState::Unavailable + | DeviceState::Disconnected + | DeviceState::Prepare + | DeviceState::Config + | DeviceState::NeedAuth + | DeviceState::IpConfig + | DeviceState::IpCheck + | DeviceState::Secondaries + | DeviceState::Deactivating + | DeviceState::Failed => Some("icon:network-wired-disconnected-symbolic"), + DeviceState::Activated => Some("icon:network-wired-symbolic"), + _ => None, + }, + DeviceType::Wifi => match state { + DeviceState::Unavailable => Some("icon:network-wireless-hardware-disabled-symbolic"), + DeviceState::Disconnected + | DeviceState::Prepare + | DeviceState::Config + | DeviceState::NeedAuth + | DeviceState::IpConfig + | DeviceState::IpCheck + | DeviceState::Secondaries + | DeviceState::Deactivating + | DeviceState::Failed => Some("icon:network-wireless-offline-symbolic"), + DeviceState::Activated => Some("icon:network-wireless-connected-symbolic"), + _ => None, + }, + DeviceType::Tun | DeviceType::Wireguard => match state { + DeviceState::Activated => Some("icon:network-vpn-symbolic"), + _ => None, + }, + _ => None, } }