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(),
+ }
+ }
+}