feat(networking): add VPN, WiFi, and Wired network pages
This commit is contained in:
parent
d035ba0cf7
commit
fa22b556dd
22 changed files with 2876 additions and 131 deletions
|
|
@ -11,7 +11,7 @@ use crate::pages::desktop::{
|
|||
},
|
||||
};
|
||||
use crate::pages::input::{self};
|
||||
use crate::pages::{self, display, power, sound, system, time};
|
||||
use crate::pages::{self, display, networking, power, sound, system, time};
|
||||
use crate::subscription::desktop_files;
|
||||
use crate::widget::{page_title, search_header};
|
||||
use crate::PageCommands;
|
||||
|
|
@ -77,10 +77,13 @@ impl SettingsApp {
|
|||
PageCommands::Time => self.pages.page_id::<time::Page>(),
|
||||
PageCommands::Touchpad => self.pages.page_id::<input::touchpad::Page>(),
|
||||
PageCommands::Users => self.pages.page_id::<system::users::Page>(),
|
||||
PageCommands::Vpn => self.pages.page_id::<networking::vpn::Page>(),
|
||||
PageCommands::Wallpaper => self.pages.page_id::<desktop::wallpaper::Page>(),
|
||||
PageCommands::WindowManagement => {
|
||||
self.pages.page_id::<desktop::window_management::Page>()
|
||||
}
|
||||
PageCommands::Wired => self.pages.page_id::<networking::wired::Page>(),
|
||||
PageCommands::Wireless => self.pages.page_id::<networking::wifi::Page>(),
|
||||
PageCommands::Workspaces => self.pages.page_id::<desktop::workspaces::Page>(),
|
||||
}
|
||||
}
|
||||
|
|
@ -141,6 +144,7 @@ impl cosmic::Application for SettingsApp {
|
|||
search_selections: Vec::default(),
|
||||
};
|
||||
|
||||
app.insert_page::<networking::Page>();
|
||||
let desktop_id = app.insert_page::<desktop::Page>().id();
|
||||
app.insert_page::<display::Page>();
|
||||
app.insert_page::<sound::Page>();
|
||||
|
|
@ -452,9 +456,27 @@ impl cosmic::Application for SettingsApp {
|
|||
page::update!(self.pages, message, power::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::Vpn(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<networking::vpn::Page>() {
|
||||
return page.update(message).map(Into::into);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::WiFi(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<networking::wifi::Page>() {
|
||||
return page.update(message).map(Into::into);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::WindowManagement(message) => {
|
||||
page::update!(self.pages, message, desktop::window_management::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::Wired(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<networking::wired::Page>() {
|
||||
return page.update(message).map(Into::into);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Message::OutputAdded(info, output) => {
|
||||
|
|
@ -730,7 +752,7 @@ impl SettingsApp {
|
|||
custom_header.map(Message::from)
|
||||
} else if let Some(parent) = page_info.parent {
|
||||
let page_header = crate::widget::sub_page_header(
|
||||
page_info.title.as_str(),
|
||||
page.title().unwrap_or_else(|| page_info.title.as_str()),
|
||||
self.pages.info[parent].title.as_str(),
|
||||
Message::Page(parent),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -78,10 +78,16 @@ pub enum PageCommands {
|
|||
Touchpad,
|
||||
/// Users settings page
|
||||
Users,
|
||||
/// VPN settings page
|
||||
Vpn,
|
||||
/// Wallpaper settings page
|
||||
Wallpaper,
|
||||
/// Window management settings page
|
||||
WindowManagement,
|
||||
/// Wired settings page
|
||||
Wired,
|
||||
/// WiFi settings page
|
||||
Wireless,
|
||||
/// Workspaces settings page
|
||||
Workspaces,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,18 +24,11 @@ use cosmic::{
|
|||
},
|
||||
};
|
||||
use cosmic::{
|
||||
iced::{wayland::actions::window::SctkWindowSettings, window, Color, Length},
|
||||
iced::{Color, Length},
|
||||
prelude::CollectionWidget,
|
||||
};
|
||||
use cosmic::{
|
||||
iced_core::Alignment,
|
||||
iced_sctk::commands::window::{close_window, get_window},
|
||||
widget::icon,
|
||||
};
|
||||
use cosmic::{
|
||||
iced_core::{alignment, layout},
|
||||
iced_runtime::core::image::Handle as ImageHandle,
|
||||
};
|
||||
use cosmic::{iced_core::alignment, iced_runtime::core::image::Handle as ImageHandle};
|
||||
use cosmic::{iced_core::Alignment, widget::icon};
|
||||
use cosmic::{
|
||||
widget::{color_picker::ColorPickerUpdate, ColorPickerModel},
|
||||
Element,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use cosmic_settings_page::Entity;
|
|||
pub mod desktop;
|
||||
pub mod display;
|
||||
pub mod input;
|
||||
pub mod networking;
|
||||
pub mod power;
|
||||
pub mod sound;
|
||||
pub mod system;
|
||||
|
|
@ -37,7 +38,10 @@ pub enum Message {
|
|||
Sound(sound::Message),
|
||||
SystemShortcuts(input::keyboard::shortcuts::ShortcutMessage),
|
||||
TilingShortcuts(input::keyboard::shortcuts::ShortcutMessage),
|
||||
Vpn(networking::vpn::Message),
|
||||
WiFi(networking::wifi::Message),
|
||||
WindowManagement(desktop::window_management::Message),
|
||||
Wired(networking::wired::Message),
|
||||
}
|
||||
|
||||
impl From<Message> for crate::Message {
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
pub fn info() -> page::Info {
|
||||
page::Info::new("online-accounts", "goa-panel-symbolic")
|
||||
.title(fl!("online-accounts"))
|
||||
.description(fl!("online-accounts", "desc"))
|
||||
}
|
||||
|
|
@ -1,5 +1,62 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod accounts;
|
||||
pub mod vpn;
|
||||
pub mod wifi;
|
||||
pub mod wired;
|
||||
|
||||
use std::{ffi::OsStr, io, process::ExitStatus};
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
static NM_CONNECTION_EDITOR: &str = "nm-connection-editor";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> cosmic_settings_page::Info {
|
||||
page::Info::new(
|
||||
"network-and-wireless",
|
||||
"preferences-network-and-wireless-symbolic",
|
||||
)
|
||||
.title(fl!("network-and-wireless"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(
|
||||
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>()
|
||||
}
|
||||
}
|
||||
|
||||
async fn nm_add_vpn_file<P: AsRef<OsStr>>(type_: &str, path: P) -> io::Result<ExitStatus> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(["connection", "import", "type", type_, "file"])
|
||||
.arg(path)
|
||||
.status()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn nm_add_wired() -> io::Result<ExitStatus> {
|
||||
nm_connection_editor(&["--type=802-3-ethernet", "-c"]).await
|
||||
}
|
||||
|
||||
async fn nm_add_wifi() -> io::Result<ExitStatus> {
|
||||
nm_connection_editor(&["--type=802-11-wireless", "-c"]).await
|
||||
}
|
||||
|
||||
async fn nm_edit_connection(uuid: &str) -> io::Result<ExitStatus> {
|
||||
nm_connection_editor(&[&["--edit=", uuid].concat()]).await
|
||||
}
|
||||
|
||||
async fn nm_connection_editor(args: &[&str]) -> io::Result<ExitStatus> {
|
||||
tokio::process::Command::new(NM_CONNECTION_EDITOR)
|
||||
.args(args)
|
||||
.status()
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
877
cosmic-settings/src/pages/networking/vpn/mod.rs
Normal file
877
cosmic-settings/src/pages/networking/vpn/mod.rs
Normal file
|
|
@ -0,0 +1,877 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
mod nmcli;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use ashpd::desktop::file_chooser::FileFilter;
|
||||
use cosmic::{
|
||||
iced::{alignment, Length},
|
||||
iced_core::text::Wrap,
|
||||
prelude::CollectionWidget,
|
||||
widget::{self, icon},
|
||||
Apply, Command, Element,
|
||||
};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use cosmic_settings_subscriptions::network_manager::{
|
||||
self, current_networks::ActiveConnectionInfo, NetworkManagerState, UUID,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use indexmap::IndexMap;
|
||||
use secure_string::SecureString;
|
||||
use slab::Slab;
|
||||
|
||||
pub type ConnectionId = Arc<str>;
|
||||
pub type InterfaceId = String;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
/// Activate a connection
|
||||
Activate(ConnectionId),
|
||||
/// Add a network connection
|
||||
AddNetwork,
|
||||
/// Cancels an active dialog.
|
||||
CancelDialog,
|
||||
/// Connect to a VPN with the given username and password
|
||||
ConnectWithPassword,
|
||||
/// Deactivate a connection.
|
||||
Deactivate(ConnectionId),
|
||||
/// An error occurred.
|
||||
Error(String),
|
||||
/// Update the list of known connections.
|
||||
KnownConnections(IndexMap<UUID, VpnConnectionSettings>),
|
||||
/// An update from the network manager daemon
|
||||
NetworkManager(network_manager::Event),
|
||||
/// Successfully connected to the system dbus.
|
||||
NetworkManagerConnect(
|
||||
(
|
||||
zbus::Connection,
|
||||
tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
),
|
||||
),
|
||||
/// Updates the password text input
|
||||
PasswordUpdate(SecureString),
|
||||
/// Refresh devices and their connection profiles
|
||||
Refresh,
|
||||
/// Create a dialog to ask for confirmation of removal.
|
||||
RemoveProfileRequest(ConnectionId),
|
||||
/// Remove a connection profile
|
||||
RemoveProfile(ConnectionId),
|
||||
/// Opens settings page for the access point.
|
||||
Settings(ConnectionId),
|
||||
/// Toggles visibility of password input.
|
||||
TogglePasswordVisibility,
|
||||
/// Update NetworkManagerState
|
||||
UpdateState(NetworkManagerState),
|
||||
/// Update the devices lists
|
||||
UpdateDevices(Vec<network_manager::devices::DeviceInfo>),
|
||||
/// Updates the username text input
|
||||
UsernameUpdate(String),
|
||||
/// Display more options for an access point
|
||||
ViewMore(Option<ConnectionId>),
|
||||
}
|
||||
|
||||
impl From<Message> for crate::app::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::Vpn(message).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for crate::pages::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::Vpn(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct VpnConnectionSettings {
|
||||
id: String,
|
||||
username: Option<String>,
|
||||
connection_type: Option<ConnectionType>,
|
||||
password_flag: Option<PasswordFlag>,
|
||||
}
|
||||
|
||||
impl VpnConnectionSettings {
|
||||
fn password_flag(&self) -> Option<PasswordFlag> {
|
||||
self.connection_type
|
||||
.as_ref()
|
||||
.map_or(false, |ct| match ct {
|
||||
ConnectionType::Password => true,
|
||||
})
|
||||
.then(|| self.password_flag)
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum ConnectionType {
|
||||
Password,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum PasswordFlag {
|
||||
/// The system is responsible for providing and storing this secret.
|
||||
None = 0,
|
||||
/// A user-session secret agent is responsible for providing and storing
|
||||
/// this secret; when it is required, agents will be asked to provide it.
|
||||
AgentOwned = 1,
|
||||
/// This secret should not be saved but should be requested from the user
|
||||
/// each time it is required. This flag should be used for One-Time-Pad
|
||||
/// secrets, PIN codes from hardware tokens, or if the user simply does not
|
||||
/// want to save the secret.
|
||||
NotSaved = 2,
|
||||
/// in some situations it cannot be automatically determined that a secret is required or not. This flag hints that the secret is not required and should not be requested from the user.
|
||||
NotRequired = 4,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum VpnDialog {
|
||||
Password {
|
||||
id: String,
|
||||
uuid: Arc<str>,
|
||||
username: String,
|
||||
password: SecureString,
|
||||
password_hidden: bool,
|
||||
},
|
||||
RemoveProfile(ConnectionId),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NmState {
|
||||
conn: zbus::Connection,
|
||||
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
|
||||
active_conns: Vec<ActiveConnectionInfo>,
|
||||
devices: Vec<network_manager::devices::DeviceInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Page {
|
||||
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
nm_state: Option<NmState>,
|
||||
dialog: Option<VpnDialog>,
|
||||
view_more_popup: Option<ConnectionId>,
|
||||
known_connections: IndexMap<UUID, VpnConnectionSettings>,
|
||||
/// Withhold device update if the view more popup is shown.
|
||||
withheld_devices: Option<Vec<network_manager::devices::DeviceInfo>>,
|
||||
/// Withhold active connections update if the view more popup is shown.
|
||||
withheld_active_conns: Option<Vec<ActiveConnectionInfo>>,
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> cosmic_settings_page::Info {
|
||||
page::Info::new("vpn", "preferences-vpn-symbolic")
|
||||
.title(fl!("vpn"))
|
||||
.description(fl!("connections-and-profiles", variant = "vpn"))
|
||||
}
|
||||
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(devices_view())])
|
||||
}
|
||||
|
||||
fn dialog(&self) -> Option<Element<crate::pages::Message>> {
|
||||
self.dialog.as_ref().map(|dialog| match dialog {
|
||||
VpnDialog::Password {
|
||||
username,
|
||||
password,
|
||||
password_hidden,
|
||||
..
|
||||
} => {
|
||||
let username = widget::text_input(fl!("username"), username.as_str())
|
||||
.on_input(Message::UsernameUpdate);
|
||||
|
||||
let password = widget::text_input::secure_input(
|
||||
fl!("password"),
|
||||
password.unsecure(),
|
||||
Some(Message::TogglePasswordVisibility),
|
||||
*password_hidden,
|
||||
)
|
||||
.on_input(|input| Message::PasswordUpdate(SecureString::from(input)))
|
||||
.on_submit(Message::ConnectWithPassword);
|
||||
|
||||
let controls = widget::column::with_capacity(2)
|
||||
.spacing(12)
|
||||
.push(username)
|
||||
.push(password)
|
||||
.apply(Element::from);
|
||||
|
||||
let primary_action = widget::button::suggested(fl!("connect"))
|
||||
.on_press(Message::ConnectWithPassword);
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
||||
widget::dialog(fl!("auth-dialog"))
|
||||
.icon(icon::from_name("network-vpn-symbolic").size(64))
|
||||
.body(fl!("auth-dialog", "vpn-description"))
|
||||
.control(controls)
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Vpn)
|
||||
}
|
||||
|
||||
VpnDialog::RemoveProfile(uuid) => {
|
||||
let primary_action = widget::button::destructive(fl!("remove"))
|
||||
.on_press(Message::RemoveProfile(uuid.clone()));
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
||||
widget::dialog(fl!("remove-connection-dialog"))
|
||||
.icon(icon::from_name("dialog-information").size(64))
|
||||
.body(fl!("remove-connection-dialog", "vpn-description"))
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Vpn)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn header_view(&self) -> Option<Element<'_, crate::pages::Message>> {
|
||||
Some(
|
||||
widget::button::standard(fl!("add-network"))
|
||||
.trailing_icon(icon::from_name("window-pop-out-symbolic"))
|
||||
.on_press(Message::AddNetwork)
|
||||
.apply(widget::container)
|
||||
.width(Length::Fill)
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Vpn),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_enter(
|
||||
&mut self,
|
||||
_page: cosmic_settings_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())),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> Command<crate::pages::Message> {
|
||||
self.view_more_popup = None;
|
||||
self.nm_state = None;
|
||||
self.withheld_active_conns = None;
|
||||
self.withheld_devices = None;
|
||||
self.dialog = None;
|
||||
|
||||
if let Some(cancel) = self.nm_task.take() {
|
||||
_ = cancel.send(());
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
|
||||
match message {
|
||||
Message::NetworkManager(network_manager::Event::RequestResponse {
|
||||
req,
|
||||
state,
|
||||
success,
|
||||
}) => {
|
||||
if !success {
|
||||
tracing::error!(request = ?req, "network-manager request failed");
|
||||
}
|
||||
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
let conn = conn.clone();
|
||||
self.update_active_conns(state);
|
||||
return cosmic::command::batch(vec![
|
||||
connection_settings(conn.clone()),
|
||||
update_devices(conn),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::KnownConnections(connections) => {
|
||||
self.known_connections = connections;
|
||||
}
|
||||
|
||||
Message::UpdateDevices(devices) => {
|
||||
self.update_devices(devices);
|
||||
}
|
||||
|
||||
Message::UpdateState(state) => {
|
||||
self.update_active_conns(state);
|
||||
}
|
||||
|
||||
Message::NetworkManager(
|
||||
network_manager::Event::ActiveConns | network_manager::Event::Devices,
|
||||
) => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::command::batch(vec![
|
||||
update_state(conn.clone()),
|
||||
update_devices(conn.clone()),
|
||||
connection_settings(conn.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::NetworkManager(network_manager::Event::Init {
|
||||
conn,
|
||||
sender,
|
||||
state,
|
||||
}) => {
|
||||
self.nm_state = Some(NmState {
|
||||
conn: conn.clone(),
|
||||
sender,
|
||||
devices: Vec::new(),
|
||||
active_conns: state
|
||||
.active_conns
|
||||
.into_iter()
|
||||
.filter(|info| matches!(info, ActiveConnectionInfo::Vpn { .. }))
|
||||
.collect(),
|
||||
});
|
||||
|
||||
return cosmic::command::batch(vec![
|
||||
connection_settings(conn.clone()),
|
||||
update_devices(conn),
|
||||
]);
|
||||
}
|
||||
|
||||
Message::NetworkManager(_event) => (),
|
||||
|
||||
Message::AddNetwork => return add_network(),
|
||||
|
||||
Message::Activate(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
if let Some(settings) = self.known_connections.get(&uuid) {
|
||||
match settings.password_flag() {
|
||||
Some(PasswordFlag::NotSaved | PasswordFlag::AgentOwned) => {
|
||||
self.view_more_popup = None;
|
||||
self.dialog = Some(VpnDialog::Password {
|
||||
id: settings.id.clone(),
|
||||
uuid: uuid.clone(),
|
||||
username: settings.username.clone().unwrap_or_default(),
|
||||
password: SecureString::from(""),
|
||||
password_hidden: true,
|
||||
});
|
||||
}
|
||||
|
||||
_ => {
|
||||
let connection_name = settings.id.clone();
|
||||
return cosmic::command::future(async move {
|
||||
if let Err(why) = nmcli::connect(&connection_name).await {
|
||||
return Message::Error(format!(
|
||||
"failed to connect to VPN: {why}"
|
||||
));
|
||||
}
|
||||
|
||||
Message::Refresh
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::Deactivate(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender.unbounded_send(network_manager::Request::Deactivate(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::RemoveProfileRequest(uuid) => {
|
||||
self.view_more_popup = None;
|
||||
self.dialog = Some(VpnDialog::RemoveProfile(uuid));
|
||||
}
|
||||
|
||||
Message::RemoveProfile(uuid) => {
|
||||
self.dialog = None;
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender.unbounded_send(network_manager::Request::Remove(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::ViewMore(uuid) => {
|
||||
self.view_more_popup = uuid;
|
||||
if self.view_more_popup.is_none() {
|
||||
self.close_popup_and_apply_updates();
|
||||
}
|
||||
}
|
||||
|
||||
Message::Settings(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
return cosmic::command::future(async move {
|
||||
super::nm_edit_connection(uuid.as_ref())
|
||||
.then(|res| async move {
|
||||
match res.context("failed to open connection editor") {
|
||||
Ok(_) => Message::Refresh,
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
Message::Refresh => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::command::batch(vec![
|
||||
update_state(conn.clone()),
|
||||
update_devices(conn.clone()),
|
||||
connection_settings(conn.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::PasswordUpdate(pass) => {
|
||||
if let Some(VpnDialog::Password {
|
||||
ref mut password, ..
|
||||
}) = self.dialog
|
||||
{
|
||||
*password = pass;
|
||||
}
|
||||
}
|
||||
|
||||
Message::ConnectWithPassword => {
|
||||
let Some(dialog) = self.dialog.take() else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
if let VpnDialog::Password {
|
||||
id,
|
||||
username,
|
||||
password,
|
||||
..
|
||||
} = dialog
|
||||
{
|
||||
return self
|
||||
.activate_with_password(id, username, password)
|
||||
.map(crate::app::Message::from);
|
||||
}
|
||||
}
|
||||
|
||||
Message::UsernameUpdate(user) => {
|
||||
if let Some(VpnDialog::Password {
|
||||
ref mut username, ..
|
||||
}) = self.dialog
|
||||
{
|
||||
*username = user;
|
||||
}
|
||||
}
|
||||
|
||||
Message::CancelDialog => {
|
||||
self.dialog = None;
|
||||
}
|
||||
|
||||
Message::TogglePasswordVisibility => {
|
||||
if let Some(VpnDialog::Password {
|
||||
ref mut password_hidden,
|
||||
..
|
||||
}) = self.dialog
|
||||
{
|
||||
*password_hidden = !*password_hidden;
|
||||
}
|
||||
}
|
||||
|
||||
Message::Error(why) => {
|
||||
tracing::error!(why, "error in VPN settings page");
|
||||
}
|
||||
|
||||
Message::NetworkManagerConnect((conn, output)) => {
|
||||
self.connect(conn.clone(), output);
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn activate_with_password(
|
||||
&mut self,
|
||||
connection_name: String,
|
||||
username: String,
|
||||
password: SecureString,
|
||||
) -> Command<Message> {
|
||||
cosmic::command::future(async move {
|
||||
if let Err(why) = nmcli::set_username(&connection_name, &username).await {
|
||||
return Message::Error(format!("failed to set VPN username: {why}"));
|
||||
}
|
||||
|
||||
if let Err(why) = nmcli::set_password_flags_none(&connection_name).await {
|
||||
return Message::Error(format!(
|
||||
"failed to call nmcli to set VPN password-flags parameter: {why}"
|
||||
));
|
||||
}
|
||||
|
||||
if let Err(why) = nmcli::set_password(&connection_name, password.unsecure()).await {
|
||||
return Message::Error(format!("failed to call nmcli to set VPN password: {why}"));
|
||||
}
|
||||
|
||||
if let Err(why) = nmcli::connect(&connection_name).await {
|
||||
return Message::Error(format!("failed to connect to VPN: {why}"));
|
||||
}
|
||||
|
||||
Message::Refresh
|
||||
})
|
||||
}
|
||||
|
||||
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::Vpn(Message::NetworkManager(event)),
|
||||
move |tx| async move {
|
||||
futures::join!(
|
||||
network_manager::watch(conn.clone(), tx.clone()),
|
||||
network_manager::active_conns::watch(conn.clone(), tx.clone()),
|
||||
network_manager::devices::watch(conn, true, tx)
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the view more popup and applies any withheld updates.
|
||||
fn close_popup_and_apply_updates(&mut self) {
|
||||
self.view_more_popup = None;
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if let Some(active_conns) = self.withheld_active_conns.take() {
|
||||
nm_state.active_conns = active_conns;
|
||||
}
|
||||
|
||||
if let Some(devices) = self.withheld_devices.take() {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_devices(&mut self, devices: Vec<network_manager::devices::DeviceInfo>) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_devices = Some(devices);
|
||||
} else {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_active_conns(&mut self, state: NetworkManagerState) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
let conns = state
|
||||
.active_conns
|
||||
.into_iter()
|
||||
.filter(|info| matches!(info, ActiveConnectionInfo::Vpn { .. }))
|
||||
.collect();
|
||||
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_active_conns = Some(conns);
|
||||
} else {
|
||||
nm_state.active_conns = conns;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn devices_view() -> Section<crate::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
vpn_conns_txt = fl!("vpn", "connections");
|
||||
remove_txt = fl!("vpn", "remove");
|
||||
connect_txt = fl!("connect");
|
||||
connected_txt = fl!("connected");
|
||||
settings_txt = fl!("settings");
|
||||
disconnect_txt = fl!("disconnect");
|
||||
});
|
||||
|
||||
Section::default()
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let Some(NmState {
|
||||
ref active_conns, ..
|
||||
}) = page.nm_state
|
||||
else {
|
||||
return cosmic::widget::column().into();
|
||||
};
|
||||
|
||||
let theme = cosmic::theme::active();
|
||||
let spacing = &theme.cosmic().spacing;
|
||||
|
||||
let mut view = widget::column::with_capacity(4);
|
||||
|
||||
let vpn_connections =
|
||||
widget::settings::view_section(§ion.descriptions[vpn_conns_txt]);
|
||||
|
||||
if page.known_connections.is_empty() {
|
||||
view = view.push(vpn_connections.add(widget::settings::item_row(vec![
|
||||
widget::text::body(fl!("no-vpn")).into(),
|
||||
])));
|
||||
} else {
|
||||
let known_networks = page.known_connections.iter().fold(
|
||||
vpn_connections,
|
||||
|networks, (uuid, connection)| {
|
||||
let is_connected = active_conns.iter().any(|conn| match conn {
|
||||
ActiveConnectionInfo::Vpn { name, .. } => {
|
||||
name.as_str() == connection.id.as_str()
|
||||
}
|
||||
|
||||
_ => false,
|
||||
});
|
||||
|
||||
let (connect_txt, connect_msg) = if is_connected {
|
||||
(§ion.descriptions[connected_txt], None)
|
||||
} else {
|
||||
(
|
||||
§ion.descriptions[connect_txt],
|
||||
Some(Message::Activate(uuid.clone())),
|
||||
)
|
||||
};
|
||||
|
||||
let identifier =
|
||||
widget::text::body(connection.id.as_str()).wrap(Wrap::Glyph);
|
||||
|
||||
let connect: Element<'_, Message> = if let Some(msg) = connect_msg {
|
||||
widget::button::text(connect_txt).on_press(msg).into()
|
||||
} else {
|
||||
widget::text::body(connect_txt)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.into()
|
||||
};
|
||||
|
||||
let view_more_button =
|
||||
widget::button::icon(widget::icon::from_name("view-more-symbolic"));
|
||||
|
||||
let view_more: Option<Element<_>> = if page
|
||||
.view_more_popup
|
||||
.as_deref()
|
||||
.map_or(false, |id| id == uuid.as_ref())
|
||||
{
|
||||
widget::popover(view_more_button.on_press(Message::ViewMore(None)))
|
||||
.position(widget::popover::Position::Bottom)
|
||||
.on_close(Message::ViewMore(None))
|
||||
.popup({
|
||||
widget::column()
|
||||
.push_maybe(is_connected.then(|| {
|
||||
popup_button(
|
||||
Message::Deactivate(uuid.clone()),
|
||||
§ion.descriptions[disconnect_txt],
|
||||
)
|
||||
}))
|
||||
.push(popup_button(
|
||||
Message::Settings(uuid.clone()),
|
||||
§ion.descriptions[settings_txt],
|
||||
))
|
||||
.push(popup_button(
|
||||
Message::RemoveProfileRequest(uuid.clone()),
|
||||
§ion.descriptions[remove_txt],
|
||||
))
|
||||
.width(Length::Fixed(200.0))
|
||||
.apply(widget::container)
|
||||
.style(cosmic::style::Container::Dialog)
|
||||
})
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
} else {
|
||||
view_more_button
|
||||
.on_press(Message::ViewMore(Some(uuid.clone())))
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
};
|
||||
|
||||
let controls = widget::row::with_capacity(2)
|
||||
.push(connect)
|
||||
.push_maybe(view_more)
|
||||
.align_items(alignment::Alignment::Center)
|
||||
.spacing(spacing.space_xxs);
|
||||
|
||||
let widget = widget::settings::item_row(vec![
|
||||
identifier.into(),
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
controls.into(),
|
||||
]);
|
||||
|
||||
networks.add(widget)
|
||||
},
|
||||
);
|
||||
|
||||
view = view.push(known_networks);
|
||||
}
|
||||
|
||||
view.spacing(spacing.space_l)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Vpn)
|
||||
})
|
||||
}
|
||||
|
||||
fn popup_button<'a>(message: Message, text: &'a str) -> Element<'a, Message> {
|
||||
widget::text::body(text)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.apply(widget::button)
|
||||
.padding([4, 16])
|
||||
.width(Length::Fill)
|
||||
.style(cosmic::theme::Button::MenuItem)
|
||||
.on_press(message)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn update_state(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
match NetworkManagerState::new(&conn).await {
|
||||
Ok(state) => Message::UpdateState(state),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn update_devices(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
let filter =
|
||||
|device_type| matches!(device_type, network_manager::devices::DeviceType::WireGuard);
|
||||
|
||||
match network_manager::devices::list(&conn, filter).await {
|
||||
Ok(devices) => Message::UpdateDevices(devices),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn add_network() -> Command<crate::app::Message> {
|
||||
let Some(dir) = dirs::download_dir().or_else(dirs::home_dir) else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
cosmic::dialog::file_chooser::open::Dialog::new()
|
||||
.directory(dir)
|
||||
.title(fl!("vpn", "select-file"))
|
||||
.filter(
|
||||
FileFilter::new("OpenVPN")
|
||||
.mimetype("application/x-openvpn-profile")
|
||||
.glob("*.ovpn"),
|
||||
)
|
||||
.open_file()
|
||||
.then(|result| async move {
|
||||
match result {
|
||||
Ok(response) => {
|
||||
_ = super::nm_add_vpn_file("openvpn", response.url().path()).await;
|
||||
Message::Refresh
|
||||
}
|
||||
Err(why) => {
|
||||
return Message::Error(why.to_string());
|
||||
}
|
||||
}
|
||||
})
|
||||
.apply(cosmic::command::future)
|
||||
}
|
||||
|
||||
fn connection_settings(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
let settings = async move {
|
||||
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
|
||||
|
||||
_ = settings.load_connections(&[]).await;
|
||||
|
||||
let settings = settings
|
||||
// Get a list of known connections.
|
||||
.list_connections()
|
||||
.await?
|
||||
// Prepare for wrapping in a concurrent stream.
|
||||
.into_iter()
|
||||
.map(|conn| async move { conn })
|
||||
// Create a concurrent stream for each connection.
|
||||
.apply(futures::stream::FuturesOrdered::from_iter)
|
||||
// Concurrently fetch settings for each connection, and filter for VPN.
|
||||
.filter_map(|conn| async move {
|
||||
let settings = conn.get_settings().await.ok()?;
|
||||
|
||||
let (connection, vpn) = settings.get("connection").zip(settings.get("vpn"))?;
|
||||
|
||||
if connection.get("type")?.downcast_ref::<String>().ok()? != "vpn" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
|
||||
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
|
||||
|
||||
let (username, connection_type, password_flag) = vpn
|
||||
.get("data")
|
||||
.and_then(|data| data.downcast_ref::<zbus::zvariant::Dict>().ok())
|
||||
.map(|dict| {
|
||||
let (mut connection_type, mut password_flag) = (None, None);
|
||||
|
||||
let username = dict
|
||||
.get::<String, String>(&String::from("username"))
|
||||
.ok()
|
||||
.flatten()
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
if let Some("password") = dict
|
||||
.get::<String, String>(&String::from("connection-type"))
|
||||
.ok()
|
||||
.flatten()
|
||||
.as_deref()
|
||||
{
|
||||
connection_type = Some(ConnectionType::Password);
|
||||
|
||||
password_flag = dict
|
||||
.get::<String, String>(&String::from("password-flags"))
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| match value.as_str() {
|
||||
"0" => Some(PasswordFlag::None),
|
||||
"1" => Some(PasswordFlag::AgentOwned),
|
||||
"2" => Some(PasswordFlag::NotSaved),
|
||||
"4" => Some(PasswordFlag::NotRequired),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
|
||||
(username, connection_type, password_flag)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Some((
|
||||
Arc::from(uuid),
|
||||
VpnConnectionSettings {
|
||||
id,
|
||||
connection_type,
|
||||
password_flag,
|
||||
username,
|
||||
},
|
||||
))
|
||||
})
|
||||
// Reduce the settings list into
|
||||
.fold(IndexMap::new(), |mut set, (uuid, data)| async move {
|
||||
set.insert(uuid, data);
|
||||
set
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok::<_, zbus::Error>(settings)
|
||||
};
|
||||
|
||||
cosmic::command::future(async move {
|
||||
settings
|
||||
.await
|
||||
.context("failed to get connection settings")
|
||||
.map_or_else(
|
||||
|why| Message::Error(why.to_string()),
|
||||
Message::KnownConnections,
|
||||
)
|
||||
})
|
||||
}
|
||||
49
cosmic-settings/src/pages/networking/vpn/nmcli.rs
Normal file
49
cosmic-settings/src/pages/networking/vpn/nmcli.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use as_result::IntoResult;
|
||||
use std::io;
|
||||
|
||||
pub async fn set_username(connection_name: &str, username: &str) -> io::Result<()> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(&["con", "mod", connection_name, "vpn.user-name", username])
|
||||
.status()
|
||||
.await
|
||||
.and_then(IntoResult::into_result)
|
||||
}
|
||||
|
||||
pub async fn set_password_flags_none(connection_name: &str) -> io::Result<()> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(&[
|
||||
"con",
|
||||
"mod",
|
||||
connection_name,
|
||||
"+vpn.data",
|
||||
"password-flags=0",
|
||||
])
|
||||
.status()
|
||||
.await
|
||||
.and_then(IntoResult::into_result)
|
||||
}
|
||||
|
||||
pub async fn set_password(connection_name: &str, password: &str) -> io::Result<()> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(&[
|
||||
"con",
|
||||
"mod",
|
||||
&connection_name,
|
||||
"vpn.secrets",
|
||||
&format!("password={password}"),
|
||||
])
|
||||
.status()
|
||||
.await
|
||||
.and_then(IntoResult::into_result)
|
||||
}
|
||||
|
||||
pub async fn connect(connection_name: &str) -> io::Result<()> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(&["con", "up", &connection_name])
|
||||
.status()
|
||||
.await
|
||||
.and_then(IntoResult::into_result)
|
||||
}
|
||||
787
cosmic-settings/src/pages/networking/wifi.rs
Normal file
787
cosmic-settings/src/pages/networking/wifi.rs
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use anyhow::Context;
|
||||
use cosmic::{
|
||||
iced::{alignment, Length},
|
||||
iced_core::text::Wrap,
|
||||
prelude::CollectionWidget,
|
||||
widget::{self, icon},
|
||||
Apply, Command, Element,
|
||||
};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use cosmic_settings_subscriptions::network_manager::{
|
||||
self, available_wifi::AccessPoint, current_networks::ActiveConnectionInfo, NetworkManagerState,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use secure_string::SecureString;
|
||||
use slab::Slab;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
/// Add a network connection with nm-connection-editor
|
||||
AddNetwork,
|
||||
/// Cancels a dialog.
|
||||
CancelDialog,
|
||||
/// Connect to a WiFi network access point.
|
||||
Connect(network_manager::SSID),
|
||||
/// Connect with a password
|
||||
ConnectWithPassword,
|
||||
/// Settings for known connections.
|
||||
ConnectionSettings(BTreeMap<Box<str>, Box<str>>),
|
||||
/// Disconnect from an access point.
|
||||
Disconnect(network_manager::SSID),
|
||||
/// An error occurred.
|
||||
Error(String),
|
||||
/// Create a dialog to ask for confirmation on forgetting a connection.
|
||||
ForgetRequest(network_manager::SSID),
|
||||
/// Forget a known access point.
|
||||
Forget(network_manager::SSID),
|
||||
/// An update from the network manager daemon
|
||||
NetworkManager(network_manager::Event),
|
||||
/// Successfully connected to the system dbus.
|
||||
NetworkManagerConnect(
|
||||
(
|
||||
zbus::Connection,
|
||||
tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
),
|
||||
),
|
||||
/// Request an auth dialog
|
||||
PasswordRequest(network_manager::SSID),
|
||||
/// Update the password from the dialog
|
||||
PasswordUpdate(SecureString),
|
||||
/// Opens settings page for the access point.
|
||||
Settings(network_manager::SSID),
|
||||
/// Toggles visibility of the password input
|
||||
TogglePasswordVisibility,
|
||||
/// Update NetworkManagerState
|
||||
UpdateState(NetworkManagerState),
|
||||
/// Update the devices lists
|
||||
UpdateDevices(Vec<network_manager::devices::DeviceInfo>),
|
||||
/// Display more options for an access point
|
||||
ViewMore(Option<network_manager::SSID>),
|
||||
/// Toggle WiFi access
|
||||
WiFiEnable(bool),
|
||||
}
|
||||
|
||||
impl From<Message> for crate::app::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::WiFi(message).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for crate::pages::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::WiFi(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum WiFiDialog {
|
||||
Forget(network_manager::SSID),
|
||||
Password {
|
||||
ssid: network_manager::SSID,
|
||||
password: SecureString,
|
||||
password_hidden: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Page {
|
||||
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
nm_state: Option<NmState>,
|
||||
dialog: Option<WiFiDialog>,
|
||||
view_more_popup: Option<network_manager::SSID>,
|
||||
connecting: BTreeSet<network_manager::SSID>,
|
||||
ssid_to_uuid: BTreeMap<Box<str>, Box<str>>,
|
||||
/// Withhold device update if the view more popup is shown.
|
||||
withheld_devices: Option<Vec<network_manager::devices::DeviceInfo>>,
|
||||
/// Withhold state update if the view more popup is shown.
|
||||
withheld_state: Option<NetworkManagerState>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NmState {
|
||||
conn: zbus::Connection,
|
||||
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
|
||||
state: network_manager::NetworkManagerState,
|
||||
devices: Vec<network_manager::devices::DeviceInfo>,
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> cosmic_settings_page::Info {
|
||||
page::Info::new("wifi", "preferences-wireless-symbolic")
|
||||
.title(fl!("wifi"))
|
||||
.description(fl!("connections-and-profiles", variant = "wifi"))
|
||||
}
|
||||
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(devices_view())])
|
||||
}
|
||||
|
||||
fn dialog(&self) -> Option<Element<crate::pages::Message>> {
|
||||
self.dialog.as_ref().map(|dialog| match dialog {
|
||||
WiFiDialog::Password {
|
||||
password,
|
||||
password_hidden,
|
||||
..
|
||||
} => {
|
||||
let password = widget::text_input::secure_input(
|
||||
fl!("password"),
|
||||
password.unsecure(),
|
||||
Some(Message::TogglePasswordVisibility),
|
||||
*password_hidden,
|
||||
)
|
||||
.on_input(|input| Message::PasswordUpdate(SecureString::from(input)))
|
||||
.on_submit(Message::ConnectWithPassword);
|
||||
|
||||
let primary_action = widget::button::suggested(fl!("connect"))
|
||||
.on_press(Message::ConnectWithPassword);
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
||||
widget::dialog(fl!("auth-dialog"))
|
||||
.icon(icon::from_name("preferences-wireless-symbolic").size(64))
|
||||
.body(fl!("auth-dialog", "wifi-description"))
|
||||
.control(password)
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::WiFi)
|
||||
}
|
||||
|
||||
WiFiDialog::Forget(ssid) => {
|
||||
let primary_action = widget::button::destructive(fl!("forget"))
|
||||
.on_press(Message::Forget(ssid.clone()));
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
||||
widget::dialog(fl!("forget-dialog"))
|
||||
.icon(icon::from_name("dialog-information").size(64))
|
||||
.body(fl!("forget-dialog", "description"))
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::WiFi)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn header_view(&self) -> Option<cosmic::Element<'_, crate::pages::Message>> {
|
||||
Some(
|
||||
widget::button::standard(fl!("add-network"))
|
||||
.trailing_icon(icon::from_name("window-pop-out-symbolic"))
|
||||
.on_press(Message::AddNetwork)
|
||||
.apply(widget::container)
|
||||
.width(Length::Fill)
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::WiFi),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_enter(
|
||||
&mut self,
|
||||
_page: cosmic_settings_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::WiFi)
|
||||
});
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> Command<crate::pages::Message> {
|
||||
self.view_more_popup = None;
|
||||
self.nm_state = None;
|
||||
self.ssid_to_uuid.clear();
|
||||
self.connecting.clear();
|
||||
self.withheld_state = None;
|
||||
self.withheld_devices = None;
|
||||
|
||||
if let Some(cancel) = self.nm_task.take() {
|
||||
_ = cancel.send(());
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
|
||||
match message {
|
||||
Message::NetworkManager(network_manager::Event::RequestResponse {
|
||||
req,
|
||||
state,
|
||||
success,
|
||||
}) => {
|
||||
if !success {
|
||||
tracing::error!(request = ?req, "network-manager request failed");
|
||||
}
|
||||
|
||||
match req {
|
||||
network_manager::Request::Password(ssid, _) => {
|
||||
if success {
|
||||
self.connecting.remove(&ssid);
|
||||
} else {
|
||||
// Request to retry
|
||||
self.dialog = Some(WiFiDialog::Password {
|
||||
ssid,
|
||||
password: SecureString::from(""),
|
||||
password_hidden: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
network_manager::Request::SelectAccessPoint(ssid) => {
|
||||
self.connecting.remove(&ssid);
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.update_state(state);
|
||||
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return update_devices(conn.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Message::UpdateDevices(devices) => {
|
||||
self.update_devices(devices);
|
||||
}
|
||||
|
||||
Message::UpdateState(state) => {
|
||||
self.update_state(state);
|
||||
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return connection_settings(conn.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Message::NetworkManager(
|
||||
network_manager::Event::ActiveConns
|
||||
| network_manager::Event::Devices
|
||||
| network_manager::Event::WiFiEnabled(_)
|
||||
| network_manager::Event::WirelessAccessPoints,
|
||||
) => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::command::batch(vec![
|
||||
update_state(conn.clone()),
|
||||
update_devices(conn.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::ConnectionSettings(settings) => {
|
||||
self.ssid_to_uuid = settings;
|
||||
}
|
||||
|
||||
Message::NetworkManager(network_manager::Event::Init {
|
||||
conn,
|
||||
sender,
|
||||
state,
|
||||
}) => {
|
||||
self.nm_state = Some(NmState {
|
||||
conn: conn.clone(),
|
||||
sender,
|
||||
state,
|
||||
devices: Vec::new(),
|
||||
});
|
||||
|
||||
return update_devices(conn);
|
||||
}
|
||||
|
||||
Message::AddNetwork => {
|
||||
tokio::task::spawn(super::nm_add_wifi());
|
||||
}
|
||||
|
||||
Message::Connect(ssid) => {
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
self.connecting.insert(ssid.clone());
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::SelectAccessPoint(ssid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::PasswordRequest(ssid) => {
|
||||
self.dialog = Some(WiFiDialog::Password {
|
||||
ssid,
|
||||
password: SecureString::from(""),
|
||||
password_hidden: true,
|
||||
});
|
||||
}
|
||||
|
||||
Message::PasswordUpdate(pass) => {
|
||||
if let Some(WiFiDialog::Password {
|
||||
ref mut password, ..
|
||||
}) = self.dialog
|
||||
{
|
||||
*password = pass;
|
||||
}
|
||||
}
|
||||
|
||||
Message::ConnectWithPassword => {
|
||||
let Some(dialog) = self.dialog.take() else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
if let WiFiDialog::Password { ssid, password, .. } = dialog {
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
self.connecting.insert(ssid.clone());
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::Password(ssid, password));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::TogglePasswordVisibility => {
|
||||
if let Some(WiFiDialog::Password {
|
||||
ref mut password_hidden,
|
||||
..
|
||||
}) = self.dialog
|
||||
{
|
||||
*password_hidden = !*password_hidden;
|
||||
}
|
||||
}
|
||||
|
||||
Message::ViewMore(ssid) => {
|
||||
self.view_more_popup = ssid;
|
||||
if self.view_more_popup.is_none() {
|
||||
self.close_popup_and_apply_updates();
|
||||
}
|
||||
}
|
||||
|
||||
Message::Disconnect(ssid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::Disconnect(ssid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::ForgetRequest(ssid) => {
|
||||
self.dialog = Some(WiFiDialog::Forget(ssid));
|
||||
self.view_more_popup = None;
|
||||
}
|
||||
|
||||
Message::Forget(ssid) => {
|
||||
self.dialog = None;
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::Forget(ssid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::Settings(ssid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
if let Some(uuid) = self.ssid_to_uuid.get(ssid.as_ref()).cloned() {
|
||||
tokio::task::spawn(
|
||||
async move { super::nm_edit_connection(uuid.as_ref()).await },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Message::WiFiEnable(enable) => {
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::SetWiFi(enable));
|
||||
_ = nm.sender.unbounded_send(network_manager::Request::Reload);
|
||||
}
|
||||
}
|
||||
|
||||
Message::CancelDialog => {
|
||||
self.dialog = None;
|
||||
}
|
||||
|
||||
Message::Error(why) => {
|
||||
tracing::error!(why, "error in wifi settings page");
|
||||
}
|
||||
|
||||
Message::NetworkManagerConnect((conn, output)) => {
|
||||
self.connect(conn.clone(), output);
|
||||
|
||||
return connection_settings(conn);
|
||||
}
|
||||
}
|
||||
|
||||
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::WiFi(Message::NetworkManager(event)),
|
||||
move |tx| async move {
|
||||
futures::join!(
|
||||
network_manager::watch(conn.clone(), tx.clone()),
|
||||
network_manager::active_conns::watch(conn.clone(), tx.clone()),
|
||||
network_manager::wireless_enabled::watch(conn.clone(), tx.clone()),
|
||||
network_manager::watch_connections_changed(conn, tx)
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the view more popup and applies any withheld updates.
|
||||
fn close_popup_and_apply_updates(&mut self) {
|
||||
self.view_more_popup = None;
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if let Some(state) = self.withheld_state.take() {
|
||||
nm_state.state = state;
|
||||
}
|
||||
|
||||
if let Some(devices) = self.withheld_devices.take() {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_devices(&mut self, devices: Vec<network_manager::devices::DeviceInfo>) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_devices = Some(devices);
|
||||
} else {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_state(&mut self, state: NetworkManagerState) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_state = Some(state);
|
||||
} else {
|
||||
nm_state.state = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn devices_view() -> Section<crate::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
airplane_mode_txt = fl!("airplane-on");
|
||||
connect_txt = fl!("connect");
|
||||
connected_txt = fl!("connected");
|
||||
connecting_txt = fl!("connecting");
|
||||
disconnect_txt = fl!("disconnect");
|
||||
forget_txt = fl!("wifi", "forget");
|
||||
known_networks_txt = fl!("known-networks");
|
||||
no_networks_txt = fl!("no-networks");
|
||||
settings_txt = fl!("settings");
|
||||
visible_networks_txt = fl!("visible-networks");
|
||||
wifi_txt = fl!("wifi");
|
||||
});
|
||||
|
||||
Section::default()
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let Some(NmState { ref state, .. }) = page.nm_state else {
|
||||
return cosmic::widget::column().into();
|
||||
};
|
||||
|
||||
let theme = cosmic::theme::active();
|
||||
let spacing = &theme.cosmic().spacing;
|
||||
|
||||
let wifi_enable =
|
||||
widget::settings::item::builder(§ion.descriptions[wifi_txt]).control(
|
||||
widget::toggler(None, state.wifi_enabled, Message::WiFiEnable),
|
||||
);
|
||||
|
||||
let mut view = widget::column::with_capacity(4)
|
||||
.push(widget::list_column().add(wifi_enable))
|
||||
.push_maybe(state.airplane_mode.then(|| {
|
||||
widget::row::with_capacity(2)
|
||||
.push(icon::from_name("airplane-mode-symbolic"))
|
||||
.push(widget::text::body(§ion.descriptions[airplane_mode_txt]))
|
||||
.spacing(8)
|
||||
.align_items(alignment::Alignment::Center)
|
||||
.apply(widget::container)
|
||||
.width(Length::Fill)
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
}));
|
||||
|
||||
if !state.airplane_mode
|
||||
&& state.known_access_points.is_empty()
|
||||
&& state.wireless_access_points.is_empty()
|
||||
{
|
||||
let no_networks_found =
|
||||
widget::container(widget::text::body(§ion.descriptions[no_networks_txt]))
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
.width(Length::Fill);
|
||||
|
||||
view = view.push(no_networks_found);
|
||||
} else {
|
||||
let mut has_known = false;
|
||||
let mut has_visible = false;
|
||||
|
||||
// Create separate sections for known and visible networks.
|
||||
let (known_networks, visible_networks) = state.wireless_access_points.iter().fold(
|
||||
(
|
||||
widget::settings::view_section(§ion.descriptions[known_networks_txt]),
|
||||
widget::settings::view_section(§ion.descriptions[visible_networks_txt]),
|
||||
),
|
||||
|(mut known_networks, mut visible_networks), network| {
|
||||
let is_connected = is_connected(state, network);
|
||||
|
||||
let is_known = state
|
||||
.known_access_points
|
||||
.iter()
|
||||
.map(|known| known.ssid.as_ref())
|
||||
.chain(state.active_conns.iter().filter_map(|active| {
|
||||
if let ActiveConnectionInfo::WiFi { name, .. } = active {
|
||||
Some(name.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}))
|
||||
.any(|known| known == network.ssid.as_ref());
|
||||
|
||||
// TODO: detect if access point is secured or not.
|
||||
let is_encrypted = true;
|
||||
|
||||
let (connect_txt, connect_msg) = if is_connected {
|
||||
(§ion.descriptions[connected_txt], None)
|
||||
} else if page.connecting.contains(&network.ssid) {
|
||||
(§ion.descriptions[connecting_txt], None)
|
||||
} else {
|
||||
(
|
||||
§ion.descriptions[connect_txt],
|
||||
Some(if is_known || !is_encrypted {
|
||||
Message::Connect(network.ssid.clone())
|
||||
} else {
|
||||
Message::PasswordRequest(network.ssid.clone())
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
let identifier = widget::row::with_capacity(3)
|
||||
.push(widget::icon::from_name("network-wireless-good-symbolic"))
|
||||
.push_maybe(
|
||||
is_encrypted
|
||||
.then(|| widget::icon::from_name("connection-secure-symbolic")),
|
||||
)
|
||||
.push(widget::text::body(network.ssid.as_ref()).wrap(Wrap::Glyph))
|
||||
.spacing(spacing.space_xxs);
|
||||
|
||||
let connect: Element<'_, Message> = if let Some(msg) = connect_msg {
|
||||
widget::button::text(connect_txt).on_press(msg).into()
|
||||
} else {
|
||||
widget::text::body(connect_txt)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.into()
|
||||
};
|
||||
|
||||
let view_more_button =
|
||||
widget::button::icon(widget::icon::from_name("view-more-symbolic"));
|
||||
|
||||
let view_more: Option<Element<_>> = if page
|
||||
.view_more_popup
|
||||
.as_deref()
|
||||
.map_or(false, |id| id == network.ssid.as_ref())
|
||||
{
|
||||
widget::popover(view_more_button.on_press(Message::ViewMore(None)))
|
||||
.position(widget::popover::Position::Bottom)
|
||||
.on_close(Message::ViewMore(None))
|
||||
.popup({
|
||||
widget::column()
|
||||
.push_maybe(is_connected.then(|| {
|
||||
popup_button(
|
||||
Message::Disconnect(network.ssid.clone()),
|
||||
§ion.descriptions[disconnect_txt],
|
||||
)
|
||||
}))
|
||||
.push(popup_button(
|
||||
Message::Settings(network.ssid.clone()),
|
||||
§ion.descriptions[settings_txt],
|
||||
))
|
||||
.push_maybe(is_known.then(|| {
|
||||
popup_button(
|
||||
Message::ForgetRequest(network.ssid.clone()),
|
||||
§ion.descriptions[forget_txt],
|
||||
)
|
||||
}))
|
||||
.width(Length::Fixed(170.0))
|
||||
.apply(widget::container)
|
||||
.style(cosmic::style::Container::Dialog)
|
||||
})
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
} else if is_known {
|
||||
view_more_button
|
||||
.on_press(Message::ViewMore(Some(network.ssid.clone())))
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let controls = widget::row::with_capacity(2)
|
||||
.push(connect)
|
||||
.push_maybe(view_more)
|
||||
.align_items(alignment::Alignment::Center)
|
||||
.spacing(spacing.space_xxs);
|
||||
|
||||
let widget = widget::settings::item_row(vec![
|
||||
identifier.into(),
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
controls.into(),
|
||||
]);
|
||||
|
||||
if is_known {
|
||||
has_known = true;
|
||||
known_networks = known_networks.add(widget);
|
||||
} else {
|
||||
has_visible = true;
|
||||
visible_networks = visible_networks.add(widget);
|
||||
}
|
||||
|
||||
(known_networks, visible_networks)
|
||||
},
|
||||
);
|
||||
|
||||
if has_known {
|
||||
view = view.push(known_networks);
|
||||
}
|
||||
|
||||
if has_visible {
|
||||
view = view.push(visible_networks);
|
||||
}
|
||||
};
|
||||
|
||||
view.spacing(spacing.space_l)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::WiFi)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_connected(state: &NetworkManagerState, network: &AccessPoint) -> bool {
|
||||
state.active_conns.iter().any(|active| {
|
||||
if let ActiveConnectionInfo::WiFi { ref name, .. } = active {
|
||||
*name == network.ssid.as_ref()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn popup_button<'a>(message: Message, text: &'a str) -> Element<'a, Message> {
|
||||
widget::text::body(text)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.apply(widget::button)
|
||||
.padding([4, 16])
|
||||
.width(Length::Fill)
|
||||
.style(cosmic::theme::Button::MenuItem)
|
||||
.on_press(message)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn connection_settings(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
let settings = async move {
|
||||
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
|
||||
|
||||
_ = settings.load_connections(&[]).await;
|
||||
|
||||
let settings = settings
|
||||
// Get a list of known connections.
|
||||
.list_connections()
|
||||
.await?
|
||||
// Prepare for wrapping in a concurrent stream.
|
||||
.into_iter()
|
||||
.map(|conn| async move { conn })
|
||||
// Create a concurrent stream for each connection.
|
||||
.apply(futures::stream::FuturesOrdered::from_iter)
|
||||
// Concurrently fetch settings for each connection.
|
||||
.filter_map(|conn| async move {
|
||||
conn.get_settings()
|
||||
.await
|
||||
.map(network_manager::Settings::new)
|
||||
.ok()
|
||||
})
|
||||
// Reduce the settings list into a SSID->UUID map.
|
||||
.fold(BTreeMap::new(), |mut set, settings| async move {
|
||||
if let Some(ref wifi) = settings.wifi {
|
||||
if let Some(ssid) = wifi
|
||||
.ssid
|
||||
.clone()
|
||||
.and_then(|ssid| String::from_utf8(ssid).ok())
|
||||
{
|
||||
if let Some(ref connection) = settings.connection {
|
||||
if let Some(uuid) = connection.uuid.clone() {
|
||||
set.insert(ssid.into(), uuid.into());
|
||||
return set;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok::<_, zbus::Error>(settings)
|
||||
};
|
||||
|
||||
cosmic::command::future(async move {
|
||||
settings
|
||||
.await
|
||||
.context("failed to get connection settings")
|
||||
.map_or_else(
|
||||
|why| Message::Error(why.to_string()),
|
||||
Message::ConnectionSettings,
|
||||
)
|
||||
.apply(crate::pages::Message::WiFi)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_state(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
match NetworkManagerState::new(&conn).await {
|
||||
Ok(state) => Message::UpdateState(state),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_devices(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
let filter =
|
||||
|device_type| matches!(device_type, network_manager::devices::DeviceType::Wifi);
|
||||
match network_manager::devices::list(&conn, filter).await {
|
||||
Ok(devices) => Message::UpdateDevices(devices),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,10 +1,690 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
pub fn info() -> page::Info {
|
||||
page::Info::new("wired", "network-workgroup-symbolic")
|
||||
.title(fl!("wired"))
|
||||
.description(fl!("wired", "desc"))
|
||||
use anyhow::Context;
|
||||
use cosmic::{
|
||||
iced::{alignment, Length},
|
||||
iced_core::text::Wrap,
|
||||
prelude::CollectionWidget,
|
||||
widget::{self, icon},
|
||||
Apply, Command, Element,
|
||||
};
|
||||
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>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
/// Activate a connection
|
||||
Activate(ConnectionId),
|
||||
/// Add a network connection with nm-connection-editor
|
||||
AddNetwork,
|
||||
/// Cancels an active dialog.
|
||||
CancelDialog,
|
||||
/// Deactivate a connection.
|
||||
Deactivate(ConnectionId),
|
||||
/// An error occurred.
|
||||
Error(String),
|
||||
/// An update from the network manager daemon
|
||||
NetworkManager(network_manager::Event),
|
||||
/// Successfully connected to the system dbus.
|
||||
NetworkManagerConnect(
|
||||
(
|
||||
zbus::Connection,
|
||||
tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
),
|
||||
),
|
||||
/// Refresh devices and their connection profiles
|
||||
Refresh,
|
||||
/// Create a dialog to ask for confirmation of removal.
|
||||
RemoveProfileRequest(ConnectionId),
|
||||
/// Remove a connection profile
|
||||
RemoveProfile(ConnectionId),
|
||||
/// Selects a device to display connections from
|
||||
SelectDevice(Arc<network_manager::devices::DeviceInfo>),
|
||||
/// Opens settings page for the access point.
|
||||
Settings(ConnectionId),
|
||||
/// Update NetworkManagerState
|
||||
UpdateState(NetworkManagerState),
|
||||
/// Update the devices lists
|
||||
UpdateDevices(Vec<network_manager::devices::DeviceInfo>),
|
||||
/// Display more options for an access point
|
||||
ViewMore(Option<ConnectionId>),
|
||||
}
|
||||
|
||||
impl From<Message> for crate::app::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::Wired(message).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for crate::pages::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::Wired(message)
|
||||
}
|
||||
}
|
||||
|
||||
pub type InterfaceId = String;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum WiredDialog {
|
||||
RemoveProfile(ConnectionId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Page {
|
||||
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
nm_state: Option<NmState>,
|
||||
dialog: Option<WiredDialog>,
|
||||
/// When defined, displays connections for the specific device.
|
||||
active_device: Option<Arc<network_manager::devices::DeviceInfo>>,
|
||||
/// Tracks which connections are in the act of connecting.
|
||||
connecting: BTreeSet<ConnectionId>,
|
||||
/// Displays a popup when set.
|
||||
view_more_popup: Option<ConnectionId>,
|
||||
/// Withhold device update if the view more popup is shown.
|
||||
withheld_devices: Option<Vec<Arc<network_manager::devices::DeviceInfo>>>,
|
||||
/// Withhold active connections update if the view more popup is shown.
|
||||
withheld_active_conns: Option<Vec<ActiveConnectionInfo>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NmState {
|
||||
conn: zbus::Connection,
|
||||
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
|
||||
active_conns: Vec<ActiveConnectionInfo>,
|
||||
devices: Vec<Arc<network_manager::devices::DeviceInfo>>,
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> cosmic_settings_page::Info {
|
||||
page::Info::new("wired", "preferences-wired-symbolic")
|
||||
.title(fl!("wired"))
|
||||
.description(fl!("connections-and-profiles", variant = "wired"))
|
||||
}
|
||||
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(devices_view())])
|
||||
}
|
||||
|
||||
fn dialog(&self) -> Option<Element<crate::pages::Message>> {
|
||||
self.dialog.as_ref().map(|dialog| match dialog {
|
||||
WiredDialog::RemoveProfile(uuid) => {
|
||||
let primary_action = widget::button::destructive(fl!("remove"))
|
||||
.on_press(Message::RemoveProfile(uuid.clone()));
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
||||
widget::dialog(fl!("remove-connection-dialog"))
|
||||
.icon(icon::from_name("dialog-information").size(64))
|
||||
.body(fl!("remove-connection-dialog", "wired-description"))
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Wired)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn header_view(&self) -> Option<cosmic::Element<'_, crate::pages::Message>> {
|
||||
Some(
|
||||
widget::button::standard(fl!("add-network"))
|
||||
.trailing_icon(icon::from_name("window-pop-out-symbolic"))
|
||||
.on_press(Message::AddNetwork)
|
||||
.apply(widget::container)
|
||||
.width(Length::Fill)
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Wired),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_enter(
|
||||
&mut self,
|
||||
_page: cosmic_settings_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::Wired)
|
||||
});
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> Command<crate::pages::Message> {
|
||||
self.active_device = None;
|
||||
self.view_more_popup = None;
|
||||
self.nm_state = None;
|
||||
self.withheld_active_conns = None;
|
||||
self.withheld_devices = None;
|
||||
self.connecting.clear();
|
||||
|
||||
if let Some(cancel) = self.nm_task.take() {
|
||||
_ = cancel.send(());
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn title(&self) -> Option<&str> {
|
||||
self.active_device
|
||||
.as_ref()
|
||||
.map(|device| device.interface.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
|
||||
match message {
|
||||
Message::NetworkManager(network_manager::Event::RequestResponse {
|
||||
req,
|
||||
state,
|
||||
success,
|
||||
}) => {
|
||||
if !success {
|
||||
tracing::error!(request = ?req, "network-manager request failed");
|
||||
}
|
||||
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
let conn = conn.clone();
|
||||
self.update_active_conns(state);
|
||||
return update_devices(conn);
|
||||
}
|
||||
}
|
||||
|
||||
Message::UpdateDevices(devices) => {
|
||||
self.update_devices(devices);
|
||||
}
|
||||
|
||||
Message::UpdateState(state) => {
|
||||
self.update_active_conns(state);
|
||||
}
|
||||
|
||||
Message::SelectDevice(device) => {
|
||||
self.active_device = Some(device);
|
||||
}
|
||||
|
||||
Message::NetworkManager(
|
||||
network_manager::Event::ActiveConns | network_manager::Event::Devices,
|
||||
) => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::command::batch(vec![
|
||||
update_state(conn.clone()),
|
||||
update_devices(conn.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::NetworkManager(network_manager::Event::Init {
|
||||
conn,
|
||||
sender,
|
||||
state,
|
||||
}) => {
|
||||
self.nm_state = Some(NmState {
|
||||
conn: conn.clone(),
|
||||
sender,
|
||||
devices: Vec::new(),
|
||||
active_conns: state
|
||||
.active_conns
|
||||
.into_iter()
|
||||
.filter(|info| matches!(info, ActiveConnectionInfo::Wired { .. }))
|
||||
.collect(),
|
||||
});
|
||||
|
||||
return update_devices(conn);
|
||||
}
|
||||
|
||||
Message::NetworkManager(_event) => (),
|
||||
|
||||
Message::AddNetwork => {
|
||||
return cosmic::command::future(async move {
|
||||
_ = super::nm_add_wired().await;
|
||||
// TODO: Update when iced is rebased to use then method.
|
||||
Message::Refresh
|
||||
});
|
||||
}
|
||||
|
||||
Message::Activate(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
if let Some(NmState {
|
||||
ref devices,
|
||||
ref sender,
|
||||
..
|
||||
}) = self.nm_state
|
||||
{
|
||||
for device in devices {
|
||||
let device_conn = device
|
||||
.available_connections
|
||||
.iter()
|
||||
.find(|conn| conn.uuid.as_ref() == uuid.as_ref());
|
||||
|
||||
if let Some(device_conn) = device_conn {
|
||||
let device_path = device.path.clone();
|
||||
let conn_path = device_conn.path.clone();
|
||||
|
||||
_ = sender.unbounded_send(network_manager::Request::Activate(
|
||||
device_path,
|
||||
conn_path,
|
||||
));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::Deactivate(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender.unbounded_send(network_manager::Request::Deactivate(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::RemoveProfileRequest(uuid) => {
|
||||
self.view_more_popup = None;
|
||||
self.dialog = Some(WiredDialog::RemoveProfile(uuid));
|
||||
}
|
||||
|
||||
Message::RemoveProfile(uuid) => {
|
||||
self.dialog = None;
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender.unbounded_send(network_manager::Request::Remove(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::ViewMore(uuid) => {
|
||||
self.view_more_popup = uuid;
|
||||
if self.view_more_popup.is_none() {
|
||||
self.close_popup_and_apply_updates();
|
||||
}
|
||||
}
|
||||
|
||||
Message::Settings(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
return cosmic::command::future(async move {
|
||||
_ = super::nm_edit_connection(uuid.as_ref()).await;
|
||||
// TODO: Update when iced is rebased to use then method.
|
||||
Message::Refresh
|
||||
});
|
||||
}
|
||||
|
||||
Message::Refresh => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::command::batch(vec![
|
||||
update_state(conn.clone()),
|
||||
update_devices(conn.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::CancelDialog => {
|
||||
self.dialog = None;
|
||||
}
|
||||
|
||||
Message::Error(why) => {
|
||||
tracing::error!(why, "error in wired settings page");
|
||||
}
|
||||
|
||||
Message::NetworkManagerConnect((conn, output)) => {
|
||||
self.connect(conn.clone(), output);
|
||||
}
|
||||
}
|
||||
|
||||
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::Wired(Message::NetworkManager(event)),
|
||||
move |tx| async move {
|
||||
futures::join!(
|
||||
network_manager::watch(conn.clone(), tx.clone()),
|
||||
network_manager::active_conns::watch(conn.clone(), tx.clone()),
|
||||
network_manager::devices::watch(conn, true, tx)
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the view more popup and applies any withheld updates.
|
||||
fn close_popup_and_apply_updates(&mut self) {
|
||||
self.view_more_popup = None;
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if let Some(active_conns) = self.withheld_active_conns.take() {
|
||||
nm_state.active_conns = active_conns;
|
||||
}
|
||||
|
||||
if let Some(devices) = self.withheld_devices.take() {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
|
||||
self.update_active_device();
|
||||
}
|
||||
|
||||
fn update_active_device(&mut self) {
|
||||
if let Some((nm_state, active)) = self.nm_state.as_ref().zip(self.active_device.as_ref()) {
|
||||
self.active_device = nm_state
|
||||
.devices
|
||||
.iter()
|
||||
.find(|device| device.path == active.path)
|
||||
.map(Arc::clone);
|
||||
}
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_devices(&mut self, devices: Vec<network_manager::devices::DeviceInfo>) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
let devices = devices.into_iter().map(Arc::new).collect();
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_devices = Some(devices);
|
||||
} else {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
|
||||
self.update_active_device();
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_active_conns(&mut self, state: NetworkManagerState) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
let conns = state
|
||||
.active_conns
|
||||
.into_iter()
|
||||
.filter(|info| matches!(info, ActiveConnectionInfo::Wired { .. }))
|
||||
.collect();
|
||||
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_active_conns = Some(conns);
|
||||
} else {
|
||||
nm_state.active_conns = conns;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn device_view<'a>(
|
||||
&'a self,
|
||||
spacing: &cosmic::cosmic_theme::Spacing,
|
||||
nm_state: &'a NmState,
|
||||
connect_txt: &'a str,
|
||||
connected_txt: &'a str,
|
||||
disconnect_txt: &'a str,
|
||||
remove_txt: &'a str,
|
||||
settings_txt: &'a str,
|
||||
wired_conns_txt: &'a str,
|
||||
device: &'a network_manager::devices::DeviceInfo,
|
||||
) -> Element<'a, Message> {
|
||||
let has_multiple_connection_profiles = device.available_connections.len() > 1;
|
||||
let header_txt = format!("{}", wired_conns_txt);
|
||||
|
||||
device
|
||||
.available_connections
|
||||
.iter()
|
||||
.fold(
|
||||
widget::settings::view_section(header_txt),
|
||||
|networks, connection| {
|
||||
let is_connected = nm_state.active_conns.iter().any(|conn| match conn {
|
||||
ActiveConnectionInfo::Wired { name, .. } => {
|
||||
name.as_str() == connection.id.as_str()
|
||||
}
|
||||
|
||||
_ => false,
|
||||
});
|
||||
|
||||
let (connect_txt, connect_msg) = if is_connected {
|
||||
(connected_txt, None)
|
||||
} else {
|
||||
(
|
||||
connect_txt,
|
||||
Some(Message::Activate(connection.uuid.clone())),
|
||||
)
|
||||
};
|
||||
|
||||
let identifier = widget::text::body(&connection.id).wrap(Wrap::Glyph);
|
||||
|
||||
let connect: Element<'_, Message> = if let Some(msg) = connect_msg {
|
||||
widget::button::text(connect_txt).on_press(msg).into()
|
||||
} else {
|
||||
widget::text::body(connect_txt)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.into()
|
||||
};
|
||||
|
||||
let view_more_button =
|
||||
widget::button::icon(widget::icon::from_name("view-more-symbolic"));
|
||||
|
||||
let view_more: Option<Element<_>> = if self
|
||||
.view_more_popup
|
||||
.as_deref()
|
||||
.map_or(false, |id| id == connection.uuid.as_ref())
|
||||
{
|
||||
widget::popover(view_more_button.on_press(Message::ViewMore(None)))
|
||||
.position(widget::popover::Position::Bottom)
|
||||
.on_close(Message::ViewMore(None))
|
||||
.popup({
|
||||
widget::column()
|
||||
.push_maybe(is_connected.then(|| {
|
||||
popup_button(
|
||||
Message::Deactivate(connection.uuid.clone()),
|
||||
&disconnect_txt,
|
||||
)
|
||||
}))
|
||||
.push(popup_button(
|
||||
Message::Settings(connection.uuid.clone()),
|
||||
&settings_txt,
|
||||
))
|
||||
.push_maybe(has_multiple_connection_profiles.then(|| {
|
||||
popup_button(
|
||||
Message::RemoveProfileRequest(connection.uuid.clone()),
|
||||
&remove_txt,
|
||||
)
|
||||
}))
|
||||
.width(Length::Fixed(200.0))
|
||||
.apply(widget::container)
|
||||
.style(cosmic::style::Container::Dialog)
|
||||
})
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
} else {
|
||||
view_more_button
|
||||
.on_press(Message::ViewMore(Some(connection.uuid.clone())))
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
};
|
||||
|
||||
let controls = widget::row::with_capacity(2)
|
||||
.push(connect)
|
||||
.push_maybe(view_more)
|
||||
.align_items(alignment::Alignment::Center)
|
||||
.spacing(spacing.space_xxs);
|
||||
|
||||
let widget = widget::settings::item_row(vec![
|
||||
identifier.into(),
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
controls.into(),
|
||||
]);
|
||||
|
||||
networks.add(widget)
|
||||
},
|
||||
)
|
||||
.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> {
|
||||
crate::slab!(descriptions {
|
||||
wired_conns_txt = fl!("wired", "connections");
|
||||
wired_devices_txt = fl!("wired", "devices");
|
||||
remove_txt = fl!("wired", "remove");
|
||||
connect_txt = fl!("connect");
|
||||
connected_txt = fl!("connected");
|
||||
settings_txt = fl!("settings");
|
||||
disconnect_txt = fl!("disconnect");
|
||||
});
|
||||
|
||||
Section::default()
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let Some(ref nm_state) = page.nm_state else {
|
||||
return cosmic::widget::column().into();
|
||||
};
|
||||
|
||||
let theme = cosmic::theme::active();
|
||||
let spacing = &theme.cosmic().spacing;
|
||||
|
||||
let mut view = widget::column::with_capacity(4);
|
||||
|
||||
// Displays device connections if a device is selected, or only device exists.
|
||||
let active_device = page
|
||||
.active_device
|
||||
.as_ref()
|
||||
.or_else(|| (nm_state.devices.len() == 1).then(|| nm_state.devices.get(0))?)
|
||||
.filter(|device| !matches!(device.state, DeviceState::Unavailable));
|
||||
|
||||
view = match active_device {
|
||||
Some(device) => view.push(page.device_view(
|
||||
spacing,
|
||||
nm_state,
|
||||
§ion.descriptions[connect_txt],
|
||||
§ion.descriptions[connected_txt],
|
||||
§ion.descriptions[disconnect_txt],
|
||||
§ion.descriptions[remove_txt],
|
||||
§ion.descriptions[settings_txt],
|
||||
§ion.descriptions[wired_conns_txt],
|
||||
device,
|
||||
)),
|
||||
|
||||
None => view.push(page.device_list_view(
|
||||
spacing,
|
||||
nm_state,
|
||||
§ion.descriptions[wired_devices_txt],
|
||||
)),
|
||||
};
|
||||
|
||||
view.spacing(spacing.space_l)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Wired)
|
||||
})
|
||||
}
|
||||
|
||||
fn popup_button<'a>(message: Message, text: &'a str) -> Element<'a, Message> {
|
||||
widget::text::body(text)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.apply(widget::button)
|
||||
.padding([4, 16])
|
||||
.width(Length::Fill)
|
||||
.style(cosmic::theme::Button::MenuItem)
|
||||
.on_press(message)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn update_state(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
match NetworkManagerState::new(&conn).await {
|
||||
Ok(state) => Message::UpdateState(state),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn update_devices(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
let filter =
|
||||
|device_type| matches!(device_type, network_manager::devices::DeviceType::Ethernet);
|
||||
|
||||
match network_manager::devices::list(&conn, filter).await {
|
||||
Ok(devices) => Message::UpdateDevices(devices),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,3 +46,15 @@ pub fn forward_event_loop<M: 'static + Send, T: Future<Output = ()> + Send + 'st
|
|||
|
||||
cancel_tx
|
||||
}
|
||||
|
||||
/// Creates a slab with predefined items
|
||||
#[macro_export]
|
||||
macro_rules! slab {
|
||||
( $descriptions:ident { $( $txt_id:ident = $txt_expr:expr; )+ } ) => {
|
||||
let mut $descriptions = Slab::new();
|
||||
|
||||
$(
|
||||
let $txt_id = $descriptions.insert($txt_expr);
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue