From 02bd9bbc728227da5fd8781778ffd8e89bd0acee Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 22 Jul 2025 11:50:07 -0400 Subject: [PATCH] networking: improve wifi experience and bugs --- Modules/ControlCenter/NetworkTab.qml | 496 ++++++++++++++++++++------- Modules/NetworkInfoDialog.qml | 208 +++++++++++ Modules/WifiPasswordDialog.qml | 30 ++ Services/NetworkService.qml | 364 ++++++++++---------- Services/ProcessMonitorService.qml | 37 +- Services/WifiService.qml | 422 ++++++++++++++++++----- shell.qml | 4 + 7 files changed, 1166 insertions(+), 395 deletions(-) create mode 100644 Modules/NetworkInfoDialog.qml diff --git a/Modules/ControlCenter/NetworkTab.qml b/Modules/ControlCenter/NetworkTab.qml index dbb5e973..fc4265bc 100644 --- a/Modules/ControlCenter/NetworkTab.qml +++ b/Modules/ControlCenter/NetworkTab.qml @@ -28,14 +28,25 @@ Item { return []; } - var networks = [...WifiService.wifiNetworks]; + // Explicitly reference both arrays to ensure reactivity + var allNetworks = WifiService.wifiNetworks; + var savedNetworks = WifiService.savedWifiNetworks; + var currentSSID = WifiService.currentWifiSSID; + var signalStrength = WifiService.wifiSignalStrength; + var refreshTrigger = forceRefresh; // Force recalculation - // Update connected status and signal strength based on current WiFi SSID + var networks = [...allNetworks]; + + // Update connected status, saved status and signal strength based on current state networks.forEach(function(network) { - network.connected = (network.ssid === WifiService.currentWifiSSID); + network.connected = (network.ssid === currentSSID); + // Update saved status based on savedWifiNetworks + network.saved = savedNetworks.some(function(saved) { + return saved.ssid === network.ssid; + }); // Use current connection's signal strength for connected network - if (network.connected && WifiService.wifiSignalStrength) { - network.signalStrength = WifiService.wifiSignalStrength; + if (network.connected && signalStrength) { + network.signalStrength = signalStrength; } }); @@ -51,6 +62,16 @@ Item { return networks; } + // Force refresh of sortedWifiNetworks when networks are updated + property int forceRefresh: 0 + + Connections { + target: WifiService + function onNetworksUpdated() { + forceRefresh++; + } + } + // Auto-enable WiFi auto-refresh when network tab is visible Component.onCompleted: { WifiService.autoRefreshEnabled = true; @@ -166,6 +187,29 @@ Item { } + // Loading spinner for preference changes + DankIcon { + id: wifiLoadingSpinner + name: "refresh" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.right: wifiToggle.left + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + visible: NetworkService.changingPreference && NetworkService.targetPreference === "wifi" + z: 10 + + RotationAnimation { + target: wifiLoadingSpinner + property: "rotation" + running: wifiLoadingSpinner.visible + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + } + } + // WiFi toggle switch DankToggle { id: wifiToggle @@ -219,111 +263,6 @@ Item { } } - // Connection status indicator - Rectangle { - width: parent.width - height: 32 - radius: Theme.cornerRadius - color: { - if (WifiService.connectionStatus === "connecting") - return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12); - else if (WifiService.connectionStatus === "failed") - return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12); - else if (WifiService.connectionStatus === "connected") - return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.12); - return "transparent"; - } - border.color: { - if (WifiService.connectionStatus === "connecting") - return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3); - else if (WifiService.connectionStatus === "failed") - return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3); - else if (WifiService.connectionStatus === "connected") - return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3); - return "transparent"; - } - border.width: WifiService.connectionStatus !== "" ? 1 : 0 - visible: WifiService.connectionStatus !== "" - - Row { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankIcon { - id: connectionIcon - - name: { - if (WifiService.connectionStatus === "connecting") - return "sync"; - if (WifiService.connectionStatus === "failed") - return "error"; - if (WifiService.connectionStatus === "connected") - return "check_circle"; - return ""; - } - size: Theme.iconSize - 6 - color: { - if (WifiService.connectionStatus === "connecting") - return Theme.warning; - if (WifiService.connectionStatus === "failed") - return Theme.error; - if (WifiService.connectionStatus === "connected") - return Theme.success; - return Theme.surfaceText; - } - anchors.verticalCenter: parent.verticalCenter - rotation: WifiService.connectionStatus === "connecting" ? connectionIcon.rotation : 0 - - RotationAnimation { - target: connectionIcon - property: "rotation" - running: WifiService.connectionStatus === "connecting" - from: 0 - to: 360 - duration: 1000 - loops: Animation.Infinite - } - - Behavior on rotation { - RotationAnimation { - duration: 200 - easing.type: Easing.OutQuad - } - } - } - - Text { - text: { - if (WifiService.connectionStatus === "connecting") - return "Connecting to " + WifiService.connectingSSID; - if (WifiService.connectionStatus === "failed") - return "Failed to connect to " + WifiService.connectingSSID; - if (WifiService.connectionStatus === "connected") - return "Connected to " + WifiService.connectingSSID; - return ""; - } - font.pixelSize: Theme.fontSizeSmall - color: { - if (WifiService.connectionStatus === "connecting") - return Theme.warning; - if (WifiService.connectionStatus === "failed") - return Theme.error; - if (WifiService.connectionStatus === "connected") - return Theme.success; - return Theme.surfaceText; - } - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - } } @@ -410,6 +349,29 @@ Item { } } + // Loading spinner for preference changes + DankIcon { + id: ethernetLoadingSpinner + name: "refresh" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.right: ethernetToggle.left + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + visible: NetworkService.changingPreference && NetworkService.targetPreference === "ethernet" + z: 10 + + RotationAnimation { + target: ethernetLoadingSpinner + property: "rotation" + running: ethernetLoadingSpinner.visible + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + } + } + // Ethernet toggle switch (matching WiFi style) DankToggle { id: ethernetToggle @@ -604,6 +566,7 @@ Item { Item { anchors.fill: parent anchors.margins: Theme.spacingXS + anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar // Signal strength icon DankIcon { @@ -638,12 +601,22 @@ Item { text: { if (modelData.connected) return "Connected"; + if (WifiService.connectionStatus === "connecting" && WifiService.connectingSSID === modelData.ssid) + return "Connecting..."; + if (WifiService.connectionStatus === "invalid_password" && WifiService.connectingSSID === modelData.ssid) + return "Invalid password"; if (modelData.saved) return "Saved" + (modelData.secured ? " • Secured" : " • Open"); return modelData.secured ? "Secured" : "Open"; } font.pixelSize: Theme.fontSizeSmall - 1 - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + color: { + if (WifiService.connectionStatus === "connecting" && WifiService.connectingSSID === modelData.ssid) + return Theme.primary; + if (WifiService.connectionStatus === "invalid_password" && WifiService.connectingSSID === modelData.ssid) + return Theme.error; + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7); + } elide: Text.ElideRight } } @@ -664,28 +637,37 @@ Item { anchors.verticalCenter: parent.verticalCenter } - // Forget button (for saved networks) + // Context menu button Rectangle { + id: wifiMenuButton width: 24 height: 24 radius: 12 - color: forgetArea2.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" - visible: modelData.saved || modelData.connected + color: wifiMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" DankIcon { - anchors.centerIn: parent - name: "delete" + name: "more_vert" size: Theme.iconSize - 8 - color: forgetArea2.containsMouse ? Theme.error : Theme.surfaceText + color: Theme.surfaceText + opacity: 0.6 + anchors.centerIn: parent } MouseArea { - id: forgetArea2 + id: wifiMenuButtonArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - WifiService.forgetWifiNetwork(modelData.ssid); + wifiContextMenuWindow.networkData = modelData; + let localPos = wifiMenuButtonArea.mapToItem(networkTab, wifiMenuButtonArea.width / 2, wifiMenuButtonArea.height); + wifiContextMenuWindow.show(localPos.x, localPos.y); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration } } } @@ -695,6 +677,7 @@ Item { MouseArea { id: networkArea2 anchors.fill: parent + anchors.rightMargin: 32 // Exclude menu button area hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { @@ -841,4 +824,279 @@ Item { wifiMonitorTimer.stop(); } } + + // WiFi Context Menu Window + Rectangle { + id: wifiContextMenuWindow + + property var networkData: null + property bool menuVisible: false + + function show(x, y) { + const menuWidth = 160; + const menuHeight = wifiMenuColumn.implicitHeight + Theme.spacingS * 2; + let finalX = x - menuWidth / 2; + let finalY = y; + finalX = Math.max(0, Math.min(finalX, networkTab.width - menuWidth)); + finalY = Math.max(0, Math.min(finalY, networkTab.height - menuHeight)); + wifiContextMenuWindow.x = finalX; + wifiContextMenuWindow.y = finalY; + wifiContextMenuWindow.visible = true; + wifiContextMenuWindow.menuVisible = true; + } + + function hide() { + wifiContextMenuWindow.menuVisible = false; + Qt.callLater(() => { + wifiContextMenuWindow.visible = false; + }); + } + + visible: false + width: 160 + height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadiusLarge + color: Theme.popupBackground() + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + z: 1000 + opacity: menuVisible ? 1 : 0 + scale: menuVisible ? 1 : 0.85 + + // Drop shadow + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: parent.z - 1 + } + + Column { + id: wifiMenuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 1 + + // Connect/Disconnect option + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadiusSmall + color: connectWifiArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: wifiContextMenuWindow.networkData && wifiContextMenuWindow.networkData.connected ? "wifi_off" : "wifi" + size: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: wifiContextMenuWindow.networkData && wifiContextMenuWindow.networkData.connected ? "Disconnect" : "Connect" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: connectWifiArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (wifiContextMenuWindow.networkData) { + if (wifiContextMenuWindow.networkData.connected) { + // Disconnect from current network + WifiService.disconnectWifi(); + } else { + // Connect to selected network + if (wifiContextMenuWindow.networkData.saved) { + WifiService.connectToWifi(wifiContextMenuWindow.networkData.ssid); + } else if (wifiContextMenuWindow.networkData.secured) { + // Show password dialog for secured networks + wifiPasswordDialog.wifiPasswordSSID = wifiContextMenuWindow.networkData.ssid; + wifiPasswordDialog.wifiPasswordInput = ""; + wifiPasswordDialog.wifiPasswordDialogVisible = true; + } else { + WifiService.connectToWifi(wifiContextMenuWindow.networkData.ssid); + } + } + } + wifiContextMenuWindow.hide(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + // Separator + Rectangle { + width: parent.width - Theme.spacingS * 2 + height: 5 + anchors.horizontalCenter: parent.horizontalCenter + color: "transparent" + + Rectangle { + anchors.centerIn: parent + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + } + + // Forget Network option (only for saved networks) + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadiusSmall + color: forgetWifiArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" + visible: wifiContextMenuWindow.networkData && (wifiContextMenuWindow.networkData.saved || wifiContextMenuWindow.networkData.connected) + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "delete" + size: Theme.iconSize - 2 + color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: "Forget Network" + font.pixelSize: Theme.fontSizeSmall + color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: forgetWifiArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (wifiContextMenuWindow.networkData) { + WifiService.forgetWifiNetwork(wifiContextMenuWindow.networkData.ssid); + } + wifiContextMenuWindow.hide(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + // Network Info option + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadiusSmall + color: infoWifiArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "info" + size: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: "Network Info" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: infoWifiArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (wifiContextMenuWindow.networkData) { + networkInfoDialog.showNetworkInfo(wifiContextMenuWindow.networkData.ssid, wifiContextMenuWindow.networkData); + } + wifiContextMenuWindow.hide(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + // Background MouseArea to close the context menu + MouseArea { + anchors.fill: parent + visible: wifiContextMenuWindow.visible + onClicked: { + wifiContextMenuWindow.hide(); + } + + MouseArea { + x: wifiContextMenuWindow.x + y: wifiContextMenuWindow.y + width: wifiContextMenuWindow.width + height: wifiContextMenuWindow.height + onClicked: { + // Prevent clicks on menu from closing it + } + } + } } \ No newline at end of file diff --git a/Modules/NetworkInfoDialog.qml b/Modules/NetworkInfoDialog.qml new file mode 100644 index 00000000..ebdd3565 --- /dev/null +++ b/Modules/NetworkInfoDialog.qml @@ -0,0 +1,208 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets + +PanelWindow { + id: root + + property bool networkInfoDialogVisible: false + property string networkSSID: "" + property var networkData: null + property string networkDetails: "" + + visible: networkInfoDialogVisible + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: networkInfoDialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + color: "transparent" + + function showNetworkInfo(ssid, data) { + networkSSID = ssid; + networkData = data; + networkInfoDialogVisible = true; + WifiService.fetchNetworkInfo(ssid); + } + + function hideDialog() { + networkInfoDialogVisible = false; + networkSSID = ""; + networkData = null; + networkDetails = ""; + } + + anchors { + top: true + left: true + right: true + bottom: true + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.5) + opacity: networkInfoDialogVisible ? 1 : 0 + + MouseArea { + anchors.fill: parent + onClicked: { + root.hideDialog(); + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + } + + Rectangle { + width: Math.min(600, parent.width - Theme.spacingL * 2) + height: Math.min(500, parent.height - Theme.spacingL * 2) + anchors.centerIn: parent + color: Theme.surfaceContainer + radius: Theme.cornerRadiusLarge + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + opacity: networkInfoDialogVisible ? 1 : 0 + scale: networkInfoDialogVisible ? 1 : 0.9 + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + // Header + Row { + width: parent.width + + Column { + width: parent.width - 40 + spacing: Theme.spacingXS + + Text { + text: "Network Information" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + Text { + text: "Details for \"" + networkSSID + "\"" + font.pixelSize: Theme.fontSizeMedium + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + width: parent.width + elide: Text.ElideRight + } + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) + onClicked: { + root.hideDialog(); + } + } + } + + // Network Details + ScrollView { + width: parent.width + height: parent.height - 140 + clip: true + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + Flickable { + contentWidth: parent.width + contentHeight: detailsRect.height + + Rectangle { + id: detailsRect + width: parent.width + height: Math.max(parent.parent.height, detailsText.contentHeight + Theme.spacingM * 2) + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + + Text { + id: detailsText + anchors.fill: parent + anchors.margins: Theme.spacingM + text: WifiService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + wrapMode: Text.WordWrap + lineHeight: 1.5 + } + } + } + } + + // Close Button + Item { + width: parent.width + height: 40 + + Rectangle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2) + height: 36 + radius: Theme.cornerRadius + color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary + + Text { + id: closeText + anchors.centerIn: parent + text: "Close" + font.pixelSize: Theme.fontSizeMedium + color: Theme.background + font.weight: Font.Medium + } + + MouseArea { + id: closeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.hideDialog(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } +} \ No newline at end of file diff --git a/Modules/WifiPasswordDialog.qml b/Modules/WifiPasswordDialog.qml index 44c2a5b8..2004d4e7 100644 --- a/Modules/WifiPasswordDialog.qml +++ b/Modules/WifiPasswordDialog.qml @@ -72,6 +72,14 @@ PanelWindow { opacity: wifiPasswordDialogVisible ? 1 : 0 scale: wifiPasswordDialogVisible ? 1 : 0.9 + // Prevent clicks inside dialog from closing it + MouseArea { + anchors.fill: parent + onClicked: { + // Do nothing - prevent propagation to background + } + } + Column { anchors.fill: parent anchors.margins: Theme.spacingL @@ -131,6 +139,7 @@ PanelWindow { anchors.fill: parent font.pixelSize: Theme.fontSizeMedium textColor: Theme.surfaceText + text: wifiPasswordInput echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password enabled: wifiPasswordDialogVisible placeholderText: "Enter password" @@ -142,6 +151,10 @@ PanelWindow { } onAccepted: { WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput); + // Close dialog immediately after pressing Enter + passwordInput.enabled = false; + wifiPasswordDialogVisible = false; + wifiPasswordInput = ""; } Component.onCompleted: { if (wifiPasswordDialogVisible) @@ -267,6 +280,10 @@ PanelWindow { enabled: parent.enabled onClicked: { WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput); + // Close dialog immediately after clicking connect + passwordInput.enabled = false; + wifiPasswordDialogVisible = false; + wifiPasswordInput = ""; } } @@ -304,4 +321,17 @@ PanelWindow { } + // Auto-reopen dialog on invalid password + Connections { + target: WifiService + function onPasswordDialogShouldReopenChanged() { + if (WifiService.passwordDialogShouldReopen && WifiService.connectingSSID !== "") { + wifiPasswordSSID = WifiService.connectingSSID; + wifiPasswordInput = ""; + wifiPasswordDialogVisible = true; + WifiService.passwordDialogShouldReopen = false; + } + } + } + } diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 2846665a..94b08f1d 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -75,35 +75,7 @@ Singleton { console.log("User prefers Ethernet, setting status to ethernet") } else { // Auto mode - check which interface has the default route - let priorityChecker = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["sh", "-c", "ip route show default | head -1 | cut -d\\\" \\\" -f5"] - running: true - stdout: SplitParser { - splitMarker: "\\n" - onRead: function(data) { - let defaultInterface = data.trim() - console.log("Default route interface:", defaultInterface) - // Check if the interface is wifi or ethernet - if (defaultInterface.startsWith("wl") || defaultInterface.includes("wifi")) { - root.networkStatus = "wifi" - console.log("WiFi interface has default route, setting status to wifi") - // Trigger WiFi SSID update - if (root.wifiEnabled) { - WifiService.updateCurrentWifiInfo() - } - } else if (defaultInterface.startsWith("en") || defaultInterface.includes("eth")) { - root.networkStatus = "ethernet" - console.log("Ethernet interface has default route, setting status to ethernet") - } else { - root.networkStatus = "disconnected" - console.log("Unknown interface type:", defaultInterface) - } - } - } - } - `, root) + defaultRouteChecker.running = true } } else if (hasWifi) { root.networkStatus = "wifi" @@ -157,6 +129,35 @@ Singleton { } } + Process { + id: defaultRouteChecker + command: ["sh", "-c", "ip route show default | head -1 | cut -d' ' -f5"] + running: false + + stdout: SplitParser { + splitMarker: "\n" + onRead: function(data) { + let defaultInterface = data.trim() + console.log("Default route interface:", defaultInterface) + // Check if the interface is wifi or ethernet + if (defaultInterface.startsWith("wl") || defaultInterface.includes("wifi")) { + root.networkStatus = "wifi" + console.log("WiFi interface has default route, setting status to wifi") + // Trigger WiFi SSID update + if (root.wifiEnabled) { + WifiService.updateCurrentWifiInfo() + } + } else if (defaultInterface.startsWith("en") || defaultInterface.includes("eth")) { + root.networkStatus = "ethernet" + console.log("Ethernet interface has default route, setting status to ethernet") + } else { + root.networkStatus = "disconnected" + console.log("Unknown interface type:", defaultInterface) + } + } + } + } + Process { id: wifiRadioChecker command: ["nmcli", "radio", "wifi"] @@ -186,25 +187,7 @@ Singleton { console.log("Ethernet IP:", root.ethernetIP) // Get the ethernet interface name - let ethInterfaceProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["sh", "-c", "nmcli -t -f DEVICE,TYPE device | grep ethernet | grep connected | cut -d: -f1 | head -1"] - running: true - stdout: SplitParser { - splitMarker: "\\n" - onRead: function(interfaceData) { - if (interfaceData.trim()) { - root.ethernetInterface = interfaceData.trim() - console.log("Ethernet Interface:", root.ethernetInterface) - - // Ethernet interface detected - status will be determined by route checking - console.log("Ethernet interface detected:", root.ethernetInterface) - } - } - } - } - `, root) + ethernetInterfaceChecker.running = true } else { console.log("No ethernet IP found") root.ethernetIP = "" @@ -214,6 +197,25 @@ Singleton { } } + Process { + id: ethernetInterfaceChecker + command: ["sh", "-c", "nmcli -t -f DEVICE,TYPE device | grep ethernet | grep connected | cut -d: -f1 | head -1"] + running: false + + stdout: SplitParser { + splitMarker: "\n" + onRead: function(interfaceData) { + if (interfaceData.trim()) { + root.ethernetInterface = interfaceData.trim() + console.log("Ethernet Interface:", root.ethernetInterface) + + // Ethernet interface detected - status will be determined by route checking + console.log("Ethernet interface detected:", root.ethernetInterface) + } + } + } + } + Process { id: wifiIPChecker command: ["sh", "-c", "WIFI_DEV=$(nmcli -t -f DEVICE,TYPE device | grep wifi | grep connected | cut -d: -f1 | head -1); if [ -n \"$WIFI_DEV\" ]; then nmcli -t -f IP4.ADDRESS dev show \"$WIFI_DEV\" | cut -d: -f2 | cut -d/ -f1 | head -1; fi"] @@ -236,76 +238,154 @@ Singleton { } } + // Static processes for network operations + Process { + id: ethernetDisconnector + command: ["sh", "-c", "nmcli device disconnect $(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1)"] + running: false + + onExited: function(exitCode) { + console.log("Ethernet disconnect result:", exitCode) + delayedRefreshNetworkStatus() + } + + stderr: SplitParser { + splitMarker: "\n" + onRead: function(data) { + console.log("Ethernet disconnect stderr:", data) + } + } + } + + Process { + id: ethernetConnector + command: ["sh", "-c", "ETH_DEV=$(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1); if [ -n \"$ETH_DEV\" ]; then nmcli device connect \"$ETH_DEV\"; ETH_CONN=$(nmcli -t -f NAME,DEVICE connection show --active | grep \"$ETH_DEV\" | cut -d: -f1); if [ -n \"$ETH_CONN\" ]; then nmcli connection modify \"$ETH_CONN\" connection.autoconnect-priority 100; nmcli connection down \"$ETH_CONN\"; nmcli connection up \"$ETH_CONN\"; fi; else echo \"No ethernet device found\"; exit 1; fi"] + running: false + + onExited: function(exitCode) { + console.log("Ethernet connect result:", exitCode) + if (exitCode === 0) { + console.log("Ethernet connected successfully with higher priority") + } else { + console.log("Ethernet connection failed") + } + delayedRefreshNetworkStatus() + } + + stderr: SplitParser { + splitMarker: "\n" + onRead: function(data) { + console.log("Ethernet connect stderr:", data) + } + } + } + + Process { + id: wifiDeviceConnector + command: ["sh", "-c", "WIFI_DEV=$(nmcli -t -f DEVICE,TYPE device | grep wifi | cut -d: -f1 | head -1); if [ -n \"$WIFI_DEV\" ]; then nmcli device connect \"$WIFI_DEV\"; else echo \"No WiFi device found\"; exit 1; fi"] + running: false + + onExited: function(exitCode) { + console.log("WiFi device connect result:", exitCode) + delayedRefreshNetworkStatus() + } + + stderr: SplitParser { + splitMarker: "\n" + onRead: function(data) { + console.log("WiFi device connect stderr:", data) + } + } + } + + Process { + id: wifiSwitcher + command: ["sh", "-c", "ETH_DEV=$(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1); WIFI_DEV=$(nmcli -t -f DEVICE,TYPE device | grep wifi | cut -d: -f1 | head -1); [ -n \"$ETH_DEV\" ] && nmcli device disconnect \"$ETH_DEV\" 2>/dev/null; [ -n \"$WIFI_DEV\" ] && nmcli device connect \"$WIFI_DEV\" 2>/dev/null || true"] + running: false + + onExited: function(exitCode) { + console.log("Switch to wifi result:", exitCode) + delayedRefreshNetworkStatus() + } + + stderr: SplitParser { + splitMarker: "\n" + onRead: function(data) { + console.log("Switch to wifi stderr:", data) + } + } + } + + Process { + id: ethernetSwitcher + command: ["sh", "-c", "WIFI_DEV=$(nmcli -t -f DEVICE,TYPE device | grep wifi | cut -d: -f1 | head -1); ETH_DEV=$(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1); [ -n \"$WIFI_DEV\" ] && nmcli device disconnect \"$WIFI_DEV\" 2>/dev/null; [ -n \"$ETH_DEV\" ] && nmcli device connect \"$ETH_DEV\" 2>/dev/null || true"] + running: false + + onExited: function(exitCode) { + console.log("Switch to ethernet result:", exitCode) + delayedRefreshNetworkStatus() + } + + stderr: SplitParser { + splitMarker: "\n" + onRead: function(data) { + console.log("Switch to ethernet stderr:", data) + } + } + } + + Process { + id: wifiRadioToggler + command: ["nmcli", "radio", "wifi", root.wifiEnabled ? "off" : "on"] + running: false + + onExited: { + root.wifiToggling = false + networkStatusChecker.running = true + } + } + + Process { + id: wifiPriorityChanger + command: ["sh", "-c", "nmcli -t -f NAME,TYPE connection show | grep 802-11-wireless | cut -d: -f1 | while read conn; do nmcli connection modify \"$conn\" ipv4.route-metric 50; done; nmcli -t -f NAME,TYPE connection show | grep 802-3-ethernet | cut -d: -f1 | while read conn; do nmcli connection modify \"$conn\" ipv4.route-metric 200; done; nmcli -t -f NAME,TYPE connection show --active | grep -E \"(802-11-wireless|802-3-ethernet)\" | cut -d: -f1 | while read conn; do nmcli connection down \"$conn\" && nmcli connection up \"$conn\"; done"] + running: false + + onExited: function(exitCode) { + console.log("WiFi route metric set to 50, ethernet to 200, connections restarted, exit code:", exitCode) + // Don't reset changingPreference here - let network status check handle it + delayedRefreshNetworkStatus() + } + } + + Process { + id: ethernetPriorityChanger + command: ["sh", "-c", "nmcli -t -f NAME,TYPE connection show | grep 802-3-ethernet | cut -d: -f1 | while read conn; do nmcli connection modify \"$conn\" ipv4.route-metric 50; done; nmcli -t -f NAME,TYPE connection show | grep 802-11-wireless | cut -d: -f1 | while read conn; do nmcli connection modify \"$conn\" ipv4.route-metric 200; done; nmcli -t -f NAME,TYPE connection show --active | grep -E \"(802-11-wireless|802-3-ethernet)\" | cut -d: -f1 | while read conn; do nmcli connection down \"$conn\" && nmcli connection up \"$conn\"; done"] + running: false + + onExited: function(exitCode) { + console.log("Ethernet route metric set to 50, WiFi to 200, connections restarted, exit code:", exitCode) + // Don't reset changingPreference here - let network status check handle it + delayedRefreshNetworkStatus() + } + } + function toggleNetworkConnection(type) { if (type === "ethernet") { // Toggle ethernet connection if (root.networkStatus === "ethernet") { // Disconnect ethernet console.log("Disconnecting ethernet...") - let disconnectProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["sh", "-c", "nmcli device disconnect $(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1)"] - running: true - onExited: function(exitCode) { - console.log("Ethernet disconnect result:", exitCode) - delayedRefreshNetworkStatus() - } - stderr: SplitParser { - splitMarker: "\\n" - onRead: function(data) { - console.log("Ethernet disconnect stderr:", data) - } - } - } - `, root) + ethernetDisconnector.running = true } else { // Connect ethernet and set higher priority console.log("Connecting ethernet...") - let connectProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["sh", "-c", "ETH_DEV=$(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1); if [ -n \\"$ETH_DEV\\" ]; then nmcli device connect \\"$ETH_DEV\\"; ETH_CONN=$(nmcli -t -f NAME,DEVICE connection show --active | grep \\"$ETH_DEV\\" | cut -d: -f1); if [ -n \\"$ETH_CONN\\" ]; then nmcli connection modify \\"$ETH_CONN\\" connection.autoconnect-priority 100; nmcli connection down \\"$ETH_CONN\\"; nmcli connection up \\"$ETH_CONN\\"; fi; else echo \\"No ethernet device found\\"; exit 1; fi"] - running: true - onExited: function(exitCode) { - console.log("Ethernet connect result:", exitCode) - if (exitCode === 0) { - console.log("Ethernet connected successfully with higher priority") - } else { - console.log("Ethernet connection failed") - } - delayedRefreshNetworkStatus() - } - stderr: SplitParser { - splitMarker: "\\n" - onRead: function(data) { - console.log("Ethernet connect stderr:", data) - } - } - } - `, root) + ethernetConnector.running = true } } else if (type === "wifi") { // Connect to WiFi if disconnected if (root.networkStatus !== "wifi" && root.wifiEnabled) { console.log("Connecting to WiFi device...") - let connectProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["sh", "-c", "WIFI_DEV=$(nmcli -t -f DEVICE,TYPE device | grep wifi | cut -d: -f1 | head -1); if [ -n \\"$WIFI_DEV\\" ]; then nmcli device connect \\"$WIFI_DEV\\"; else echo \\"No WiFi device found\\"; exit 1; fi"] - running: true - onExited: function(exitCode) { - console.log("WiFi device connect result:", exitCode) - delayedRefreshNetworkStatus() - } - stderr: SplitParser { - splitMarker: "\\n" - onRead: function(data) { - console.log("WiFi device connect stderr:", data) - } - } - } - `, root) + wifiDeviceConnector.running = true } } } @@ -313,63 +393,21 @@ Singleton { function switchToWifi() { console.log("Switching to WiFi") // Disconnect ethernet first, then try to connect to a known WiFi network - let switchProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["sh", "-c", "ETH_DEV=$(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1); WIFI_DEV=$(nmcli -t -f DEVICE,TYPE device | grep wifi | cut -d: -f1 | head -1); [ -n \\"$ETH_DEV\\" ] && nmcli device disconnect \\"$ETH_DEV\\" 2>/dev/null; [ -n \\"$WIFI_DEV\\" ] && nmcli device connect \\"$WIFI_DEV\\" 2>/dev/null || true"] - running: true - onExited: function(exitCode) { - console.log("Switch to wifi result:", exitCode) - delayedRefreshNetworkStatus() - } - stderr: SplitParser { - splitMarker: "\\n" - onRead: function(data) { - console.log("Switch to wifi stderr:", data) - } - } - } - `, root) + wifiSwitcher.running = true } function switchToEthernet() { console.log("Switching to Ethernet") // Disconnect WiFi first, then connect ethernet - let switchProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["sh", "-c", "WIFI_DEV=$(nmcli -t -f DEVICE,TYPE device | grep wifi | cut -d: -f1 | head -1); ETH_DEV=$(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1); [ -n \\"$WIFI_DEV\\" ] && nmcli device disconnect \\"$WIFI_DEV\\" 2>/dev/null; [ -n \\"$ETH_DEV\\" ] && nmcli device connect \\"$ETH_DEV\\" 2>/dev/null || true"] - running: true - onExited: function(exitCode) { - console.log("Switch to ethernet result:", exitCode) - delayedRefreshNetworkStatus() - } - stderr: SplitParser { - splitMarker: "\\n" - onRead: function(data) { - console.log("Switch to ethernet stderr:", data) - } - } - } - `, root) + ethernetSwitcher.running = true } function toggleWifiRadio() { if (root.wifiToggling) return root.wifiToggling = true - let action = root.wifiEnabled ? "off" : "on" - let toggleProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["nmcli", "radio", "wifi", "${action}"] - running: true - onExited: { - root.wifiToggling = false - networkStatusChecker.running = true - } - } - `, root) + wifiRadioToggler.command = ["nmcli", "radio", "wifi", root.wifiEnabled ? "off" : "on"] + wifiRadioToggler.running = true } function refreshNetworkStatus() { @@ -391,32 +429,10 @@ Singleton { if (preference === "wifi") { // Set WiFi to low route metric (high priority), ethernet to high route metric (low priority) - let wifiPriorityProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["sh", "-c", "nmcli -t -f NAME,TYPE connection show | grep 802-11-wireless | cut -d: -f1 | while read conn; do nmcli connection modify \\\\\\"$conn\\\\\\" ipv4.route-metric 50; done; nmcli -t -f NAME,TYPE connection show | grep 802-3-ethernet | cut -d: -f1 | while read conn; do nmcli connection modify \\\\\\"$conn\\\\\\" ipv4.route-metric 200; done; nmcli -t -f NAME,TYPE connection show --active | grep -E \\\\\\"(802-11-wireless|802-3-ethernet)\\\\\\" | cut -d: -f1 | while read conn; do nmcli connection down \\\\\\"$conn\\\\\\" && nmcli connection up \\\\\\"$conn\\\\\\"; done"] - running: true - onExited: function(exitCode) { - console.log("WiFi route metric set to 50, ethernet to 200, connections restarted, exit code:", exitCode) - // Don't reset changingPreference here - let network status check handle it - delayedRefreshNetworkStatus() - } - } - `, root) + wifiPriorityChanger.running = true } else if (preference === "ethernet") { // Set ethernet to low route metric (high priority), WiFi to high route metric (low priority) - let ethernetPriorityProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["sh", "-c", "nmcli -t -f NAME,TYPE connection show | grep 802-3-ethernet | cut -d: -f1 | while read conn; do nmcli connection modify \\\\\\"$conn\\\\\\" ipv4.route-metric 50; done; nmcli -t -f NAME,TYPE connection show | grep 802-11-wireless | cut -d: -f1 | while read conn; do nmcli connection modify \\\\\\"$conn\\\\\\" ipv4.route-metric 200; done; nmcli -t -f NAME,TYPE connection show --active | grep -E \\\\\\"(802-11-wireless|802-3-ethernet)\\\\\\" | cut -d: -f1 | while read conn; do nmcli connection down \\\\\\"$conn\\\\\\" && nmcli connection up \\\\\\"$conn\\\\\\"; done"] - running: true - onExited: function(exitCode) { - console.log("Ethernet route metric set to 50, WiFi to 200, connections restarted, exit code:", exitCode) - // Don't reset changingPreference here - let network status check handle it - delayedRefreshNetworkStatus() - } - } - `, root) + ethernetPriorityChanger.running = true } } diff --git a/Services/ProcessMonitorService.qml b/Services/ProcessMonitorService.qml index 6e8d6904..3f394ab3 100644 --- a/Services/ProcessMonitorService.qml +++ b/Services/ProcessMonitorService.qml @@ -119,25 +119,13 @@ Singleton { updateProcessList(); } + property int killPid: 0 + function killProcess(pid) { if (pid > 0) { - const killCmd = ["bash", "-c", "kill " + pid]; - const killProcess = Qt.createQmlObject(` - import QtQuick - import Quickshell.Io - Process { - command: ${JSON.stringify(killCmd)} - running: true - onExited: (exitCode) => { - if (exitCode === 0) { - console.log("Process killed successfully:", ${pid}) - } else { - console.warn("Failed to kill process:", ${pid}, "exit code:", exitCode) - } - destroy() - } - } - `, root); + root.killPid = pid + processKiller.command = ["bash", "-c", "kill " + pid] + processKiller.running = true } } @@ -438,6 +426,21 @@ Singleton { } + Process { + id: processKiller + command: ["bash", "-c", "kill " + root.killPid] + running: false + + onExited: (exitCode) => { + if (exitCode === 0) { + console.log("Process killed successfully:", root.killPid) + } else { + console.warn("Failed to kill process:", root.killPid, "exit code:", exitCode) + } + root.killPid = 0 + } + } + Timer { id: processTimer diff --git a/Services/WifiService.qml b/Services/WifiService.qml index 6fc26e5b..412ccf03 100644 --- a/Services/WifiService.qml +++ b/Services/WifiService.qml @@ -13,10 +13,19 @@ Singleton { property var wifiNetworks: [] property var savedWifiNetworks: [] property bool isScanning: false - property string connectionStatus: "" // "cosnnecting", "connected", "failed", "" + property string connectionStatus: "" // "connecting", "connected", "failed", "invalid_password", "" property string connectingSSID: "" + property string lastConnectionError: "" + property bool passwordDialogShouldReopen: false // Auto-refresh timer for when control center is open property bool autoRefreshEnabled: false + + signal networksUpdated() + + // Network info properties + property string networkInfoSSID: "" + property string networkInfoDetails: "" + property bool networkInfoLoading: false function scanWifi() { if (root.isScanning) @@ -33,107 +42,49 @@ Singleton { console.log("Connecting to WiFi:", ssid); root.connectionStatus = "connecting"; root.connectingSSID = ssid; - let connectProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["bash", "-c", "nmcli dev wifi connect \\"' + ssid + '\\" || nmcli connection up \\"' + ssid + '\\"; if [ $? -eq 0 ]; then nmcli connection modify \\"' + ssid + '\\" connection.autoconnect-priority 50; nmcli connection down \\"' + ssid + '\\"; nmcli connection up \\"' + ssid + '\\"; fi"] - running: true - onExited: (exitCode) => { - console.log("WiFi connection result:", exitCode) - if (exitCode === 0) { - root.connectionStatus = "connected" - console.log("Connected to WiFi successfully") - // Set user preference to WiFi when manually connecting - NetworkService.setNetworkPreference("wifi") - // Force network status refresh after successful connection - NetworkService.delayedRefreshNetworkStatus() - } else { - root.connectionStatus = "failed" - console.log("WiFi connection failed") - } - scanWifi() - - statusResetTimer.start() - } - - stderr: SplitParser { - splitMarker: "\\n" - onRead: (data) => { - console.log("WiFi connection stderr:", data) - } - } - } - `, root); + ToastService.showInfo("Connecting to " + ssid + "..."); + wifiConnector.running = true; } + property string wifiPassword: "" + function connectToWifiWithPassword(ssid, password) { console.log("Connecting to WiFi with password:", ssid); root.connectionStatus = "connecting"; root.connectingSSID = ssid; - let connectProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["bash", "-c", "nmcli dev wifi connect \\"' + ssid + '\\" password \\"' + password + '\\"; if [ $? -eq 0 ]; then nmcli connection modify \\"' + ssid + '\\" connection.autoconnect-priority 50; nmcli connection down \\"' + ssid + '\\"; nmcli connection up \\"' + ssid + '\\"; fi"] - running: true - onExited: (exitCode) => { - console.log("WiFi connection with password result:", exitCode) - if (exitCode === 0) { - root.connectionStatus = "connected" - console.log("Connected to WiFi with password successfully") - // Set user preference to WiFi when manually connecting - NetworkService.setNetworkPreference("wifi") - // Force network status refresh after successful connection - NetworkService.delayedRefreshNetworkStatus() - } else { - root.connectionStatus = "failed" - console.log("WiFi connection with password failed") - } - scanWifi() - - statusResetTimer.start() - } - - stderr: SplitParser { - splitMarker: "\\n" - onRead: (data) => { - console.log("WiFi connection with password stderr:", data) - } - } - } - `, root); + root.wifiPassword = password; + root.lastConnectionError = ""; + root.passwordDialogShouldReopen = false; + ToastService.showInfo("Connecting to " + ssid + "..."); + wifiPasswordConnector.running = true; } + function disconnectWifi() { + console.log("Disconnecting from current WiFi network"); + wifiDisconnector.running = true; + } + + property string forgetSSID: "" + function forgetWifiNetwork(ssid) { console.log("Forgetting WiFi network:", ssid); - let forgetProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["bash", "-c", "nmcli connection delete \\"' + ssid + '\\" || nmcli connection delete id \\"' + ssid + '\\""] - running: true - onExited: (exitCode) => { - console.log("WiFi forget result:", exitCode) - if (exitCode === 0) { - console.log("Successfully forgot WiFi network:", "' + ssid + '") - } else { - console.log("Failed to forget WiFi network:", "' + ssid + '") - } - scanWifi() - } + root.forgetSSID = ssid; + wifiForget.running = true; + } - stderr: SplitParser { - splitMarker: "\\n" - onRead: (data) => { - console.log("WiFi forget stderr:", data) - } - } - } - `, root); + function fetchNetworkInfo(ssid) { + console.log("Fetching network info for:", ssid); + root.networkInfoSSID = ssid; + root.networkInfoLoading = true; + root.networkInfoDetails = "Loading network information..."; + wifiInfoFetcher.running = true; } function updateCurrentWifiInfo() { currentWifiInfo.running = true; } + Process { id: currentWifiInfo @@ -245,6 +196,7 @@ Singleton { } + Timer { id: fallbackTimer @@ -273,4 +225,304 @@ Singleton { onTriggered: root.scanWifi() } + // WiFi Connection Process + Process { + id: wifiConnector + command: ["bash", "-c", "timeout 30 nmcli dev wifi connect \"" + root.connectingSSID + "\" || nmcli connection up \"" + root.connectingSSID + "\"; exit_code=$?; echo \"nmcli exit code: $exit_code\" >&2; if [ $exit_code -eq 0 ]; then nmcli connection modify \"" + root.connectingSSID + "\" connection.autoconnect-priority 50; sleep 2; if nmcli -t -f ACTIVE,SSID dev wifi | grep -q \"^yes:" + root.connectingSSID + "\"; then echo \"Connection verified\" >&2; exit 0; else echo \"Connection failed verification\" >&2; exit 4; fi; else exit $exit_code; fi"] + running: false + + stderr: StdioCollector { + onStreamFinished: { + console.log("WiFi connection debug output:", text.trim()) + } + } + + onExited: (exitCode) => { + console.log("WiFi connection result:", exitCode) + if (exitCode === 0) { + root.connectionStatus = "connected" + root.passwordDialogShouldReopen = false + console.log("Connected to WiFi successfully") + ToastService.showInfo("Connected to " + root.connectingSSID) + NetworkService.setNetworkPreference("wifi") + NetworkService.delayedRefreshNetworkStatus() + + // Immediately update savedWifiNetworks to include the new connection + if (!root.savedWifiNetworks.some((saved) => saved.ssid === root.connectingSSID)) { + let updatedSaved = [...root.savedWifiNetworks]; + updatedSaved.push({"ssid": root.connectingSSID, "saved": true}); + root.savedWifiNetworks = updatedSaved; + } + + // Update wifiNetworks to reflect the change + let updatedNetworks = [...root.wifiNetworks]; + for (let i = 0; i < updatedNetworks.length; i++) { + if (updatedNetworks[i].ssid === root.connectingSSID) { + updatedNetworks[i].saved = true; + updatedNetworks[i].connected = true; + break; + } + } + root.wifiNetworks = updatedNetworks; + } else if (exitCode === 4) { + // Connection failed - likely needs password for saved network + root.connectionStatus = "invalid_password" + root.passwordDialogShouldReopen = true + console.log("Saved network connection failed - password required") + ToastService.showError("Authentication failed for " + root.connectingSSID) + } else { + root.connectionStatus = "failed" + console.log("WiFi connection failed") + ToastService.showError("Failed to connect to " + root.connectingSSID) + } + scanWifi() + statusResetTimer.start() + } + + } + + // WiFi Connection with Password Process + Process { + id: wifiPasswordConnector + command: ["bash", "-c", "nmcli connection delete \"" + root.connectingSSID + "\" 2>/dev/null || true; timeout 30 nmcli dev wifi connect \"" + root.connectingSSID + "\" password \"" + root.wifiPassword + "\"; exit_code=$?; echo \"nmcli exit code: $exit_code\" >&2; if [ $exit_code -eq 0 ]; then nmcli connection modify \"" + root.connectingSSID + "\" connection.autoconnect-priority 50; sleep 2; if nmcli -t -f ACTIVE,SSID dev wifi | grep -q \"^yes:" + root.connectingSSID + "\"; then echo \"Connection verified\" >&2; exit 0; else echo \"Connection failed verification\" >&2; exit 4; fi; else exit $exit_code; fi"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + console.log("WiFi connection stdout:", text.trim()) + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.lastConnectionError = text.trim() + console.log("WiFi connection debug output:", text.trim()) + } + } + + onExited: (exitCode) => { + console.log("WiFi connection with password result:", exitCode) + console.log("Error output:", root.lastConnectionError) + + if (exitCode === 0) { + root.connectionStatus = "connected" + root.passwordDialogShouldReopen = false + console.log("Connected to WiFi with password successfully") + ToastService.showInfo("Connected to " + root.connectingSSID) + NetworkService.setNetworkPreference("wifi") + NetworkService.delayedRefreshNetworkStatus() + + // Immediately update savedWifiNetworks to include the new connection + if (!root.savedWifiNetworks.some((saved) => saved.ssid === root.connectingSSID)) { + let updatedSaved = [...root.savedWifiNetworks]; + updatedSaved.push({"ssid": root.connectingSSID, "saved": true}); + root.savedWifiNetworks = updatedSaved; + } + + // Update wifiNetworks to reflect the change + let updatedNetworks = [...root.wifiNetworks]; + for (let i = 0; i < updatedNetworks.length; i++) { + if (updatedNetworks[i].ssid === root.connectingSSID) { + updatedNetworks[i].saved = true; + updatedNetworks[i].connected = true; + break; + } + } + root.wifiNetworks = updatedNetworks; + } else if (exitCode === 4) { + // Connection activation failed - likely invalid credentials + if (root.lastConnectionError.includes("Secrets were required") || + root.lastConnectionError.includes("authentication") || + root.lastConnectionError.includes("AUTH_TIMED_OUT")) { + root.connectionStatus = "invalid_password" + root.passwordDialogShouldReopen = true + console.log("Invalid password detected") + ToastService.showError("Invalid password for " + root.connectingSSID) + } else { + root.connectionStatus = "failed" + console.log("Connection failed - not password related") + ToastService.showError("Failed to connect to " + root.connectingSSID) + } + } else if (exitCode === 3 || exitCode === 124) { + root.connectionStatus = "failed" + console.log("Connection timed out") + ToastService.showError("Connection to " + root.connectingSSID + " timed out") + } else { + root.connectionStatus = "failed" + console.log("WiFi connection with password failed") + ToastService.showError("Failed to connect to " + root.connectingSSID) + } + root.wifiPassword = "" // Clear password + scanWifi() + statusResetTimer.start() + } + + } + + // WiFi Disconnect Process + Process { + id: wifiDisconnector + command: ["bash", "-c", "WIFI_DEV=$(nmcli -t -f DEVICE,TYPE device | grep wifi | cut -d: -f1 | head -1); [ -n \"$WIFI_DEV\" ] && nmcli device disconnect \"$WIFI_DEV\""] + running: false + + onExited: (exitCode) => { + console.log("WiFi disconnect result:", exitCode) + if (exitCode === 0) { + console.log("Successfully disconnected from WiFi") + ToastService.showInfo("Disconnected from WiFi") + root.currentWifiSSID = "" + root.connectionStatus = "" + NetworkService.refreshNetworkStatus() + } else { + console.log("Failed to disconnect from WiFi") + ToastService.showError("Failed to disconnect from WiFi") + } + } + + stderr: SplitParser { + splitMarker: "\\n" + onRead: (data) => { + console.log("WiFi disconnect stderr:", data) + } + } + } + + // WiFi Forget Network Process + Process { + id: wifiForget + command: ["bash", "-c", "nmcli connection delete \"" + root.forgetSSID + "\" || nmcli connection delete id \"" + root.forgetSSID + "\""] + running: false + + onExited: (exitCode) => { + console.log("WiFi forget result:", exitCode) + if (exitCode === 0) { + console.log("Successfully forgot WiFi network:", root.forgetSSID) + ToastService.showInfo("Forgot network \"" + root.forgetSSID + "\"") + + // If we forgot the currently connected network, clear connection status + if (root.forgetSSID === root.currentWifiSSID) { + root.currentWifiSSID = ""; + root.connectionStatus = ""; + NetworkService.refreshNetworkStatus(); + } + + // Update savedWifiNetworks to remove the forgotten network + root.savedWifiNetworks = root.savedWifiNetworks.filter((saved) => { + return saved.ssid !== root.forgetSSID; + }); + + // Update wifiNetworks - create new array with updated objects + let updatedNetworks = []; + for (let i = 0; i < root.wifiNetworks.length; i++) { + let network = root.wifiNetworks[i]; + if (network.ssid === root.forgetSSID) { + let updatedNetwork = Object.assign({}, network); + updatedNetwork.saved = false; + updatedNetwork.connected = false; + updatedNetworks.push(updatedNetwork); + } else { + updatedNetworks.push(network); + } + } + root.wifiNetworks = updatedNetworks; + root.networksUpdated(); + } else { + console.log("Failed to forget WiFi network:", root.forgetSSID) + ToastService.showError("Failed to forget network \"" + root.forgetSSID + "\"") + } + root.forgetSSID = "" // Clear SSID + } + + stderr: SplitParser { + splitMarker: "\\n" + onRead: (data) => { + console.log("WiFi forget stderr:", data) + } + } + } + + // WiFi Network Info Fetcher Process - Using detailed nmcli output + Process { + id: wifiInfoFetcher + command: ["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,FREQ,RATE,MODE,CHAN,WPA-FLAGS,RSN-FLAGS", "dev", "wifi", "list"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + let details = ""; + if (text.trim()) { + let lines = text.trim().split('\n'); + for (let line of lines) { + let parts = line.split(':'); + if (parts.length >= 9 && parts[0] === root.networkInfoSSID) { + let ssid = parts[0] || "Unknown"; + let signal = parts[1] || "0"; + let security = parts[2] || "Open"; + let freq = parts[3] || "Unknown"; + let rate = parts[4] || "Unknown"; + let mode = parts[5] || "Unknown"; + let channel = parts[6] || "Unknown"; + let wpaFlags = parts[7] || ""; + let rsnFlags = parts[8] || ""; + + // Determine band from frequency + let band = "Unknown"; + let freqNum = parseInt(freq); + if (freqNum >= 2400 && freqNum <= 2500) { + band = "2.4 GHz"; + } else if (freqNum >= 5000 && freqNum <= 6000) { + band = "5 GHz"; + } else if (freqNum >= 6000) { + band = "6 GHz"; + } + + details = "Network Name: " + ssid + "\\n"; + details += "Signal Strength: " + signal + "%\\n"; + details += "Security: " + (security === "" ? "Open" : security) + "\\n"; + details += "Frequency: " + freq + " MHz\\n"; + details += "Band: " + band + "\\n"; + details += "Channel: " + channel + "\\n"; + details += "Mode: " + mode + "\\n"; + details += "Max Rate: " + rate + " Mbit/s\\n"; + + if (wpaFlags !== "") { + details += "WPA Flags: " + wpaFlags + "\\n"; + } + if (rsnFlags !== "") { + details += "RSN Flags: " + rsnFlags + "\\n"; + } + + break; + } + } + } + + if (details === "") { + details = "Network information not found or network not available."; + } + + root.networkInfoDetails = details; + root.networkInfoLoading = false; + console.log("Network info fetched for:", root.networkInfoSSID); + } + } + + onExited: (exitCode) => { + root.networkInfoLoading = false; + if (exitCode !== 0) { + console.log("Failed to fetch network info, exit code:", exitCode); + root.networkInfoDetails = "Failed to fetch network information"; + } + } + + stderr: SplitParser { + splitMarker: "\\n" + onRead: (data) => { + console.log("WiFi info stderr:", data); + } + } + } + } diff --git a/shell.qml b/shell.qml index 4be6c134..e167ce09 100644 --- a/shell.qml +++ b/shell.qml @@ -43,6 +43,10 @@ ShellRoot { id: wifiPasswordDialog } + NetworkInfoDialog { + id: networkInfoDialog + } + InputDialog { id: globalInputDialog }