Merge branch 'master' into bulgarian

This commit is contained in:
Jeremy Soller 2026-01-27 10:38:05 -07:00 committed by GitHub
commit 055befb30a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
272 changed files with 4273 additions and 4972 deletions

View file

@ -1,18 +1,18 @@
[package]
name = "cosmic-applet-network"
version = "0.1.0"
version = "1.0.2"
edition = "2024"
license = "GPL-3.0-or-later"
[dependencies]
anyhow.workspace = true
async-fn-stream = "0.3"
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" }
cosmic-time.workspace = true
futures.workspace = true
futures-util.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
itertools = "0.14.0"
libcosmic = { workspace = true, features = [
"applet",
"applet-token",
@ -27,3 +27,15 @@ tracing-log.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
zbus.workspace = true
nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings/" }
indexmap = "2.13.0"
secure-string = "0.3.0"
uuid = { version = "1.19.0", features = ["v4"] }
[dependencies.cosmic-settings-network-manager-subscription]
git = "https://github.com/pop-os/cosmic-settings/"
[dependencies.cosmic-settings-airplane-mode-subscription]
git = "https://github.com/pop-os/cosmic-settings/"

View file

@ -12,6 +12,7 @@ Name[de]=Netzwerk
Name[sk]=Sieť
Name[sv]=Nätverk
Name[es]=Red
Name[it]=Rete
Type=Application
Exec=cosmic-applet-network
Terminal=false
@ -22,5 +23,6 @@ Icon=com.system76.CosmicAppletNetwork-symbolic
StartupNotify=true
NoDisplay=true
X-CosmicApplet=true
X-CosmicShrinkable=true
X-CosmicHoverPopup=Auto
X-OverflowPriority=10

View file

@ -1,21 +1,23 @@
network = الشبكة
airplane-mode = وضع الطيران
airplane-mode-on = وضع الطيران مُفعّل
turn-off-airplane-mode = أوقفه لتمكين الواي فاي، البلوتوث، والنطاق العريض المتنقل.
turn-off-airplane-mode = عطّله لتفعيل الواي فاي والبلوتوث وشبكة هاتف محمول.
wifi = واي فاي
identity = الهوية
ipv4 = عنوان IPv4
ipv6 = عنوان IPv6
mac = MAC
megabits-per-second = ميغابت/ثانية
megabits-per-second = م.بت/ث
connected = متصل
connecting = جارٍ الاتصال
connect = توصيل
cancel = إلغاء
connect = اتصل
cancel = ألغِ
settings = إعدادات الشبكة...
visible-wireless-networks = الشبكات اللاسلكية الظاهرة
enter-password = أدخل كلمة المرور أو مفتاح التشفير
enter-password = أدخل كلمة السر أو مفتاح التعمية
router-wps-button = يمكنك أيضًا الاتصال بالضغط على زر "WPS" في جهاز التوجيه
unable-to-connect = لا يمكن الاتصال بالشبكة
check-wifi-connection = تأكد من اتصال الواي فاي بالإنترنت وصحة كلمة المرور
reset = إعادة تعيين
reset = صفّر
gigabits-per-second = ج.بت/ث
terabits-per-second = ت.بت/ث

View file

@ -19,3 +19,4 @@ router-wps-button = Може да се свържете и чрез натиск
unable-to-connect = Свързването към мрежата е неуспешно
check-wifi-connection = Уверете се, че Wi-Fi е свързан с Интернет и паролата е правилна
reset = Връщане на настройките
vpn-connections = ВЧМ връзки

View file

@ -7,16 +7,18 @@ identity = Identita
ipv4 = IPv4 adresa
ipv6 = IPv6 adresa
mac = MAC
megabits-per-second = Mbps
megabits-per-second = Mb/s
connected = Připojeno
connecting = Připojuje se
connecting = Připojování
connect = Připojit
cancel = Zrušit
settings = Nastavení sítě...
visible-wireless-networks = Viditelné bezdrátové sítě
enter-password = Zadejte heslo nebo šifrovací klíč
router-wps-button = Můžete se také připojit stisknutím tlačítka "WPS" na vašem routeru
router-wps-button = Můžete se také připojit stisknutím tlačítka „WPS“ na vašem routeru
unable-to-connect = Nepodařilo se připojit k síti
check-wifi-connection = Ujistěte se, že Wi-Fi má připojení k internetu a heslo je správné
reset = Obnovit
vpn-connections = Připojení VPN
gigabits-per-second = Gb/s
terabits-per-second = Tb/s

View file

@ -8,6 +8,8 @@ ipv4 = IPv4 address
ipv6 = IPv6 address
mac = MAC
megabits-per-second = Mbps
gigabits-per-second = Gbps
terabits-per-second = Tbps
connected = Connected
connecting = Connecting
connect = Connect

View file

@ -8,13 +8,17 @@ ipv6 = Adresse IPv6
mac = MAC
megabits-per-second = Mbps
connected = Connecté
connecting = Connexion en cours
connect = Se connecter
connecting = Connexion
connect = Connecter
cancel = Annuler
settings = Paramètres réseaux...
visible-wireless-networks = Réseaux sans-fil visibles
enter-password = Entrez le mot de passe ou la clé de cryptage
visible-wireless-networks = Réseaux Wi-Fi disponibles
enter-password = Entrez le mot de passe ou la clé de chiffrement
router-wps-button = Vous pouvez aussi vous connecter en appuyant sur le bouton "WPS" du routeur
unable-to-connect = Impossible de se connecter au réseau
check-wifi-connection = Vérifiez que le Wi-Fi est relié à internet et que le mot de passe est correct
check-wifi-connection = Vérifiez que le Wi-Fi est connecté à internet et que le mot de passe est correct
reset = Réinitialiser
identity = Identité
vpn-connections = Connexions VPN
gigabits-per-second = Gbps
terabits-per-second = Tbps

View file

@ -1,6 +1,6 @@
network = Líonra
airplane-mode = Mód Eitleáin
airplane-mode-on = Tá Mód Eitleáin ar siúl
airplane-mode = Mód eitleáin
airplane-mode-on = Tá mód eitleáin ar siúl
turn-off-airplane-mode = Múch é chun Wi-Fi, Bluetooth agus leathanbhanda soghluaiste a chumasú.
wifi = Wi-Fi
ipv4 = Seoladh IPv4
@ -14,9 +14,11 @@ cancel = Cealaigh
settings = Socruithe Líonra...
visible-wireless-networks = Líonraí gan sreang infheicthe
enter-password = Cuir isteach an focal faire nó an eochair chriptiúcháin
router-wps-button = Is féidir leat nascadh freisin trí chnaipe "WPS" an ródaire a bhrú
router-wps-button = Is féidir leat ceangal freisin trí chnaipe "WPS" an ródaire a bhrú
unable-to-connect = Ní féidir ceangal leis an líonra
check-wifi-connection = Cinntigh go bhfuil Wi-Fi ceangailte leis an idirlíon agus go bhfuil an focal faire ceart
reset = Athshocraigh
identity = Céannacht
vpn-connections = Naisc VPN
gigabits-per-second = Gbps
terabits-per-second = Tbps

View file

@ -0,0 +1,3 @@
connect = התחברות
gigabits-per-second = Gbps
terabits-per-second = Tbps

View file

@ -1,18 +1,18 @@
network = Hálózat
airplane-mode = Repülőgép üzemmód
airplane-mode-on = A repülőgép üzemmód be van kapcsolva
airplane-mode = Repülőgép-üzemmód
airplane-mode-on = A repülőgép-üzemmód be van kapcsolva
turn-off-airplane-mode = Kapcsold ki a Wi-Fi, Bluetooth és mobil szélessáv engedélyezéséhez.
wifi = Wi-Fi
identity = Azonosító
ipv4 = IPv4 cím
ipv6 = IPv6 cím
ipv4 = IPv4-cím
ipv6 = IPv6-cím
mac = MAC
megabits-per-second = Mbps
connected = Csatlakoztatva
connecting = Csatlakozás
connected = Csatlakozva
connecting = Csatlakozás
connect = Csatlakozás
cancel = Mégse
settings = Hálózati beállítások...
settings = Hálózati beállítások
visible-wireless-networks = Látható vezeték nélküli hálózatok
enter-password = Add meg a jelszót vagy a titkosítási kulcsot
router-wps-button = A router „WPS” gombjának megnyomásával is csatlakozhatsz
@ -20,3 +20,5 @@ unable-to-connect = Nem lehet csatlakozni a hálózathoz
check-wifi-connection = Győződj meg arról, hogy a Wi-Fi csatlakozik az internethez, és a jelszó helyes
reset = Visszaállítás
vpn-connections = VPN-kapcsolatok
gigabits-per-second = Gbps
terabits-per-second = Tbps

View file

@ -0,0 +1,24 @@
cancel = Бас тарту
connect = Байланысу
connected = Қосылды
identity = Сәйкестендіру
wifi = Wi-Fi
network = Желі
airplane-mode = Ұшақ режимі
airplane-mode-on = Ұшақ режимі қосулы
turn-off-airplane-mode = Wi-Fi, Bluetooth және мобильді кең жолақты байланысты іске қосу үшін өшіріңіз.
ipv4 = IPv4 адресі
ipv6 = IPv6 адресі
mac = MAC
megabits-per-second = Мбит/с
gigabits-per-second = Гбит/с
terabits-per-second = Тбит/с
connecting = Қосылуда
settings = Желі баптаулары...
visible-wireless-networks = Көрінетін сымсыз желілер
vpn-connections = VPN қосылымдары
enter-password = Парольді немесе шифрлау кілтін енгізіңіз
router-wps-button = Сондай-ақ роутердегі "WPS" батырмасын басу арқылы қосылуға болады
unable-to-connect = Желіге қосылу мүмкін емес
check-wifi-connection = Wi-Fi интернетке қосылғанына және пароль дұрыс екеніне көз жеткізіңіз
reset = Қалпына келтіру

View file

@ -12,7 +12,13 @@ connecting = 연결 중
connect = 연결
cancel = 취소
visible-wireless-networks = 이용 가능한 무선 네트워크
enter-password = 비밀번호나 암호화 키 입력
router-wps-button = 라우터의 "WPS" 버튼을 눌러서 연결하실 수도 있습니다
unable-to-connect = 네트워크에 연결할 수 없음
check-wifi-connection = Wi-Fi가 인터넷에 연결되어 있거나 비밀번호가 올바른지 확인하십시오
enter-password = 암호 또는 암호화 키 입력
router-wps-button = 라우터의 "WPS" 버튼을 눌러서 연결할 수도 있습니다
unable-to-connect = 네트워크에 연결할 수 없습니다
check-wifi-connection = Wi-Fi가 인터넷에 연결되어 있고 암호가 일치하는지 확인하세요
reset = 초기화
identity = 식별자
vpn-connections = VPN 연결
settings = 네트워크 설정...
gigabits-per-second = Gbps
terabits-per-second = Tbps

View file

@ -0,0 +1,24 @@
connect = Prijungti
check-wifi-connection = Įsitikinkite, kad Wi-Fi yra prijungtas prie interneto ir slaptažodis yra teisingas
reset = Atkurti
visible-wireless-networks = Matomi bevieliai ryšiai
enter-password = Įveskite slaptažodį ar šifravimo raktą
ipv6 = IPv6 adresas
airplane-mode-on = Įjungtas lėktuvo režimas
connecting = Jungiamasi
airplane-mode = Lėktuvo rėžimas
wifi = Wi-Fi
ipv4 = IPv4 adresas
identity = Tapatybė
unable-to-connect = Nepavyko prisijungti prie tinklo
turn-off-airplane-mode = Išjunkite, kad būtų galima įjungti Wi-Fi, Bluetooth ir mobilųjų ryšį.
network = Tinklas
cancel = Atšaukti
connected = Prisijungta
vpn-connections = VPN ryšiai
mac = MAC adresas
router-wps-button = Taip pat galite prisijungti paspausdami „WPS“ mygtuką esantį ant maršrutizatoriaus
settings = Tinklo nustatymai...
megabits-per-second = Mbps
gigabits-per-second = Gbps
terabits-per-second = Tbps

View file

@ -1,7 +1,7 @@
network = Netwerk
airplane-mode = Vliegtuigmodus
airplane-mode-on = Vliegtuigmodus ingeschakeld
turn-off-airplane-mode = Schakel vliegtuigmodus uit om wifi, bluetooth en mobiele gegevens te activeren.
turn-off-airplane-mode = Schakel vliegtuigmodus uit zodat je wifi, bluetooth en mobiele gegevens weer kan gebruiken.
wifi = Wifi
identity = Identiteit
ipv4 = IPv4-adres
@ -9,13 +9,14 @@ ipv6 = IPv6-adres
mac = MAC
megabits-per-second = Mbit/s
connected = Verbonden
connecting = Verbinding maken...
connecting = Verbinding maken
connect = Verbind
cancel = Annuleren
settings = Netwerkinstellingen...
cancel = Annuleer
settings = Netwerkinstellingen
visible-wireless-networks = Zichtbare draadloze netwerken
enter-password = Voer het wachtwoord of encryptiesleutel in
router-wps-button = U kunt ook verbinden door op de "WPS"-knop op de router te drukken
unable-to-connect = Kon niet met het netwerk verbinden
check-wifi-connection = Zorg ervoor dat de wifi verbonden is met het internet en dat het wachtwoord correct is
reset = Resetten
enter-password = Voer wachtwoord of encryptiesleutel in
router-wps-button = Je kunt ook verbinden door op de “WPS”-knop op de router te drukken
unable-to-connect = Kon niet met netwerk verbinden
check-wifi-connection = Controleer of de wifi met het internet verbonden is en of het wachtwoord juist is
reset = Reset
vpn-connections = VPN-verbindingen

View file

@ -20,3 +20,5 @@ unable-to-connect = Nie można połączyć się z siecią
check-wifi-connection = Upewnij się, że Wi-Fi ma połączenie z internetem oraz hasło jest poprawne
reset = Reset
vpn-connections = Połączenia VPN
gigabits-per-second = Gbps
terabits-per-second = Tbps

View file

@ -20,3 +20,5 @@ unable-to-connect = Kan inte ansluta till nätverket
check-wifi-connection = Säkerställ att Wi-Fi är anslutet till internet och att lösenordet är korrekt
reset = Återställ
vpn-connections = VPN-anslutningar
gigabits-per-second = Gbps
terabits-per-second = Tbps

View file

@ -3,8 +3,8 @@ airplane-mode = Режим польоту
airplane-mode-on = Режим польоту увімкнено
turn-off-airplane-mode = Вимкніть, щоб увімкнути Wi-Fi, Bluetooth і мобільний інтернет.
wifi = Wi-Fi
ipv4 = Адреса IPv4
ipv6 = Адреса IPv6
ipv4 = IPv4-адреса
ipv6 = IPv6-адреса
mac = MAC
megabits-per-second = Мбіт/с
connected = Підключено
@ -14,9 +14,11 @@ cancel = Скасувати
settings = Налаштування мережі...
visible-wireless-networks = Видимі бездротові мережі
enter-password = Введіть пароль або ключ шифрування
router-wps-button = Також можна підключитися, натиснувши кнопку "WPS" на маршрутизаторі
router-wps-button = Також можна підключитися, натиснувши кнопку «WPS» на маршрутизаторі
unable-to-connect = Не вдалося підключитися до мережі
check-wifi-connection = Переконайтеся, що Wi-Fi підключено до Інтернету, а пароль — правильний
reset = Скинути
identity = Ідентичність
vpn-connections = VPN-з'єднання
gigabits-per-second = Гбіт/с
terabits-per-second = Тбіт/с

View file

@ -1,7 +1,7 @@
network = 网络
airplane-mode = 飞行模式
airplane-mode-on = 飞行模式已开启
turn-off-airplane-mode = 关闭飞行模式启用 Wi-Fi、蓝牙和移动宽带。
turn-off-airplane-mode = 关闭飞行模式即可启用 Wi-Fi、蓝牙和移动宽带。
wifi = Wi-Fi
identity = 标识
ipv4 = IPv4 地址
@ -15,8 +15,10 @@ cancel = 取消
settings = 网络设置...
visible-wireless-networks = 可见的无线网络
enter-password = 输入密码或加密密钥
router-wps-button = 您也可以通过路由器上的“WPS”按钮连接
router-wps-button = 您也可以通过路由器上的“WPS”按钮连接
unable-to-connect = 无法连接到网络
check-wifi-connection = 确保 Wi-Fi 已连接互联网并且密码正确
check-wifi-connection = 确保 Wi-Fi 已连接互联网并且密码正确
reset = 重置
vpn-connections = VPN连接
vpn-connections = VPN 连接
gigabits-per-second = Gbps
terabits-per-second = Tbps

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@
mod app;
mod config;
mod localize;
mod network_manager;
mod utils;
use crate::localize::localize;

View file

@ -1,66 +0,0 @@
use super::{NetworkManagerEvent, NetworkManagerState};
use cosmic::{
iced::{self, Subscription},
iced_futures::stream,
};
use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::{SinkExt, StreamExt};
use std::{fmt::Debug, hash::Hash};
use zbus::Connection;
pub fn active_conns_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
conn: Connection,
) -> iced::Subscription<NetworkManagerEvent> {
let initial = State::Continue(conn);
Subscription::run_with_id(
id,
stream::channel(50, move |mut output| {
let mut state = initial;
async move {
loop {
state = start_listening(state, &mut output).await;
}
}
}),
)
}
#[derive(Debug, Clone)]
pub enum State {
Continue(Connection),
Error,
}
async fn start_listening(
state: State,
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
) -> State {
let conn = match state {
State::Continue(conn) => conn,
State::Error => iced::futures::future::pending().await,
};
let network_manager = match NetworkManager::new(&conn).await {
Ok(n) => n,
Err(why) => {
tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager");
return State::Error;
}
};
let mut active_conns_changed = network_manager.receive_active_connections_changed().await;
active_conns_changed.next().await;
while let (Some(_change), ()) = tokio::join!(
active_conns_changed.next(),
tokio::time::sleep(tokio::time::Duration::from_secs(1))
) {
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
_ = output
.send(NetworkManagerEvent::ActiveConns(new_state))
.await;
}
State::Continue(conn)
}

View file

@ -1,48 +0,0 @@
// 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)
}

View file

@ -1,106 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use cosmic_dbus_networkmanager::{
device::wireless::WirelessDevice,
interface::{
access_point::AccessPointProxy,
enums::{ApFlags, ApSecurityFlags, DeviceState},
},
};
use futures_util::StreamExt;
use rustc_hash::FxHashMap;
use std::collections::HashMap;
use zbus::zvariant::ObjectPath;
use super::hw_address::HwAddress;
pub async fn handle_wireless_device(
device: WirelessDevice<'_>,
hw_address: Option<String>,
) -> zbus::Result<Vec<AccessPoint>> {
device.request_scan(HashMap::new()).await?;
let mut scan_changed = device.receive_last_scan_changed().await;
if let Some(t) = scan_changed.next().await {
if let Ok(-1) = t.get().await {
eprintln!("scan errored");
return Ok(Vec::new());
}
}
let access_points = device.get_access_points().await?;
let state: DeviceState = device
.upcast()
.await
.and_then(|dev| dev.cached_state())
.unwrap_or_default()
.map_or(DeviceState::Unknown, |s| s.into());
// Sort by strength and remove duplicates
let mut aps = FxHashMap::<String, AccessPoint>::default();
for ap in access_points {
let ssid = String::from_utf8_lossy(ap.ssid().await?.as_slice()).into_owned();
let wps_push = ap.flags().await?.contains(ApFlags::WPS_PBC);
let strength = ap.strength().await?;
if let Some(access_point) = aps.get(&ssid) {
if access_point.strength > strength {
continue;
}
}
let proxy: &AccessPointProxy = &ap;
let Ok(flags) = ap.rsn_flags().await else {
continue;
};
let network_type = if flags.intersects(ApSecurityFlags::KEY_MGMT_802_1X) {
NetworkType::EAP
} else if flags.intersects(ApSecurityFlags::KEY_MGMTPSK) {
NetworkType::PSK
} else if flags.is_empty() {
NetworkType::Open
} else {
continue;
};
aps.insert(
ssid.clone(),
AccessPoint {
ssid,
strength,
state,
working: false,
path: ap.inner().path().to_owned(),
hw_address: hw_address
.as_ref()
.and_then(|str_addr| HwAddress::from_str(str_addr))
.unwrap_or_default(),
wps_push,
network_type,
},
);
}
let mut aps = aps.into_values().collect::<Vec<_>>();
aps.sort_unstable_by_key(|ap| ap.strength);
Ok(aps)
}
#[derive(Debug, Clone)]
pub struct AccessPoint {
pub ssid: String,
pub strength: u8,
pub state: DeviceState,
pub working: bool,
pub path: ObjectPath<'static>,
pub hw_address: HwAddress,
pub wps_push: bool,
pub network_type: NetworkType,
}
// TODO do we want to support eap methods other than peap in the applet?
// Then we'd need a dropdown for the eap method,
// and tls requires a cert instead of a password
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, Copy)]
pub enum NetworkType {
Open,
PSK,
EAP,
}

View file

@ -1,172 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use cosmic_dbus_networkmanager::{
active_connection::ActiveConnection, device::SpecificDevice,
interface::enums::ActiveConnectionState,
};
use std::net::Ipv4Addr;
use super::hw_address::HwAddress;
pub async fn active_connections(
active_connections: Vec<ActiveConnection<'_>>,
) -> zbus::Result<Vec<ActiveConnectionInfo>> {
let mut info = Vec::<ActiveConnectionInfo>::with_capacity(active_connections.len());
for connection in active_connections {
let ipv4 = connection
.ip4_config()
.await?
.address_data()
.await
.unwrap_or_default();
let addresses: Vec<_> = ipv4.iter().map(|d| d.address).collect();
let state = connection
.state()
.await
.unwrap_or(ActiveConnectionState::Unknown);
if connection.vpn().await.unwrap_or_default() {
info.push(ActiveConnectionInfo::Vpn {
name: connection.id().await?,
ip_addresses: addresses.clone(),
});
continue;
}
for device in connection.devices().await.unwrap_or_default() {
match device
.downcast_to_device()
.await
.ok()
.and_then(|inner| inner)
{
Some(SpecificDevice::Wired(wired_device)) => {
info.push(ActiveConnectionInfo::Wired {
name: connection.id().await?,
hw_address: HwAddress::from_str(&wired_device.hw_address().await?)
.unwrap_or_default(),
speed: wired_device.speed().await?,
ip_addresses: addresses.clone(),
});
}
Some(SpecificDevice::Wireless(wireless_device)) => {
if let Ok(access_point) = wireless_device.active_access_point().await {
info.push(ActiveConnectionInfo::WiFi {
name: String::from_utf8_lossy(&access_point.ssid().await?).into_owned(),
ip_addresses: addresses.clone(),
hw_address: HwAddress::from_str(&wireless_device.hw_address().await?)
.unwrap_or_default(),
state,
strength: access_point.strength().await.unwrap_or_default(),
});
}
}
Some(SpecificDevice::WireGuard(_)) => {
info.push(ActiveConnectionInfo::Vpn {
name: connection.id().await?,
ip_addresses: addresses.clone(),
});
}
_ => {}
}
}
}
info.sort_unstable();
Ok(info)
}
#[derive(Debug, Clone)]
pub enum ActiveConnectionInfo {
Wired {
name: String,
hw_address: HwAddress,
speed: u32,
ip_addresses: Vec<Ipv4Addr>,
},
WiFi {
name: String,
ip_addresses: Vec<Ipv4Addr>,
hw_address: HwAddress,
state: ActiveConnectionState,
strength: u8,
},
Vpn {
name: String,
ip_addresses: Vec<Ipv4Addr>,
},
}
impl ActiveConnectionInfo {
pub fn name(&self) -> String {
match &self {
Self::Wired { name, .. } | Self::WiFi { name, .. } | Self::Vpn { name, .. } => {
name.clone()
}
}
}
pub fn hw_address(&self) -> HwAddress {
match &self {
Self::Wired { hw_address, .. } | Self::WiFi { hw_address, .. } => *hw_address,
Self::Vpn { .. } => HwAddress::default(),
}
}
}
impl std::cmp::Ord for ActiveConnectionInfo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(Self::Vpn { .. }, Self::Wired { .. } | Self::WiFi { .. })
| (Self::Wired { .. }, Self::WiFi { .. }) => std::cmp::Ordering::Less,
(Self::WiFi { .. }, Self::Wired { .. } | Self::Vpn { .. })
| (Self::Wired { .. }, Self::Vpn { .. }) => std::cmp::Ordering::Greater,
(Self::Vpn { name: n1, .. }, Self::Vpn { name: n2, .. })
| (Self::Wired { name: n1, .. }, Self::Wired { name: n2, .. })
| (Self::WiFi { name: n1, .. }, Self::WiFi { name: n2, .. }) => n1.cmp(n2),
}
}
}
impl std::cmp::Eq for ActiveConnectionInfo {}
impl std::cmp::PartialOrd for ActiveConnectionInfo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::cmp::PartialEq for ActiveConnectionInfo {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Self::Wired {
name: n1,
hw_address: a1,
..
},
Self::Wired {
name: n2,
hw_address: a2,
..
},
)
| (
Self::WiFi {
name: n1,
hw_address: a1,
..
},
Self::WiFi {
name: n2,
hw_address: a2,
..
},
) => n1 == n2 && a1 == a2,
(Self::Vpn { name: n1, .. }, Self::Vpn { name: n2, .. }) => n1 == n2,
_ => false,
}
}
}

View file

@ -1,65 +0,0 @@
use super::{NetworkManagerEvent, NetworkManagerState};
use cosmic::iced::{self, Subscription, stream};
use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::{SinkExt, StreamExt};
use std::{fmt::Debug, hash::Hash};
use zbus::Connection;
pub fn devices_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
has_popup: bool,
conn: Connection,
) -> iced::Subscription<NetworkManagerEvent> {
let initial = State::Continue(conn);
Subscription::run_with_id(
(id, has_popup),
stream::channel(50, move |mut output| {
let mut state = initial.clone();
async move {
loop {
state = start_listening(state, has_popup, &mut output).await;
}
}
}),
)
}
#[derive(Debug, Clone)]
pub enum State {
Continue(Connection),
Error,
}
async fn start_listening(
state: State,
has_popup: bool,
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
) -> State {
let conn = match state {
State::Continue(conn) => conn,
State::Error => iced::futures::future::pending().await,
};
let network_manager = match NetworkManager::new(&conn).await {
Ok(n) => n,
Err(why) => {
tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager");
return State::Error;
}
};
let mut devices_changed = network_manager.receive_devices_changed().await;
let secs = if has_popup { 4 } else { 60 };
while let (Some(_change), ()) = tokio::join!(
devices_changed.next(),
tokio::time::sleep(tokio::time::Duration::from_secs(secs))
) {
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
_ = output
.send(NetworkManagerEvent::WirelessAccessPoints(new_state))
.await;
}
State::Continue(conn)
}

View file

@ -1,36 +0,0 @@
use std::fmt::Write;
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug, PartialOrd, Ord)]
pub struct HwAddress {
address: u64,
}
impl HwAddress {
pub fn from_str(arg: &str) -> Option<Self> {
let columnless_vec = arg.split(':').collect::<Box<[_]>>();
if columnless_vec.len() * 3 - 1 != arg.len() {
return None;
}
for byte in &columnless_vec {
if byte.len() != 2 {
return None;
}
}
u64::from_str_radix(columnless_vec.join("").as_str(), 16)
.ok()
.map(|address| HwAddress { address })
}
}
impl std::fmt::Display for HwAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (index, c) in format!("{:x}", self.address).char_indices() {
if index != 0 && index % 2 == 0 {
f.write_char(':')?;
}
f.write_char(c)?;
}
Ok(())
}
}

View file

@ -1,822 +0,0 @@
pub mod active_conns;
pub mod available_vpns;
pub mod available_wifi;
pub mod current_networks;
pub mod devices;
pub mod hw_address;
pub mod wireless_enabled;
use std::{collections::HashMap, fmt::Debug, time::Duration};
use available_wifi::NetworkType;
use cosmic::{
iced::{self, Subscription},
iced_futures::stream,
};
use cosmic_dbus_networkmanager::{
active_connection::ActiveConnection,
device::SpecificDevice,
interface::{
active_connection::ActiveConnectionProxy,
enums::{self, ActiveConnectionState, DeviceType, NmConnectivityState},
},
nm::NetworkManager,
settings::{NetworkManagerSettings, connection::Settings},
};
use futures::{
SinkExt, StreamExt,
channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded},
};
use hw_address::HwAddress;
use tokio::process::Command;
use zbus::{
Connection,
zvariant::{self, Value},
};
use self::{
available_vpns::{VpnConnection, load_vpn_connections},
available_wifi::{AccessPoint, handle_wireless_device},
current_networks::{ActiveConnectionInfo, active_connections},
};
// In some distros, rfkill is only in sbin, which isn't normally in PATH
// TODO: Directly access `/dev/rfkill`
fn rfkill_path_var() -> std::ffi::OsString {
let mut path = std::env::var_os("PATH").unwrap_or_default();
path.push(":/usr/sbin");
path
}
#[derive(Debug)]
pub enum State {
Ready,
Waiting(Connection, UnboundedReceiver<NetworkManagerRequest>),
Finished,
}
pub fn network_manager_subscription<I: Copy + Debug + std::hash::Hash + 'static>(
id: I,
) -> iced::Subscription<NetworkManagerEvent> {
Subscription::run_with_id(
id,
stream::channel(50, |mut output| async move {
let mut state = State::Ready;
loop {
state = start_listening(state, &mut output).await;
}
}),
)
}
async fn start_listening(
state: State,
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
) -> State {
match state {
State::Ready => {
let Ok(conn) = Connection::system().await else {
return State::Finished;
};
let (tx, rx) = unbounded();
let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
if output
.send(NetworkManagerEvent::Init {
conn: conn.clone(),
sender: tx,
state: nm_state,
})
.await
.is_ok()
{
State::Waiting(conn, rx)
} else {
State::Finished
}
}
State::Waiting(conn, mut rx) => {
let Ok(network_manager) = NetworkManager::new(&conn).await else {
return State::Finished;
};
match rx.next().await {
Some(NetworkManagerRequest::Disconnect(ssid, hw_address)) => {
let mut success = false;
for c in network_manager
.active_connections()
.await
.unwrap_or_default()
{
if c.id().await.unwrap_or_default() != ssid {
continue;
}
let mut is_there_device = false;
for device in c.devices().await.unwrap_or_default() {
if HwAddress::from_str(device.hw_address().await.as_ref().unwrap())
== Some(hw_address)
{
is_there_device = true;
}
}
if is_there_device
&& network_manager.deactivate_connection(&c).await.is_ok()
{
success = true;
if let Ok(ActiveConnectionState::Deactivated) = c.state().await {
break;
} else {
let mut changed = c.receive_state_changed().await;
_ = tokio::time::timeout(Duration::from_secs(5), async move {
loop {
if let Some(next) = changed.next().await {
if let Ok(ActiveConnectionState::Deactivated) =
next.get().await.map(ActiveConnectionState::from)
{
break;
}
}
}
})
.await;
}
break;
}
}
_ = output
.send(NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Disconnect(ssid.clone(), hw_address),
success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
})
.await;
}
Some(NetworkManagerRequest::SetAirplaneMode(airplane_mode)) => {
// wifi
let mut success = network_manager
.set_wireless_enabled(!airplane_mode)
.await
.is_ok();
// bluetooth
success = success
&& Command::new("rfkill")
.env("PATH", rfkill_path_var())
.arg(if airplane_mode { "block" } else { "unblock" })
.arg("bluetooth")
.output()
.await
.is_ok();
let mut state = NetworkManagerState::new(&conn).await.unwrap_or_default();
state.airplane_mode = if success {
airplane_mode
} else {
!airplane_mode
};
if state.airplane_mode {
state.wifi_enabled = false;
}
_ = output
.send(NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::SetAirplaneMode(airplane_mode),
success,
state,
})
.await;
}
Some(NetworkManagerRequest::SetWiFi(enabled)) => {
let success = network_manager.set_wireless_enabled(enabled).await.is_ok();
let mut state = NetworkManagerState::new(&conn).await.unwrap_or_default();
state.wifi_enabled = if success { enabled } else { !enabled };
let response = NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::SetWiFi(enabled),
success,
state,
};
_ = output.send(response).await;
}
Some(NetworkManagerRequest::Authenticate {
ssid,
identity,
password,
hw_address,
}) => {
let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
let mut success = true;
let err = nm_state
.connect_wifi(
&conn,
&ssid,
identity.as_deref(),
Some(&password),
hw_address,
)
.await;
if let Err(err) = err {
success = false;
tracing::error!("{:?}", &err);
}
_ = output
.send(NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Authenticate {
ssid: ssid.clone(),
identity: identity.clone(),
password: password.clone(),
hw_address,
},
success,
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
})
.await;
}
Some(NetworkManagerRequest::SelectAccessPoint(ssid, hw_address, network_type)) => {
if matches!(network_type, NetworkType::Open) {
attempt_wifi_connection(&conn, ssid, hw_address, network_type, output)
.await;
} else {
// For secured networks, check if we have saved credentials
if !has_saved_wifi_credentials(&conn, &ssid).await {
return State::Waiting(conn, rx);
}
// We have saved credentials, attempt connection
attempt_wifi_connection(&conn, ssid, hw_address, network_type, output)
.await;
}
}
Some(NetworkManagerRequest::Reload) => {
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
_ = output
.send(NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Reload,
success: true,
state,
})
.await;
}
Some(NetworkManagerRequest::Forget(ssid, hw_address)) => {
let s = NetworkManagerSettings::new(&conn).await.unwrap();
let known_conns = s.list_connections().await.unwrap_or_default();
let mut success = false;
for c in known_conns {
let settings = c.get_settings().await.ok().unwrap_or_default();
let s = Settings::new(settings);
if s.wifi
.as_ref()
.and_then(|w| w.ssid.as_deref())
.is_some_and(|s| std::str::from_utf8(s).is_ok_and(|s| s == ssid))
{
// todo most likely we can here forget ssid from wrong hw_address
_ = c.delete().await;
success = true;
break;
}
}
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
_ = output
.send(NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::Forget(ssid.clone(), hw_address),
success,
state,
})
.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;
}
}
State::Waiting(conn, rx)
}
State::Finished => iced::futures::future::pending().await,
}
}
async fn has_saved_wifi_credentials(conn: &Connection, ssid: &str) -> bool {
let Ok(nm_settings) = NetworkManagerSettings::new(conn).await else {
return false;
};
let known_conns = nm_settings.list_connections().await.unwrap_or_default();
for connection in known_conns {
if let Ok(settings) = connection.get_settings().await {
let settings = Settings::new(settings);
if let Some(saved_ssid) = settings
.wifi
.and_then(|w| w.ssid)
.and_then(|ssid| String::from_utf8(ssid).ok())
{
if saved_ssid == ssid {
return true;
}
}
}
}
false
}
async fn attempt_wifi_connection(
conn: &Connection,
ssid: String,
hw_address: HwAddress,
network_type: NetworkType,
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
) {
let state = NetworkManagerState::new(conn).await.unwrap_or_default();
let success = if let Err(err) = state
.connect_wifi(conn, ssid.as_ref(), None, None, hw_address)
.await
{
tracing::error!("Failed to connect to access point: {:?}", err);
false
} else {
true
};
_ = output
.send(NetworkManagerEvent::RequestResponse {
req: NetworkManagerRequest::SelectAccessPoint(ssid, hw_address, network_type),
success,
state: NetworkManagerState::new(conn).await.unwrap_or_default(),
})
.await;
}
#[derive(Debug, Clone)]
pub enum NetworkManagerRequest {
SetAirplaneMode(bool),
SetWiFi(bool),
SelectAccessPoint(String, HwAddress, NetworkType),
Disconnect(String, HwAddress),
Authenticate {
ssid: String,
identity: Option<String>,
password: String,
hw_address: HwAddress,
},
Forget(String, HwAddress),
Reload,
ActivateVpn(String), // UUID of VPN connection to activate
DeactivateVpn(String), // Name of active VPN connection to deactivate
}
#[derive(Debug, Clone)]
pub enum NetworkManagerEvent {
RequestResponse {
req: NetworkManagerRequest,
state: NetworkManagerState,
success: bool,
},
Init {
conn: Connection,
sender: UnboundedSender<NetworkManagerRequest>,
state: NetworkManagerState,
},
WiFiEnabled(NetworkManagerState),
WirelessAccessPoints(NetworkManagerState),
ActiveConns(NetworkManagerState),
}
#[derive(Debug, Clone)]
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,
}
impl Default for NetworkManagerState {
fn default() -> Self {
Self {
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,
}
}
}
impl NetworkManagerState {
pub async fn new(conn: &Connection) -> anyhow::Result<Self> {
let network_manager = NetworkManager::new(conn).await?;
let mut self_ = Self::default();
// airplane mode
let airplaine_mode = Command::new("rfkill")
.env("PATH", rfkill_path_var())
.arg("list")
.arg("bluetooth")
.output()
.await?;
let airplane_mode = std::str::from_utf8(&airplaine_mode.stdout).unwrap_or_default();
self_.wifi_enabled = network_manager.wireless_enabled().await.unwrap_or_default();
self_.airplane_mode = airplane_mode.contains("Soft blocked: yes") && !self_.wifi_enabled;
let s = NetworkManagerSettings::new(conn).await?;
_ = s.load_connections(&[]).await;
let known_conns = s.list_connections().await.unwrap_or_default();
let active_conns = active_connections(
network_manager
.active_connections()
.await
.unwrap_or_default(),
)
.await
.unwrap_or_default();
// active_conns.sort(); active_connections should have already sorted the vector
let devices = network_manager.devices().await.ok().unwrap_or_default();
let wireless_access_point_futures: Vec<_> = devices
.into_iter()
.map(|device| async move {
if let Ok(Some(SpecificDevice::Wireless(wireless_device))) =
device.downcast_to_device().await
{
handle_wireless_device(wireless_device, device.hw_address().await.ok())
.await
.unwrap_or_default()
} else {
Vec::new()
}
})
.collect();
let mut wireless_access_points = Vec::with_capacity(wireless_access_point_futures.len());
for f in wireless_access_point_futures {
let mut access_points = f.await;
wireless_access_points.append(&mut access_points);
}
let mut known_ssid = Vec::with_capacity(known_conns.len());
for c in known_conns {
let Ok(s) = c.get_settings().await else {
tracing::info!("Failed to get settings for known connection");
continue;
};
let s = Settings::new(s);
if let Some(cur_ssid) = s
.wifi
.clone()
.and_then(|w| w.ssid)
.and_then(|ssid| String::from_utf8(ssid).ok())
{
known_ssid.push(cur_ssid);
}
}
let known_access_points: Vec<_> = wireless_access_points
.iter()
.filter(|a| {
known_ssid.contains(&a.ssid)
&& !active_conns
.iter()
.any(|ac| ac.name() == a.ssid && ac.hw_address() == a.hw_address)
})
.cloned()
.collect();
wireless_access_points.sort_by(|a, b| b.strength.cmp(&a.strength));
self_.wireless_access_points = wireless_access_points;
for ap in &self_.wireless_access_points {
tracing::info!(
"AP ssid: {},\ttype: {:?},\tworking: {},\tstate: {:?}",
ap.ssid,
ap.network_type,
ap.working,
ap.state
);
}
self_.active_conns = active_conns;
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_)
}
#[allow(dead_code)]
pub fn clear(&mut self) {
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(
&self,
conn: &Connection,
ssid: &str,
identity: Option<&str>,
password: Option<&str>,
hw_address: HwAddress,
) -> anyhow::Result<()> {
let nm = NetworkManager::new(conn).await?;
for c in nm.active_connections().await.unwrap_or_default() {
if self.wireless_access_points.iter().any(|w| {
c.cached_id()
.is_ok_and(|opt| opt.is_some_and(|id| id == w.ssid))
&& w.hw_address == hw_address
}) {
_ = nm.deactivate_connection(&c).await;
}
}
let Some(ap) = self
.wireless_access_points
.iter()
.find(|ap| ap.ssid == ssid && ap.hw_address == hw_address)
else {
return Err(anyhow::anyhow!("Access point not found"));
};
let mut conn_settings: HashMap<&str, HashMap<&str, zvariant::Value>> = HashMap::from([
(
"802-11-wireless",
HashMap::from([("ssid", Value::Array(ssid.as_bytes().into()))]),
),
(
"connection",
HashMap::from([
("id", Value::Str(ssid.into())),
("type", Value::Str("802-11-wireless".into())),
]),
),
]);
if let Some(identity) = identity {
conn_settings.insert(
"802-1x",
HashMap::from([
("identity", Value::Str(identity.into())),
// most common default
("eap", Value::Array(["peap"].as_slice().into())),
// most common default
("phase2-auth", Value::Str("mschapv2".into())),
("password", Value::Str(password.unwrap_or("").into())),
]),
);
let wireless = conn_settings.get_mut("802-11-wireless").unwrap();
wireless.insert("security", Value::Str("802-11-wireless-security".into()));
wireless.insert("mode", Value::Str("infrastructure".into()));
conn_settings.insert(
"802-11-wireless-security",
HashMap::from([("key-mgmt", Value::Str("wpa-eap".into()))]),
);
} else if let Some(pass) = password {
conn_settings.insert(
"802-11-wireless-security",
HashMap::from([
("psk", Value::Str(pass.into())),
("key-mgmt", Value::Str("wpa-psk".into())),
]),
);
}
let devices = nm.devices().await?;
for device in devices {
let device_hw_address = device
.hw_address()
.await
.ok()
.and_then(|device_address| HwAddress::from_str(&device_address))
.unwrap_or_default();
if device_hw_address != hw_address {
continue;
}
if !matches!(
device.device_type().await.unwrap_or(DeviceType::Other),
DeviceType::Wifi
) {
continue;
}
let s = NetworkManagerSettings::new(conn).await?;
let known_conns = s.list_connections().await.unwrap_or_default();
let mut known_conn = None;
for c in known_conns {
let settings = c.get_settings().await.ok().unwrap_or_default();
let s = Settings::new(settings);
// todo try to add hw_address comparing here if it changes anything
if s.wifi
.as_ref()
.and_then(|w| w.ssid.as_deref())
.is_some_and(|s| std::str::from_utf8(s).is_ok_and(|cur_ssid| cur_ssid == ssid))
{
known_conn = Some(c);
break;
}
}
let active_conn = if let Some(known_conn) = known_conn.as_ref() {
// update settings if needed
if password.is_some() {
known_conn.update(conn_settings).await?;
}
nm.activate_connection(known_conn, &device).await?
} else {
let (_, active_conn) = nm
.add_and_activate_connection(conn_settings, device.inner().path(), &ap.path)
.await?;
let dummy = ActiveConnectionProxy::new(conn, active_conn).await?;
let active = ActiveConnectionProxy::builder(conn)
.destination(dummy.inner().destination().to_owned())
.unwrap()
.interface(dummy.inner().interface().to_owned())
.unwrap()
.path(dummy.inner().path().to_owned())
.unwrap()
.build()
.await
.unwrap();
ActiveConnection::from(active)
};
let mut changes = active_conn.receive_state_changed().await;
() = tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let mut count = 5;
loop {
let state = active_conn.state().await;
if let Ok(enums::ActiveConnectionState::Activated) = state {
return Ok(());
} else if let Ok(enums::ActiveConnectionState::Deactivated) = state {
anyhow::bail!("Failed to activate connection");
}
if let Ok(Some(s)) =
tokio::time::timeout(Duration::from_secs(20), changes.next()).await
{
let state = s.get().await.unwrap_or_default().into();
if matches!(state, enums::ActiveConnectionState::Activated) {
return Ok(());
}
}
count -= 1;
if count <= 0 {
anyhow::bail!("Failed to activate connection");
}
}
}
Err(anyhow::anyhow!("No wifi device found"))
}
}

View file

@ -1,62 +0,0 @@
use super::{NetworkManagerEvent, NetworkManagerState};
use cosmic::{
iced::{self, Subscription},
iced_futures::stream,
};
use cosmic_dbus_networkmanager::nm::NetworkManager;
use futures::{SinkExt, StreamExt};
use std::{fmt::Debug, hash::Hash};
use zbus::Connection;
pub fn wireless_enabled_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
conn: Connection,
) -> iced::Subscription<NetworkManagerEvent> {
let initial = State::Continue(conn);
Subscription::run_with_id(
id,
stream::channel(50, move |mut output| {
let mut state = initial;
async move {
loop {
state = start_listening(state, &mut output).await;
}
}
}),
)
}
#[derive(Debug, Clone)]
pub enum State {
Continue(Connection),
Error,
}
async fn start_listening(
state: State,
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
) -> State {
let conn = match state {
State::Continue(conn) => conn,
State::Error => iced::futures::future::pending().await,
};
let network_manager = match NetworkManager::new(&conn).await {
Ok(n) => n,
Err(why) => {
tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager");
return State::Error;
}
};
let mut wireless_enabled_changed = network_manager.receive_wireless_enabled_changed().await;
while let Some(_change) = wireless_enabled_changed.next().await {
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
_ = output
.send(NetworkManagerEvent::WiFiEnabled(new_state))
.await;
}
State::Continue(conn)
}

View file

@ -0,0 +1,18 @@
use futures_util::future::select;
/// Spawn a background tasks and forward its messages
pub fn forward_event_loop<M: 'static + Send, T: Future<Output = ()> + Send + 'static>(
event_loop: impl FnOnce(async_fn_stream::StreamEmitter<M>) -> T + Send + 'static,
) -> (tokio::sync::oneshot::Sender<()>, cosmic::Task<M>) {
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
let task = cosmic::Task::stream(async_fn_stream::fn_stream(|emitter| async move {
select(
std::pin::pin!(cancel_rx),
std::pin::pin!(event_loop(emitter)),
)
.await;
}));
(cancel_tx, task)
}