feat(networking): display list of devices on page

This commit is contained in:
Michael Aaron Murphy 2024-09-20 16:20:00 +02:00
parent 2c07dd8bef
commit c6cd78ec9c
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
9 changed files with 356 additions and 91 deletions

View file

@ -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::<networking::Page>() {
return page.update(message).map(Into::into);
}
}
crate::pages::Message::Panel(message) => {
if let Some(page) = self.pages.page_mut::<panel::Page>() {
return page.update(message).map(Into::into);

View file

@ -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),

View file

@ -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<tokio::sync::oneshot::Sender<()>>,
devices: Vec<Arc<network_manager::devices::DeviceInfo>>,
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<crate::pages::Message>,
),
),
/// Open the wifi settings page with the selected device.
OpenPage {
page: page::Entity,
device: Option<DeviceVariant>,
},
/// Update the devices lists
UpdateDevices(Vec<Arc<network_manager::devices::DeviceInfo>>),
}
#[derive(Debug, Clone)]
pub enum DeviceVariant {
Wired(Arc<network_manager::devices::DeviceInfo>),
WiFi(Arc<network_manager::devices::DeviceInfo>),
}
impl From<Message> for crate::app::Message {
fn from(message: Message) -> Self {
crate::pages::Message::Networking(message).into()
}
}
impl From<Message> for crate::pages::Message {
fn from(message: Message) -> Self {
crate::pages::Message::Networking(message)
}
}
impl page::Page<crate::pages::Message> for Page {
fn info(&self) -> cosmic_settings_page::Info {
@ -22,15 +75,248 @@ impl page::Page<crate::pages::Message> for Page {
)
.title(fl!("network-and-wireless"))
}
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
crate::slab!(descriptions {
vpn_txt = fl!("connections-and-profiles", variant = "vpn");
});
let device_list = Section::default().descriptions(descriptions).view::<Self>(
move |_binder, page, section| {
let descs = &section.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<crate::pages::Message>,
) -> cosmic::Command<crate::pages::Message> {
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<crate::pages::Message> {
self.devices = Vec::new();
if let Some(cancel) = self.nm_task.take() {
_ = cancel.send(());
}
Command::none()
}
}
impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(
page: cosmic_settings_page::Insert<crate::pages::Message>,
mut page: cosmic_settings_page::Insert<crate::pages::Message>,
) -> cosmic_settings_page::Insert<crate::pages::Message> {
page.sub_page::<wired::Page>()
.sub_page::<wifi::Page>()
.sub_page::<vpn::Page>()
let vpn = page.sub_page_with_id::<vpn::Page>();
let wifi = page.sub_page_with_id::<wifi::Page>();
let wired = page.sub_page_with_id::<wired::Page>();
let model = page.model.page_mut::<Self>().unwrap();
model.vpn = vpn;
model.wifi = wifi;
model.wired = wired;
page
}
}
impl Page {
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
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::<Command<crate::app::Message>>::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<crate::pages::Message>,
) {
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;
}
},
));
}
}
}

View file

@ -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<str>;
pub type InterfaceId = String;
@ -285,6 +284,9 @@ impl page::Page<crate::pages::Message> for Page {
impl Page {
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
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)) => {

View file

@ -1,7 +1,10 @@
// Copyright 2024 System76 <info@system76.com>
// 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<network_manager::devices::DeviceInfo>),
/// 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<tokio::sync::oneshot::Sender<()>>,
nm_state: Option<NmState>,
/// When defined, displays connections for the specific device.
active_device: Option<Arc<network_manager::devices::DeviceInfo>>,
dialog: Option<WiFiDialog>,
view_more_popup: Option<network_manager::SSID>,
connecting: BTreeSet<network_manager::SSID>,
@ -211,6 +217,7 @@ impl page::Page<crate::pages::Message> for Page {
}
fn on_leave(&mut self) -> Command<crate::pages::Message> {
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<crate::pages::Message> for Page {
impl Page {
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
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)) => {

View file

@ -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<str>;
@ -196,6 +195,9 @@ impl page::Page<crate::pages::Message> for Page {
impl Page {
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
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<crate::pages::Message> {
@ -644,11 +586,7 @@ fn devices_view() -> Section<crate::pages::Message> {
device,
)),
None => view.push(page.device_list_view(
spacing,
nm_state,
&section.descriptions[wired_devices_txt],
)),
None => view,
};
view.spacing(spacing.space_l)

View file

@ -51,7 +51,7 @@ pub fn forward_event_loop<M: 'static + Send, T: Future<Output = ()> + 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);

View file

@ -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<Cow<'a, str>>,
description: impl Into<Cow<'a, str>>,
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)

View file

@ -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