diff --git a/Cargo.lock b/Cargo.lock index 1a77cd71..48237b07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -800,12 +800,6 @@ dependencies = [ "windows-sys 0.61.0", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "dispatch2" version = "0.3.0" @@ -1109,22 +1103,6 @@ dependencies = [ "syn 2.0.106", ] -[[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" @@ -1223,18 +1201,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.106", - "uuid", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -1957,7 +1923,6 @@ dependencies = [ "dirs", "evdev-rs", "futures-lite", - "futures-signals", "glib", "gtk4", "gtk4-layer-shell", diff --git a/Cargo.toml b/Cargo.toml index ea905d3f..aab5b581 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ music = [] "music+mpris" = ["music", "mpris"] "music+mpd" = ["music", "mpd-utils"] -network_manager = ["futures-lite", "futures-signals", "zbus"] +network_manager = ["futures-lite", "zbus"] notifications = ["zbus"] @@ -176,9 +176,6 @@ evdev-rs = { version = "0.6.3", optional = true } mpd-utils = { version = "0.2.1", optional = true } mpris = { version = "2.0.1", optional = true } -# network_manager -futures-signals = { version = "0.3.34", optional = true } - # sys_info sysinfo = { version = "0.37.2", optional = true } diff --git a/docs/modules/Network-Manager.md b/docs/modules/Network-Manager.md index 689de6e5..e8824969 100644 --- a/docs/modules/Network-Manager.md +++ b/docs/modules/Network-Manager.md @@ -1,80 +1,114 @@ -Displays the current network connection state of NetworkManager. -Supports wired ethernet, wifi, cellular data and VPN connections among others. - -> [!NOTE] -> This module is currently a basic skeleton implementation and only offers the most basic functionality currently. -> It uses NetworkManager's so-called primary connection, -> and therefore inherits its limitation of only being able to display the "top-level" connection. -> For example, if we have a VPN connection over a wifi connection it will only display the former, -> until it is disconnected, at which point it will display the latter. -> A solution to this is currently in the works. +Displays the state of each network device managed by NetworkManager. Each device +type will show an icon representing its current state (connected, acquiring, +disconnected). ## Configuration > Type: `network_manager` -| Name | Type | Default | Description | -|-------------|-----------|---------|-------------------------| -| `icon_size` | `integer` | `24` | Size to render icon at. | - -> [!NOTE] -> This module does not support module-level [layout options](module-level-options#layout). +| Name | Type | Default | Description | +| ----------------------------- | ---------- | ------------------------------------------ | ----------------------------------------------------------------------------------- | +| `icon_size` | `integer` | `24` | Size to render icon at. | +| `types_blacklist` | `string[]` | `[]` | Any device with a type in this list will not be shown. | +| `types_whitelist` | `string[]` | `[]` | If not empty, only devices with a type in this list will be shown. | +| `interface_blacklist` | `string[]` | `[]` | Any device whose interface name is in this list will not be shown. | +| `interface_whitelist` | `string[]` | `[]` | If not empty, only devices whose interface name is in this list will be shown. | +| `icons.wired.connected` | `string` | `icon:network-wired-symbolic` | Icon for connected wired device. | +| `icons.wired.acquiring` | `string` | `icon:network-wired-acquiring-symbolic` | Icon for acquiring wired device. | +| `icons.wired.disconnected` | `string` | `""` | Icon for disconnected wired device. | +| `icons.wifi.levels` | `string[]` | See below | Icon for each strengh level of a connected wifi connection, from lowest to highest. | +| `icons.wifi.acquiring` | `string` | `icon:network-wireless-acquiring-symbolic` | Icon for acquiring wifi device. | +| `icons.wifi.disconnected` | `string` | `""` | Icon for disconnected wifi connection. | +| `icons.cellular.connected` | `string` | `icon:network-cellular-connected-symbolic` | Icon for connected cellular device. | +| `icons.cellular.acquiring` | `string` | `icon:network-cellular-acquiring-symbolic` | Icon for acquiring cellular device. | +| `icons.cellular.disconnected` | `string` | `""` | Icon for disconnected cellular device. | +| `icons.vpn.connected` | `string` | `icon:network-vpn-symbolic` | Icon for connected VPN device. | +| `icons.vpn.acquiring` | `string` | `icon:network-vpn-acquiring-symbolic` | Icon for acquiring VPN device. | +| `icons.vpn.disconnected` | `string` | `""` | Icon for disconnected VPN device. | +| `unkown` | `string` | `icon:dialog-question-symbolic` | Icon for device in unkown state. | + +**Device Types:** The device types used in `types_whitelist` and +`types_blacklists` are the same as those used by NetworkManager. You can find +the type of the devices on your system by running `nmcli device status` in a +terminal. The possible device types are: `unknown`, `ethernet`, `wifi`, `bt`, +`olpc_mesh`, `wimax`, `modem`, `infiniband`, `bond`, `vlan`, `adsl`, `bridge`, +`generic`, `team`, `tun`, `ip_tunnel`, `macvlan`, `vxlan`, `veth`, `macsec`, +`dummy`, `ppp`, `ovs_interface`, `ovs_port`, `ovs_bridge`, `wpan`, `six_lowpan`, +`wireguard`, `wifi_p2p`, `vrf`, `loopback`, `hsr` and `ipvlan`. + +**Default `icons.wifi.levels`:** Contains the 5 GTK symbolic icons for wireless signal strength: + +- `"icon:network-wireless-signal-none-symbolic"` +- `"icon:network-wireless-signal-weak-symbolic"` +- `"icon:network-wireless-signal-ok-symbolic"` +- `"icon:network-wireless-signal-good-symbolic"` +- `"icon:network-wireless-signal-excellent-symbolic"`
- JSON - - ```json - { - "end": [ - { - "type": "network_manager", - "icon_size": 32 - } - ] - } - ``` +JSON + +```json +{ + "end": [ + { + "type": "network_manager", + "icon_size": 24, + "types_blacklist": ["loopback", "bridge"] + } + ] +} +``` +
- TOML +TOML + +```toml +[[end]] +type = "network_manager" +icon_size = 24 +types_blacklist = ["loopback", "bridge"] +``` - ```toml - [[end]] - type = "network_manager" - icon_size = 32 - ```
- YAML +YAML + +```yaml +end: + - type: "network_manager" + icon_size: 24 + types_blacklist: + - loopback + - bridge +``` - ```yaml - end: - - type: "network_manager" - icon_size: 32 - ```
- Corn - - ```corn - { - end = [ - { - type = "network_manager" - icon_size = 32 - } - ] - } - ``` +Corn + +```corn +{ + end = [ + { + type = "network_manager" + icon_size = 24 + types_blacklist = [ "loopback" "bridge" ] + } + ] +} +``` +
## Styling | Selector | Description | -|--------------------------|----------------------------------| +| ------------------------ | -------------------------------- | | `.network_manager` | NetworkManager widget container. | -| `.network_manager .icon` | NetworkManager widget icon. | +| `.network_manager .icon` | NetworkManager widget icons. | For more information on styling, please see the [styling guide](styling-guide). diff --git a/src/clients/networkmanager.rs b/src/clients/networkmanager.rs deleted file mode 100644 index db34e2d9..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..0839fc3b --- /dev/null +++ b/src/clients/networkmanager/dbus.rs @@ -0,0 +1,249 @@ +//! NetworkManager D-Bus interface bindings. +//! +//! Upstream documentation: https://networkmanager.dev/docs/api/latest/ + +use std::collections::HashMap; + +use color_eyre::Result; +use serde::Deserialize; +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 trait Dbus { + #[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; +} + +#[proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager.Connection.Active" +)] +pub trait ActiveConnectionDbus { + // #[zbus(property)] + // fn connection(&self) -> Result; + + // #[zbus(property)] + // fn default(&self) -> Result; + + // #[zbus(property)] + // fn default6(&self) -> Result; + + #[zbus(property)] + fn devices(&self) -> Result>>; + + // #[zbus(property)] + // fn id(&self) -> Result; + + #[zbus(property)] + fn type_(&self) -> Result>; + + // #[zbus(property)] + // fn uuid(&self) -> Result; +} + +#[proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager.Device" +)] +pub trait DeviceDbus { + // #[zbus(property)] + // fn active_connection(&self) -> Result; + + #[zbus(property)] + fn device_type(&self) -> Result; + + // #[zbus(property)] + // fn path(&self) -> Result>; + + #[zbus(property)] + fn state(&self) -> Result; + + #[zbus(property)] + fn interface(&self) -> Result>; + + #[zbus(property)] + fn ip4_config(&self) -> Result>; +} + +#[proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager.Device.Wireless" +)] +pub trait DeviceWirelessDbus { + #[zbus(property)] + fn active_access_point(&self) -> zbus::Result; +} + +// based on code generated by `zbus-xmlgen system org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/AccessPoint/1` +#[proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager.AccessPoint" +)] +pub trait AccessPointDbus { + // #[zbus(property)] + // fn path(&self) -> Result; + + #[zbus(property)] + fn ssid(&self) -> zbus::Result>; + + #[zbus(property)] + fn strength(&self) -> Result; + + #[zbus(property)] + fn hw_address(&self) -> Result>; +} + +// based on code generated by `zbus-xmlgen system org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/AccessPoint/1` +#[proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager.IP4Config" +)] +pub trait Ip4ConfigDbus { + #[zbus(property)] + fn address_data(&self) -> Result>>; +} + +/// Indicate the type of hardware represented by a device object. +/// +/// See: https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceType +#[derive(Clone, Copy, Debug, OwnedValue, PartialEq, Deserialize)] +#[repr(u32)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub enum DeviceType { + /// unknown device + Unknown = 0, + /// a wired ethernet device + Ethernet = 1, + /// an 802.11 Wi-Fi device + Wifi = 2, + /// not used + Unused1 = 3, + /// not used + Unused2 = 4, + /// a Bluetooth device supporting PAN or DUN access protocols + Bt = 5, + /// an OLPC XO mesh networking device + OlpcMesh = 6, + /// an 802.16e Mobile WiMAX broadband device + Wimax = 7, + /// a modem supporting analog telephone, CDMA/EVDO, GSM/UMTS, or LTE network access protocols + Modem = 8, + /// an IP-over-InfiniBand device + Infiniband = 9, + /// a bond controller interface + Bond = 10, + /// an 802.1Q VLAN interface + Vlan = 11, + /// ADSL modem + Adsl = 12, + /// a bridge controller interface + Bridge = 13, + /// generic support for unrecognized device types + Generic = 14, + /// a team controller interface + Team = 15, + /// a TUN or TAP interface + Tun = 16, + /// an IP tunnel interface + IpTunnel = 17, + /// a MACVLAN interface + Macvlan = 18, + /// a VXLAN interface + Vxlan = 19, + /// a VETH interface + Veth = 20, + /// a MACsec interface + Macsec = 21, + /// a dummy interface + Dummy = 22, + /// a PPP interface + Ppp = 23, + /// an Open vSwitch interface + OvsInterface = 24, + /// an Open vSwitch port + OvsPort = 25, + /// an Open vSwitch bridge + OvsBridge = 26, + /// a IEEE 802.15.4 (WPAN) MAC Layer Device + Wpan = 27, + /// 6LoWPAN interface + SixLowpan = 28, + /// a WireGuard interface + Wireguard = 29, + /// an 802.11 Wi-Fi P2P device + WifiP2p = 30, + /// a VRF (Virtual Routing and Forwarding) interface + Vrf = 31, + /// a loopback interface + Loopback = 32, + /// a HSR/PRP device + Hsr = 33, + /// an IPVLAN device + Ipvlan = 34, +} + +/// See: https://people.freedesktop.org/~lkundrak/nm-docs/nm-dbus-types.html#NMDeviceState +#[derive(Clone, Copy, Debug, OwnedValue, PartialEq, Eq)] +#[repr(u32)] +pub enum DeviceState { + /// The device's state is unknown + Unknown = 0, + /// The device is recognized, but not managed by NetworkManager + Unmanaged = 10, + /// The device is managed by NetworkManager, but is not available for use. Reasons may include + /// the wireless switched off, missing firmware, no ethernet carrier, missing supplicant or + /// modem manager, etc. + Unavailable = 20, + /// The device can be activated, but is currently idle and not connected to a network. + Disconnected = 30, + /// The device is preparing the connection to the network. This may include operations like + /// changing the MAC address, setting physical link properties, and anything else required to + /// connect to the requested network. + Prepare = 40, + /// The device is connecting to the requested network. This may include operations like + /// associating with the WiFi AP, dialing the modem, connecting to the remote Bluetooth device, + /// etc. + Config = 50, + /// The device requires more information to continue connecting to the requested network. This + /// includes secrets like WiFi passphrases, login passwords, PIN codes, etc. + NeedAuth = 60, + /// The device is requesting IPv4 and/or IPv6 addresses and routing information from the + /// network. + IpConfig = 70, + /// The device is checking whether further action is required for the requested network + /// connection. This may include checking whether only local network access is available, + /// whether a captive portal is blocking access to the Internet, etc. + IpCheck = 80, + /// The device is waiting for a secondary connection (like a VPN) which must activated before + /// the device can be activated + Secondaries = 90, + /// The device has a network connection, either local or global. + Activated = 100, + /// T disconnection from the current network connection was requested, and the device is + /// cleaning up resources used for that connection. The network connection may still be valid. + Deactivating = 110, + /// The device failed to connect to the requested network and is cleaning up the connection + /// request + Failed = 120, +} diff --git a/src/clients/networkmanager/mod.rs b/src/clients/networkmanager/mod.rs new file mode 100644 index 00000000..81a00f96 --- /dev/null +++ b/src/clients/networkmanager/mod.rs @@ -0,0 +1,459 @@ +use color_eyre::eyre::Report; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::{RwLock as AsyncRwLock, broadcast}; + +use color_eyre::Result; +use zbus::Connection; +use zbus::zvariant::ObjectPath; + +use crate::clients::networkmanager::dbus::{AccessPointDbusProxy, DbusProxy, DeviceDbusProxy}; +use crate::{register_fallible_client, spawn}; +use futures_lite::StreamExt; + +pub use self::dbus::{DeviceState, DeviceType, DeviceWirelessDbusProxy, Ip4ConfigDbusProxy}; + +mod dbus; +pub mod state; + +type PathMap<'l, ValueType> = HashMap, ValueType>; + +#[derive(Clone, Debug)] +pub enum NetworkManagerUpdate { + /// Update all devices + Devices(Vec), + /// Update a single device. + /// + /// The `usize` is the index of the device in the list of devices received in the previous + /// `Devices` update. + Device(usize, state::Device), +} + +#[derive(Debug)] +struct ClientDevice { + state: state::Device, + index: usize, + state_handle: tokio::task::JoinHandle<()>, +} + +#[derive(Debug)] +struct ClientIp4Config { + state_handle: tokio::task::JoinHandle<()>, +} + +#[derive(Debug)] +struct ClientAccessPoint { + state_handle: tokio::task::JoinHandle<()>, +} + +#[derive(Clone, Debug)] +pub struct Client { + dbus_connection: Connection, + root_object: &'static DbusProxy<'static>, + tx: broadcast::Sender, + device_map: Arc, ClientDevice>>>, + ip4config_map: Arc>>, + access_point_map: Arc>>, +} +impl Client { + async fn new() -> Result { + let dbus_connection = Connection::system().await?; + let root_object = { + let root_object = DbusProxy::new(&dbus_connection).await?; + // Workaround for the fact that zbus (unnecessarily) requires a static lifetime here + Box::leak(Box::new(root_object)) + }; + + let (tx, rx) = broadcast::channel(32); + + std::mem::forget(rx); + + Ok(Client { + root_object, + dbus_connection, + tx, + device_map: Arc::new(AsyncRwLock::new(HashMap::new())), + ip4config_map: Arc::new(AsyncRwLock::new(HashMap::new())), + access_point_map: Arc::new(AsyncRwLock::new(HashMap::new())), + }) + } + + async fn fetch_devices(&self, device_paths: &[ObjectPath<'_>]) -> Result> { + let mut devices = Vec::with_capacity(device_paths.len()); + for device_path in device_paths { + let device = self.fetch_device(device_path).await?; + devices.push(device); + } + + devices.sort_by(|a, b| { + fn key(d: &state::Device) -> (u32, &str) { + (d.device_type as u32, &d.interface) + } + key(a).cmp(&key(b)) + }); + + Ok(devices) + } + + async fn fetch_device(&self, path: &ObjectPath<'_>) -> Result { + let device_object = DeviceDbusProxy::new(&self.dbus_connection, path).await?; + let state = device_object.state().await?; + let device_type = device_object.device_type().await?; + let interface = device_object.interface().await?; + let ip4_config = if state == DeviceState::Activated { + let ip4_config_path = device_object.ip4_config().await; + match ip4_config_path { + Ok(ip4_config_path) => Some(self.fetch_ip4_config(&ip4_config_path).await?), + Err(_) => { + tracing::error!("Device is activated but has no IP4 config"); + None + } + } + } else { + None + }; + + let device_type_data = self + .fetch_device_wireless(path) + .await + .unwrap_or(state::DeviceTypeData::None); + + Ok(state::Device { + path: path.to_owned(), + interface: interface.to_string(), + state, + device_type, + ip4_config, + device_type_data, + }) + } + + async fn fetch_ip4_config(&self, path: &ObjectPath<'_>) -> Result { + let ipconfig_object = Ip4ConfigDbusProxy::new(&self.dbus_connection, path).await?; + + let address_data = ipconfig_object.address_data().await?; + let address_data = address_data + .iter() + .map(|address_data| -> Result { + let address = address_data.get("address").ok_or_else(|| { + Report::msg("Ip4config address data does not have field 'address'") + })?; + let prefix = address_data.get("prefix").ok_or_else(|| { + Report::msg("Ip4config address data does not have field 'prefix'") + })?; + let address = String::try_from(address.try_clone()?)?; + let prefix = u32::try_from(prefix)?; + Ok(state::AddressData { address, prefix }) + }) + .collect::>>()?; + + Ok(state::Ip4Config { + path: path.to_owned(), + address_data, + }) + } + + async fn fetch_device_wireless(&self, path: &ObjectPath<'_>) -> Result { + let device_object = DeviceWirelessDbusProxy::new(&self.dbus_connection, path).await?; + let active_access_point_path = device_object.active_access_point().await?; + let active_access_point = if active_access_point_path.as_ref() != "/" { + match self.fetch_access_point(&active_access_point_path).await { + Ok(x) => Some(x), + Err(e) => { + tracing::error!("failed to fetch access point: {e}"); + None + } + } + } else { + None + }; + + Ok(state::DeviceTypeData::Wireless(state::DeviceWireless { + active_access_point, + })) + } + + async fn fetch_access_point(&self, path: &ObjectPath<'_>) -> Result { + let access_point_object = AccessPointDbusProxy::new(&self.dbus_connection, path).await?; + let ssid = access_point_object.ssid().await?; + let strength = access_point_object.strength().await?; + Ok(state::AccessPoint { + path: path.to_owned(), + ssid, + strength, + }) + } + + async fn watch_devices_changed(&self) { + let mut x = self.root_object.receive_devices_changed().await; + while let Some(_change) = x.next().await { + self.send_device_update().await; + } + } + + async fn send_device_update(&self) { + let mut device_map = self.device_map.write().await; + let mut ip4config_map = self.ip4config_map.write().await; + let mut access_point_map = self.access_point_map.write().await; + + let device_paths = self.root_object.devices().await; + let Ok(device_paths) = device_paths else { + tracing::error!("Failed to get device paths"); + return; + }; + let devices = self.fetch_devices(&device_paths).await; + match devices { + Ok(devices) => { + let mut new_device_map = HashMap::new(); + let mut new_ip4config_map = HashMap::new(); + let mut new_access_point_map = HashMap::new(); + + for (index, device) in devices.iter().enumerate() { + let path = &device.path; + match device_map.remove(path) { + Some(client_device) => { + let path = path.to_owned(); + new_device_map.insert( + path, + ClientDevice { + state: device.clone(), + index, + state_handle: client_device.state_handle, + }, + ); + } + None => { + let this = self.clone(); + let path2 = path.to_owned(); + let v = ClientDevice { + state: device.clone(), + index, + state_handle: spawn(async move { + this.watch_device_change(path2).await + }), + }; + let path = path.to_owned(); + new_device_map.insert(path, v); + } + } + + match device + .ip4_config + .as_ref() + .and_then(|ip4| ip4config_map.remove(&ip4.path)) + { + Some(client_ipconfig) => { + if let Some(ip4config) = device.ip4_config.as_ref() { + new_ip4config_map + .insert(ip4config.path.to_owned(), client_ipconfig); + } + } + None => { + if let Some(ip4config) = device.ip4_config.as_ref() { + let this = self.clone(); + let device_path = path.to_owned(); + let path2 = ip4config.path.to_owned(); + let v = ClientIp4Config { + state_handle: spawn(async move { + this.watch_ip4config_change(device_path, path2).await + }), + }; + new_ip4config_map.insert(ip4config.path.to_owned(), v); + } + } + } + + if let state::DeviceTypeData::Wireless(wireless) = &device.device_type_data { + match wireless + .active_access_point + .as_ref() + .and_then(|ap| access_point_map.remove(&ap.path)) + { + Some(client_ap) => { + if let Some(ap) = wireless.active_access_point.as_ref() { + new_access_point_map.insert(ap.path.to_owned(), client_ap); + } + } + None => { + if let Some(ap) = wireless.active_access_point.as_ref() { + let this = self.clone(); + let device_path = path.to_owned(); + let path2 = ap.path.to_owned(); + let access_point_object = match AccessPointDbusProxy::new( + &this.dbus_connection, + &path2, + ) + .await + { + Ok(proxy) => proxy, + Err(e) => { + tracing::error!( + "Failed to create access point proxy: {e}" + ); + continue; + } + }; + let v = ClientAccessPoint { + state_handle: spawn(async move { + this.watch_access_point_change( + device_path, + access_point_object, + ) + .await + }), + }; + new_access_point_map.insert(ap.path.to_owned(), v); + } + } + } + } + } + + for device in device_map.values() { + device.state_handle.abort(); + } + + for ipconfig in ip4config_map.values() { + ipconfig.state_handle.abort(); + } + + for ap in access_point_map.values() { + ap.state_handle.abort(); + } + + *device_map = new_device_map; + *ip4config_map = new_ip4config_map; + *access_point_map = new_access_point_map; + + self.tx.send(NetworkManagerUpdate::Devices(devices)).ok(); + } + Err(e) => { + tracing::error!("Failed to fetch devices: {e}"); + } + } + } + + async fn update_device(&self, device: &ObjectPath<'_>) -> Result<()> { + let mut device_map = self.device_map.write().await; + let entry = device_map + .get_mut(&device.to_owned()) + .ok_or_else(|| Report::msg("Device changed but not in device map"))?; + let device = self.fetch_device(device).await; + + match device { + Ok(device) => { + entry.state = device.clone(); + let index = entry.index; + self.tx.send(NetworkManagerUpdate::Device(index, device))?; + } + Err(e) => { + tracing::error!("Failed to fetch device: {e}"); + } + } + + Ok(()) + } + + async fn watch_device_change(&self, device: ObjectPath<'_>) { + let device = match DeviceDbusProxy::new(&self.dbus_connection, &device).await { + Ok(device) => device, + Err(e) => { + tracing::error!("Failed to create device proxy: {e}"); + return; + } + }; + let mut state = device.receive_state_changed().await; + let mut device_type = device.receive_device_type_changed().await; + let mut ip4_config = device.receive_ip4_config_changed().await; + + loop { + // Wait for any of the properties to change + tokio::select! { + biased; + _ = state.next() => (), + _ = device_type.next() => (), + _ = ip4_config.next() => (), + }; + + let path = device.inner().path(); + match self.update_device(path).await { + Ok(_) => (), + Err(e) => { + tracing::error!("Failed to update device: {e}"); + break; + } + } + } + } + + async fn watch_ip4config_change(&self, device: ObjectPath<'_>, ip4config: ObjectPath<'_>) { + let ip4config = match Ip4ConfigDbusProxy::new(&self.dbus_connection, &ip4config).await { + Ok(ip4config) => ip4config, + Err(e) => { + tracing::error!("Failed to create ip4config proxy: {e}"); + return; + } + }; + let mut address_data = ip4config.receive_address_data_changed().await; + loop { + address_data.next().await; + match self.update_device(&device).await { + Ok(_) => (), + Err(e) => { + tracing::error!("Failed to update ip4config: {e}"); + break; + } + } + } + } + + async fn watch_access_point_change( + &self, + device: ObjectPath<'_>, + access_point: AccessPointDbusProxy<'_>, + ) { + let mut ssid = access_point.receive_ssid_changed().await; + let mut strength = access_point.receive_strength_changed().await; + loop { + tokio::select! { + biased; + _ = ssid.next() => (), + _ = strength.next() => (), + }; + tracing::debug!("Access point changed for device {device}"); + match self.update_device(&device).await { + Ok(_) => (), + Err(e) => { + tracing::error!("Failed to update access point: {e}"); + break; + } + } + } + } + + async fn run(&self) -> Result<()> { + let this = self.clone(); + spawn(async move { this.watch_devices_changed().await }); + Ok(()) + } + + pub async fn subscribe(self: &Arc) -> broadcast::Receiver { + let rx = self.tx.subscribe(); + let this = Arc::clone(self); + spawn(async move { + this.send_device_update().await; + }); + rx + } +} + +pub async fn create_client() -> Result> { + let client = Arc::new(Client::new().await?); + { + let client = client.clone(); + spawn(async move { client.run().await }); + } + Ok(client) +} + +register_fallible_client!(Client, network_manager); diff --git a/src/clients/networkmanager/state.rs b/src/clients/networkmanager/state.rs new file mode 100644 index 00000000..3ab5bc89 --- /dev/null +++ b/src/clients/networkmanager/state.rs @@ -0,0 +1,128 @@ +use zbus::zvariant::ObjectPath; + +use super::dbus::{DeviceState, DeviceType}; + +#[derive(Clone, Debug)] +pub struct Device { + pub path: ObjectPath<'static>, + /// Interface readable s + pub interface: String, + + /// State readable u + /// + /// The current state of the device. + pub state: DeviceState, + + /// Ip4Config readable o + /// + /// Object path of the Ip4Config object describing the configuration of the device. Only valid + /// when the device is in the NM_DEVICE_STATE_ACTIVATED state. + pub ip4_config: Option, + + /// DeviceType readable u + /// + /// The general type of the network device; ie Ethernet, WiFi, etc. + pub device_type: DeviceType, + /// Device data specific to the device type. + pub device_type_data: DeviceTypeData, + // + // # Unmapped properties: + // Udi readable s + // IpInterface readable s + // Driver readable s + // DriverVersion readable s + // FirmwareVersion readable s + // Capabilities readable u + // Ip4Address readable u + // StateReason readable (uu) + // ActiveConnection readable o + // Dhcp4Config readable o + // Ip6Config readable o + // Dhcp6Config readable o + // Managed readwrite b + // Autoconnect readwrite b + // FirmwareMissing readable b + // NmPluginMissing readable b + // AvailableConnections readable ao + // PhysicalPortId readable s + // Mtu readable u + // Metered readable u + // LldpNeighbors readable aa{sv} + // Real readable b +} + +#[derive(Clone, Debug)] +pub struct Ip4Config { + pub path: ObjectPath<'static>, + /// AddressData readable aa{sv} + /// + /// Array of IP address data objects. All addresses will include "address" (an IP address + /// string), and "prefix" (a uint). Some addresses may include additional attributes. + pub address_data: Vec, + // + // # Unmapped properties: + // Addresses readable aau + // Gateway readable s + // Routes readable aau + // RouteData readable aa{sv} + // Nameservers readable au + // Domains readable as + // Searches readable as + // DnsOptions readable as + // DnsPriority readable i + // WinsServers readable au +} + +#[derive(Clone, Debug)] +pub struct AddressData { + pub address: String, + pub prefix: u32, +} + +/// The sub-interface data for the device, e.g. wifi, etc. +#[derive(Clone, Debug)] +pub enum DeviceTypeData { + /// The device does not have a specific type, or it is unimplemented. + None, + Wireless(DeviceWireless), +} + +#[derive(Clone, Debug)] +pub struct DeviceWireless { + /// ActiveAccessPoint readable o + /// + /// Object path of the access point currently used by the wireless device. + pub active_access_point: Option, + // + // # Unmapped properties: + // HwAddress readable s + // PermHwAddress readable s + // Mode readable u + // Bitrate readable u + // AccessPoints readable ao + // WirelessCapabilities readable u +} + +#[derive(Clone, Debug)] +pub struct AccessPoint { + pub path: ObjectPath<'static>, + /// Ssid readable ay + /// + /// The Service Set Identifier identifying the access point. + pub ssid: Vec, + + /// Strength readable y + /// + /// The current signal quality of the access point, in percent. + pub strength: u8, + // + // # Unmapped properties: + // Frequency readable u + // HwAddress readable s + // Mode readable u + // MaxBitrate readable u + // Flags readable u + // WpaFlags readable u + // RsnFlags readable u + // LastSeen readable i +} diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index 8a45cb51..9526f265 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -1,37 +1,189 @@ use crate::channels::{AsyncSenderExt, BroadcastReceiverExt}; -use crate::clients::networkmanager::{Client, ClientState}; +use crate::clients::networkmanager::state::DeviceTypeData; +use crate::clients::networkmanager::{Client, DeviceState, DeviceType, NetworkManagerUpdate}; use crate::config::{CommonConfig, default}; +use crate::gtk_helpers::IronbarGtkExt; 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::WidgetExt; use gtk::prelude::*; use gtk::{Box as GtkBox, ContentFit, Picture}; use serde::Deserialize; use tokio::sync::mpsc::Receiver; +mod config; + +/// A simplified version of [`DeviceState`] used for icon selection. +enum State { + Disconnected, + Acquiring, + Connected, +} + +impl From for State { + fn from(state: DeviceState) -> Self { + match state { + DeviceState::Unknown + | DeviceState::Unmanaged + | DeviceState::Unavailable + | DeviceState::Deactivating + | DeviceState::Failed + | DeviceState::Disconnected => State::Disconnected, + DeviceState::Prepare + | DeviceState::Config + | DeviceState::NeedAuth + | DeviceState::IpConfig + | DeviceState::IpCheck + | DeviceState::Secondaries => State::Acquiring, + DeviceState::Activated => State::Connected, + } + } +} + #[derive(Debug, Deserialize, Clone)] #[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] #[serde(default)] pub struct NetworkManagerModule { + /// The size of the icon for each network device, in pixels. icon_size: i32, + /// The configuraiton for the icons used to represent network devices. + #[serde(default)] + icons: config::Icons, + + /// Any device with a type in this list will not be shown. The type is a string matching + /// [`DeviceType`] variants (e.g. `"Wifi"`, `"Ethernet", etc.). + #[serde(default)] + types_blacklist: Vec, + + /// If not empty, only devices with a type in this list will be shown. The type is a string + /// matching [`DeviceType`] variants (e.g. `"Wifi"`, `"Ethernet", etc.). + #[serde(default)] + types_whitelist: Vec, + + /// Any device whose interface name is in this list will not be shown. + #[serde(default)] + interface_blacklist: Vec, + + /// If not empty, only devices whose interface name is in this list will be shown. + #[serde(default)] + interface_whitelist: Vec, + #[serde(flatten)] pub common: Option, } +impl NetworkManagerModule { + async fn update_icon( + &self, + image_provider: &crate::image::Provider, + device: &crate::clients::networkmanager::state::Device, + icon: &Picture, + ) { + fn whitelisted(list: &[T], x: &T) -> bool { + list.is_empty() || list.contains(x) + } + + let type_whitelisted = whitelisted(&self.types_whitelist, &device.device_type); + let interface_whitelisted = whitelisted(&self.interface_whitelist, &device.interface); + let type_blacklisted = self.types_blacklist.contains(&device.device_type); + let interface_blacklisted = self.interface_blacklist.contains(&device.interface); + + if !type_whitelisted || !interface_whitelisted || type_blacklisted || interface_blacklisted + { + icon.set_visible(false); + return; + } + + let mut tooltip = device.interface.clone(); + if let Some(ip) = &device.ip4_config { + for x in &ip.address_data { + tooltip.push('\n'); + tooltip.push_str(&x.address); + tooltip.push('/'); + tooltip.push_str(&x.prefix.to_string()); + } + } + + let state = State::from(device.state); + + let icon_name = match device.device_type { + DeviceType::Wifi => match state { + State::Acquiring => self.icons.wifi.acquiring.as_str(), + State::Disconnected => self.icons.wifi.disconnected.as_str(), + State::Connected => match &device.device_type_data { + DeviceTypeData::Wireless(wireless) => match &wireless.active_access_point { + Some(connection) => { + tooltip.push('\n'); + tooltip.push_str(&String::from_utf8_lossy(&connection.ssid)); + + if self.icons.wifi.levels.is_empty() { + "" + } else { + let level = strength_to_level( + connection.strength, + self.icons.wifi.levels.len(), + ); + self.icons.wifi.levels[level].as_str() + } + } + None => self.icons.wifi.disconnected.as_str(), + }, + _ => self.icons.unknown.as_str(), + }, + }, + DeviceType::Modem | DeviceType::Wimax => match state { + State::Acquiring => self.icons.cellular.acquiring.as_ref(), + State::Disconnected => self.icons.cellular.disconnected.as_ref(), + State::Connected => self.icons.cellular.connected.as_ref(), + }, + DeviceType::Wireguard + | DeviceType::Tun + | DeviceType::IpTunnel + | DeviceType::Vxlan + | DeviceType::Macsec => match state { + State::Acquiring => self.icons.vpn.acquiring.as_ref(), + State::Disconnected => self.icons.vpn.disconnected.as_ref(), + State::Connected => self.icons.vpn.connected.as_ref(), + }, + _ => match state { + State::Acquiring => self.icons.wired.acquiring.as_ref(), + State::Disconnected => self.icons.wired.disconnected.as_ref(), + State::Connected => self.icons.wired.connected.as_ref(), + }, + }; + + if icon_name.is_empty() { + icon.set_visible(false); + return; + } + + image_provider + .load_into_picture_silent(icon_name, self.icon_size, false, icon) + .await; + icon.set_tooltip_text(Some(&tooltip)); + + icon.set_visible(true); + } +} impl Default for NetworkManagerModule { fn default() -> Self { Self { icon_size: default::IconSize::Small as i32, common: Some(CommonConfig::default()), + icons: config::Icons::default(), + types_blacklist: Vec::new(), + types_whitelist: Vec::new(), + interface_blacklist: Vec::new(), + interface_whitelist: Vec::new(), } } } impl Module for NetworkManagerModule { - type SendMessage = ClientState; + type SendMessage = NetworkManagerUpdate; type ReceiveMessage = (); module_impl!("network_manager"); @@ -39,15 +191,15 @@ impl Module for NetworkManagerModule { fn spawn_controller( &self, _: &ModuleInfo, - context: &WidgetContext, + context: &WidgetContext, _: Receiver<()>, ) -> Result<()> { let client = context.try_client::()?; - let mut client_signal = client.subscribe().to_stream(); let tx = context.tx.clone(); spawn(async move { - while let Some(state) = client_signal.next().await { + let mut client_signal = client.subscribe().await; + while let Ok(state) = client_signal.recv().await { tx.send_update(state).await; } }); @@ -57,52 +209,133 @@ impl Module for NetworkManagerModule { fn into_widget( self, - context: WidgetContext, + context: WidgetContext, info: &ModuleInfo, ) -> Result> { - const INITIAL_ICON_NAME: &str = "content-loading-symbolic"; - let container = GtkBox::new(info.bar_position.orientation(), 0); - let icon = Picture::builder() - .content_fit(ContentFit::ScaleDown) - .build(); - icon.add_css_class("icon"); - container.append(&icon); let image_provider = context.ironbar.image_provider(); - glib::spawn_future_local({ + let container_clone = container.clone(); + context.subscribe().recv_glib_async((), move |(), update| { + let container = container.clone(); let image_provider = image_provider.clone(); - let icon = icon.clone(); - + let this = self.clone(); async move { - image_provider - .load_into_picture_silent(INITIAL_ICON_NAME, self.icon_size, false, &icon) - .await; - } - }); + match update { + NetworkManagerUpdate::Devices(devices) => { + tracing::debug!("NetworkManager devices updated"); + tracing::trace!("NetworkManager devices updated: {devices:#?}"); - 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", - }; + // resize the container's children to match the number of devices + if container.children().count() > devices.len() { + for child in container.children().skip(devices.len()) { + container.remove(&child); + } + } else { + while container.children().count() < devices.len() { + let icon = Picture::builder() + .content_fit(ContentFit::ScaleDown) + .css_classes(["icon"]) + .build(); + container.append(&icon); + } + } - async move { - image_provider - .load_into_picture_silent(icon_name, self.icon_size, false, &icon) - .await; + // update each icon to match the device state + for (device, widget) in devices.iter().zip(container.children()) { + this.update_icon( + &image_provider, + device, + widget.downcast_ref::().expect("should be Picture"), + ) + .await; + } + } + NetworkManagerUpdate::Device(idx, device) => { + tracing::debug!( + "NetworkManager device {idx} updated: {}", + device.interface + ); + tracing::trace!("NetworkManager device {idx} updated: {device:#?}"); + if let Some(widget) = container.children().nth(idx) { + this.update_icon( + &image_provider, + &device, + widget.downcast_ref::().expect("should be Picture"), + ) + .await; + } else { + tracing::warn!("No widget found for device index {idx}"); + } + } + } } }); - Ok(ModuleParts::new(container, None)) + Ok(ModuleParts::new(container_clone, None)) + } +} + +/// Convert strength level (from 0-100), to a level (from 0 to `number_of_levels-1`). +fn strength_to_level(strength: u8, levels: usize) -> usize { + // Strength levels based for the one show by [`nmcli dev wifi list`](https://github.com/NetworkManager/NetworkManager/blob/83a259597000a88217f3ccbdfe71c8114242e7a6/src/libnmc-base/nm-client-utils.c#L700-L727): + // match strength { + // 0..=4 => 0, + // 5..=29 => 1, + // 30..=54 => 2, + // 55..=79 => 3, + // 80.. => 4, + // } + + // to make it work with a custom number of levels, we approach the logic above with a + // piece-wise linear interpolation: + // - 0 to 5 -> 0 to 0.2 + // - 5 to 80 -> 0.2 to 0.8 + // - 80 to 100 -> 0.8 to 1.0 + + if levels <= 1 { + return 0; + } + + let strength = strength.clamp(0, 100); + + let pos = if strength < 5 { + // Linear interpolation between 0..5 + (strength as f32 / 5.0) * 0.2 + } else if strength < 80 { + // Linear interpolation between 5..80 + 0.2 + ((strength - 5) as f32 / 75.0) * 0.6 + } else { + // Linear interpolation between 80..100 + 0.8 + ((strength as f32 - 80.0) / 20.0) * 0.2 + }; + + // Scale to discrete levels + let level = (pos * levels as f32).floor() as usize; + level.min(levels - 1) +} + +// Just to make sure the implementation still follow the reference logic +#[cfg(test)] +#[test] +fn test_strength_to_level() { + for levels in 0..=10 { + println!("Levels: {}", levels); + for strength in (0..=100).step_by(5) { + let level = strength_to_level(strength, levels); + println!(" Strength: {:3} => Level: {}", strength, level); + } } + assert_eq!(strength_to_level(0, 5), 0); + assert_eq!(strength_to_level(4, 5), 0); + assert_eq!(strength_to_level(5, 5), 1); + assert_eq!(strength_to_level(6, 5), 1); + assert_eq!(strength_to_level(29, 5), 1); + assert_eq!(strength_to_level(30, 5), 2); + assert_eq!(strength_to_level(54, 5), 2); + assert_eq!(strength_to_level(55, 5), 3); + assert_eq!(strength_to_level(79, 5), 3); + assert_eq!(strength_to_level(80, 5), 4); + assert_eq!(strength_to_level(100, 5), 4); } diff --git a/src/modules/networkmanager/config.rs b/src/modules/networkmanager/config.rs new file mode 100644 index 00000000..b126941f --- /dev/null +++ b/src/modules/networkmanager/config.rs @@ -0,0 +1,102 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +#[serde(default)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub struct Icons { + pub wired: IconsWired, + pub wifi: IconsWifi, + pub cellular: IconsCellular, + pub vpn: IconsVpn, + pub unknown: String, +} +impl Default for Icons { + fn default() -> Self { + Self { + wired: IconsWired::default(), + wifi: IconsWifi::default(), + cellular: IconsCellular::default(), + vpn: IconsVpn::default(), + unknown: "icon:dialog-question-symbolic".to_string(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(default)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub struct IconsWired { + pub connected: String, + pub acquiring: String, + pub disconnected: String, +} +impl Default for IconsWired { + fn default() -> Self { + Self { + connected: "icon:network-wired-symbolic".to_string(), + acquiring: "icon:network-wired-acquiring-symbolic".to_string(), + disconnected: "".to_string(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(default)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub struct IconsWifi { + pub levels: Vec, + pub acquiring: String, + pub disconnected: String, +} + +impl Default for IconsWifi { + fn default() -> Self { + Self { + levels: vec![ + "icon:network-wireless-signal-none-symbolic".to_string(), + "icon:network-wireless-signal-weak-symbolic".to_string(), + "icon:network-wireless-signal-ok-symbolic".to_string(), + "icon:network-wireless-signal-good-symbolic".to_string(), + "icon:network-wireless-signal-excellent-symbolic".to_string(), + ], + acquiring: "icon:network-wireless-acquiring-symbolic".to_string(), + disconnected: "".to_string(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(default)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub struct IconsCellular { + pub connected: String, + pub acquiring: String, + pub disconnected: String, +} +impl Default for IconsCellular { + fn default() -> Self { + Self { + connected: "icon:network-cellular-connected-symbolic".to_string(), + acquiring: "icon:network-cellular-acquiring-symbolic".to_string(), + disconnected: "".to_string(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(default)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub struct IconsVpn { + pub connected: String, + pub acquiring: String, + pub disconnected: String, +} +impl Default for IconsVpn { + fn default() -> Self { + Self { + connected: "icon:network-vpn-symbolic".to_string(), + acquiring: "icon:network-vpn-acquiring-symbolic".to_string(), + disconnected: "".to_string(), + } + } +}