diff --git a/Assets/settings-default.json b/Assets/settings-default.json index a1157bd07..ff8556b6b 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -378,7 +378,6 @@ "indicatorOpacity": 0.6 }, "network": { - "wifiEnabled": true, "airplaneModeEnabled": false, "bluetoothRssiPollingEnabled": false, "bluetoothRssiPollIntervalMs": 60000, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 435e966a7..4b19074db 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -575,7 +575,6 @@ Singleton { // network property JsonObject network: JsonObject { - property bool wifiEnabled: true property bool airplaneModeEnabled: false property bool bluetoothRssiPollingEnabled: false // Opt-in Bluetooth RSSI polling (uses bluetoothctl) property int bluetoothRssiPollIntervalMs: 60000 // Polling interval in milliseconds for RSSI queries diff --git a/Modules/Bar/Widgets/Network.qml b/Modules/Bar/Widgets/Network.qml index 3f4a2e828..3435b323c 100644 --- a/Modules/Bar/Widgets/Network.qml +++ b/Modules/Bar/Widgets/Network.qml @@ -46,9 +46,9 @@ Item { model: [ { - "label": Settings.data.network.wifiEnabled ? I18n.tr("actions.disable-wifi") : I18n.tr("actions.enable-wifi"), + "label": NetworkService.wifiEnabled ? I18n.tr("actions.disable-wifi") : I18n.tr("actions.enable-wifi"), "action": "toggle-wifi", - "icon": Settings.data.network.wifiEnabled ? "wifi-off" : "wifi", + "icon": NetworkService.wifiEnabled ? "wifi-off" : "wifi", "enabled": !Settings.data.network.airplaneModeEnabled && NetworkService.wifiAvailable }, { @@ -68,7 +68,7 @@ Item { PanelService.closeContextMenu(screen); if (action === "toggle-wifi") { - NetworkService.setWifiEnabled(!Settings.data.network.wifiEnabled); + NetworkService.setWifiEnabled(!NetworkService.wifiEnabled); } else if (action === "wifi-settings") { SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 0, screen); } else if (action === "widget-settings") { @@ -84,7 +84,7 @@ Item { customIconColor: Color.resolveColorKeyOptional(root.iconColorKey) customTextColor: Color.resolveColorKeyOptional(root.textColorKey) icon: NetworkService.getIcon() - text: NetworkService.getStatusText() + text: NetworkService.getStatusText(false) autoHide: false forceOpen: !isBarVertical && root.displayMode === "alwaysShow" forceClose: isBarVertical || root.displayMode === "alwaysHide" || text === "" @@ -99,7 +99,7 @@ Item { if (PanelService.getPanel("networkPanel", screen)?.isPanelOpen) { return ""; } - return pill.text; // pill.text is exact copy of getStatusText + return NetworkService.getStatusText(true); } } } diff --git a/Modules/Panels/ControlCenter/Widgets/Network.qml b/Modules/Panels/ControlCenter/Widgets/Network.qml index fd0abc35b..82bb55e03 100644 --- a/Modules/Panels/ControlCenter/Widgets/Network.qml +++ b/Modules/Panels/ControlCenter/Widgets/Network.qml @@ -8,14 +8,14 @@ import qs.Widgets NIconButtonHot { property ShellScreen screen icon: NetworkService.getIcon() - tooltipText: NetworkService.getStatusText() + tooltipText: NetworkService.getStatusText(true) onClicked: { var panel = PanelService.getPanel("networkPanel", screen); panel?.toggle(this); } onRightClicked: { if (!Settings.data.network.airplaneModeEnabled) { - NetworkService.setWifiEnabled(!Settings.data.network.wifiEnabled); + NetworkService.setWifiEnabled(!NetworkService.wifiEnabled); } } } diff --git a/Modules/Panels/Network/NetworkPanel.qml b/Modules/Panels/Network/NetworkPanel.qml index 6def94d85..c0718d380 100644 --- a/Modules/Panels/Network/NetworkPanel.qml +++ b/Modules/Panels/Network/NetworkPanel.qml @@ -34,14 +34,13 @@ SmartPanel { } if (panelViewMode === "wifi") { ethernetInfoExpanded = false; - if (Settings.data.network.wifiEnabled && !NetworkService.scanning) { + if (NetworkService.wifiEnabled && !NetworkService.scanningActive) { NetworkService.scan(); + NetworkService.refreshActiveWifiDetails(); } } else { if (NetworkService.ethernetConnected) { NetworkService.refreshActiveEthernetDetails(); - } else { - NetworkService.refreshEthernet(); } } } @@ -52,11 +51,13 @@ SmartPanel { onEffectivelyVisibleChanged: { if (effectivelyVisible) { SystemStatService.registerComponent("network-panel"); - NetworkService.scan(); - // Preload active Wi‑Fi details so Info shows instantly - NetworkService.refreshActiveWifiDetails(); - // Also fetch Ethernet details if connected - NetworkService.refreshActiveEthernetDetails(); + if (NetworkService.wifiEnabled && !NetworkService.scanningActive) { + NetworkService.scan(); + NetworkService.refreshActiveWifiDetails(); + } + if (NetworkService.ethernetConnected) { + NetworkService.refreshActiveEthernetDetails(); + } } else { SystemStatService.unregisterComponent("network-panel"); } @@ -66,13 +67,13 @@ SmartPanel { // Restore last view if valid, otherwise choose what's available (prefer Wi‑Fi when both exist) if (Settings.data.network.networkPanelView) { const last = Settings.data.network.networkPanelView; - if (last === "ethernet" && NetworkService.hasEthernet()) { + if (last === "ethernet" && NetworkService.ethernetAvailable) { panelViewMode = "ethernet"; } else { panelViewMode = "wifi"; } } else { - if (!Settings.data.network.wifiEnabled && NetworkService.hasEthernet()) { + if (!NetworkService.wifiEnabled && NetworkService.ethernetAvailable) { panelViewMode = "ethernet"; } else { panelViewMode = "wifi"; @@ -106,11 +107,11 @@ SmartPanel { RowLayout { NIcon { id: modeIcon - icon: panelViewMode === "wifi" ? (Settings.data.network.wifiEnabled ? "wifi" : "wifi-off") : (NetworkService.hasEthernet() ? (NetworkService.ethernetConnected ? "ethernet" : "ethernet") : "ethernet-off") + icon: panelViewMode === "wifi" ? (NetworkService.wifiEnabled ? "wifi" : "wifi-off") : (NetworkService.ethernetAvailable ? (NetworkService.ethernetConnected ? "ethernet" : "ethernet") : "ethernet-off") pointSize: Style.fontSizeXXL color: { if (panelViewMode === "wifi") { - return Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant; + return NetworkService.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant; } else { return NetworkService.ethernetConnected ? Color.mPrimary : Color.mOnSurfaceVariant; } @@ -120,7 +121,7 @@ SmartPanel { hoverEnabled: true onClicked: { if (panelViewMode === "wifi") { - if (NetworkService.hasEthernet()) { + if (NetworkService.ethernetAvailable) { panelViewMode = "ethernet"; } else { TooltipService.show(parent, I18n.tr("wifi.panel.no-ethernet-devices")); @@ -142,7 +143,7 @@ SmartPanel { NToggle { id: wifiSwitch visible: panelViewMode === "wifi" - checked: Settings.data.network.wifiEnabled + checked: NetworkService.wifiEnabled enabled: !Settings.data.network.airplaneModeEnabled && NetworkService.wifiAvailable onToggled: checked => NetworkService.setWifiEnabled(checked) baseSize: Style.baseWidgetSize * 0.7 // Slightly smaller @@ -166,7 +167,7 @@ SmartPanel { // Mode switch (Wi‑Fi / Ethernet) NTabBar { id: modeTabBar - visible: NetworkService.hasEthernet() + visible: NetworkService.ethernetAvailable && NetworkService.wifiAvailable margins: Style.marginS Layout.fillWidth: true spacing: Style.marginM @@ -254,7 +255,7 @@ SmartPanel { // Wi‑Fi disabled state NBox { id: disabledBox - visible: panelViewMode === "wifi" && !Settings.data.network.wifiEnabled + visible: panelViewMode === "wifi" && !NetworkService.wifiEnabled Layout.fillWidth: true Layout.preferredHeight: disabledColumn.implicitHeight + Style.margin2M @@ -300,7 +301,7 @@ SmartPanel { // Scanning state (show when no networks and we haven't had any yet) NBox { id: scanningBox - visible: panelViewMode === "wifi" && Settings.data.network.wifiEnabled && Object.keys(NetworkService.networks).length === 0 && NetworkService.scanning + visible: panelViewMode === "wifi" && NetworkService.wifiEnabled && Object.keys(NetworkService.networks).length === 0 && NetworkService.scanningActive Layout.fillWidth: true Layout.preferredHeight: scanningColumn.implicitHeight + Style.margin2M @@ -337,7 +338,7 @@ SmartPanel { // Empty state when no networks (only show after we've had networks before, meaning a real empty result) NBox { id: emptyBox - visible: panelViewMode === "wifi" && Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 && !NetworkService.scanning + visible: panelViewMode === "wifi" && NetworkService.wifiEnabled && Object.keys(NetworkService.networks).length === 0 && !NetworkService.scanningActive Layout.fillWidth: true Layout.preferredHeight: emptyColumn.implicitHeight + Style.margin2M @@ -352,7 +353,7 @@ SmartPanel { } NIcon { - icon: "search" + icon: "wifi-question" pointSize: 48 color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter @@ -374,7 +375,7 @@ SmartPanel { // Networks list container (Wi‑Fi) ColumnLayout { id: networksList - visible: panelViewMode === "wifi" && Settings.data.network.wifiEnabled && Object.keys(NetworkService.networks).length > 0 + visible: panelViewMode === "wifi" && NetworkService.wifiEnabled && Object.keys(NetworkService.networks).length > 0 width: parent.width spacing: Style.marginM @@ -578,6 +579,7 @@ SmartPanel { colorBorder: "transparent" colorBorderHover: "transparent" enabled: true + visible: NetworkService.ethernetConnected onClicked: { if (NetworkService.activeEthernetIf === modelData.ifname && ethernetInfoExpanded) { ethernetInfoExpanded = false; diff --git a/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml b/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml index c617ac710..918d5dd5d 100644 --- a/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml @@ -49,46 +49,41 @@ Item { readonly property var activeNetworks: (passwordSsid && passwordSsid.length > 0) ? Object.values(cachedNetworks) : Object.values(NetworkService.networks) readonly property var connectedNetworks: { - if (!Settings.data.network.wifiEnabled) { + if (!NetworkService.wifiEnabled) { return []; } return activeNetworks.filter(n => n.connected).sort((a, b) => b.signal - a.signal); } readonly property var savedNetworks: { - if (!Settings.data.network.wifiEnabled) { + if (!NetworkService.wifiEnabled) { return []; } - return activeNetworks.filter(n => !n.connected && (n.existing || n.cached)).sort((a, b) => b.signal - a.signal); + return activeNetworks.filter(n => !n.connected && n.existing).sort((a, b) => b.signal - a.signal); } readonly property var availableNetworks: { - if (!Settings.data.network.wifiEnabled) { + if (!NetworkService.wifiEnabled) { return []; } - return activeNetworks.filter(n => !n.connected && !n.existing && !n.cached).sort((a, b) => b.signal - a.signal); + return activeNetworks.filter(n => !n.connected && !n.existing).sort((a, b) => b.signal - a.signal); } // Combined visibility check: tab must be visible AND the window must be visible readonly property bool effectivelyVisible: root.visible && Window.window && Window.window.visible onEffectivelyVisibleChanged: { - if (effectivelyVisible && Settings.data.network.wifiEnabled && !showOnlyLists) { - NetworkService.scan(); - } if (effectivelyVisible) { SystemStatService.registerComponent("wifi-subtab"); + if (NetworkService.wifiEnabled && !NetworkService.scanningActive && !showOnlyLists) { + NetworkService.scan(); + NetworkService.refreshActiveWifiDetails(); + } } else { SystemStatService.unregisterComponent("wifi-subtab"); } } - Component.onCompleted: { - if (effectivelyVisible) { - SystemStatService.registerComponent("wifi-subtab"); - } - } - Component.onDestruction: { SystemStatService.unregisterComponent("wifi-subtab"); } @@ -151,8 +146,8 @@ Item { NToggle { label: I18n.tr("common.wifi") - icon: NetworkService.getIcon(false) - checked: Settings.data.network.wifiEnabled + icon: NetworkService.wifiEnabled ? "wifi" : "wifi-off" + checked: NetworkService.wifiEnabled onToggled: checked => NetworkService.setWifiEnabled(checked) enabled: ProgramCheckerService.nmcliAvailable && !Settings.data.network.airplaneModeEnabled && NetworkService.wifiAvailable Layout.alignment: Qt.AlignVCenter @@ -161,11 +156,11 @@ Item { NDivider { Layout.fillWidth: true - visible: Settings.data.network.wifiEnabled && root.connectedNetworks.length > 0 + visible: NetworkService.wifiEnabled } NText { - visible: !root.showOnlyLists && Settings.data.network.wifiEnabled + visible: !root.showOnlyLists && NetworkService.wifiEnabled Layout.fillWidth: true text: I18n.tr("panels.connections.wifi-header-text") color: Color.mOnSurfaceVariant @@ -184,7 +179,7 @@ Item { // Network List [1] (Connected) NBox { id: connectedBox - visible: root.connectedNetworks.length > 0 && Settings.data.network.wifiEnabled + visible: root.connectedNetworks.length > 0 && NetworkService.wifiEnabled Layout.fillWidth: true Layout.preferredHeight: connectedCol.implicitHeight + Style.margin2M border.color: showOnlyLists ? Style.boxBorderColor : "transparent" @@ -215,7 +210,7 @@ Item { // Network List [2] (Saved) NBox { id: savedBox - visible: root.savedNetworks.length > 0 && Settings.data.network.wifiEnabled + visible: root.savedNetworks.length > 0 && NetworkService.wifiEnabled Layout.fillWidth: true Layout.preferredHeight: savedCol.implicitHeight + Style.margin2M border.color: showOnlyLists ? Style.boxBorderColor : "transparent" @@ -246,7 +241,7 @@ Item { // Network List [3] (Available) NBox { id: availableBox - visible: root.availableNetworks.length > 0 && Settings.data.network.wifiEnabled + visible: root.availableNetworks.length > 0 && NetworkService.wifiEnabled Layout.fillWidth: true Layout.preferredHeight: availableCol.implicitHeight + Style.margin2M border.color: showOnlyLists ? Style.boxBorderColor : "transparent" @@ -272,15 +267,6 @@ Item { } } - // Auto-scan timer when panel is visible - Timer { - id: autoScanTimer - interval: 5000 - running: root.effectivelyVisible && Settings.data.network.wifiEnabled - repeat: true - onTriggered: NetworkService.scan() - } - Repeater { model: root.availableNetworks delegate: nboxDelegate @@ -331,7 +317,7 @@ Item { } Item { - visible: !showOnlyLists && Settings.data.network.wifiEnabled + visible: !showOnlyLists && NetworkService.wifiEnabled Layout.fillWidth: true } @@ -450,12 +436,12 @@ Item { onTextChanged: addNetworkPopup.customSsid = text onEditingFinished: { if (addNetworkPopup.customSsid.length > 0 && (addNetworkPopup.customSecurityKey === "open" || addNetworkPopup.customPassword.length > 0)) { - NetworkService.connectManual(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity, { - eap: addNetworkPopup.customEnterpriseEap, - phase2: addNetworkPopup.customEnterprisePhase2, - anonIdentity: addNetworkPopup.customEnterpriseAnonIdentity, - caCert: addNetworkPopup.customEnterpriseCaCert - }, addNetworkPopup.customIsHidden); + NetworkService.connect(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customIsHidden, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity, { + eap: addNetworkPopup.customEnterpriseEap, + phase2: addNetworkPopup.customEnterprisePhase2, + anonIdentity: addNetworkPopup.customEnterpriseAnonIdentity, + caCert: addNetworkPopup.customEnterpriseCaCert + }); addNetworkPopup.close(); } } @@ -574,12 +560,12 @@ Item { inputItem.echoMode: addNetworkPopup.customShowPassword ? TextInput.Normal : TextInput.Password onEditingFinished: { if (addNetworkPopup.customSsid.length > 0 && addNetworkPopup.customPassword.length > 0) { - NetworkService.connectManual(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity, { - eap: addNetworkPopup.customEnterpriseEap, - phase2: addNetworkPopup.customEnterprisePhase2, - anonIdentity: addNetworkPopup.customEnterpriseAnonIdentity, - caCert: addNetworkPopup.customEnterpriseCaCert - }, addNetworkPopup.customIsHidden); + NetworkService.connect(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customIsHidden, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity, { + eap: addNetworkPopup.customEnterpriseEap, + phase2: addNetworkPopup.customEnterprisePhase2, + anonIdentity: addNetworkPopup.customEnterpriseAnonIdentity, + caCert: addNetworkPopup.customEnterpriseCaCert + }); addNetworkPopup.close(); } } @@ -629,12 +615,12 @@ Item { textColor: Color.mOnPrimary enabled: addNetworkPopup.customSsid.length > 0 && (addNetworkPopup.customSecurityKey === "open" || addNetworkPopup.customPassword.length > 0) && (addNetworkPopup.customSecurityKey.indexOf("-eap") === -1 || addNetworkPopup.customIdentity.length > 0) onClicked: { - NetworkService.connectManual(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity, { - eap: addNetworkPopup.customEnterpriseEap, - phase2: addNetworkPopup.customEnterprisePhase2, - anonIdentity: addNetworkPopup.customEnterpriseAnonIdentity, - caCert: addNetworkPopup.customEnterpriseCaCert - }, addNetworkPopup.customIsHidden); + NetworkService.connect(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customIsHidden, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity, { + eap: addNetworkPopup.customEnterpriseEap, + phase2: addNetworkPopup.customEnterprisePhase2, + anonIdentity: addNetworkPopup.customEnterpriseAnonIdentity, + caCert: addNetworkPopup.customEnterpriseCaCert + }); addNetworkPopup.close(); } } @@ -745,9 +731,6 @@ Item { return NetworkService.networkConnectivity; } } - if (modelData.cached && !modelData.existing) { - return I18n.tr("wifi.panel.saved"); - } return NetworkService.isSecured(modelData.security) ? modelData.security : I18n.tr("wifi.panel.security-open"); } pointSize: Style.fontSizeXXS @@ -834,7 +817,7 @@ Item { } NIconButton { - visible: !root.showOnlyLists && (modelData.existing || modelData.cached) && !modelData.connected && !networkItem.isBusy + visible: !root.showOnlyLists && modelData.existing && !modelData.connected && !networkItem.isBusy icon: "trash" tooltipText: I18n.tr("tooltips.forget-network") baseSize: Style.baseWidgetSize * 0.75 @@ -854,7 +837,7 @@ Item { textColor: Color.mOnPrimary text: I18n.tr("common.connect") onClicked: { - if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) { + if (modelData.existing || !NetworkService.isSecured(modelData.security)) { NetworkService.connect(modelData.ssid); } else { root.requestPassword(modelData.ssid); diff --git a/Services/Control/IPCService.qml b/Services/Control/IPCService.qml index f54dac46b..f9a57c395 100644 --- a/Services/Control/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -623,7 +623,7 @@ Singleton { IpcHandler { target: "wifi" function toggle() { - NetworkService.setWifiEnabled(!Settings.data.network.wifiEnabled); + NetworkService.setWifiEnabled(!NetworkService.wifiEnabled); } function enable() { NetworkService.setWifiEnabled(true); diff --git a/Services/Networking/NetworkService.qml b/Services/Networking/NetworkService.qml index 6e4582a7f..43ec1216d 100644 --- a/Services/Networking/NetworkService.qml +++ b/Services/Networking/NetworkService.qml @@ -3,44 +3,21 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Io +import Quickshell.Networking import qs.Commons import qs.Services.System import qs.Services.UI Singleton { id: root - + // Shared core (read-only) properties readonly property bool wifiAvailable: _wifiAvailable readonly property bool ethernetAvailable: _ethernetAvailable - - property bool _wifiAvailable: false - property bool _ethernetAvailable: false - - // Core state - property var networks: ({}) - property bool scanning: false - property bool connecting: false - property string connectingTo: "" - property string lastError: "" - property bool ethernetConnected: false - // Each item: { ifname: string, state: string, connected: bool } - property var ethernetInterfaces: ([]) - // Active Ethernet connection details - property var activeEthernetDetails: ({}) - property string activeEthernetIf: "" - property bool ethernetDetailsLoading: false - property double activeEthernetDetailsTimestamp: 0 - // Keep same TTL policy for both kinds of links - property int activeEthernetDetailsTtlMs: 5000 - property string disconnectingFrom: "" - property string forgettingNetwork: "" - property string networkConnectivity: "unknown" - property bool internetConnectivity: true - property bool ignoreScanResults: false - property bool scanPending: false + readonly property bool internetConnectivity: _internetConnectivity + readonly property string networkConnectivity: _networkConnectivity // Supported Wi-Fi security types - property var supportedSecurityTypes: [ + readonly property var supportedSecurityTypes: [ { key: "open", name: I18n.tr("wifi.panel.security-open") @@ -75,54 +52,45 @@ Singleton { } ] - // Active Wi‑Fi connection details (for info panel) + // Core properties + property bool _wifiAvailable: false + property bool _ethernetAvailable: false + property string _networkConnectivity: "unknown" + property bool _internetConnectivity: false + property string lastError: "" + property int activeDetailsTtlMs: 10000 + // Ethernet properties + property var ethernetInterfaces: ([]) + property var activeEthernetDetails: ({}) + property bool ethernetConnected: false + property string activeEthernetIf: "" + property bool ethernetDetailsLoading: false + property double activeEthernetDetailsTimestamp: 0 + // Wi-Fi properties + readonly property bool wifiEnabled: Networking.wifiEnabled + property var networks: ({}) property var activeWifiDetails: ({}) + property bool wifiConnected: false property string activeWifiIf: "" - property bool detailsLoading: false + property bool wifiDetailsLoading: false property double activeWifiDetailsTimestamp: 0 - // Cache TTL to avoid spamming nmcli on rapid toggles - property int activeWifiDetailsTtlMs: 5000 - - // Persistent cache - property string cacheFile: Settings.cacheDir + "network.json" - readonly property string cachedLastConnected: cacheAdapter.lastConnected - readonly property var cachedNetworks: cacheAdapter.knownNetworks - - // Cache file handling - FileView { - id: cacheFileView - path: root.cacheFile - printErrors: false - - JsonAdapter { - id: cacheAdapter - property var knownNetworks: ({}) - property string lastConnected: "" - } - - onLoadFailed: { - cacheAdapter.knownNetworks = ({}); - cacheAdapter.lastConnected = ""; - } - } + property bool wifiInit: false + // Wi-Fi adapter/connection properties + property bool connecting: false + property string connectingTo: "" + property string disconnectingFrom: "" + property string forgettingNetwork: "" + property bool scanPending: false + property bool scanningActive: false + property var existingProfiles: ({}) Connections { - target: Settings.data.network + target: root function onWifiEnabledChanged() { - if (Settings.data.network.wifiEnabled) { - if (!BluetoothService.airplaneModeToggled) { - ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.enabled"), "wifi"); - } - // Perform a scan to update the UI - delayedScanTimer.interval = 3000; - delayedScanTimer.restart(); - } else { - if (!BluetoothService.airplaneModeToggled) { - ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.disabled"), "wifi-off"); - } - // Clear networks so the widget icon changes - root.networks = ({}); + if (!root.wifiInit || BluetoothService.airplaneModeToggled) { + return; } + wifiDebounce.restart(); } } @@ -131,78 +99,70 @@ Singleton { target: Time function onResumed() { Logger.i("Network", "System resumed - forcing state poll"); - ethernetStateProcess.running = true; - root.scan(); - root.refreshActiveWifiDetails(); - root.refreshActiveEthernetDetails(); + deviceStatusProcess.running = true; connectivityCheckProcess.running = true; } } - Component.onCompleted: { - Logger.i("Network", "Service started"); - if (ProgramCheckerService.nmcliAvailable) { - detectNetworkCapabilities(); - syncWifiState(); - scan(); - // Prime ethernet state immediately so UI can reflect wired status on startup - ethernetStateProcess.running = true; - refreshActiveWifiDetails(); - refreshActiveEthernetDetails(); - } - } - // Start initial checks when nmcli becomes available Connections { target: ProgramCheckerService function onNmcliAvailableChanged() { if (ProgramCheckerService.nmcliAvailable) { - detectNetworkCapabilities(); - syncWifiState(); - scan(); - // Refresh ethernet status as soon as nmcli becomes available - ethernetStateProcess.running = true; - // Also refresh details so panels get info without waiting for timers - refreshActiveWifiDetails(); - refreshActiveEthernetDetails(); + deviceStatusProcess.running = true; + connectivityCheckProcess.running = true; } } } - // Function to detect host's networking capabilities eg has WiFi/Ethernet. - function detectNetworkCapabilities() { + Component.onCompleted: { + Logger.i("Network", "Service started"); + wifiInitTimer.running = true; + + // Ensure initial detection if nmcli is already available at startup if (ProgramCheckerService.nmcliAvailable) { - Logger.d("Network", "Refreshing network status and capabilities..."); - ethernetStateProcess.running = true; + deviceStatusProcess.running = true; + connectivityCheckProcess.running = true; } } - // Save cache with debounce + // Prevent an initial "Wi-Fi enabled" toast and trigger initial scan Timer { - id: saveDebounce - interval: 1000 - onTriggered: cacheFileView.writeAdapter() + id: wifiInitTimer + interval: 100 + onTriggered: { + root.wifiInit = true; + if (root.wifiEnabled) { + root.scan(); + } + } } - // Refresh details for the currently active Wi‑Fi link - function refreshActiveWifiDetails() { - const now = Date.now(); - // If we're already fetching, don't start a new one - if (detailsLoading) { - return; + // Debounce to prevent multiple toast notifications from transient states + Timer { + id: wifiDebounce + interval: 300 + onTriggered: { + if (root.wifiEnabled) { + ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.enabled"), "wifi"); + connectivityCheckProcess.running = true; + deviceStatusProcess.running = true; + root.scan(); + } else { + ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.disabled"), "wifi-off"); + root.scanningActive = false; + root.networks = ({}); + } } - - // Use cached details if they are fresh - if (activeWifiIf && activeWifiDetails && (now - activeWifiDetailsTimestamp) < activeWifiDetailsTtlMs) { - return; - } - - detailsLoading = true; - wifiDeviceListProcess.running = true; } - function saveCache() { - saveDebounce.restart(); + // Internet connectivity check timer + Timer { + id: connectivityCheckTimer + interval: 15000 + running: ProgramCheckerService.nmcliAvailable && (root.ethernetConnected || root.wifiConnected) + repeat: true + onTriggered: connectivityCheckProcess.running = true } // Delayed scan timer @@ -212,149 +172,90 @@ Singleton { onTriggered: scan() } - // Ethernet check timer - // Runs every 30s if nmcli is available - Timer { - id: ethernetCheckTimer - interval: 30000 - running: ProgramCheckerService.nmcliAvailable - repeat: true - onTriggered: ethernetStateProcess.running = true + // Refresh details for the currently active Wi‑Fi link + function refreshActiveWifiDetails() { + const now = Date.now(); + if (wifiDetailsLoading || (activeWifiIf && wifiConnected && activeWifiDetails && (now - activeWifiDetailsTimestamp) < activeDetailsTtlMs)) { + return; + } + if (wifiConnected && activeWifiIf) { + wifiDetailsLoading = true; + deviceStatusProcess.running = true; + } } // Refresh details for the currently active Ethernet link function refreshActiveEthernetDetails() { const now = Date.now(); - if (ethernetDetailsLoading) { + if (ethernetDetailsLoading || activeEthernetIf && activeEthernetDetails && (now - activeEthernetDetailsTimestamp) < activeDetailsTtlMs) { return; } - if (!root.ethernetConnected) { - // Link is down: keep the selected interface so UI can still show its info as disconnected - // Only clear details to avoid showing stale IP/speed/etc. - root.activeEthernetDetails = ({}); - root.activeEthernetDetailsTimestamp = now; - return; + if (ethernetConnected && activeEthernetIf) { + ethernetDetailsLoading = true; + deviceStatusProcess.running = true; } - // If we have fresh details for the same iface, skip - if (activeEthernetIf && activeEthernetDetails && (now - activeEthernetDetailsTimestamp) < activeEthernetDetailsTtlMs) { - return; - } - - ethernetDetailsLoading = true; - ethernetDeviceListProcess.running = true; - } - - // Internet connectivity check timer - Timer { - id: connectivityCheckTimer - interval: 15000 - running: ProgramCheckerService.nmcliAvailable - repeat: true - onTriggered: connectivityCheckProcess.running = true } // Core functions - function syncWifiState() { - if (!ProgramCheckerService.nmcliAvailable) { - return; - } - wifiStateProcess.running = true; - } - function setWifiEnabled(enabled) { if (!ProgramCheckerService.nmcliAvailable) { return; } Logger.i("Wi-Fi", "SetWifiEnabled", enabled); - Settings.data.network.wifiEnabled = enabled; - wifiStateEnableProcess.running = true; + Networking.wifiEnabled = enabled; } function scan() { - if (!ProgramCheckerService.nmcliAvailable || !Settings.data.network.wifiEnabled) { + if (!ProgramCheckerService.nmcliAvailable || !root.wifiEnabled) { return; } - if (scanning) { - // Mark current scan results to be ignored and schedule a new scan - Logger.d("Network", "Scan already in progress, will ignore results and rescan"); - ignoreScanResults = true; - scanPending = true; - return; - } - - scanning = true; lastError = ""; - ignoreScanResults = false; + + // If scanning in progress, mark as pending to trigger another scan when current when finished. + if (profileCheckProcess.running || scanProcess.running) { + root.scanPending = true; + return; + } // Get existing profiles first, then scan profileCheckProcess.running = true; - Logger.d("Network", "Wi-Fi scan in progress..."); + root.scanningActive = true; + Logger.d("Network", "Scanning Wi-Fi networks..."); } - // Returns true if we currently have any detectable Ethernet interfaces - function hasEthernet() { - return root.ethernetInterfaces && root.ethernetInterfaces.length > 0; - } - - // Refresh only Ethernet state/details - function refreshEthernet() { - if (!ProgramCheckerService.nmcliAvailable) { - return; - } - ethernetStateProcess.running = true; - refreshActiveEthernetDetails(); - } - - function connect(ssid, password = "", isHidden = false, identity = "", enterpriseConfig = {}) { + function connect(ssid, password = "", isHidden = false, securityKey = "", identity = "", enterpriseConfig = {}) { if (!ProgramCheckerService.nmcliAvailable || connecting) { return; } - if (isEnterprise(networks[ssid] ? networks[ssid].security : "")) { - connectManual(ssid, password, "wpa-eap", identity, enterpriseConfig); - return; - } + const isSaved = (networks[ssid] && networks[ssid].existing); + const isEnt = securityKey ? isEnterprise(securityKey) : isEnterprise(networks[ssid] ? networks[ssid].security : ""); connecting = true; connectingTo = ssid; lastError = ""; - // Check if we have a saved connection - if ((networks[ssid] && networks[ssid].existing) || cachedNetworks[ssid]) { + connectProcess.ssid = ssid; + connectProcess.password = password; + connectProcess.isHidden = isHidden; + + if (isSaved) { connectProcess.mode = "saved"; - connectProcess.ssid = ssid; - connectProcess.password = ""; - connectProcess.isHidden = false; + } else if (isEnt || securityKey === "wep" || (securityKey && securityKey !== "open" && securityKey !== "wpa-psk" && securityKey !== "wpa2-psk")) { + connectProcess.mode = "manual"; + connectProcess.securityKey = securityKey || (networks[ssid] ? networks[ssid].security : "wpa-psk"); + connectProcess.identity = identity; + connectProcess.eap = enterpriseConfig.eap || "peap"; + connectProcess.phase2 = enterpriseConfig.phase2 || "mschapv2"; + connectProcess.anonIdentity = enterpriseConfig.anonIdentity || ""; + connectProcess.caCert = enterpriseConfig.caCert || ""; } else { connectProcess.mode = "new"; - connectProcess.ssid = ssid; - connectProcess.password = password; - connectProcess.isHidden = isHidden; } connectProcess.running = true; } - function connectManual(ssid, password, securityKey, identity = "", enterpriseConfig = {}, isHidden = false) { - if (!ProgramCheckerService.nmcliAvailable || connecting) { - return; - } - connecting = true; - connectingTo = ssid; - lastError = ""; - - manualConnectProcess.ssid = ssid; - manualConnectProcess.password = password; - manualConnectProcess.securityKey = securityKey; - manualConnectProcess.identity = identity; - manualConnectProcess.isHidden = isHidden; - manualConnectProcess.eap = enterpriseConfig.eap || "peap"; - manualConnectProcess.phase2 = enterpriseConfig.phase2 || "mschapv2"; - manualConnectProcess.anonIdentity = enterpriseConfig.anonIdentity || ""; - manualConnectProcess.caCert = enterpriseConfig.caCert || ""; - manualConnectProcess.running = true; - } - function disconnect(ssid) { if (!ProgramCheckerService.nmcliAvailable) { return; @@ -370,17 +271,6 @@ Singleton { } forgettingNetwork = ssid; - // Remove from cache - let known = cacheAdapter.knownNetworks; - delete known[ssid]; - cacheAdapter.knownNetworks = known; - - if (cacheAdapter.lastConnected === ssid) { - cacheAdapter.lastConnected = ""; - } - - saveCache(); - // Remove from system forgetProcess.ssid = ssid; forgetProcess.running = true; @@ -396,12 +286,10 @@ Singleton { nets[key].connected = false; } } - // Update the target network if it exists if (nets[ssid]) { nets[ssid].connected = connected; nets[ssid].existing = true; - nets[ssid].cached = true; } else if (connected) { // Create a temporary entry if network doesn't exist yet nets[ssid] = { @@ -409,11 +297,9 @@ Singleton { "security": "--", "signal": 100, "connected": true, - "existing": true, - "cached": true + "existing": true }; } - // Trigger property change notification networks = ({}); networks = nets; @@ -423,9 +309,9 @@ Singleton { function getSignalInfo(signal, isConnected) { let icon = ""; if (isConnected) { - if (root.networkConnectivity === "limited") { + if (root._networkConnectivity === "limited") { icon = "wifi-exclamation"; - } else if (root.networkConnectivity === "portal" || root.networkConnectivity === "unknown") { + } else if (root._networkConnectivity === "portal" || root._networkConnectivity === "unknown") { icon = "wifi-question"; } } @@ -461,7 +347,7 @@ Singleton { gateway6: [], dns6: [], hwAddr: "", - nmSpeed: "" + speed: "" }; const addUnique = (arr, val) => { if (val && arr.indexOf(val) === -1) { @@ -476,7 +362,9 @@ Singleton { details.hwAddr = v; }, "CAPABILITIES.SPEED": v => { - details.nmSpeed = v; + if (v && v !== "unknown") { + details.speed = v; + } }, "IP4.ADDRESS": v => { details.ipv4 = v.split("/")[0]; @@ -513,431 +401,287 @@ Singleton { handlers[key](val); } } - return details; } // Functions used in /Modules/Panels/ControlCenter/Widgets/Network.qml & /Modules/Bar/Widgets/Network.qml - function getStatusText() { + function getStatusText(showSpeed = false) { + // This variable can be tied to a toggle if (root.connecting) { return root.connectingTo ? I18n.tr("common.connecting") + " " + root.connectingTo : I18n.tr("common.connecting"); } - let p = []; + if (Settings.data.network.airplaneModeEnabled) { + return I18n.tr("toast.airplane-mode.title"); + } + if (!root.wifiEnabled) { + return ""; + } // Ethernet if (root.ethernetConnected) { const eth = root.activeEthernetDetails; const name = eth.connectionName || (root.ethernetInterfaces.length > 0 ? root.ethernetInterfaces[0].connectionName : "") || ""; const speed = eth.speed || ""; - p.push(name + (speed ? " - " + speed : "")); + return (name + (showSpeed && speed ? " - " + speed : "")); } // Wi-Fi - if (root.activeWifiIf) { + if (root.wifiConnected) { const wl = root.activeWifiDetails; const speed = wl.rateShort || wl.rate || ""; const connectedNet = Object.values(root.networks).find(net => net.connected); const name = connectedNet ? connectedNet.ssid : (wl.connectionName || ""); - p.push(name + (speed ? " - " + speed : "")); + return (name + (showSpeed && speed ? " - " + speed : "")); } - - if (p.length > 0) { - return p.join(" + "); // p.length > 0 & === 1 no join used. (eg: Only Wi-Fi) - } - - return I18n.tr("common.disconnected"); // p.length === 0 + return ""; } function getIcon(forceEthernet = false) { - let i = "wifi-off"; // Default to prevent empty space on launch. - // Ethernet has priority over Wi-Fi which is standard. + if (Settings.data.network.airplaneModeEnabled && !forceEthernet) { + return "plane"; + } + + // 1. Ethernet Priority: Show Ethernet icon if connected OR if specifically requested (Panel) if (root.ethernetConnected || forceEthernet) { - // Ethernet connected or forced (by the NetworkPanel) - switch (root.networkConnectivity) { + switch (root._networkConnectivity) { case "limited": - // Ethernet connected, no internet access - i = "ethernet-exclamation"; - break; + return "ethernet-exclamation"; case "portal": case "unknown": - // Ethernet connected, status unknown or behind portal - i = "ethernet-question"; - break; + return "ethernet-question"; case "full": - // Ethernet connected and internet access - i = "ethernet"; - break; + return "ethernet"; default: - // Ethernet not connected (Forced by the NetworkPanel) - i = "ethernet-off"; - break; + return "ethernet-off"; } - } else { - // Wi-Fi - const connectedNet = Object.values(root.networks).find(net => net.connected); + } + + // 2. Wi-Fi Fallback + if (root.wifiAvailable || !forceEthernet) { const networkCount = Object.values(root.networks).length; - if (connectedNet) { - // Wi-Fi Connected - i = root.getSignalInfo(connectedNet.signal, true).icon; - } else if (root.connecting || networkCount > 0) { - // Wi-Fi Connecting or Wi-Fi available but not connected - i = "wifi-question"; - } else if (!Settings.data.network.wifiEnabled) { - // Wi-Fi is off - i = "wifi-off"; + if (!root.wifiEnabled) { + return "wifi-off"; + } + if (root.wifiConnected) { + let s = (root.activeWifiDetails && root.activeWifiDetails.signal !== undefined && root.activeWifiDetails.signal !== "") ? root.activeWifiDetails.signal : 0; + return root.getSignalInfo(s, true).icon; + } + if (root.connecting || networkCount > 0) { + return "wifi-question"; } } - if (Settings.data.network.airplaneModeEnabled && !forceEthernet) { - i = "plane"; - } - return i; + return (root.ethernetAvailable || root.ethernetConnected) ? "ethernet-off" : root.wifiAvailable ? "wifi-0" : "wifi-off"; } // Processes + // Discover connected interface[s] and fetch details [1] Process { - id: ethernetStateProcess - running: ProgramCheckerService.nmcliAvailable - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device"] + id: deviceStatusProcess + running: false + command: ["sh", "-c", "nmcli -t -f GENERAL.DEVICE,GENERAL.TYPE,GENERAL.STATE,GENERAL.CONNECTION,GENERAL.HWADDR,IP4.ADDRESS,IP4.GATEWAY,IP4.DNS,IP6.ADDRESS,IP6.GATEWAY,IP6.DNS,CAPABILITIES.SPEED device show; echo \"------\"; nmcli -t -f IN-USE,SIGNAL,RATE,CHAN,FREQ,BANDWIDTH device wifi list"] + environment: ({ + "LC_ALL": "C" + }) stdout: StdioCollector { onStreamFinished: { - var connected = false; - var wifiAvailable = false; - var ethernetAvailable = false; - var devIf = ""; - var lines = text.trim().split("\n"); - var ethList = []; - for (var i = 0; i < lines.length; i++) { - var parts = lines[i].split(":"); - if (parts.length >= 3) { - var ifname = parts[0]; - var type = parts[1]; - var state = parts[2]; - var conName = parts.slice(3).join(":") || ""; + const outputParts = text.split("------"); + const deviceText = outputParts[0]; + const wifiText = outputParts[1] || ""; - if (type === "wifi") { - wifiAvailable = true; - } else if (type === "ethernet" && state !== "unmanaged") { - ethernetAvailable = true; - var isConn = state === "connected"; - ethList.push({ - ifname: ifname, - state: state, - connected: isConn, - connectionName: conName - }); - if (isConn && !connected) { - connected = true; - devIf = ifname; + let lines = deviceText.split("\n"); + let deviceBlocks = []; + let currentBlock = []; + + for (let i = 0; i < lines.length; i++) { + let line = lines[i].trim(); + if (!line) { + continue; + } + if (line.startsWith("GENERAL.DEVICE:")) { + if (currentBlock.length > 0) { + deviceBlocks.push(currentBlock); + } + currentBlock = [line]; + } else if (currentBlock.length > 0) { + currentBlock.push(line); + } + } + if (currentBlock.length > 0) { + deviceBlocks.push(currentBlock); + } + + let activeEthIf = ""; + let activeWifiIf = ""; + let wifiAvailable = false; + let ethernetAvailable = false; + let ethList = []; + + let newActiveWifiDetails = ({}); + let newActiveEthernetDetails = ({}); + + for (let b = 0; b < deviceBlocks.length; b++) { + let block = deviceBlocks[b]; + let blockText = block.join("\n"); + let details = root.parseIpDetails(blockText); + + let name = ""; + let type = ""; + let stateStr = ""; + + for (let l = 0; l < block.length; l++) { + let line = block[l]; + if (line.startsWith("GENERAL.DEVICE:")) { + name = line.substring(15).trim(); + } else if (line.startsWith("GENERAL.TYPE:")) { + type = line.substring(13).trim(); + } else if (line.startsWith("GENERAL.STATE:")) { + stateStr = line.substring(14).trim(); + } + } + + if (stateStr.indexOf("(unmanaged)") !== -1) { + continue; + } + let isConnected = stateStr.indexOf("(connected)") !== -1; + + if (type === "ethernet") { + ethernetAvailable = true; + let stateName = stateStr.split(" ")[1] ? stateStr.split(" ")[1].replace(/[()]/g, "") : stateStr; + ethList.push({ + ifname: name, + state: stateName, + connected: isConnected, + connectionName: details.connectionName + }); + if (isConnected && !activeEthIf) { + activeEthIf = name; + newActiveEthernetDetails = details; + newActiveEthernetDetails.ifname = name; + } + } else if (type === "wifi") { + wifiAvailable = true; + if (isConnected && !activeWifiIf) { + activeWifiIf = name; + newActiveWifiDetails = details; + newActiveWifiDetails.ifname = name; + } + } + } + + // Parse Wi-Fi details if active + if (activeWifiIf && wifiText) { + let rate = ""; + let freq = ""; + let channel = ""; + let width = ""; + let signal = ""; + + const wifiLines = wifiText.split("\n"); + for (let i = 0; i < wifiLines.length; i++) { + const line = wifiLines[i].trim(); + if (line.startsWith("*")) { + const parts = line.split(":"); + if (parts.length >= 6) { + signal = parts[1]; + rate = parts[2]; + channel = parts[3]; + freq = parts[4].replace(" MHz", ""); + width = parts[5]; } - } - } - } - - // Update capabilities - root._wifiAvailable = wifiAvailable; - root._ethernetAvailable = ethernetAvailable; - - // Sort interfaces: connected first, then by name - ethList.sort(function (a, b) { - if (a.connected !== b.connected) { - return a.connected ? -1 : 1; - } - return a.ifname.localeCompare(b.ifname); - }); - root.ethernetInterfaces = ethList; - - if (root.ethernetConnected !== connected) { - root.ethernetConnected = connected; - Logger.d("Network", "Ethernet connected:", root.ethernetConnected); - } - if (connected) { - if (root.activeEthernetIf !== devIf) { - root.activeEthernetIf = devIf; - // refresh details for the new interface - root.activeEthernetDetailsTimestamp = 0; - } - root.refreshActiveEthernetDetails(); - } else { - // Preserve the selected interface; just clear details so UI shows a disconnected state - root.activeEthernetDetails = ({}); - root.activeEthernetDetailsTimestamp = Date.now(); - } - } - } - stderr: StdioCollector { - onStreamFinished: { - if (text && text.trim()) { - Logger.w("Network", "ethernetState nmcli stderr:", text.trim()); - } - } - } - } - - // Discover connected Ethernet interface and fetch details - Process { - id: ethernetDeviceListProcess - running: false - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device"] - - stdout: StdioCollector { - onStreamFinished: { - let ifname = ""; - const lines = text.split("\n"); - const ethList = []; - for (let i = 0; i < lines.length; i++) { - const parts = lines[i].trim().split(":"); - if (parts.length >= 3) { - const dev = parts[0]; - const type = parts[1]; - const state = parts[2]; - const conName = parts.slice(3).join(":") || ""; - if (state === "unmanaged") { - continue; - } - if (type === "ethernet" && state === "connected") { - ifname = dev; - } - if (type === "ethernet") { - ethList.push({ - ifname: dev, - state: state, - connected: state === "connected", - connectionName: conName - }); - } - } - } - ethList.sort(function (a, b) { - if (a.connected !== b.connected) { - return a.connected ? -1 : 1; - } - return a.ifname.localeCompare(b.ifname); - }); - root.ethernetInterfaces = ethList; - if (ifname) { - if (root.activeEthernetIf !== ifname) { - root.activeEthernetIf = ifname; - } - ethernetDeviceShowProcess.ifname = ifname; - ethernetDeviceShowProcess.running = true; - } else { - root.activeEthernetDetailsTimestamp = Date.now(); - root.ethernetDetailsLoading = false; - } - } - } - stderr: StdioCollector { - onStreamFinished: { - if (text && text.trim()) { - Logger.w("Network", "nmcli device list (eth) stderr:", text.trim()); - } - if (!root.activeEthernetIf) { - root.activeEthernetDetailsTimestamp = Date.now(); - root.ethernetDetailsLoading = false; - } - } - } - } - - // Fetch IPv4/Gateway/DNS and Connection Name for Ethernet iface - Process { - id: ethernetDeviceShowProcess - property string ifname: "" - running: false - // Speed is resolved via ethtool fallback below to avoid stderr warnings - command: ["nmcli", "-t", "-f", "GENERAL.CONNECTION,GENERAL.HWADDR,IP4.ADDRESS,IP4.GATEWAY,IP4.DNS,IP6.ADDRESS,IP6.GATEWAY,IP6.DNS", "device", "show", ifname] - - stdout: StdioCollector { - onStreamFinished: { - const details = root.activeEthernetDetails || ({}); - const parsed = root.parseIpDetails(text); - - details.ifname = ethernetDeviceShowProcess.ifname; - details.connectionName = parsed.connectionName; - // No speed from nmcli: keep empty so ethtool fallback below fills it - details.speed = details.speed && details.speed.length > 0 ? details.speed : ""; - details.ipv4 = parsed.ipv4; - details.gateway4 = parsed.gateway4; - details.ipv6 = parsed.ipv6; - details.gateway6 = parsed.gateway6; - details.dns4 = parsed.dns4; - details.dns6 = parsed.dns6; - details.hwAddr = parsed.hwAddr; - - root.activeEthernetDetails = details; - // If speed missing, try sysfs first, then fallback to ethtool - if (!details.speed || details.speed.length === 0) { - ethernetSysfsSpeedProcess.ifname = ethernetDeviceShowProcess.ifname; - ethernetSysfsSpeedProcess.running = true; - } else { - root.activeEthernetDetailsTimestamp = Date.now(); - root.ethernetDetailsLoading = false; - } - } - } - stderr: StdioCollector { - onStreamFinished: { - if (text && text.trim()) { - Logger.w("Network", "nmcli device show (eth) stderr:", text.trim()); - } - root.activeEthernetDetailsTimestamp = Date.now(); - root.ethernetDetailsLoading = false; - } - } - } - - // Try to read Ethernet speed from sysfs first: /sys/class/net//speed (numeric Mbit/s) - Process { - id: ethernetSysfsSpeedProcess - property string ifname: "" - running: false - command: ["sh", "-c", "cat '/sys/class/net/" + ifname + "/speed' 2>/dev/null || true"] - - stdout: StdioCollector { - onStreamFinished: { - const details = root.activeEthernetDetails || ({}); - let speedText = ""; - const v = text.trim(); - // Expect a number like 1000 - const num = parseFloat(v); - if (!isNaN(num) && num > 0) { - details.speed = Math.round(num) + " Mbit/s"; - details.speedMbit = num; - root.activeEthernetDetails = details; - root.activeEthernetDetailsTimestamp = Date.now(); - root.ethernetDetailsLoading = false; - } else if (ProgramCheckerService.ethtoolAvailable) { - // Fallback to ethtool if sysfs unreadable or invalid - ethernetEthtoolProcess.ifname = ethernetSysfsSpeedProcess.ifname; - ethernetEthtoolProcess.running = true; - } else { - root.activeEthernetDetailsTimestamp = Date.now(); - root.ethernetDetailsLoading = false; - } - } - } - stderr: StdioCollector {} - } - - // Optional: query Ethernet speed via ethtool as a fallback - Process { - id: ethernetEthtoolProcess - property string ifname: "" - running: false - command: ["sh", "-c", "ethtool '" + ifname + "' 2>/dev/null || true"] - - stdout: StdioCollector { - onStreamFinished: { - const details = root.activeEthernetDetails || ({}); - let speedText = ""; - const lines = text.split("\n"); - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (line.toLowerCase().indexOf("speed:") === 0) { - // Example: "Speed: 1000Mb/s" - const v = line.substring(6).trim(); - if (v) { - // Normalize to "1000 Mbit/s" - const normalized = v.replace(/mb\/?s/i, "Mbit/s").replace(/\s+/g, " "); - speedText = normalized; - } - break; - } - } - if (speedText && speedText.length > 0) { - details.speed = speedText; - // Try to derive numeric value - const m = speedText.match(/([0-9]+(?:\.[0-9]+)?)\s*Mbit\/s/i); - if (m) { - details.speedMbit = parseFloat(m[1]); - } - root.activeEthernetDetails = details; - } - root.activeEthernetDetailsTimestamp = Date.now(); - root.ethernetDetailsLoading = false; - } - } - stderr: StdioCollector {} - } - - // Discover connected Wi‑Fi interface - Process { - id: wifiDeviceListProcess - running: false - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] - - stdout: StdioCollector { - onStreamFinished: { - let ifname = ""; - const lines = text.split("\n"); - for (let i = 0; i < lines.length; i++) { - const parts = lines[i].trim().split(":"); - if (parts.length >= 3) { - const dev = parts[0]; - const type = parts[1]; - const state = parts[2]; - if (state === "unmanaged") { - continue; - } - if (type === "wifi" && state === "connected") { - ifname = dev; break; } } - } - root.activeWifiIf = ifname; - if (ifname) { - wifiDeviceShowProcess.ifname = ifname; - wifiDeviceShowProcess.running = true; - } else { - // Nothing to fetch - root.activeWifiDetailsTimestamp = Date.now(); - root.detailsLoading = false; - } - } - } - stderr: StdioCollector { - onStreamFinished: { - if (text && text.trim()) { - Logger.w("Network", "nmcli device list stderr:", text.trim()); - } - // Fail-safe to avoid spinner - if (!root.activeWifiIf) { - root.activeWifiDetailsTimestamp = Date.now(); - root.detailsLoading = false; - } - } - } - } - // Fetch IP info for the interface - Process { - id: wifiDeviceShowProcess - property string ifname: "" - running: false - command: ["nmcli", "-t", "-f", "GENERAL.CONNECTION,IP4.ADDRESS,IP4.GATEWAY,IP4.DNS,IP6.ADDRESS,IP6.GATEWAY,IP6.DNS,CAPABILITIES.SPEED", "device", "show", ifname] - // Find Signal (%) Bandwidth (MHz) Rate (Mbit/s) Freq (MHz) Chan (int) + let band = ""; + if (freq) { + const f = +freq; + if (f) { + switch (true) { + case (f >= 5925 && f < 7125): + band = "6 GHz"; + break; + case (f >= 5150 && f < 5925): + band = "5 GHz"; + break; + case (f >= 2400 && f < 2500): + band = "2.4 GHz"; + break; + default: + band = `${f} MHz`; + } + } + } - stdout: StdioCollector { - onStreamFinished: { - const details = root.activeWifiDetails || ({}); - const parsed = root.parseIpDetails(text); + let rateShort = ""; + if (rate) { + var rparts = rate.trim().split(" "); + var compact = []; + for (var i = 0; i < rparts.length; i++) { + if (rparts[i]) { + compact.push(rparts[i]); + } + } + var unitIdx = -1; + for (var j = 0; j < compact.length; j++) { + var token = compact[j].toLowerCase(); + if (token === "mbit/s" || token === "mb/s" || token === "mbits/s") { + unitIdx = j; + break; + } + } + if (unitIdx > 0) { + var num = compact[unitIdx - 1]; + var parsed = parseFloat(num); + if (!isNaN(parsed)) { + rateShort = parsed + " Mbit/s"; + } + } + if (!rateShort) { + rateShort = compact.slice(0, 2).join(" "); + } + } - details.connectionName = parsed.connectionName; - details.ipv4 = parsed.ipv4; - details.gateway4 = parsed.gateway4; - details.ipv6 = parsed.ipv6; - details.gateway6 = parsed.gateway6; - details.dns4 = parsed.dns4; - details.dns6 = parsed.dns6; - details.nmSpeed = parsed.nmSpeed; - root.activeWifiDetails = details; + let enhancedBand = band; + if (channel && width) { + enhancedBand = `${band} / ${channel} (${width})`; + } else if (channel) { + enhancedBand = `${band} / ${channel}`; + } - // Try to get link rate (best effort) - wifiDetailsProcess.ifname = wifiDeviceShowProcess.ifname; - wifiDetailsProcess.running = true; + if (newActiveWifiDetails.speed) { + newActiveWifiDetails.rate = newActiveWifiDetails.speed.replace(/Mb\/s/i, "Mbit/s"); + newActiveWifiDetails.rateShort = newActiveWifiDetails.rate; + } else { + newActiveWifiDetails.rate = rate; + newActiveWifiDetails.rateShort = rateShort; + } + newActiveWifiDetails.band = enhancedBand; + newActiveWifiDetails.channel = channel; + newActiveWifiDetails.width = width; + newActiveWifiDetails.signal = signal; + } + + root._wifiAvailable = wifiAvailable; + root._ethernetAvailable = ethernetAvailable; + root.ethernetConnected = (activeEthIf !== ""); + root.wifiConnected = (activeWifiIf !== ""); + + Logger.d("Network", "Device sync: wifiAvailable: " + wifiAvailable + ", ethAvailable: " + ethernetAvailable + ", wifiConnected: " + root.wifiConnected + " (" + activeWifiIf + "), ethConnected: " + root.ethernetConnected + " (" + activeEthIf + ")"); + + ethList.sort((a, b) => (a.connected !== b.connected) ? (a.connected ? -1 : 1) : a.ifname.localeCompare(b.ifname)); + root.ethernetInterfaces = ethList; + + root.activeEthernetIf = activeEthIf; + root.activeEthernetDetails = newActiveEthernetDetails; + root.activeEthernetDetailsTimestamp = Date.now(); + root.ethernetDetailsLoading = false; + + root.activeWifiIf = activeWifiIf; + root.activeWifiDetails = newActiveWifiDetails; + root.activeWifiDetailsTimestamp = Date.now(); + root.wifiDetailsLoading = false; } } stderr: StdioCollector { @@ -945,190 +689,8 @@ Singleton { if (text && text.trim()) { Logger.w("Network", "nmcli device show stderr:", text.trim()); } - // Still proceed to finalize details to avoid UI waiting forever - root.activeWifiDetailsTimestamp = Date.now(); - } - } - } - - // Optional: query Wi‑Fi bitrate and link info via nmcli - Process { - id: wifiDetailsProcess - property string ifname: "" - running: false - command: ["nmcli", "-t", "-f", "IN-USE,SIGNAL,RATE,CHAN,FREQ,BANDWIDTH", "device", "wifi", "list", "ifname", ifname] - - stdout: StdioCollector { - onStreamFinished: { - const details = root.activeWifiDetails || ({}); - let rate = ""; - let freq = ""; - let channel = ""; - let width = ""; - let signal = ""; - - const lines = text.split("\n"); - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (line.startsWith("*")) { - // Found the in-use network - // Format: *:SIGNAL:RATE:CHAN:FREQ:BANDWIDTH - const parts = line.split(":"); - if (parts.length >= 6) { - signal = parts[1]; - rate = parts[2]; - channel = parts[3]; - freq = parts[4].replace(" MHz", ""); - width = parts[5]; - } - break; - } - } - - // Determine band from frequency - // https://en.wikipedia.org/wiki/List_of_WLAN_channels - let band = ""; - if (freq) { - const f = +freq; - if (f) { - switch (true) { - // https://en.wikipedia.org/wiki/List_of_WLAN_channels#6_GHz_(802.11ax_and_802.11be) - case (f >= 5925 && f < 7125): - band = "6 GHz"; - break; - // https://en.wikipedia.org/wiki/List_of_WLAN_channels#5_GHz_(802.11a/h/n/ac/ax/be) - case (f >= 5150 && f < 5925): - band = "5 GHz"; - break; - // https://en.wikipedia.org/wiki/List_of_WLAN_channels#2.4_GHz_(802.11b/g/n/ax/be) - case (f >= 2400 && f < 2500): - band = "2.4 GHz"; - break; - default: - band = `${f} MHz`; - } - } - } - - // Shorten verbose bitrate strings like: "360.0 MBit/s VHT-MCS 8 40MHz short GI" - let rateShort = ""; - if (rate) { - var parts = rate.trim().split(" "); - // compact consecutive spaces - var compact = []; - for (var i = 0; i < parts.length; i++) { - var p = parts[i]; - if (p && p.length > 0) { - compact.push(p); - } - } - // Find a token that represents Mbit/s and use the previous number - var unitIdx = -1; - for (var j = 0; j < compact.length; j++) { - var token = compact[j].toLowerCase(); - if (token === "mbit/s" || token === "mb/s" || token === "mbits/s") { - unitIdx = j; - break; - } - } - if (unitIdx > 0) { - var num = compact[unitIdx - 1]; - // Basic numeric check - var parsed = parseFloat(num); - if (!isNaN(parsed)) { - rateShort = parsed + " Mbit/s"; - } - } - if (!rateShort) { - // Fallback to first two tokens - rateShort = compact.slice(0, 2).join(" "); - } - } - - // Enhance band string with channel and width: "Band / Channel (Width)" - let enhancedBand = band; - if (channel && width) { - enhancedBand = `${band} / ${channel} (${width})`; - } else if (channel) { - enhancedBand = `${band} / ${channel}`; - } - - // Use dynamic speed if available - if (details.nmSpeed) { - rate = details.nmSpeed; - // Standardize 'Mb/s' to 'Mbit/s' for UI consistency - rate = rate.replace(/Mb\/s/i, "Mbit/s"); - rateShort = rate; - } - - details.rate = rate; - details.rateShort = rateShort; - details.band = enhancedBand; - details.channel = channel; - details.width = width; - details.signal = signal; - root.activeWifiDetails = details; - root.activeWifiDetailsTimestamp = Date.now(); - root.detailsLoading = false; - } - } - stderr: StdioCollector { - onStreamFinished: { - if (text && text.trim()) { - Logger.w("Network", "nmcli wifi details stderr:", text.trim()); - } - root.activeWifiDetailsTimestamp = Date.now(); - root.detailsLoading = false; - } - } - } - - // Only check the state of the actual interface - // and update our setting to be in sync. - Process { - id: wifiStateProcess - running: false - command: ["nmcli", "radio", "wifi"] - environment: ({ - "LC_ALL": "C" - }) - - stdout: StdioCollector { - onStreamFinished: { - const enabled = text.trim() === "enabled"; - Logger.d("Network", "Wi-Fi adapter was detect as enabled:", enabled); - if (Settings.data.network.wifiEnabled !== enabled) { - Settings.data.network.wifiEnabled = enabled; - } - } - } - stderr: StdioCollector { - onStreamFinished: { - if (text && text.trim()) { - Logger.w("Network", "Wi-Fi state query stderr:", text.trim()); - } - } - } - } - - // Process to enable/disable the Wi-Fi interface - Process { - id: wifiStateEnableProcess - running: false - command: ["nmcli", "radio", "wifi", Settings.data.network.wifiEnabled ? "on" : "off"] - - stdout: StdioCollector { - onStreamFinished: { - // Re-check the state to ensure it's in sync - syncWifiState(); - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.w("Network", "Error changing Wi-Fi state: " + text); - } + root.ethernetDetailsLoading = false; + root.wifiDetailsLoading = false; } } } @@ -1138,38 +700,16 @@ Singleton { id: connectivityCheckProcess running: false command: ["nmcli", "networking", "connectivity", "check"] - - property int failedChecks: 0 - stdout: StdioCollector { onStreamFinished: { - const result = text.trim(); - if (!result) { + const r = text.trim(); + if (!r) { return; } - if (result === "full" || result === "none" || result === "unknown") { - if (connectivityCheckProcess.failedChecks !== 0) { - connectivityCheckProcess.failedChecks = 0; - } - if (result !== root.networkConnectivity) { - if (result === "full") { - root.internetConnectivity = true; - } - root.networkConnectivity = result; - root.scan(); - } - return; - } - if ((result === "limited" || result === "portal") && result !== root.networkConnectivity) { - connectivityCheckProcess.failedChecks++; - if (connectivityCheckProcess.failedChecks === 3) { - root.networkConnectivity = result; - pingCheckProcess.running = true; - } - } + root._networkConnectivity = (r === "none") ? "unknown" : r; + root._internetConnectivity = (r === "full"); } } - stderr: StdioCollector { onStreamFinished: { if (text.trim()) { @@ -1179,23 +719,6 @@ Singleton { } } - Process { - id: pingCheckProcess - command: ["sh", "-c", "ping -c1 -W2 ping.archlinux.org >/dev/null 2>&1 || " + "ping -c1 -W2 1.1.1.1 >/dev/null 2>&1 || " + "curl -fsI --max-time 5 https://cloudflare.com/cdn-cgi/trace >/dev/null 2>&1"] - - onExited: function (exitCode, exitStatus) { - if (exitCode === 0) { - connectivityCheckProcess.failedChecks = 0; - } else { - root.internetConnectivity = false; - Logger.i("Network", "No internet connectivity"); - ToastService.showWarning(root.cachedLastConnected, I18n.tr("toast.internet-limited")); - connectivityCheckProcess.failedChecks = 0; - } - root.scan(); - } - } - // Helper process to get existing profiles Process { id: profileCheckProcess @@ -1204,19 +727,6 @@ Singleton { stdout: StdioCollector { onStreamFinished: { - if (root.ignoreScanResults) { - Logger.d("Network", "Ignoring profile check results (new scan requested)"); - root.scanning = false; - - // Check if we need to start a new scan - if (root.scanPending) { - root.scanPending = false; - delayedScanTimer.interval = 100; - delayedScanTimer.restart(); - } - return; - } - var profiles = {}; var lines = text.split("\n"); for (var i = 0; i < lines.length; i++) { @@ -1225,7 +735,7 @@ Singleton { profiles[l.trim()] = true; } } - scanProcess.existingProfiles = profiles; + root.existingProfiles = profiles; scanProcess.running = true; } } @@ -1233,10 +743,13 @@ Singleton { onStreamFinished: { if (text && text.trim()) { Logger.w("Network", "Profile check stderr:", text.trim()); - // Fail safe - only restart scan on actual error - if (root.scanning) { - root.scanning = false; - delayedScanTimer.interval = 5000; + if (root.scanningActive) { + if (root.scanPending) { + root.scanPending = false; + delayedScanTimer.interval = 3000; + } else { + delayedScanTimer.interval = 5000; + } delayedScanTimer.restart(); } } @@ -1244,181 +757,167 @@ Singleton { } } + // Scan for Wi-Fi networks Process { id: scanProcess running: false command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list", "--rescan", "yes"] - property var existingProfiles: ({}) - stdout: StdioCollector { onStreamFinished: { - if (root.ignoreScanResults) { - Logger.d("Network", "Ignoring scan results (new scan requested)"); - root.scanning = false; - - // Check if we need to start a new scan - if (root.scanPending) { - root.scanPending = false; - delayedScanTimer.interval = 100; - delayedScanTimer.restart(); - } - return; - } - - // Process the scan results as before... - const lines = text.split("\n"); + const lines = text.trim().split("\n"); const networksMap = {}; - for (var i = 0; i < lines.length; ++i) { + for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) { continue; } - // Parse from the end to handle SSIDs with colons - // Format is SSID:SECURITY:SIGNAL:IN-USE - // We know the last 3 fields, so everything else is SSID - const lastColonIdx = line.lastIndexOf(":"); - if (lastColonIdx === -1) { - Logger.w("Network", "Malformed nmcli output line:", line); + // Parse SSID:SECURITY:SIGNAL:IN-USE + const parts = line.split(":"); + if (parts.length < 4) { continue; } - const inUse = line.substring(lastColonIdx + 1); - const remainingLine = line.substring(0, lastColonIdx); - - const secondLastColonIdx = remainingLine.lastIndexOf(":"); - if (secondLastColonIdx === -1) { - Logger.w("Network", "Malformed nmcli output line:", line); - continue; - } - - const signal = remainingLine.substring(secondLastColonIdx + 1); - const remainingLine2 = remainingLine.substring(0, secondLastColonIdx); - - const thirdLastColonIdx = remainingLine2.lastIndexOf(":"); - if (thirdLastColonIdx === -1) { - Logger.w("Network", "Malformed nmcli output line:", line); - continue; - } - - let security = remainingLine2.substring(thirdLastColonIdx + 1); - // This change will add a slash where mixed security protocols are used. + const inUse = parts[parts.length - 1]; + const signal = parseInt(parts[parts.length - 2]) || 0; + let security = parts[parts.length - 3]; if (security) { security = security.replace("WPA2 WPA3", "WPA2/WPA3").replace("WPA1 WPA2", "WPA1/WPA2"); } - - const ssid = remainingLine2.substring(0, thirdLastColonIdx); + const ssid = parts.slice(0, parts.length - 3).join(":"); if (ssid) { - const signalInt = parseInt(signal) || 0; - const connected = inUse === "*"; - - // Track connected network in cache - if (connected && cacheAdapter.lastConnected !== ssid) { - cacheAdapter.lastConnected = ssid; - saveCache(); - } - + const isConnected = (inUse === "*"); if (!networksMap[ssid]) { networksMap[ssid] = { "ssid": ssid, "security": security || "--", - "signal": signalInt, - "connected": connected, - "existing": ssid in scanProcess.existingProfiles, - "cached": ssid in cacheAdapter.knownNetworks + "signal": signal, + "connected": isConnected, + "existing": !!root.existingProfiles[ssid] }; } else { - // Keep the best signal for duplicate SSIDs - const existingNet = networksMap[ssid]; - if (connected) { - existingNet.connected = true; - } - if (signalInt > existingNet.signal) { - existingNet.signal = signalInt; - existingNet.security = security || "--"; + if (isConnected) { + networksMap[ssid].connected = true; + networksMap[ssid].signal = signal; + connectivityCheckProcess.running = true; + } else if (!networksMap[ssid].connected && signal > networksMap[ssid].signal) { + networksMap[ssid].signal = signal; } } } } - // Logging + // Logging & Diffing const oldSSIDs = Object.keys(root.networks); const newSSIDs = Object.keys(networksMap); - const newNetworks = newSSIDs.filter(function (ssid) { - return oldSSIDs.indexOf(ssid) === -1; - }); - const lostNetworks = oldSSIDs.filter(function (ssid) { - return newSSIDs.indexOf(ssid) === -1; - }); + const newNetworks = newSSIDs.filter(s => oldSSIDs.indexOf(s) === -1); + const lostNetworks = oldSSIDs.filter(s => newSSIDs.indexOf(s) === -1); + + // Always update networks, this makes more reflective of state/signal. + root.networks = networksMap; if (newNetworks.length > 0 || lostNetworks.length > 0) { if (newNetworks.length > 0) { - Logger.d("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", ")); + Logger.d("Network", "New Wi-Fi network appeared:", newNetworks.join(", ")); } if (lostNetworks.length > 0) { - Logger.d("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", ")); + Logger.d("Network", "Wi-Fi network disappeared:", lostNetworks.join(", ")); } - Logger.d("Network", "Total Wi-Fi SSIDs:", Object.keys(networksMap).length); + Logger.d("Network", "Total Wi-Fi networks:", Object.keys(networksMap).length); } - Logger.d("Network", "Wi-Fi scan completed"); - root.networks = networksMap; - root.scanning = false; - - // Preload active Wi‑Fi details so Info panel shows instantly when opened - // This is lightweight and guarded by detailsLoading + TTL. - var hasConnected = false; - for (var ssid in networksMap) { - if (networksMap.hasOwnProperty(ssid)) { - var net = networksMap[ssid]; - if (net && net.connected) { - hasConnected = true; - break; - } - } - } - if (hasConnected) { + if (Object.values(networksMap).some(n => n.connected)) { root.refreshActiveWifiDetails(); } - // Check if we need to start a new scan if (root.scanPending) { root.scanPending = false; delayedScanTimer.interval = 100; delayedScanTimer.restart(); } + root.scanningActive = false; } } stderr: StdioCollector { onStreamFinished: { - root.scanning = false; if (text.trim()) { Logger.w("Network", "Scan error: " + text); - // If scan fails, retry - delayedScanTimer.interval = 5000; + // Even on error, if a scan was pending, try again + if (root.scanPending) { + root.scanPending = false; + delayedScanTimer.interval = 3000; + } else if (root.scanningActive) { + delayedScanTimer.interval = 10000; + } delayedScanTimer.restart(); } + root.scanningActive = false; } } } + + // Connect to Wi-Fi network Process { id: connectProcess - property string mode: "new" + property string mode: "new" // "saved", "new", or "manual" property string ssid: "" property string password: "" property bool isHidden: false + // Manual properties + property string securityKey: "" + property string identity: "" + property string eap: "peap" + property string phase2: "mschapv2" + property string anonIdentity: "" + property string caCert: "" running: false command: { if (mode === "saved") { - return ["nmcli", "connection", "up", "id", ssid]; + return ["nmcli", "-t", "connection", "up", "id", ssid]; + } else if (mode === "manual") { + const nmArgs = ["connection", "add", "type", "wifi", "con-name", ssid, "ssid", ssid, "--", "802-11-wireless.hidden", isHidden ? "yes" : "no"]; + + if (securityKey === "wpa-psk" || securityKey === "wpa2-psk") { + nmArgs.push("wifi-sec.key-mgmt", "wpa-psk", "wifi-sec.psk", password); + } else if (securityKey === "sae") { + nmArgs.push("wifi-sec.key-mgmt", "sae", "wifi-sec.psk", password); + } else if (securityKey === "wep") { + nmArgs.push("wifi-sec.key-mgmt", "none", "wifi-sec.wep-key0", password); + } else if (securityKey && securityKey.indexOf("-eap") !== -1) { + nmArgs.push("wifi-sec.key-mgmt", "wpa-eap", "802-1x.eap", eap, "802-1x.phase2-auth", phase2, "802-1x.identity", identity, "802-1x.password", password); + if (anonIdentity) { + nmArgs.push("802-1x.anonymous-identity", anonIdentity); + } + if (caCert) { + nmArgs.push("802-1x.ca-cert", caCert); + } + } + + const script = ` + SSID="$1" + shift + # Find existing profile by Name and Type + UUID=$(nmcli -t -f NAME,UUID,TYPE connection show | awk -F: -v target="$SSID" '$1 == target && $3 == "802-11-wireless" { print $2; exit }') + + if [ -n "$UUID" ]; then + echo "Using existing profile: $UUID" + nmcli connection delete uuid "$UUID" 2>/dev/null || true + else + echo "Creating new profile for $SSID" + fi + nmcli "$@" + nmcli connection up id "$SSID" + `; + + return ["sh", "-c", script, "--", ssid].concat(nmArgs); } else { - var cmd = ["nmcli", "device", "wifi", "connect", ssid]; + var cmd = ["nmcli", "-t", "device", "wifi", "connect", ssid]; if (isHidden) { cmd.push("hidden", "yes"); } @@ -1431,47 +930,29 @@ Singleton { return cmd; } } + environment: ({ "LC_ALL": "C" }) stdout: StdioCollector { onStreamFinished: { - // Check if the output actually indicates success - // nmcli outputs "Device '...' successfully activated" or "Connection successfully activated" - // on success. Empty output or other messages indicate failure. const output = text.trim(); - if (!output || (output.indexOf("successfully activated") === -1 && output.indexOf("Connection successfully") === -1)) { - // No success message - likely an error occurred - // Don't update anything, let stderr handler deal with it return; } - // Success - update cache - let known = cacheAdapter.knownNetworks; - known[connectProcess.ssid] = { - "profileName": connectProcess.ssid, - "lastConnected": Date.now() - }; - cacheAdapter.knownNetworks = known; - cacheAdapter.lastConnected = connectProcess.ssid; - saveCache(); - - // Immediately update the UI before scanning + root.wifiConnected = true; root.updateNetworkStatus(connectProcess.ssid, true); - - // Preload details immediately so Info panel has data instantly - root.refreshActiveWifiDetails(); + root.refreshActiveWifiDetails(); // This needs wifiConnected true. root.connecting = false; root.connectingTo = ""; - Logger.i("Network", "Connected to network: '" + connectProcess.ssid + "'"); + Logger.i("Network", "Connected to network: '" + connectProcess.ssid + "' (" + connectProcess.mode + ")"); ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.wifi.connected", { "ssid": connectProcess.ssid - }), "wifi"); + }), root.getIcon(false)); - // Still do a scan to get accurate signal and security info delayedScanTimer.interval = 5000; delayedScanTimer.restart(); } @@ -1483,7 +964,6 @@ Singleton { root.connecting = false; root.connectingTo = ""; - // Parse common errors if (text.indexOf("Secrets were required") !== -1 || text.indexOf("no secrets provided") !== -1) { root.lastError = I18n.tr("toast.wifi.incorrect-password"); forget(connectProcess.ssid); @@ -1492,115 +972,18 @@ Singleton { } else if (text.indexOf("Timeout") !== -1) { root.lastError = I18n.tr("toast.wifi.connection-timeout"); } else { - // Generic fallback root.lastError = I18n.tr("toast.wifi.connection-failed"); } - Logger.w("Network", "Connect error: " + text); - // Notify user about the failure - ToastService.showWarning(I18n.tr("common.wifi"), root.lastError || I18n.tr("toast.wifi.connection-failed")); - } - } - } - } - - Process { - id: manualConnectProcess - property string ssid: "" - property string password: "" - property string securityKey: "" - property string identity: "" - property string eap: "peap" - property string phase2: "mschapv2" - property string anonIdentity: "" - property string caCert: "" - property bool isHidden: false - running: false - - command: { - const nmArgs = ["connection", "add", "type", "wifi", "con-name", ssid, "ssid", ssid, "--", "802-11-wireless.hidden", isHidden ? "yes" : "no"]; - - if (securityKey === "wpa-psk" || securityKey === "wpa2-psk") { - nmArgs.push("wifi-sec.key-mgmt", "wpa-psk", "wifi-sec.psk", password); - } else if (securityKey === "sae") { - nmArgs.push("wifi-sec.key-mgmt", "sae", "wifi-sec.psk", password); - } else if (securityKey === "wep") { - nmArgs.push("wifi-sec.key-mgmt", "none", "wifi-sec.wep-key0", password); - } else if (securityKey && securityKey.indexOf("-eap") !== -1) { - nmArgs.push("wifi-sec.key-mgmt", "wpa-eap", "802-1x.eap", eap, "802-1x.phase2-auth", phase2, "802-1x.identity", identity, "802-1x.password", password); - - if (anonIdentity) { - nmArgs.push("802-1x.anonymous-identity", anonIdentity); - } - - if (caCert) { - nmArgs.push("802-1x.ca-cert", caCert); - } - } - - const script = ` - SSID="$1" - shift - # Remove existing wifi profile with same SSID to avoid conflict - UUID=$(nmcli -t -f UUID,TYPE,NAME connection show | grep ":802-11-wireless:$SSID$" | head -n1 | cut -d: -f1) - if [ -n "$UUID" ]; then - nmcli connection delete uuid "$UUID" 2>/dev/null || true - fi - nmcli "$@" - nmcli connection up id "$SSID" - `; - - return ["sh", "-c", script, "--", ssid].concat(nmArgs); - } - environment: ({ - "LC_ALL": "C" - }) - - stdout: StdioCollector { - onStreamFinished: { - const output = text.trim(); - if (output.indexOf("successfully activated") === -1) { - return; - } - - // Success - update cache - let known = cacheAdapter.knownNetworks; - known[manualConnectProcess.ssid] = { - "profileName": manualConnectProcess.ssid, - "lastConnected": Date.now() - }; - cacheAdapter.knownNetworks = known; - cacheAdapter.lastConnected = manualConnectProcess.ssid; - saveCache(); - - root.updateNetworkStatus(manualConnectProcess.ssid, true); - root.refreshActiveWifiDetails(); - - root.connecting = false; - root.connectingTo = ""; - Logger.i("Network", "Manually connected to hidden network: '" + manualConnectProcess.ssid + "'"); - ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.wifi.connected", { - "ssid": manualConnectProcess.ssid - }), "wifi"); - - delayedScanTimer.interval = 5000; - delayedScanTimer.restart(); - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - root.connecting = false; - root.connectingTo = ""; - root.lastError = I18n.tr("toast.wifi.connection-failed"); - Logger.w("Network", "Manual connect error: " + text); - ToastService.showWarning(I18n.tr("common.wifi"), root.lastError); + Logger.w("Network", "Connect error (" + connectProcess.mode + "): " + text); + ToastService.showWarning(I18n.tr("common.wifi"), root.lastError || I18n.tr("toast.wifi.connection-failed"), "wifi-exclamation"); + wifiConnected = false; } } } } + // Disconnect from Wi-Fi network Process { id: disconnectProcess property string ssid: "" @@ -1610,6 +993,7 @@ Singleton { stdout: StdioCollector { onStreamFinished: { Logger.i("Network", "Disconnected from network: '" + disconnectProcess.ssid + "'"); + root.wifiConnected = false; ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.wifi.disconnected", { "ssid": disconnectProcess.ssid }), "wifi-off"); @@ -1619,7 +1003,7 @@ Singleton { root.disconnectingFrom = ""; // Do a scan to refresh the list - delayedScanTimer.interval = 1000; + delayedScanTimer.interval = 3000; delayedScanTimer.restart(); } } @@ -1637,6 +1021,7 @@ Singleton { } } + // Forget given Wi-Fi network Process { id: forgetProcess property string ssid: "" @@ -1651,8 +1036,9 @@ Singleton { ssid="$1" deleted=false - # Try to find a wifi connection with this SSID and delete it - UUID=$(nmcli -t -f UUID,TYPE,NAME connection show | grep ":802-11-wireless:$ssid$" | head -n1 | cut -d: -f1) + # Find existing profile by Name and Type + UUID=$(nmcli -t -f NAME,UUID,TYPE connection show | awk -F: -v target="$ssid" '$1 == target && $3 == "802-11-wireless" { print $2; exit }') + if [ -n "$UUID" ]; then if nmcli connection delete uuid "$UUID" 2>/dev/null; then echo "Deleted profile: $ssid ($UUID)" @@ -1690,10 +1076,9 @@ Singleton { Logger.i("Network", "Forget network: \"" + forgetProcess.ssid + "\""); Logger.d("Network", text.trim().replace(/[\r\n]/g, " ")); - // Update both cached and existing status immediately + // Update existing status immediately let nets = root.networks; if (nets[forgetProcess.ssid]) { - nets[forgetProcess.ssid].cached = false; nets[forgetProcess.ssid].existing = false; // Trigger property change root.networks = ({}); @@ -1720,4 +1105,22 @@ Singleton { } } } + + // Listen to NetworkManager events in real-time (roaming, auto-connect) -- ~9mb Memory usage. + Process { + id: networkMonitorProcess + running: ProgramCheckerService.nmcliAvailable + command: ["nmcli", "-t", "monitor"] + environment: ({ + "LC_ALL": "C" + }) + stdout: SplitParser { + onRead: data => { + if (data.endsWith(": connected") || data.endsWith(": disconnected")) { + Logger.d("Network", "State changed: " + data); + deviceStatusProcess.running = true; + } + } + } + } } diff --git a/Services/System/ProgramCheckerService.qml b/Services/System/ProgramCheckerService.qml index ea2f6d0f4..7f50e88b8 100644 --- a/Services/System/ProgramCheckerService.qml +++ b/Services/System/ProgramCheckerService.qml @@ -17,7 +17,6 @@ Singleton { property bool gnomeCalendarAvailable: false property bool pythonAvailable: false property bool wtypeAvailable: false - property bool ethtoolAvailable: false // Programs to check - maps property names to commands readonly property var programsToCheck: ({ @@ -26,8 +25,7 @@ Singleton { "wlsunsetAvailable": ["sh", "-c", "command -v wlsunset"], "gnomeCalendarAvailable": ["sh", "-c", "command -v gnome-calendar"], "wtypeAvailable": ["sh", "-c", "command -v wtype"], - "pythonAvailable": ["sh", "-c", "command -v python3"], - "ethtoolAvailable": ["sh", "-c", "command -v ethtool"] + "pythonAvailable": ["sh", "-c", "command -v python3"] }) // Discord client auto-detection