From c6cd78ec9cf2a3a1522bfd724c66d81db5e1185a Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 20 Sep 2024 16:20:00 +0200 Subject: [PATCH] feat(networking): display list of devices on page --- cosmic-settings/src/app.rs | 6 + cosmic-settings/src/pages/mod.rs | 1 + cosmic-settings/src/pages/networking/mod.rs | 300 +++++++++++++++++- .../src/pages/networking/vpn/mod.rs | 6 +- cosmic-settings/src/pages/networking/wifi.rs | 21 +- cosmic-settings/src/pages/networking/wired.rs | 72 +---- cosmic-settings/src/utils.rs | 2 +- cosmic-settings/src/widget/mod.rs | 27 +- i18n/en/cosmic_settings.ftl | 12 +- 9 files changed, 356 insertions(+), 91 deletions(-) diff --git a/cosmic-settings/src/app.rs b/cosmic-settings/src/app.rs index 84c691c..b7b995d 100644 --- a/cosmic-settings/src/app.rs +++ b/cosmic-settings/src/app.rs @@ -447,6 +447,12 @@ impl cosmic::Application for SettingsApp { return self.activate_page(page); } + crate::pages::Message::Networking(message) => { + if let Some(page) = self.pages.page_mut::() { + return page.update(message).map(Into::into); + } + } + crate::pages::Message::Panel(message) => { if let Some(page) = self.pages.page_mut::() { return page.update(message).map(Into::into); diff --git a/cosmic-settings/src/pages/mod.rs b/cosmic-settings/src/pages/mod.rs index 600e756..0caa4d2 100644 --- a/cosmic-settings/src/pages/mod.rs +++ b/cosmic-settings/src/pages/mod.rs @@ -31,6 +31,7 @@ pub enum Message { ManageWindowShortcuts(input::keyboard::shortcuts::ShortcutMessage), MoveWindowShortcuts(input::keyboard::shortcuts::ShortcutMessage), NavShortcuts(input::keyboard::shortcuts::ShortcutMessage), + Networking(networking::Message), Page(Entity), Panel(desktop::panel::Message), PanelApplet(desktop::panel::applets_inner::Message), diff --git a/cosmic-settings/src/pages/networking/mod.rs b/cosmic-settings/src/pages/networking/mod.rs index ee1af44..49aa579 100644 --- a/cosmic-settings/src/pages/networking/mod.rs +++ b/cosmic-settings/src/pages/networking/mod.rs @@ -5,14 +5,67 @@ pub mod vpn; pub mod wifi; pub mod wired; -use std::{ffi::OsStr, io, process::ExitStatus}; +use std::{ffi::OsStr, io, process::ExitStatus, sync::Arc}; -use cosmic_settings_page as page; +use anyhow::Context; +use cosmic::{widget, Apply, Command, Element}; +use cosmic_dbus_networkmanager::{ + interface::enums::{DeviceState, DeviceType}, + nm::NetworkManager, +}; +use cosmic_settings_page::{self as page, section, Section}; +use cosmic_settings_subscriptions::network_manager; +use futures::{SinkExt, StreamExt}; +use slotmap::SlotMap; static NM_CONNECTION_EDITOR: &str = "nm-connection-editor"; #[derive(Debug, Default)] -pub struct Page; +pub struct Page { + nm_task: Option>, + devices: Vec>, + vpn: page::Entity, + wifi: page::Entity, + wired: page::Entity, +} + +#[derive(Debug, Clone)] +pub enum Message { + /// An error occurred. + Error(String), + /// Successfully connected to the system dbus. + NetworkManagerConnect( + ( + zbus::Connection, + tokio::sync::mpsc::Sender, + ), + ), + /// Open the wifi settings page with the selected device. + OpenPage { + page: page::Entity, + device: Option, + }, + /// Update the devices lists + UpdateDevices(Vec>), +} + +#[derive(Debug, Clone)] +pub enum DeviceVariant { + Wired(Arc), + WiFi(Arc), +} + +impl From for crate::app::Message { + fn from(message: Message) -> Self { + crate::pages::Message::Networking(message).into() + } +} + +impl From for crate::pages::Message { + fn from(message: Message) -> Self { + crate::pages::Message::Networking(message) + } +} impl page::Page for Page { fn info(&self) -> cosmic_settings_page::Info { @@ -22,15 +75,248 @@ impl page::Page for Page { ) .title(fl!("network-and-wireless")) } + + fn content( + &self, + sections: &mut SlotMap>, + ) -> Option { + crate::slab!(descriptions { + vpn_txt = fl!("connections-and-profiles", variant = "vpn"); + }); + + let device_list = Section::default().descriptions(descriptions).view::( + move |_binder, page, section| { + let descs = §ion.descriptions; + + let wifi_devices = page + .devices + .iter() + .filter(|device| device.device_type == DeviceType::Wifi) + .map(|device| { + crate::widget::page_list_item( + fl!("wifi", "adapter", id = device.interface.as_str()), + match device.state { + DeviceState::Activated => fl!("network-device-state", "activated"), + DeviceState::Config => fl!("network-device-state", "config"), + DeviceState::Deactivating => { + fl!("network-device-state", "deactivating") + } + DeviceState::Disconnected => { + fl!("network-device-state", "disconnected") + } + DeviceState::Failed => fl!("network-device-state", "failed"), + DeviceState::IpCheck => fl!("network-device-state", "ip-check"), + DeviceState::IpConfig => fl!("network-device-state", "ip-config"), + DeviceState::NeedAuth => fl!("network-device-state", "need-auth"), + DeviceState::Prepare => fl!("network-device-state", "prepare"), + DeviceState::Secondaries => { + fl!("network-device-state", "secondaries") + } + DeviceState::Unavailable => { + fl!("network-device-state", "unavailable") + } + DeviceState::Unknown => fl!("network-device-state", "unknown"), + DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"), + }, + "preferences-wireless-symbolic", + Message::OpenPage { + page: page.wifi, + device: Some(DeviceVariant::WiFi(device.clone())), + }, + ) + }); + + let wired_devices = page + .devices + .iter() + .filter(|device| device.device_type == DeviceType::Ethernet) + .map(|device| { + crate::widget::page_list_item( + fl!("wired", "adapter", id = device.interface.as_str()), + match device.state { + DeviceState::Activated => fl!("network-device-state", "activated"), + DeviceState::Config => fl!("network-device-state", "config"), + DeviceState::Deactivating => { + fl!("network-device-state", "deactivating") + } + DeviceState::Disconnected => { + fl!("network-device-state", "disconnected") + } + DeviceState::Failed => fl!("network-device-state", "failed"), + DeviceState::IpCheck => fl!("network-device-state", "ip-check"), + DeviceState::IpConfig => fl!("network-device-state", "ip-config"), + DeviceState::NeedAuth => fl!("network-device-state", "need-auth"), + DeviceState::Prepare => fl!("network-device-state", "prepare"), + DeviceState::Secondaries => { + fl!("network-device-state", "secondaries") + } + DeviceState::Unavailable => { + fl!("network-device-state", "unplugged") + } + DeviceState::Unknown => fl!("network-device-state", "unknown"), + DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"), + }, + "preferences-wired-symbolic", + Message::OpenPage { + page: page.wired, + device: Some(DeviceVariant::Wired(device.clone())), + }, + ) + }); + + let device_list = wifi_devices + .chain(wired_devices) + .fold(widget::column(), |column, device| column.push(device)) + .push(crate::widget::page_list_item( + fl!("vpn"), + &descs[vpn_txt], + "preferences-vpn-symbolic", + Message::OpenPage { + page: page.vpn, + device: None, + }, + )) + .spacing(cosmic::theme::active().cosmic().spacing.space_s); + + Element::from(device_list).map(crate::pages::Message::Networking) + }, + ); + + Some(vec![sections.insert(device_list)]) + } + + fn on_enter( + &mut self, + _page: page::Entity, + sender: tokio::sync::mpsc::Sender, + ) -> cosmic::Command { + if self.nm_task.is_none() { + return cosmic::command::future(async move { + zbus::Connection::system() + .await + .context("failed to create system dbus connection") + .map_or_else( + |why| Message::Error(why.to_string()), + |conn| Message::NetworkManagerConnect((conn, sender.clone())), + ) + .apply(crate::pages::Message::Networking) + }); + } + + Command::none() + } + + fn on_leave(&mut self) -> Command { + self.devices = Vec::new(); + + if let Some(cancel) = self.nm_task.take() { + _ = cancel.send(()); + } + + Command::none() + } } impl page::AutoBind for Page { fn sub_pages( - page: cosmic_settings_page::Insert, + mut page: cosmic_settings_page::Insert, ) -> cosmic_settings_page::Insert { - page.sub_page::() - .sub_page::() - .sub_page::() + let vpn = page.sub_page_with_id::(); + let wifi = page.sub_page_with_id::(); + let wired = page.sub_page_with_id::(); + + let model = page.model.page_mut::().unwrap(); + model.vpn = vpn; + model.wifi = wifi; + model.wired = wired; + + page + } +} + +impl Page { + pub fn update(&mut self, message: Message) -> Command { + let span = tracing::span!(tracing::Level::INFO, "networking::update"); + let _span = span.enter(); + + match message { + Message::NetworkManagerConnect((conn, output)) => { + self.connect(conn.clone(), output); + } + + Message::Error(why) => { + tracing::error!(why); + } + + Message::OpenPage { page, device } => { + let mut commands = Vec::>::new(); + + commands.push(cosmic::command::message(crate::app::Message::Page(page))); + + if let Some(device) = device { + commands.push(cosmic::command::message(crate::app::Message::PageMessage( + match device { + DeviceVariant::WiFi(device) => { + crate::pages::Message::WiFi(wifi::Message::SelectDevice(device)) + } + DeviceVariant::Wired(device) => { + crate::pages::Message::Wired(wired::Message::SelectDevice(device)) + } + }, + ))); + } + + return cosmic::command::batch(commands); + } + + Message::UpdateDevices(devices) => { + self.devices = devices; + } + } + + Command::none() + } + + fn connect( + &mut self, + conn: zbus::Connection, + sender: tokio::sync::mpsc::Sender, + ) { + if self.nm_task.is_none() { + self.nm_task = Some(crate::utils::forward_event_loop( + sender, + |event| crate::pages::Message::Networking(event), + move |mut tx| async move { + let network_manager = match NetworkManager::new(&conn).await { + Ok(n) => n, + Err(why) => { + tracing::error!( + why = why.to_string(), + "failed to connect to network_manager" + ); + + return futures::future::pending().await; + } + }; + + let mut devices_changed = std::pin::pin!(network_manager + .receive_devices_changed() + .await + .then(|_| async { + match network_manager::devices::list(&conn, |_| true).await { + Ok(devices) => Message::UpdateDevices( + devices.into_iter().map(Arc::new).collect(), + ), + Err(why) => Message::Error(why.to_string()), + } + })); + + while let Some(message) = devices_changed.next().await { + _ = tx.send(message).await; + } + }, + )); + } } } diff --git a/cosmic-settings/src/pages/networking/vpn/mod.rs b/cosmic-settings/src/pages/networking/vpn/mod.rs index 6c173cb..000bb9c 100644 --- a/cosmic-settings/src/pages/networking/vpn/mod.rs +++ b/cosmic-settings/src/pages/networking/vpn/mod.rs @@ -21,7 +21,6 @@ use cosmic_settings_subscriptions::network_manager::{ use futures::{FutureExt, StreamExt}; use indexmap::IndexMap; use secure_string::SecureString; -use slab::Slab; pub type ConnectionId = Arc; pub type InterfaceId = String; @@ -285,6 +284,9 @@ impl page::Page for Page { impl Page { pub fn update(&mut self, message: Message) -> Command { + let span = tracing::span!(tracing::Level::INFO, "vpn::update"); + let _span = span.enter(); + match message { Message::NetworkManager(network_manager::Event::RequestResponse { req, @@ -490,7 +492,7 @@ impl Page { } Message::Error(why) => { - tracing::error!(why, "error in VPN settings page"); + tracing::error!(why); } Message::NetworkManagerConnect((conn, output)) => { diff --git a/cosmic-settings/src/pages/networking/wifi.rs b/cosmic-settings/src/pages/networking/wifi.rs index e19ef37..44ee08f 100644 --- a/cosmic-settings/src/pages/networking/wifi.rs +++ b/cosmic-settings/src/pages/networking/wifi.rs @@ -1,7 +1,10 @@ // Copyright 2024 System76 // SPDX-License-Identifier: GPL-3.0-only -use std::collections::{BTreeMap, BTreeSet}; +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::Arc, +}; use anyhow::Context; use cosmic::{ @@ -17,7 +20,6 @@ use cosmic_settings_subscriptions::network_manager::{ }; use futures::StreamExt; use secure_string::SecureString; -use slab::Slab; #[derive(Clone, Debug)] pub enum Message { @@ -52,6 +54,8 @@ pub enum Message { PasswordRequest(network_manager::SSID), /// Update the password from the dialog PasswordUpdate(SecureString), + /// Selects a device to display connections from + SelectDevice(Arc), /// Opens settings page for the access point. Settings(network_manager::SSID), /// Toggles visibility of the password input @@ -92,6 +96,8 @@ enum WiFiDialog { pub struct Page { nm_task: Option>, nm_state: Option, + /// When defined, displays connections for the specific device. + active_device: Option>, dialog: Option, view_more_popup: Option, connecting: BTreeSet, @@ -211,6 +217,7 @@ impl page::Page for Page { } fn on_leave(&mut self) -> Command { + self.active_device = None; self.view_more_popup = None; self.nm_state = None; self.ssid_to_uuid.clear(); @@ -228,6 +235,9 @@ impl page::Page for Page { impl Page { pub fn update(&mut self, message: Message) -> Command { + let span = tracing::span!(tracing::Level::INFO, "vpn::update"); + let _span = span.enter(); + match message { Message::NetworkManager(network_manager::Event::RequestResponse { req, @@ -421,7 +431,12 @@ impl Page { } Message::Error(why) => { - tracing::error!(why, "error in wifi settings page"); + tracing::error!(why); + } + + Message::SelectDevice(device) => { + // TODO: Per-device wifi connection handling. + self.active_device = Some(device); } Message::NetworkManagerConnect((conn, output)) => { diff --git a/cosmic-settings/src/pages/networking/wired.rs b/cosmic-settings/src/pages/networking/wired.rs index 3677f53..7ddb0b7 100644 --- a/cosmic-settings/src/pages/networking/wired.rs +++ b/cosmic-settings/src/pages/networking/wired.rs @@ -15,7 +15,6 @@ use cosmic_settings_page::{self as page, section, Section}; use cosmic_settings_subscriptions::network_manager::{ self, current_networks::ActiveConnectionInfo, devices::DeviceState, NetworkManagerState, }; -use slab::Slab; pub type ConnectionId = Arc; @@ -196,6 +195,9 @@ impl page::Page for Page { impl Page { pub fn update(&mut self, message: Message) -> Command { + let span = tracing::span!(tracing::Level::INFO, "vpn::update"); + let _span = span.enter(); + match message { Message::NetworkManager(network_manager::Event::RequestResponse { req, @@ -346,7 +348,7 @@ impl Page { } Message::Error(why) => { - tracing::error!(why, "error in wired settings page"); + tracing::error!(why); } Message::NetworkManagerConnect((conn, output)) => { @@ -539,66 +541,6 @@ impl Page { ) .into() } - - fn device_list_view<'a>( - &'a self, - _spacing: &cosmic::cosmic_theme::Spacing, - nm_state: &'a NmState, - devices_txt: &'a str, - ) -> Element<'a, Message> { - nm_state - .devices - .iter() - .fold( - widget::settings::view_section(devices_txt), - |section, device| { - let is_unplugged = matches!(device.state, DeviceState::Unavailable); - - let device_list = - cosmic::widget::settings::item::builder(device.interface.as_str()) - .description(match device.state { - DeviceState::Activated => fl!("network-device-state", "activated"), - DeviceState::Config => fl!("network-device-state", "config"), - DeviceState::Deactivating => { - fl!("network-device-state", "deactivating") - } - DeviceState::Disconnected => { - fl!("network-device-state", "disconnected") - } - DeviceState::Failed => fl!("network-device-state", "failed"), - DeviceState::IpCheck => fl!("network-device-state", "ip-check"), - DeviceState::IpConfig => fl!("network-device-state", "ip-config"), - DeviceState::NeedAuth => fl!("network-device-state", "need-auth"), - DeviceState::Prepare => fl!("network-device-state", "prepare"), - DeviceState::Secondaries => { - fl!("network-device-state", "secondaries") - } - DeviceState::Unavailable => { - fl!("network-device-state", "unplugged") - } - DeviceState::Unknown => fl!("network-device-state", "unknown"), - DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"), - }) - .icon(icon::from_name("network-wired-symbolic").size(32)) - .control(icon::from_name("go-next-symbolic").size(20)) - .spacing(16) - .apply(widget::container) - .padding([16, 14]) - .style(cosmic::theme::Container::List) - .apply(widget::button) - .padding(0) - .style(cosmic::theme::Button::Transparent) - .on_press_maybe(if is_unplugged { - None - } else { - Some(Message::SelectDevice(device.clone())) - }); - - section.add(device_list) - }, - ) - .into() - } } fn devices_view() -> Section { @@ -644,11 +586,7 @@ fn devices_view() -> Section { device, )), - None => view.push(page.device_list_view( - spacing, - nm_state, - §ion.descriptions[wired_devices_txt], - )), + None => view, }; view.spacing(spacing.space_l) diff --git a/cosmic-settings/src/utils.rs b/cosmic-settings/src/utils.rs index efe2984..7b542b1 100644 --- a/cosmic-settings/src/utils.rs +++ b/cosmic-settings/src/utils.rs @@ -51,7 +51,7 @@ pub fn forward_event_loop + Send + 'st #[macro_export] macro_rules! slab { ( $descriptions:ident { $( $txt_id:ident = $txt_expr:expr; )+ } ) => { - let mut $descriptions = Slab::new(); + let mut $descriptions = slab::Slab::new(); $( let $txt_id = $descriptions.insert($txt_expr); diff --git a/cosmic-settings/src/widget/mod.rs b/cosmic-settings/src/widget/mod.rs index b41b893..e6190f2 100644 --- a/cosmic-settings/src/widget/mod.rs +++ b/cosmic-settings/src/widget/mod.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; +use cosmic::cosmic_theme::Spacing; use cosmic::iced::{alignment, Length}; use cosmic::iced_core::text::Wrap; use cosmic::prelude::CollectionWidget; @@ -118,18 +119,32 @@ pub fn display_container<'a, Message: 'a>(widget: Element<'a, Message>) -> Eleme #[must_use] pub fn page_list_item<'a, Message: 'static + Clone>( - title: &'a str, - description: &'a str, + title: impl Into>, + description: impl Into>, icon: &'a str, message: Message, ) -> Element<'a, Message> { - cosmic::widget::settings::item::builder(title) - .description(description) + let Spacing { + space_s, space_m, .. + } = cosmic::theme::active().cosmic().spacing; + + let mut builder = cosmic::widget::settings::item::builder(title); + + let description = description.into(); + + if !description.is_empty() { + builder = builder.description(description); + } + + builder .icon(icon::from_name(icon).size(20)) .control(icon::from_name("go-next-symbolic").size(20)) - .spacing(16) + .spacing(space_s) + .height(space_s + space_m) + .align_items(alignment::Alignment::Center) .apply(container) - .padding([16, 14]) + .padding([space_s, space_m]) + .align_x(alignment::Horizontal::Center) .style(theme::Container::List) .apply(button) .padding(0) diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index b4c51b9..d0a93ac 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -40,15 +40,15 @@ forget-dialog = Forget this Wi-Fi network? .description = You'll need to enter a password again to use this Wi-Fi network in the future. network-device-state = - .activated = Connected to network - .config = Connecting to network - .deactivating = Disconnecting from network + .activated = Connected + .config = Connecting + .deactivating = Disconnecting .disconnected = Disconnected .failed = Failed to connect .ip-check = Checking connection - .ip-config = Requesting IP and routing information + .ip-config = Requesting IP and routing info .need-auth = Needs authentication - .prepare = Preparing to connect to network + .prepare = Preparing to connect .secondaries = Waiting for secondary connection .unavailable = Unavailable .unknown = Unknown state @@ -65,11 +65,13 @@ vpn = VPN .select-file = Select a VPN configuration file wired = Wired + .adapter = Wired adapter { $id } .connections = Wired Connections .devices = Wired Devices .remove = Remove connection profile wifi = Wi-Fi + .adapter = Wi-Fi adapter { $id } .forget = Forget this network ## Networking: Online Accounts