Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cosmic-applet-network/i18n/en/cosmic_applet_network.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ connect = Connect
cancel = Cancel
settings = Network settings...
visible-wireless-networks = Visible wireless networks
vpn-connections = VPN connections
enter-password = Enter the password or encryption key
router-wps-button = You can also connect by pressing the "WPS" button on the router
unable-to-connect = Unable to connect to network
Expand Down
99 changes: 98 additions & 1 deletion cosmic-applet-network/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ struct CosmicNetworkApplet {
// UI state
nm_sender: Option<UnboundedSender<NetworkManagerRequest>>,
show_visible_networks: bool,
show_available_vpns: bool,
new_connection: Option<NewConnectionState>,
conn: Option<Connection>,
timeline: Timeline,
Expand All @@ -122,6 +123,82 @@ fn wifi_icon(strength: u8) -> &'static str {
}
}

fn vpn_section<'a>(
nm_state: &'a NetworkManagerState,
show_available_vpns: bool,
space_xxs: u16,
space_s: u16,
) -> Column<'a, Message> {
let mut vpn_col = column![];

if !nm_state.available_vpns.is_empty() {
let dropdown_icon = if show_available_vpns {
"go-up-symbolic"
} else {
"go-down-symbolic"
};

vpn_col = vpn_col.push(
padded_control(divider::horizontal::default()).padding([space_xxs, space_s])
);

let vpn_toggle_btn = menu_button(row![
text::body(fl!("vpn-connections"))
.width(Length::Fill)
.height(Length::Fixed(24.0))
.align_y(Alignment::Center),
container(icon::from_name(dropdown_icon).size(16).symbolic(true))
.center(Length::Fixed(24.0))
])
.on_press(Message::ToggleVpnList);

vpn_col = vpn_col.push(vpn_toggle_btn);

if show_available_vpns {
for vpn in &nm_state.available_vpns {
// Check if this VPN is currently active
let is_active = nm_state.active_conns.iter().any(|conn| {
matches!(conn, ActiveConnectionInfo::Vpn { name, .. } if name == &vpn.name)
});

let mut btn_content = vec![
icon::from_name("network-vpn-symbolic")
.size(24)
.symbolic(true)
.into(),
text::body(&vpn.name)
.width(Length::Fill)
.into(),
];

if is_active {
btn_content.push(
text::body(fl!("connected"))
.align_x(Alignment::End)
.into(),
);
}

let mut btn = menu_button(
Row::with_children(btn_content)
.align_y(Alignment::Center)
.spacing(8),
);

btn = if is_active {
btn.on_press(Message::DeactivateVpn(vpn.name.clone()))
} else {
btn.on_press(Message::ActivateVpn(vpn.uuid.clone()))
};

vpn_col = vpn_col.push(btn);
}
}
}

vpn_col
}

impl CosmicNetworkApplet {
fn update_nm_state(&mut self, mut new_state: NetworkManagerState) {
self.update_togglers(&new_state);
Expand Down Expand Up @@ -246,7 +323,10 @@ pub(crate) enum Message {
ResetFailedKnownSsid(String, HwAddress),
OpenHwDevice(Option<HwAddress>),
TogglePasswordVisibility,
Surface(surface::Action), // Errored(String),
Surface(surface::Action),
ActivateVpn(String), // UUID of VPN to activate
DeactivateVpn(String), // Name of VPN to deactivate
ToggleVpnList, // Show/hide available VPNs
}

impl cosmic::Application for CosmicNetworkApplet {
Expand Down Expand Up @@ -612,6 +692,19 @@ impl cosmic::Application for CosmicNetworkApplet {
*identity = new_identity;
}
}
Message::ActivateVpn(uuid) => {
if let Some(tx) = self.nm_sender.as_ref() {
let _ = tx.unbounded_send(NetworkManagerRequest::ActivateVpn(uuid));
}
}
Message::DeactivateVpn(name) => {
if let Some(tx) = self.nm_sender.as_ref() {
let _ = tx.unbounded_send(NetworkManagerRequest::DeactivateVpn(name));
}
}
Message::ToggleVpnList => {
self.show_available_vpns = !self.show_available_vpns;
}
}
Task::none()
}
Expand Down Expand Up @@ -1003,6 +1096,7 @@ impl cosmic::Application for CosmicNetworkApplet {
content = content.push(available_connections_btn);

if !self.show_visible_networks {
content = content.push(vpn_section(&self.nm_state, self.show_available_vpns, space_xxs, space_s));
return self.view_window_return(content);
}

Expand Down Expand Up @@ -1147,6 +1241,9 @@ impl cosmic::Application for CosmicNetworkApplet {
.push(scrollable(Column::with_children(list_col)).height(Length::Fixed(300.0)));
}

// Add VPN connections section after wireless networks when they are expanded
content = content.push(vpn_section(&self.nm_state, self.show_available_vpns, space_xxs, space_s));

self.view_window_return(content)
}

Expand Down
45 changes: 45 additions & 0 deletions cosmic-applet-network/src/network_manager/available_vpns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-3.0-or-later

use cosmic_dbus_networkmanager::settings::{NetworkManagerSettings, connection::Settings};
use zbus::Connection;

#[derive(Debug, Clone)]
pub struct VpnConnection {
pub name: String,
pub uuid: String,
}

/// Load all available VPN connections from NetworkManager settings
pub async fn load_vpn_connections(conn: &Connection) -> anyhow::Result<Vec<VpnConnection>> {
let nm_settings = NetworkManagerSettings::new(conn).await?;
let connections = nm_settings.list_connections().await?;

let mut vpn_connections = Vec::new();

for connection in connections {
let settings_map = match connection.get_settings().await {
Ok(s) => s,
Err(_) => continue,
};

let settings = Settings::new(settings_map);

// Check if this is a VPN connection
if let Some(connection_settings) = &settings.connection {
if let Some(conn_type) = &connection_settings.type_ {
// VPN connections have type "vpn" or "wireguard"
if conn_type == "vpn" || conn_type == "wireguard" {
let name = connection_settings.id.clone().unwrap_or_else(|| "Unknown VPN".to_string());
let uuid = connection_settings.uuid.clone().unwrap_or_default();

vpn_connections.push(VpnConnection { name, uuid });
}
}
}
}

// Sort by name for consistent UI
vpn_connections.sort_by(|a, b| a.name.cmp(&b.name));

Ok(vpn_connections)
}
127 changes: 127 additions & 0 deletions cosmic-applet-network/src/network_manager/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod active_conns;
pub mod available_vpns;
pub mod available_wifi;
pub mod current_networks;
pub mod devices;
Expand Down Expand Up @@ -34,6 +35,7 @@ use zbus::{
};

use self::{
available_vpns::{VpnConnection, load_vpn_connections},
available_wifi::{AccessPoint, handle_wireless_device},
current_networks::{ActiveConnectionInfo, active_connections},
};
Expand Down Expand Up @@ -285,6 +287,123 @@ async fn start_listening(
})
.await;
}
Some(NetworkManagerRequest::ActivateVpn(uuid)) => {
tracing::info!("Activating VPN with UUID: {}", uuid);
let network_manager = match NetworkManager::new(&conn).await {
Ok(n) => n,
Err(e) => {
tracing::error!("Failed to connect to NetworkManager: {:?}", e);
_ = output
.send(NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::ActivateVpn(uuid),
success: false,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
})
.await;
return State::Waiting(conn, rx);
}
};

let mut success = false;

// Find the connection by UUID
if let Ok(nm_settings) = NetworkManagerSettings::new(&conn).await {
if let Ok(connections) = nm_settings.list_connections().await {
for connection in connections {
if let Ok(settings) = connection.get_settings().await {
let settings = Settings::new(settings);
if let Some(conn_settings) = &settings.connection {
if conn_settings.uuid.as_ref() == Some(&uuid) {
// Activate the VPN connection without a specific device
// Call the D-Bus method directly since VPNs don't need a device
use zbus::zvariant::ObjectPath;
let empty_device = ObjectPath::try_from("/").unwrap();

match network_manager.inner()
.call_method("ActivateConnection", &(connection.inner().path(), empty_device.clone(), empty_device))
.await
{
Ok(_) => {
tracing::info!("Successfully activated VPN: {}", uuid);
success = true;
}
Err(e) => {
tracing::error!("Failed to activate VPN {}: {:?}", uuid, e);
}
}
break;
}
}
}
}
}
}

if !success {
tracing::warn!("VPN connection with UUID {} not found or failed to activate", uuid);
}

let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
_ = output
.send(NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::ActivateVpn(uuid),
success,
state,
})
.await;
}
Some(NetworkManagerRequest::DeactivateVpn(name)) => {
tracing::info!("Deactivating VPN: {}", name);
let network_manager = match NetworkManager::new(&conn).await {
Ok(n) => n,
Err(e) => {
tracing::error!("Failed to connect to NetworkManager: {:?}", e);
_ = output
.send(NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::DeactivateVpn(name),
success: false,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
})
.await;
return State::Waiting(conn, rx);
}
};

let mut success = false;

// Find and deactivate the active VPN connection by name
if let Ok(active_connections) = network_manager.active_connections().await {
for active_conn in active_connections {
if let Ok(conn_id) = active_conn.id().await {
if conn_id == name && active_conn.vpn().await.unwrap_or(false) {
match network_manager.deactivate_connection(&active_conn).await {
Ok(_) => {
tracing::info!("Successfully deactivated VPN: {}", name);
success = true;
break;
}
Err(e) => {
tracing::error!("Failed to deactivate VPN {}: {:?}", name, e);
}
}
}
}
}
}

if !success {
tracing::warn!("Active VPN connection '{}' not found or failed to deactivate", name);
}

let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
_ = output
.send(NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::DeactivateVpn(name),
success,
state,
})
.await;
}
_ => {
return State::Finished;
}
Expand Down Expand Up @@ -363,6 +482,8 @@ pub enum NetworkManagerRequest {
},
Forget(String, HwAddress),
Reload,
ActivateVpn(String), // UUID of VPN connection to activate
DeactivateVpn(String), // Name of active VPN connection to deactivate
}

#[derive(Debug, Clone)]
Expand All @@ -387,6 +508,7 @@ pub struct NetworkManagerState {
pub wireless_access_points: Vec<AccessPoint>,
pub active_conns: Vec<ActiveConnectionInfo>,
pub known_access_points: Vec<AccessPoint>,
pub available_vpns: Vec<VpnConnection>,
pub wifi_enabled: bool,
pub airplane_mode: bool,
pub connectivity: NmConnectivityState,
Expand All @@ -398,6 +520,7 @@ impl Default for NetworkManagerState {
wireless_access_points: Vec::new(),
active_conns: Vec::new(),
known_access_points: Vec::new(),
available_vpns: Vec::new(),
wifi_enabled: false,
airplane_mode: false,
connectivity: NmConnectivityState::Unknown,
Expand Down Expand Up @@ -500,6 +623,9 @@ impl NetworkManagerState {
self_.known_access_points = known_access_points;
self_.connectivity = network_manager.connectivity().await?;

// Load available VPN connections
self_.available_vpns = load_vpn_connections(conn).await.unwrap_or_default();

Ok(self_)
}

Expand All @@ -508,6 +634,7 @@ impl NetworkManagerState {
self.active_conns = Vec::new();
self.known_access_points = Vec::new();
self.wireless_access_points = Vec::new();
self.available_vpns = Vec::new();
}

async fn connect_wifi<'a>(
Expand Down