From db03af30d134ccd348529427d76802b18765f152 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Fri, 27 Sep 2024 21:48:36 -0300 Subject: [PATCH 01/23] feat: reword networkmanager module - Allow a dynamic number of devices. - Show WiFi connection strength and update in real time. - Allow configuring the icons. - Show network info as tool-tip. - Allow whitelist/blacklisting device names/types. --- docs/modules/Networkmanager.md | 91 ++++++ src/clients/networkmanager.rs | 171 ---------- src/clients/networkmanager/dbus.rs | 206 ++++++++++++ src/clients/networkmanager/mod.rs | 459 +++++++++++++++++++++++++++ src/clients/networkmanager/state.rs | 117 +++++++ src/modules/networkmanager.rs | 283 ++++++++++++++--- src/modules/networkmanager/config.rs | 137 ++++++++ 7 files changed, 1250 insertions(+), 214 deletions(-) create mode 100644 docs/modules/Networkmanager.md delete mode 100644 src/clients/networkmanager.rs create mode 100644 src/clients/networkmanager/dbus.rs create mode 100644 src/clients/networkmanager/mod.rs create mode 100644 src/clients/networkmanager/state.rs create mode 100644 src/modules/networkmanager/config.rs diff --git a/docs/modules/Networkmanager.md b/docs/modules/Networkmanager.md new file mode 100644 index 000000000..099c4e3de --- /dev/null +++ b/docs/modules/Networkmanager.md @@ -0,0 +1,91 @@ +Displays the current network connection state of NetworkManager. +Supports wired ethernet, wifi, cellular data and VPN connections among others. + +> [!NOTE] +> This module 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. + +## Configuration + +> Type: `networkmanager` + +| Name | Type | Default | Description | +| ----------------------------- | ---------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `icon_size` | `integer` | `24` | Size to render icon at. | +| `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[]` | `["icon:network-wireless-signal-none-symbolic", ...]` | Icon for each strengh level of a connected wifi connection, from lowest to highest. The default contains 5 levels. | +| `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 | + +
+JSON + +```json +{ + "end": [ + { + "type": "networkmanager", + "icon_size": 32 + } + ] +} +``` + +
+ +
+TOML + +```toml +[[end]] +type = "networkmanager" +icon_size = 32 +``` + +
+ +
+YAML + +```yaml +end: + - type: "networkmanager" + icon_size: 32 +``` + +
+ +
+Corn + +```corn +{ + end = [ + { + type = "networkmanager" + icon_size = 32 + } + ] +} +``` + +
+ +## Styling + +| Selector | Description | +| ---------------------- | -------------------------------- | +| `.networkmanager` | NetworkManager widget container. | +| `.networkmanger .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 db34e2d90..000000000 --- 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 000000000..90a3896e5 --- /dev/null +++ b/src/clients/networkmanager/dbus.rs @@ -0,0 +1,206 @@ +//! 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>>; +} + +#[derive(Clone, Copy, Debug, OwnedValue, PartialEq, Deserialize)] +#[repr(u32)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub enum DeviceType { + Unknown = 0, + Ethernet = 1, + Wifi = 2, + Bluetooth = 5, + OlpcMesh = 6, + Wimax = 7, + Modem = 8, + Infiniband = 9, + Bond = 10, + Vlan = 11, + Adsl = 12, + Bridge = 13, + Team = 15, + Tun = 16, + IpTunnel = 17, + Macvlan = 18, + Vxlan = 19, + Veth = 20, + Macsec = 21, + Dummy = 22, + Ppp = 23, + OvsInterface = 24, + OvsPort = 25, + OvsBridge = 26, + Wpan = 27, + Lowpan = 28, + Wireguard = 29, + WifiP2p = 30, + Vrf = 31, + Loopback = 32, + Hsr = 33, +} + +// https://people.freedesktop.org/~lkundrak/nm-docs/nm-dbus-types.html#NMDeviceState +#[derive(Clone, 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 000000000..4c7a8046c --- /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, 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, receive 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(RwLock::new(HashMap::new())), + ip4config_map: Arc::new(RwLock::new(HashMap::new())), + access_point_map: Arc::new(RwLock::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!("fail 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 000000000..affbddeda --- /dev/null +++ b/src/clients/networkmanager/state.rs @@ -0,0 +1,117 @@ +use zbus::zvariant::ObjectPath; + +use super::dbus::{DeviceState, DeviceType}; + +#[derive(Clone, Debug)] +pub struct Device { + pub path: ObjectPath<'static>, + // Udi readable s + /// Interface readable s + pub interface: String, + // IpInterface readable s + // Driver readable s + // DriverVersion readable s + // FirmwareVersion readable s + // Capabilities readable u + // Ip4Address readable u + /// State readable u + /// + /// The current state of the device. + pub state: DeviceState, + // StateReason readable (uu) + // ActiveConnection readable o + /// 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, + // Dhcp4Config readable o + // Ip6Config readable o + // Dhcp6Config readable o + // Managed readwrite b + // Autoconnect readwrite b + // FirmwareMissing readable b + // NmPluginMissing readable b + /// DeviceType readable u + /// + /// The general type of the network device; ie Ethernet, WiFi, etc. + pub device_type: DeviceType, + pub device_type_data: DeviceTypeData, + // 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>, + // Addresses readable aau + /// 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, + // 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 { + // address s + pub address: String, + // prefix u + 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 { + // HwAddress readable s + // PermHwAddress readable s + // Mode readable u + // Bitrate readable u + // AccessPoints readable ao + /// ActiveAccessPoint readable o + /// + /// Object path of the access point currently used by the wireless device. + pub active_access_point: Option, + // WirelessCapabilities readable u +} + +#[derive(Clone, Debug)] +pub struct AccessPoint { + pub path: ObjectPath<'static>, + // Flags readable u + // WpaFlags readable u + // RsnFlags readable u + /// Ssid readable ay + /// + /// The Service Set Identifier identifying the access point. + pub ssid: Vec, + // Frequency readable u + // HwAddress readable s + // Mode readable u + // MaxBitrate readable u + /// Strength readable y + /// + /// The current signal quality of the access point, in percent. + pub strength: u8, + // LastSeen readable i +} diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index 8a45cb515..61495db82 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -1,53 +1,192 @@ 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; + #[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::IconsConfig, + + /// 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, + ) { + let mut disconnected = false; + let mut acquiring = false; + let mut connected = false; + match device.state { + DeviceState::Unknown + | DeviceState::Unmanaged + | DeviceState::Unavailable + | DeviceState::Deactivating + | DeviceState::Failed + | DeviceState::Disconnected => disconnected = true, + DeviceState::Prepare + | DeviceState::Config + | DeviceState::NeedAuth + | DeviceState::IpConfig + | DeviceState::IpCheck + | DeviceState::Secondaries => acquiring = true, + DeviceState::Activated => connected = true, + } + + if !self.types_whitelist.is_empty() + && !self + .types_whitelist + .iter() + .any(|t| t == &device.device_type) + || self.types_blacklist.contains(&device.device_type) + || !self.interface_whitelist.is_empty() + && !self + .interface_whitelist + .iter() + .any(|n| n == &device.interface) + || self.interface_blacklist.contains(&device.interface) + { + 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 icon_name = match device.device_type { + DeviceType::Wifi => match () { + _ if acquiring => self.icons.wifi.acquiring.as_str(), + _ if disconnected => self.icons.wifi.disconnected.as_str(), + _ => 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)); + + let level = + strengh_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 () { + _ if acquiring => self.icons.cellular.acquiring.as_ref(), + _ if disconnected => self.icons.cellular.disconnected.as_ref(), + _ if connected => self.icons.cellular.connected.as_ref(), + _ => self.icons.unknown.as_ref(), + }, + DeviceType::Wireguard + | DeviceType::Tun + | DeviceType::IpTunnel + | DeviceType::Vxlan + | DeviceType::Macsec => match () { + _ if acquiring => self.icons.vpn.acquiring.as_ref(), + _ if disconnected => self.icons.vpn.disconnected.as_ref(), + _ if connected => self.icons.vpn.connected.as_ref(), + _ => self.icons.unknown.as_ref(), + }, + _ => match () { + _ if acquiring => self.icons.wired.acquiring.as_ref(), + _ if disconnected => self.icons.wired.disconnected.as_ref(), + _ if connected => self.icons.wired.connected.as_ref(), + _ => self.icons.unknown.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::IconsConfig::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"); - 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 +196,110 @@ 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::().unwrap(), + ) + .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::().unwrap(), + ) + .await; + } else { + tracing::warn!("No widget found for device index {idx}"); + } + } + } } }); - Ok(ModuleParts::new(container, None)) + Ok(ModuleParts::new(container_clone, None)) } + + module_impl!("networkmanager"); +} + +/// Convert strength level (from 0-100), to a level (from 0 to `number_of_levels-1`). +const fn strengh_to_level(strength: u8, number_of_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 the logic + // below (0 for < 5, and a linear interpolation for 5 to 105). + // TODO: if there are more than 20 levels, the last level will be out of scale, and never be + // reach. + if strength < 5 { + return 0; + } + (strength as usize - 5) * (number_of_levels - 1) / 100 + 1 +} + +// Just to make sure my implementation still follow the original logic +#[cfg(test)] +#[test] +fn test_strength_to_level() { + assert_eq!(strengh_to_level(0, 5), 0); + assert_eq!(strengh_to_level(4, 5), 0); + assert_eq!(strengh_to_level(5, 5), 1); + assert_eq!(strengh_to_level(6, 5), 1); + assert_eq!(strengh_to_level(29, 5), 1); + assert_eq!(strengh_to_level(30, 5), 2); + assert_eq!(strengh_to_level(54, 5), 2); + assert_eq!(strengh_to_level(55, 5), 3); + assert_eq!(strengh_to_level(79, 5), 3); + assert_eq!(strengh_to_level(80, 5), 4); + assert_eq!(strengh_to_level(100, 5), 4); } diff --git a/src/modules/networkmanager/config.rs b/src/modules/networkmanager/config.rs new file mode 100644 index 000000000..d8d332952 --- /dev/null +++ b/src/modules/networkmanager/config.rs @@ -0,0 +1,137 @@ +use serde::Deserialize; + +macro_rules! default_function { + ($(($name:ident, $default:expr),)*) => { + $( + fn $name() -> String { + ($default).to_string() + } + )* + }; +} + +#[derive(Debug, Deserialize, Clone, Default)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub struct IconsConfig { + #[serde(default)] + pub wired: IconsConfigWired, + #[serde(default)] + pub wifi: IconsConfigWifi, + #[serde(default)] + pub cellular: IconsConfigCellular, + #[serde(default)] + pub vpn: IconsConfigVpn, + + #[serde(default = "default_unknown")] + pub unknown: String, +} + +#[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub struct IconsConfigWired { + #[serde(default = "default_wired_connected")] + pub connected: String, + #[serde(default = "default_wired_acquiring")] + pub acquiring: String, + #[serde(default = "default_wired_disconnected")] + pub disconnected: String, +} +impl Default for IconsConfigWired { + fn default() -> Self { + Self { + connected: default_wired_connected(), + acquiring: default_wired_acquiring(), + disconnected: default_wired_disconnected(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub struct IconsConfigWifi { + #[serde(default = "default_wifi_levels")] + pub levels: Vec, + #[serde(default = "default_wifi_acquiring")] + pub acquiring: String, + #[serde(default = "default_wifi_disconnected")] + pub disconnected: String, +} + +impl Default for IconsConfigWifi { + fn default() -> Self { + Self { + levels: default_wifi_levels(), + acquiring: default_wifi_acquiring(), + disconnected: default_wifi_disconnected(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub struct IconsConfigCellular { + #[serde(default = "default_cellular_connected")] + pub connected: String, + #[serde(default = "default_cellular_acquiring")] + pub acquiring: String, + #[serde(default = "default_cellular_disconnected")] + pub disconnected: String, +} +impl Default for IconsConfigCellular { + fn default() -> Self { + Self { + connected: default_cellular_connected(), + acquiring: default_cellular_acquiring(), + disconnected: default_cellular_disconnected(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] +pub struct IconsConfigVpn { + #[serde(default = "default_vpn_connected")] + pub connected: String, + #[serde(default = "default_vpn_acquiring")] + pub acquiring: String, + #[serde(default = "default_vpn_disconnected")] + pub disconnected: String, +} +impl Default for IconsConfigVpn { + fn default() -> Self { + Self { + connected: default_vpn_connected(), + acquiring: default_vpn_acquiring(), + disconnected: default_vpn_disconnected(), + } + } +} + +pub fn default_wifi_levels() -> Vec { + 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(), + ] +} + +default_function! { + (default_wired_connected, "icon:network-wired-symbolic"), + (default_wired_acquiring, "icon:network-wired-acquiring-symbolic"), + (default_wired_disconnected, ""), + + (default_wifi_acquiring, "icon:network-wireless-acquiring-symbolic"), + (default_wifi_disconnected, ""), + + (default_cellular_connected,"icon:network-cellular-connected-symbolic"), + (default_cellular_acquiring,"icon:network-cellular-acquiring-symbolic"), + (default_cellular_disconnected,""), + + (default_vpn_connected, "icon:network-vpn-symbolic"), + (default_vpn_acquiring, "icon:network-vpn-acquiring-symbolic"), + (default_vpn_disconnected, ""), + + (default_unknown, "icon:dialog-question-symbolic"), +} From cded882b033b1e98f284a00806056d63b2ec8fb5 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 13:54:15 -0300 Subject: [PATCH 02/23] refactor: rename 'IconsConfig' to 'Icons' --- src/modules/networkmanager.rs | 4 ++-- src/modules/networkmanager/config.rs | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index 61495db82..b6d6470c0 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -24,7 +24,7 @@ pub struct NetworkManagerModule { /// The configuraiton for the icons used to represent network devices. #[serde(default)] - icons: config::IconsConfig, + 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.). @@ -162,7 +162,7 @@ impl Default for NetworkManagerModule { Self { icon_size: default::IconSize::Small as i32, common: Some(CommonConfig::default()), - icons: config::IconsConfig::default(), + icons: config::Icons::default(), types_blacklist: Vec::new(), types_whitelist: Vec::new(), interface_blacklist: Vec::new(), diff --git a/src/modules/networkmanager/config.rs b/src/modules/networkmanager/config.rs index d8d332952..44d4d52db 100644 --- a/src/modules/networkmanager/config.rs +++ b/src/modules/networkmanager/config.rs @@ -12,15 +12,15 @@ macro_rules! default_function { #[derive(Debug, Deserialize, Clone, Default)] #[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] -pub struct IconsConfig { +pub struct Icons { #[serde(default)] - pub wired: IconsConfigWired, + pub wired: IconsWired, #[serde(default)] - pub wifi: IconsConfigWifi, + pub wifi: IconsWifi, #[serde(default)] - pub cellular: IconsConfigCellular, + pub cellular: IconsCellular, #[serde(default)] - pub vpn: IconsConfigVpn, + pub vpn: IconsVpn, #[serde(default = "default_unknown")] pub unknown: String, @@ -28,7 +28,7 @@ pub struct IconsConfig { #[derive(Debug, Deserialize, Clone)] #[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] -pub struct IconsConfigWired { +pub struct IconsWired { #[serde(default = "default_wired_connected")] pub connected: String, #[serde(default = "default_wired_acquiring")] @@ -36,7 +36,7 @@ pub struct IconsConfigWired { #[serde(default = "default_wired_disconnected")] pub disconnected: String, } -impl Default for IconsConfigWired { +impl Default for IconsWired { fn default() -> Self { Self { connected: default_wired_connected(), @@ -48,7 +48,7 @@ impl Default for IconsConfigWired { #[derive(Debug, Deserialize, Clone)] #[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] -pub struct IconsConfigWifi { +pub struct IconsWifi { #[serde(default = "default_wifi_levels")] pub levels: Vec, #[serde(default = "default_wifi_acquiring")] @@ -57,7 +57,7 @@ pub struct IconsConfigWifi { pub disconnected: String, } -impl Default for IconsConfigWifi { +impl Default for IconsWifi { fn default() -> Self { Self { levels: default_wifi_levels(), @@ -69,7 +69,7 @@ impl Default for IconsConfigWifi { #[derive(Debug, Deserialize, Clone)] #[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] -pub struct IconsConfigCellular { +pub struct IconsCellular { #[serde(default = "default_cellular_connected")] pub connected: String, #[serde(default = "default_cellular_acquiring")] @@ -77,7 +77,7 @@ pub struct IconsConfigCellular { #[serde(default = "default_cellular_disconnected")] pub disconnected: String, } -impl Default for IconsConfigCellular { +impl Default for IconsCellular { fn default() -> Self { Self { connected: default_cellular_connected(), @@ -89,7 +89,7 @@ impl Default for IconsConfigCellular { #[derive(Debug, Deserialize, Clone)] #[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] -pub struct IconsConfigVpn { +pub struct IconsVpn { #[serde(default = "default_vpn_connected")] pub connected: String, #[serde(default = "default_vpn_acquiring")] @@ -97,7 +97,7 @@ pub struct IconsConfigVpn { #[serde(default = "default_vpn_disconnected")] pub disconnected: String, } -impl Default for IconsConfigVpn { +impl Default for IconsVpn { fn default() -> Self { Self { connected: default_vpn_connected(), From c5176173e9f90e52fcf066ea2212f11befb1f0d3 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 14:01:44 -0300 Subject: [PATCH 03/23] refactor: update DeviceType documentation --- src/clients/networkmanager/dbus.rs | 49 ++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/clients/networkmanager/dbus.rs b/src/clients/networkmanager/dbus.rs index 90a3896e5..e503582b0 100644 --- a/src/clients/networkmanager/dbus.rs +++ b/src/clients/networkmanager/dbus.rs @@ -123,44 +123,87 @@ pub trait Ip4ConfigDbus { 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, - Bluetooth = 5, + /// 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, - Lowpan = 28, + /// 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, } -// https://people.freedesktop.org/~lkundrak/nm-docs/nm-dbus-types.html#NMDeviceState +/// See: https://people.freedesktop.org/~lkundrak/nm-docs/nm-dbus-types.html#NMDeviceState #[derive(Clone, Debug, OwnedValue, PartialEq, Eq)] #[repr(u32)] pub enum DeviceState { From 152e9070d48c63f67fa55299d9c118fb01b1f969 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 14:14:14 -0300 Subject: [PATCH 04/23] refactor: replace mutually exclusive bools with enum --- src/clients/networkmanager/dbus.rs | 2 +- src/modules/networkmanager.rs | 81 ++++++++++++++++-------------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/src/clients/networkmanager/dbus.rs b/src/clients/networkmanager/dbus.rs index e503582b0..0839fc3b2 100644 --- a/src/clients/networkmanager/dbus.rs +++ b/src/clients/networkmanager/dbus.rs @@ -204,7 +204,7 @@ pub enum DeviceType { } /// See: https://people.freedesktop.org/~lkundrak/nm-docs/nm-dbus-types.html#NMDeviceState -#[derive(Clone, Debug, OwnedValue, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, OwnedValue, PartialEq, Eq)] #[repr(u32)] pub enum DeviceState { /// The device's state is unknown diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index b6d6470c0..0ee86e8cc 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -15,6 +15,33 @@ 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)] @@ -54,24 +81,7 @@ impl NetworkManagerModule { device: &crate::clients::networkmanager::state::Device, icon: &Picture, ) { - let mut disconnected = false; - let mut acquiring = false; - let mut connected = false; - match device.state { - DeviceState::Unknown - | DeviceState::Unmanaged - | DeviceState::Unavailable - | DeviceState::Deactivating - | DeviceState::Failed - | DeviceState::Disconnected => disconnected = true, - DeviceState::Prepare - | DeviceState::Config - | DeviceState::NeedAuth - | DeviceState::IpConfig - | DeviceState::IpCheck - | DeviceState::Secondaries => acquiring = true, - DeviceState::Activated => connected = true, - } + let state = State::from(device.state); if !self.types_whitelist.is_empty() && !self @@ -101,10 +111,10 @@ impl NetworkManagerModule { } let icon_name = match device.device_type { - DeviceType::Wifi => match () { - _ if acquiring => self.icons.wifi.acquiring.as_str(), - _ if disconnected => self.icons.wifi.disconnected.as_str(), - _ => match &device.device_type_data { + 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'); @@ -119,27 +129,24 @@ impl NetworkManagerModule { _ => self.icons.unknown.as_str(), }, }, - DeviceType::Modem | DeviceType::Wimax => match () { - _ if acquiring => self.icons.cellular.acquiring.as_ref(), - _ if disconnected => self.icons.cellular.disconnected.as_ref(), - _ if connected => self.icons.cellular.connected.as_ref(), - _ => self.icons.unknown.as_ref(), + 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 () { - _ if acquiring => self.icons.vpn.acquiring.as_ref(), - _ if disconnected => self.icons.vpn.disconnected.as_ref(), - _ if connected => self.icons.vpn.connected.as_ref(), - _ => self.icons.unknown.as_ref(), + | 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 () { - _ if acquiring => self.icons.wired.acquiring.as_ref(), - _ if disconnected => self.icons.wired.disconnected.as_ref(), - _ if connected => self.icons.wired.connected.as_ref(), - _ => self.icons.unknown.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(), }, }; From d1fb6245b3d8ac266da10d85b43578fe05109b77 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 14:14:58 -0300 Subject: [PATCH 05/23] refactor: move module_impl! to top of impl --- src/modules/networkmanager.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index 0ee86e8cc..b1a7ce440 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -182,6 +182,8 @@ impl Module for NetworkManagerModule { type SendMessage = NetworkManagerUpdate; type ReceiveMessage = (); + module_impl!("networkmanager"); + fn spawn_controller( &self, _: &ModuleInfo, @@ -269,8 +271,6 @@ impl Module for NetworkManagerModule { Ok(ModuleParts::new(container_clone, None)) } - - module_impl!("networkmanager"); } /// Convert strength level (from 0-100), to a level (from 0 to `number_of_levels-1`). From f07439b7d777a39116104887797b2ea101de42e5 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 15:51:15 -0300 Subject: [PATCH 06/23] docs(network-manager): update docs --- docs/modules/Network-Manager.md | 140 +++++++++++++++++++------------- docs/modules/Networkmanager.md | 91 --------------------- 2 files changed, 82 insertions(+), 149 deletions(-) delete mode 100644 docs/modules/Networkmanager.md diff --git a/docs/modules/Network-Manager.md b/docs/modules/Network-Manager.md index 689de6e5d..003d8d008 100644 --- a/docs/modules/Network-Manager.md +++ b/docs/modules/Network-Manager.md @@ -1,80 +1,104 @@ -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). +> Type: `networkmanager` + +| 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. | + +**Default `icons.wifi.levels`:** they contain 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": "networkmanager", + "icon_size": 24 + types_blacklist: ["loopback", "bridge"] + } + ] +} +``` +
- TOML +TOML + +```toml +[[end]] +type = "networkmanager" +icon_size = 24 +types_blacklist = ["loopback", "bridge"] +``` - ```toml - [[end]] - type = "network_manager" - icon_size = 32 - ```
- YAML +YAML + +```yaml +end: + - type: "networkmanager" + 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 = "networkmanager" + icon_size = 24 + types_blacklist = ["loopback", "bridge"] + } + ] +} +``` +
## Styling -| Selector | Description | -|--------------------------|----------------------------------| -| `.network_manager` | NetworkManager widget container. | -| `.network_manager .icon` | NetworkManager widget icon. | +| Selector | Description | +| ---------------------- | -------------------------------- | +| `.networkmanager` | NetworkManager widget container. | +| `.networkmanger .icon` | NetworkManager widget icons. | For more information on styling, please see the [styling guide](styling-guide). diff --git a/docs/modules/Networkmanager.md b/docs/modules/Networkmanager.md deleted file mode 100644 index 099c4e3de..000000000 --- a/docs/modules/Networkmanager.md +++ /dev/null @@ -1,91 +0,0 @@ -Displays the current network connection state of NetworkManager. -Supports wired ethernet, wifi, cellular data and VPN connections among others. - -> [!NOTE] -> This module 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. - -## Configuration - -> Type: `networkmanager` - -| Name | Type | Default | Description | -| ----------------------------- | ---------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `icon_size` | `integer` | `24` | Size to render icon at. | -| `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[]` | `["icon:network-wireless-signal-none-symbolic", ...]` | Icon for each strengh level of a connected wifi connection, from lowest to highest. The default contains 5 levels. | -| `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 | - -
-JSON - -```json -{ - "end": [ - { - "type": "networkmanager", - "icon_size": 32 - } - ] -} -``` - -
- -
-TOML - -```toml -[[end]] -type = "networkmanager" -icon_size = 32 -``` - -
- -
-YAML - -```yaml -end: - - type: "networkmanager" - icon_size: 32 -``` - -
- -
-Corn - -```corn -{ - end = [ - { - type = "networkmanager" - icon_size = 32 - } - ] -} -``` - -
- -## Styling - -| Selector | Description | -| ---------------------- | -------------------------------- | -| `.networkmanager` | NetworkManager widget container. | -| `.networkmanger .icon` | NetworkManager widget icons. | - -For more information on styling, please see the [styling guide](styling-guide). From 0d476fc3e8c1319e82d8cbbe7ba0868237355aed Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 16:17:17 -0300 Subject: [PATCH 07/23] fix(networkmanager): fix panic when number of wifi levels is 0 or 1 --- src/modules/networkmanager.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index b1a7ce440..6e9f5222b 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -120,9 +120,15 @@ impl NetworkManagerModule { tooltip.push('\n'); tooltip.push_str(&String::from_utf8_lossy(&connection.ssid)); - let level = - strengh_to_level(connection.strength, self.icons.wifi.levels.len()); - self.icons.wifi.levels[level].as_str() + if self.icons.wifi.levels.is_empty() { + "" + } else { + let level = strengh_to_level( + connection.strength, + self.icons.wifi.levels.len(), + ); + self.icons.wifi.levels[level].as_str() + } } None => self.icons.wifi.disconnected.as_str(), }, @@ -291,7 +297,14 @@ const fn strengh_to_level(strength: u8, number_of_levels: usize) -> usize { if strength < 5 { return 0; } - (strength as usize - 5) * (number_of_levels - 1) / 100 + 1 + + let i = (strength as usize - 5) * (number_of_levels - 1) / 100 + 1; + + if i >= number_of_levels { + number_of_levels - 1 + } else { + i + } } // Just to make sure my implementation still follow the original logic From b376f3fbd4fc4b9e3cbddd745abc9db0fa10696c Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 16:18:10 -0300 Subject: [PATCH 08/23] refactor: simplify use of `#[serder(default)]` --- src/modules/networkmanager/config.rs | 105 +++++++++------------------ 1 file changed, 35 insertions(+), 70 deletions(-) diff --git a/src/modules/networkmanager/config.rs b/src/modules/networkmanager/config.rs index 44d4d52db..b126941f0 100644 --- a/src/modules/networkmanager/config.rs +++ b/src/modules/networkmanager/config.rs @@ -1,137 +1,102 @@ use serde::Deserialize; -macro_rules! default_function { - ($(($name:ident, $default:expr),)*) => { - $( - fn $name() -> String { - ($default).to_string() - } - )* - }; -} - -#[derive(Debug, Deserialize, Clone, Default)] +#[derive(Debug, Deserialize, Clone)] +#[serde(default)] #[cfg_attr(feature = "extras", derive(schemars::JsonSchema))] pub struct Icons { - #[serde(default)] pub wired: IconsWired, - #[serde(default)] pub wifi: IconsWifi, - #[serde(default)] pub cellular: IconsCellular, - #[serde(default)] pub vpn: IconsVpn, - - #[serde(default = "default_unknown")] 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 { - #[serde(default = "default_wired_connected")] pub connected: String, - #[serde(default = "default_wired_acquiring")] pub acquiring: String, - #[serde(default = "default_wired_disconnected")] pub disconnected: String, } impl Default for IconsWired { fn default() -> Self { Self { - connected: default_wired_connected(), - acquiring: default_wired_acquiring(), - disconnected: default_wired_disconnected(), + 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 { - #[serde(default = "default_wifi_levels")] pub levels: Vec, - #[serde(default = "default_wifi_acquiring")] pub acquiring: String, - #[serde(default = "default_wifi_disconnected")] pub disconnected: String, } impl Default for IconsWifi { fn default() -> Self { Self { - levels: default_wifi_levels(), - acquiring: default_wifi_acquiring(), - disconnected: default_wifi_disconnected(), + 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 { - #[serde(default = "default_cellular_connected")] pub connected: String, - #[serde(default = "default_cellular_acquiring")] pub acquiring: String, - #[serde(default = "default_cellular_disconnected")] pub disconnected: String, } impl Default for IconsCellular { fn default() -> Self { Self { - connected: default_cellular_connected(), - acquiring: default_cellular_acquiring(), - disconnected: default_cellular_disconnected(), + 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 { - #[serde(default = "default_vpn_connected")] pub connected: String, - #[serde(default = "default_vpn_acquiring")] pub acquiring: String, - #[serde(default = "default_vpn_disconnected")] pub disconnected: String, } impl Default for IconsVpn { fn default() -> Self { Self { - connected: default_vpn_connected(), - acquiring: default_vpn_acquiring(), - disconnected: default_vpn_disconnected(), + connected: "icon:network-vpn-symbolic".to_string(), + acquiring: "icon:network-vpn-acquiring-symbolic".to_string(), + disconnected: "".to_string(), } } } - -pub fn default_wifi_levels() -> Vec { - 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(), - ] -} - -default_function! { - (default_wired_connected, "icon:network-wired-symbolic"), - (default_wired_acquiring, "icon:network-wired-acquiring-symbolic"), - (default_wired_disconnected, ""), - - (default_wifi_acquiring, "icon:network-wireless-acquiring-symbolic"), - (default_wifi_disconnected, ""), - - (default_cellular_connected,"icon:network-cellular-connected-symbolic"), - (default_cellular_acquiring,"icon:network-cellular-acquiring-symbolic"), - (default_cellular_disconnected,""), - - (default_vpn_connected, "icon:network-vpn-symbolic"), - (default_vpn_acquiring, "icon:network-vpn-acquiring-symbolic"), - (default_vpn_disconnected, ""), - - (default_unknown, "icon:dialog-question-symbolic"), -} From d632b8179980598a230bb15e025c1b21794c3b69 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 16:24:30 -0300 Subject: [PATCH 09/23] docs: fix typo --- src/clients/networkmanager/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/networkmanager/mod.rs b/src/clients/networkmanager/mod.rs index 4c7a8046c..abd240c70 100644 --- a/src/clients/networkmanager/mod.rs +++ b/src/clients/networkmanager/mod.rs @@ -24,7 +24,7 @@ pub enum NetworkManagerUpdate { Devices(Vec), /// Update a single device. /// - /// The `usize` is the index of the device in the list of devices, receive in the previous + /// The `usize` is the index of the device in the list of devices received in the previous /// `Devices` update. Device(usize, state::Device), } From 5822a86392b97ba01ce3dc8590198f051a4ca576 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 16:27:21 -0300 Subject: [PATCH 10/23] style: move comments of unmapped dbus properties So they don't merge into doc-comments, making them harder to read. --- src/clients/networkmanager/state.rs | 58 +++++++++++++++-------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/clients/networkmanager/state.rs b/src/clients/networkmanager/state.rs index affbddeda..af96dc736 100644 --- a/src/clients/networkmanager/state.rs +++ b/src/clients/networkmanager/state.rs @@ -5,26 +5,34 @@ use super::dbus::{DeviceState, DeviceType}; #[derive(Clone, Debug)] pub struct Device { pub path: ObjectPath<'static>, - // Udi readable s /// Interface readable s pub interface: String, - // IpInterface readable s - // Driver readable s - // DriverVersion readable s - // FirmwareVersion readable s - // Capabilities readable u - // Ip4Address readable u + /// State readable u /// /// The current state of the device. pub state: DeviceState, - // StateReason readable (uu) - // ActiveConnection readable o + /// 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, + pub device_type_data: DeviceTypeData, + // 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 @@ -32,11 +40,6 @@ pub struct Device { // Autoconnect readwrite b // FirmwareMissing readable b // NmPluginMissing readable b - /// DeviceType readable u - /// - /// The general type of the network device; ie Ethernet, WiFi, etc. - pub device_type: DeviceType, - pub device_type_data: DeviceTypeData, // AvailableConnections readable ao // PhysicalPortId readable s // Mtu readable u @@ -48,12 +51,12 @@ pub struct Device { #[derive(Clone, Debug)] pub struct Ip4Config { pub path: ObjectPath<'static>, - // Addresses readable aau /// 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, + // Addresses readable aau // Gateway readable s // Routes readable aau // RouteData readable aa{sv} @@ -67,9 +70,7 @@ pub struct Ip4Config { #[derive(Clone, Debug)] pub struct AddressData { - // address s pub address: String, - // prefix u pub prefix: u32, } @@ -83,35 +84,36 @@ pub enum DeviceTypeData { #[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, // HwAddress readable s // PermHwAddress readable s // Mode readable u // Bitrate readable u // AccessPoints readable ao - /// ActiveAccessPoint readable o - /// - /// Object path of the access point currently used by the wireless device. - pub active_access_point: Option, // WirelessCapabilities readable u } #[derive(Clone, Debug)] pub struct AccessPoint { pub path: ObjectPath<'static>, - // Flags readable u - // WpaFlags readable u - // RsnFlags readable u /// Ssid readable ay /// /// The Service Set Identifier identifying the access point. pub ssid: Vec, - // Frequency readable u - // HwAddress readable s - // Mode readable u - // MaxBitrate readable u + /// Strength readable y /// /// The current signal quality of the access point, in percent. pub strength: u8, + // Frequency readable u + // HwAddress readable s + // Mode readable u + // MaxBitrate readable u + // Flags readable u + // WpaFlags readable u + // RsnFlags readable u // LastSeen readable i } From 15d7f8248fd56128c78ec3b53bf2fd34c37778c9 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 16:33:33 -0300 Subject: [PATCH 11/23] chore: remove futures-signals --- Cargo.lock | 35 ----------------------------------- Cargo.toml | 5 +---- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a77cd715..48237b078 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 ea905d3fa..aab5b581f 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 } From 11bccfd7fc37a36b14f1fa549ea992f8a1e42089 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Tue, 11 Nov 2025 17:13:41 -0300 Subject: [PATCH 12/23] docs: add list of device types to documentation. --- docs/modules/Network-Manager.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/modules/Network-Manager.md b/docs/modules/Network-Manager.md index 003d8d008..c0943d0f7 100644 --- a/docs/modules/Network-Manager.md +++ b/docs/modules/Network-Manager.md @@ -27,6 +27,15 @@ disconnected). | `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`:** they contain the 5 GTK symbolic icons for wireless signal strength: - `"icon:network-wireless-signal-none-symbolic"` - `"icon:network-wireless-signal-weak-symbolic"` From f15f5238395bde043c7010f98c829a2a38f2b45a Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Thu, 13 Nov 2025 13:43:35 -0300 Subject: [PATCH 13/23] fix: fix typos in docs --- docs/modules/Network-Manager.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/Network-Manager.md b/docs/modules/Network-Manager.md index c0943d0f7..97e3ac296 100644 --- a/docs/modules/Network-Manager.md +++ b/docs/modules/Network-Manager.md @@ -36,7 +36,7 @@ terminal. The possible device types are: `unknown`, `ethernet`, `wifi`, `bt`, `dummy`, `ppp`, `ovs_interface`, `ovs_port`, `ovs_bridge`, `wpan`, `six_lowpan`, `wireguard`, `wifi_p2p`, `vrf`, `loopback`, `hsr` and `ipvlan`. -**Default `icons.wifi.levels`:** they contain the 5 GTK symbolic icons for wireless signal strength: +**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"` @@ -51,8 +51,8 @@ terminal. The possible device types are: `unknown`, `ethernet`, `wifi`, `bt`, "end": [ { "type": "networkmanager", - "icon_size": 24 - types_blacklist: ["loopback", "bridge"] + "icon_size": 24, + "types_blacklist": ["loopback", "bridge"] } ] } @@ -95,7 +95,7 @@ end: { type = "networkmanager" icon_size = 24 - types_blacklist = ["loopback", "bridge"] + types_blacklist = [ loopback bridge ] } ] } From 99c3c3f99f9aab87cc28467c67fa34d2cf145e75 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Thu, 13 Nov 2025 13:45:30 -0300 Subject: [PATCH 14/23] style: add space and header for unmapped properties comment --- src/clients/networkmanager/state.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/clients/networkmanager/state.rs b/src/clients/networkmanager/state.rs index af96dc736..3ab5bc891 100644 --- a/src/clients/networkmanager/state.rs +++ b/src/clients/networkmanager/state.rs @@ -23,7 +23,10 @@ pub struct Device { /// /// 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 @@ -56,6 +59,8 @@ pub struct Ip4Config { /// 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 @@ -88,6 +93,8 @@ pub struct DeviceWireless { /// /// 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 @@ -108,6 +115,8 @@ pub struct AccessPoint { /// /// The current signal quality of the access point, in percent. pub strength: u8, + // + // # Unmapped properties: // Frequency readable u // HwAddress readable s // Mode readable u From 657f2af2f9e4480c65f5eb4ded35cbe29dc774b5 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Thu, 13 Nov 2025 13:53:52 -0300 Subject: [PATCH 15/23] refactor: break up unwieldy expression --- src/modules/networkmanager.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index 6e9f5222b..4b3641771 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -81,20 +81,16 @@ impl NetworkManagerModule { device: &crate::clients::networkmanager::state::Device, icon: &Picture, ) { - let state = State::from(device.state); + 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 !self.types_whitelist.is_empty() - && !self - .types_whitelist - .iter() - .any(|t| t == &device.device_type) - || self.types_blacklist.contains(&device.device_type) - || !self.interface_whitelist.is_empty() - && !self - .interface_whitelist - .iter() - .any(|n| n == &device.interface) - || self.interface_blacklist.contains(&device.interface) + if !type_whitelisted || !interface_whitelisted || type_blacklisted || interface_blacklisted { icon.set_visible(false); return; @@ -110,6 +106,8 @@ impl NetworkManagerModule { } } + 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(), From 978f86f7e431cc92cb2c51650d24a5db697f47bf Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Thu, 13 Nov 2025 14:26:21 -0300 Subject: [PATCH 16/23] refactor: replace unwrap with expect --- src/modules/networkmanager.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index 4b3641771..2904e79c6 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -247,7 +247,7 @@ impl Module for NetworkManagerModule { this.update_icon( &image_provider, device, - widget.downcast_ref::().unwrap(), + widget.downcast_ref::().expect("should be Picture"), ) .await; } @@ -262,7 +262,7 @@ impl Module for NetworkManagerModule { this.update_icon( &image_provider, &device, - widget.downcast_ref::().unwrap(), + widget.downcast_ref::().expect("should be Picture"), ) .await; } else { From 5ddf6c4d7d5e3fbfd9b165eacd38a2445e682a22 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Thu, 13 Nov 2025 14:27:54 -0300 Subject: [PATCH 17/23] fix: fix todo in strenght_to_level --- src/modules/networkmanager.rs | 66 ++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index 2904e79c6..b57e9b82d 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -121,7 +121,7 @@ impl NetworkManagerModule { if self.icons.wifi.levels.is_empty() { "" } else { - let level = strengh_to_level( + let level = strength_to_level( connection.strength, self.icons.wifi.levels.len(), ); @@ -278,7 +278,7 @@ impl Module for NetworkManagerModule { } /// Convert strength level (from 0-100), to a level (from 0 to `number_of_levels-1`). -const fn strengh_to_level(strength: u8, number_of_levels: usize) -> usize { +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, @@ -288,36 +288,54 @@ const fn strengh_to_level(strength: u8, number_of_levels: usize) -> usize { // 80.. => 4, // } - // to make it work with a custom number of levels, we approach the logic above with the logic - // below (0 for < 5, and a linear interpolation for 5 to 105). - // TODO: if there are more than 20 levels, the last level will be out of scale, and never be - // reach. - if strength < 5 { + // 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 i = (strength as usize - 5) * (number_of_levels - 1) / 100 + 1; + let strength = strength.clamp(0, 100); - if i >= number_of_levels { - number_of_levels - 1 + 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 { - i - } + // 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 my implementation still follow the original logic +// Just to make sure the implementation still follow the reference logic #[cfg(test)] #[test] fn test_strength_to_level() { - assert_eq!(strengh_to_level(0, 5), 0); - assert_eq!(strengh_to_level(4, 5), 0); - assert_eq!(strengh_to_level(5, 5), 1); - assert_eq!(strengh_to_level(6, 5), 1); - assert_eq!(strengh_to_level(29, 5), 1); - assert_eq!(strengh_to_level(30, 5), 2); - assert_eq!(strengh_to_level(54, 5), 2); - assert_eq!(strengh_to_level(55, 5), 3); - assert_eq!(strengh_to_level(79, 5), 3); - assert_eq!(strengh_to_level(80, 5), 4); - assert_eq!(strengh_to_level(100, 5), 4); + 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); } From 849d620197ece0cfa979c70dad63ca00fe92dac1 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Wed, 19 Nov 2025 14:03:25 -0300 Subject: [PATCH 18/23] fix: keep `module_impl!("network_manager");` --- docs/modules/Network-Manager.md | 8 ++++---- src/modules/networkmanager.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/modules/Network-Manager.md b/docs/modules/Network-Manager.md index 97e3ac296..aaa53d436 100644 --- a/docs/modules/Network-Manager.md +++ b/docs/modules/Network-Manager.md @@ -50,7 +50,7 @@ terminal. The possible device types are: `unknown`, `ethernet`, `wifi`, `bt`, { "end": [ { - "type": "networkmanager", + "type": "network_manager", "icon_size": 24, "types_blacklist": ["loopback", "bridge"] } @@ -65,7 +65,7 @@ terminal. The possible device types are: `unknown`, `ethernet`, `wifi`, `bt`, ```toml [[end]] -type = "networkmanager" +type = "network_manager" icon_size = 24 types_blacklist = ["loopback", "bridge"] ``` @@ -77,7 +77,7 @@ types_blacklist = ["loopback", "bridge"] ```yaml end: - - type: "networkmanager" + - type: "network_manager" icon_size: 24 types_blacklist: - loopback @@ -93,7 +93,7 @@ end: { end = [ { - type = "networkmanager" + type = "network_manager" icon_size = 24 types_blacklist = [ loopback bridge ] } diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index b57e9b82d..9526f265d 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -186,7 +186,7 @@ impl Module for NetworkManagerModule { type SendMessage = NetworkManagerUpdate; type ReceiveMessage = (); - module_impl!("networkmanager"); + module_impl!("network_manager"); fn spawn_controller( &self, From b45ddb5c653fbf1bd9d35b554c3c327fb8c47df8 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Wed, 19 Nov 2025 14:03:42 -0300 Subject: [PATCH 19/23] refactor: alias tokio::sync::RwLock to AsyncRwLock --- src/clients/networkmanager/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/clients/networkmanager/mod.rs b/src/clients/networkmanager/mod.rs index abd240c70..fc67cc9fb 100644 --- a/src/clients/networkmanager/mod.rs +++ b/src/clients/networkmanager/mod.rs @@ -1,7 +1,7 @@ use color_eyre::eyre::Report; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::{RwLock, broadcast}; +use tokio::sync::{RwLock as AsyncRwLock, broadcast}; use color_eyre::Result; use zbus::Connection; @@ -51,9 +51,9 @@ 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>>, + device_map: Arc, ClientDevice>>>, + ip4config_map: Arc>>, + access_point_map: Arc>>, } impl Client { async fn new() -> Result { @@ -72,9 +72,9 @@ impl Client { root_object, dbus_connection, tx, - device_map: Arc::new(RwLock::new(HashMap::new())), - ip4config_map: Arc::new(RwLock::new(HashMap::new())), - access_point_map: Arc::new(RwLock::new(HashMap::new())), + 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())), }) } From c6ed09a42473bfb03661e2b062b9f0416a0294ba Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Wed, 19 Nov 2025 15:19:10 -0300 Subject: [PATCH 20/23] fix: fix example in docs --- docs/modules/Network-Manager.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/Network-Manager.md b/docs/modules/Network-Manager.md index aaa53d436..f872f0575 100644 --- a/docs/modules/Network-Manager.md +++ b/docs/modules/Network-Manager.md @@ -95,7 +95,7 @@ end: { type = "network_manager" icon_size = 24 - types_blacklist = [ loopback bridge ] + types_blacklist = [ "loopback" "bridge" ] } ] } From 842aa82d8512757fcec68ca4faba4bbe9c356524 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Thu, 20 Nov 2025 13:10:34 -0300 Subject: [PATCH 21/23] style: don't prefix used variable with underscore --- src/clients/networkmanager/mod.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/clients/networkmanager/mod.rs b/src/clients/networkmanager/mod.rs index fc67cc9fb..c28982ffa 100644 --- a/src/clients/networkmanager/mod.rs +++ b/src/clients/networkmanager/mod.rs @@ -33,17 +33,17 @@ pub enum NetworkManagerUpdate { struct ClientDevice { state: state::Device, index: usize, - _state_handle: tokio::task::JoinHandle<()>, + state_handle: tokio::task::JoinHandle<()>, } #[derive(Debug)] struct ClientIp4Config { - _state_handle: tokio::task::JoinHandle<()>, + state_handle: tokio::task::JoinHandle<()>, } #[derive(Debug)] struct ClientAccessPoint { - _state_handle: tokio::task::JoinHandle<()>, + state_handle: tokio::task::JoinHandle<()>, } #[derive(Clone, Debug)] @@ -218,7 +218,7 @@ impl Client { ClientDevice { state: device.clone(), index, - _state_handle: client_device._state_handle, + state_handle: client_device.state_handle, }, ); } @@ -228,7 +228,7 @@ impl Client { let v = ClientDevice { state: device.clone(), index, - _state_handle: spawn(async move { + state_handle: spawn(async move { this.watch_device_change(path2).await }), }; @@ -254,7 +254,7 @@ impl Client { let device_path = path.to_owned(); let path2 = ip4config.path.to_owned(); let v = ClientIp4Config { - _state_handle: spawn(async move { + state_handle: spawn(async move { this.watch_ip4config_change(device_path, path2).await }), }; @@ -294,7 +294,7 @@ impl Client { } }; let v = ClientAccessPoint { - _state_handle: spawn(async move { + state_handle: spawn(async move { this.watch_access_point_change( device_path, access_point_object, @@ -310,15 +310,15 @@ impl Client { } for device in device_map.values() { - device._state_handle.abort(); + device.state_handle.abort(); } for ipconfig in ip4config_map.values() { - ipconfig._state_handle.abort(); + ipconfig.state_handle.abort(); } for ap in access_point_map.values() { - ap._state_handle.abort(); + ap.state_handle.abort(); } *device_map = new_device_map; From bcd32ca1586c30580d2975d8f03011f5ac97cd6e Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Thu, 20 Nov 2025 13:10:50 -0300 Subject: [PATCH 22/23] fix: fix typo in log --- src/clients/networkmanager/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/networkmanager/mod.rs b/src/clients/networkmanager/mod.rs index c28982ffa..81a00f96a 100644 --- a/src/clients/networkmanager/mod.rs +++ b/src/clients/networkmanager/mod.rs @@ -160,7 +160,7 @@ impl Client { match self.fetch_access_point(&active_access_point_path).await { Ok(x) => Some(x), Err(e) => { - tracing::error!("fail fetch access point: {e}"); + tracing::error!("failed to fetch access point: {e}"); None } } From 9bb55cf8b62107f23ef6bf59d2651d32d0f19f3d Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Thu, 20 Nov 2025 13:11:51 -0300 Subject: [PATCH 23/23] doc: replace `networkmanager` with `network_manager` --- docs/modules/Network-Manager.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/modules/Network-Manager.md b/docs/modules/Network-Manager.md index f872f0575..e88249693 100644 --- a/docs/modules/Network-Manager.md +++ b/docs/modules/Network-Manager.md @@ -4,7 +4,7 @@ disconnected). ## Configuration -> Type: `networkmanager` +> Type: `network_manager` | Name | Type | Default | Description | | ----------------------------- | ---------- | ------------------------------------------ | ----------------------------------------------------------------------------------- | @@ -37,6 +37,7 @@ terminal. The possible device types are: `unknown`, `ethernet`, `wifi`, `bt`, `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"` @@ -105,9 +106,9 @@ end: ## Styling -| Selector | Description | -| ---------------------- | -------------------------------- | -| `.networkmanager` | NetworkManager widget container. | -| `.networkmanger .icon` | NetworkManager widget icons. | +| Selector | Description | +| ------------------------ | -------------------------------- | +| `.network_manager` | NetworkManager widget container. | +| `.network_manager .icon` | NetworkManager widget icons. | For more information on styling, please see the [styling guide](styling-guide).