diff options
| -rw-r--r-- | modules/controlcenter/dev/DevDebugPane.qml | 1837 | ||||
| -rw-r--r-- | plan.md | 137 | ||||
| -rw-r--r-- | services/Nmcli.qml | 1246 |
3 files changed, 3206 insertions, 14 deletions
diff --git a/modules/controlcenter/dev/DevDebugPane.qml b/modules/controlcenter/dev/DevDebugPane.qml index 88d6542..1150f35 100644 --- a/modules/controlcenter/dev/DevDebugPane.qml +++ b/modules/controlcenter/dev/DevDebugPane.qml @@ -5,7 +5,9 @@ import ".." import qs.components import qs.components.controls import qs.components.containers +import qs.components.effects import qs.config +import qs.services import Quickshell import Quickshell.Widgets import QtQuick @@ -18,17 +20,45 @@ Item { anchors.fill: parent - ColumnLayout { + // Track last failed connection + property string lastFailedSsid: "" + + // Connect to connection failure signal + Connections { + target: Nmcli + function onConnectionFailed(ssid: string) { + root.lastFailedSsid = ssid; + appendLog("Connection failed signal received for: " + ssid); + } + } + + StyledFlickable { + id: flickable + anchors.fill: parent anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + flickableDirection: Flickable.VerticalFlick + contentWidth: width + contentHeight: contentLayout.implicitHeight - StyledText { - text: qsTr("Debug Panel") - font.pointSize: Appearance.font.size.larger - font.weight: 500 + StyledScrollBar.vertical: StyledScrollBar { + flickable: flickable } + ColumnLayout { + id: contentLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Debug Panel") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + // Action Buttons Section StyledRect { Layout.fillWidth: true @@ -75,6 +105,1771 @@ Item { appendLog(" Active: " + (root.session.network.active ? "Yes" : "No")); } } + + TextButton { + text: qsTr("Get Device Status") + onClicked: { + appendLog("Getting device status..."); + try { + Nmcli.getDeviceStatus((output) => { + if (!output) { + appendLog(" Error: No output received"); + return; + } + appendLog("Device Status:"); + const lines = output.trim().split("\n"); + if (lines.length === 0 || (lines.length === 1 && lines[0].length === 0)) { + appendLog(" No devices found"); + } else { + for (const line of lines) { + if (line.length > 0) { + appendLog(" " + line); + } + } + } + }); + } catch (e) { + appendLog("Error: " + e); + } + } + } + + TextButton { + text: qsTr("Get Wireless Interfaces") + onClicked: { + appendLog("Getting wireless interfaces..."); + Nmcli.getWirelessInterfaces((interfaces) => { + appendLog("Wireless Interfaces: " + interfaces.length); + for (const iface of interfaces) { + appendLog(` ${iface.device}: ${iface.state} (${iface.connection})`); + } + }); + } + } + + TextButton { + text: qsTr("Get Ethernet Interfaces") + onClicked: { + appendLog("Getting ethernet interfaces..."); + Nmcli.getEthernetInterfaces((interfaces) => { + appendLog("Ethernet Interfaces: " + interfaces.length); + for (const iface of interfaces) { + appendLog(` ${iface.device}: ${iface.state} (${iface.connection})`); + } + }); + } + } + + TextButton { + text: qsTr("Refresh Status") + onClicked: { + appendLog("Refreshing connection status..."); + Nmcli.refreshStatus((status) => { + appendLog("Connection Status:"); + appendLog(" Connected: " + (status.connected ? "Yes" : "No")); + appendLog(" Interface: " + (status.interface || "None")); + appendLog(" Connection: " + (status.connection || "None")); + }); + } + } + + TextButton { + text: qsTr("Check Interface") + onClicked: { + appendLog("Checking interface connection status..."); + // Check first wireless interface if available + if (Nmcli.wirelessInterfaces.length > 0) { + const iface = Nmcli.wirelessInterfaces[0].device; + appendLog("Checking: " + iface); + Nmcli.isInterfaceConnected(iface, (connected) => { + appendLog(` ${iface}: ${connected ? "Connected" : "Disconnected"}`); + }); + } else { + appendLog("No wireless interfaces found"); + } + } + } + } + } + } + + // WiFi Radio Control Section + StyledRect { + Layout.fillWidth: true + implicitHeight: wifiRadioLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: wifiRadioLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("WiFi Radio Control") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Status: ") + (Nmcli.wifiEnabled ? qsTr("Enabled") : qsTr("Disabled")) + color: Nmcli.wifiEnabled ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Toggle WiFi") + onClicked: { + appendLog("Toggling WiFi radio..."); + Nmcli.toggleWifi((result) => { + if (result.success) { + appendLog("WiFi radio toggled: " + (Nmcli.wifiEnabled ? "Enabled" : "Disabled")); + } else { + appendLog("Failed to toggle WiFi: " + (result.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Enable") + onClicked: { + appendLog("Enabling WiFi radio..."); + Nmcli.enableWifi(true, (result) => { + if (result.success) { + appendLog("WiFi radio enabled"); + } else { + appendLog("Failed to enable WiFi: " + (result.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Disable") + onClicked: { + appendLog("Disabling WiFi radio..."); + Nmcli.enableWifi(false, (result) => { + if (result.success) { + appendLog("WiFi radio disabled"); + } else { + appendLog("Failed to disable WiFi: " + (result.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Check Status") + onClicked: { + appendLog("Checking WiFi radio status..."); + Nmcli.getWifiStatus((enabled) => { + appendLog("WiFi radio status: " + (enabled ? "Enabled" : "Disabled")); + }); + } + } + } + } + } + + // Network List Management Section + StyledRect { + Layout.fillWidth: true + implicitHeight: networkListLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: networkListLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Network List Management") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Networks: %1").arg(Nmcli.networks.length) + } + + StyledText { + visible: Nmcli.active + text: qsTr("Active: %1").arg(Nmcli.active.ssid) + color: Colours.palette.m3primary + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Networks") + onClicked: { + appendLog("Refreshing network list..."); + Nmcli.getNetworks((networks) => { + appendLog("Found " + networks.length + " networks"); + if (Nmcli.active) { + appendLog("Active network: " + Nmcli.active.ssid + " (Signal: " + Nmcli.active.strength + "%, Security: " + (Nmcli.active.isSecure ? Nmcli.active.security : "Open") + ")"); + } else { + appendLog("No active network"); + } + }); + } + } + + TextButton { + text: qsTr("List All Networks") + onClicked: { + appendLog("Network list:"); + if (Nmcli.networks.length === 0) { + appendLog(" No networks found"); + } else { + for (let i = 0; i < Nmcli.networks.length; i++) { + const net = Nmcli.networks[i]; + const activeMark = net.active ? " [ACTIVE]" : ""; + appendLog(` ${i + 1}. ${net.ssid}${activeMark}`); + appendLog(` Signal: ${net.strength}%, Freq: ${net.frequency}MHz, Security: ${net.isSecure ? net.security : "Open"}`); + if (net.bssid) { + appendLog(` BSSID: ${net.bssid}`); + } + } + } + } + } + } + } + } + + // Interface Selector Section (for future features) + Item { + Layout.fillWidth: true + implicitHeight: interfaceSelectorContainer.implicitHeight + z: 10 // Ensure dropdown menu appears above other elements + + StyledRect { + id: interfaceSelectorContainer + + anchors.fill: parent + implicitHeight: interfaceSelectorLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: interfaceSelectorLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Interface Selector") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + SplitButton { + id: interfaceSelector + + type: SplitButton.Tonal + fallbackText: qsTr("Select Interface") + fallbackIcon: "settings_ethernet" + menuItems: interfaceList.instances + menuOnTop: true // Position menu above button to avoid being covered + + property string selectedInterface: "" + + menu.onItemSelected: (item) => { + interfaceSelector.selectedInterface = item.modelData.device; + appendLog("Selected interface: " + item.modelData.device + " (" + item.modelData.type + ")"); + } + + Variants { + id: interfaceList + + model: interfaceSelector.interfaces + + MenuItem { + required property var modelData + + text: modelData.device + " (" + modelData.type + ")" + icon: modelData.type === "wifi" ? "wifi" : "settings_ethernet" + } + } + + property list<var> interfaces: [] + + function refreshInterfaces(): void { + appendLog("Refreshing interface list..."); + Nmcli.getAllInterfaces((interfaces) => { + interfaceSelector.interfaces = interfaces; + if (interfaces.length > 0) { + // Wait for Variants to create instances, then set active + Qt.callLater(() => { + if (interfaceList.instances.length > 0) { + interfaceSelector.active = interfaceList.instances[0]; + interfaceSelector.selectedInterface = interfaces[0].device; + } + }); + appendLog("Found " + interfaces.length + " interfaces"); + } else { + interfaceSelector.selectedInterface = ""; + appendLog("No interfaces found"); + } + }); + } + + Component.onCompleted: { + // Ensure menu appears above other elements + menu.z = 100; + } + } + + TextButton { + text: qsTr("Refresh") + onClicked: { + interfaceSelector.refreshInterfaces(); + } + } + + TextButton { + text: qsTr("Up") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + appendLog("Bringing interface up: " + interfaceSelector.selectedInterface); + Nmcli.bringInterfaceUp(interfaceSelector.selectedInterface, (result) => { + if (result.success) { + appendLog("Interface up: Success"); + } else { + appendLog("Interface up: Failed (exit code: " + result.exitCode + ")"); + if (result.error && result.error.length > 0) { + appendLog("Error: " + result.error); + } + } + // Refresh interface list after bringing up + Qt.callLater(() => { + interfaceSelector.refreshInterfaces(); + }, 500); + }); + } + } + } + + TextButton { + text: qsTr("Down") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + appendLog("Bringing interface down: " + interfaceSelector.selectedInterface); + Nmcli.bringInterfaceDown(interfaceSelector.selectedInterface, (result) => { + if (result.success) { + appendLog("Interface down: Success"); + } else { + appendLog("Interface down: Failed (exit code: " + result.exitCode + ")"); + if (result.error && result.error.length > 0) { + appendLog("Error: " + result.error); + } + } + // Refresh interface list after bringing down + Qt.callLater(() => { + interfaceSelector.refreshInterfaces(); + }, 500); + }); + } + } + } + } + } + } + } + + // Wireless SSID Selector Section + Item { + Layout.fillWidth: true + implicitHeight: ssidSelectorContainer.implicitHeight + z: 10 // Ensure dropdown menu appears above other elements + + StyledRect { + id: ssidSelectorContainer + + anchors.fill: parent + implicitHeight: ssidSelectorLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: ssidSelectorLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Wireless SSID Selector") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + SplitButton { + id: ssidSelector + + type: SplitButton.Tonal + fallbackText: qsTr("Select SSID") + fallbackIcon: "wifi" + menuItems: ssidList.instances + menuOnTop: true + + property string selectedSSID: "" + + menu.onItemSelected: (item) => { + ssidSelector.selectedSSID = item.modelData.ssid; + appendLog("Selected SSID: " + item.modelData.ssid + " (Signal: " + item.modelData.signal + ", Security: " + item.modelData.security + ")"); + } + + Component.onCompleted: { + // Ensure menu appears above other elements + menu.z = 100; + } + + Variants { + id: ssidList + + model: ssidSelector.ssids + + MenuItem { + required property var modelData + + text: modelData.ssid + (modelData.signal ? " (" + modelData.signal + "%)" : "") + icon: "wifi" + } + } + + property list<var> ssids: [] + + function scanForSSIDs(): void { + appendLog("Scanning for wireless networks..."); + // Use first wireless interface if available, or let nmcli choose + let iface = ""; + if (interfaceSelector.selectedInterface) { + // Check if selected interface is wireless + for (const i of interfaceSelector.interfaces) { + if (i.device === interfaceSelector.selectedInterface && i.type === "wifi") { + iface = interfaceSelector.selectedInterface; + break; + } + } + } + + // If no wireless interface selected, use first available + if (!iface && Nmcli.wirelessInterfaces.length > 0) { + iface = Nmcli.wirelessInterfaces[0].device; + } + + Nmcli.scanWirelessNetworks(iface, (scanResult) => { + if (scanResult.success) { + appendLog("Scan completed, fetching SSID list..."); + // Wait a moment for scan results to be available + Qt.callLater(() => { + Nmcli.getWirelessSSIDs(iface, (ssids) => { + ssidSelector.ssids = ssids; + if (ssids.length > 0) { + Qt.callLater(() => { + if (ssidList.instances.length > 0) { + ssidSelector.active = ssidList.instances[0]; + ssidSelector.selectedSSID = ssids[0].ssid; + } + }); + appendLog("Found " + ssids.length + " SSIDs"); + } else { + appendLog("No SSIDs found"); + } + }); + }, 1000); + } else { + appendLog("Scan failed: " + (scanResult.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Scan") + onClicked: { + ssidSelector.scanForSSIDs(); + } + } + } + } + } + } + + // Wireless Connection Test Section + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionTestLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionTestLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Wireless Connection Test") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("SSID: %1").arg(ssidSelector.selectedSSID || "None selected") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Connect (No Password)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + appendLog("Connecting to: " + ssidSelector.selectedSSID + " (no password)"); + // Find the network to get BSSID + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network ? network.bssid : ""; + Nmcli.connectWireless(ssidSelector.selectedSSID, "", bssid, (result) => { + if (result.success) { + appendLog("Connection succeeded!"); + // Refresh network list after connection + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else { + appendLog("Connection failed: " + (result.error || "Unknown error")); + // Refresh network list anyway to check status + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } + }); + appendLog("Connection initiated, tracking pending connection..."); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Password:") + } + + Item { + Layout.fillWidth: true + implicitHeight: passwordField.implicitHeight + Appearance.padding.small * 2 + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + border.width: passwordField.activeFocus ? 2 : 1 + border.color: passwordField.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + + Behavior on border.color { + CAnim {} + } + } + + StyledTextField { + id: passwordField + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + echoMode: TextField.Password + placeholderText: qsTr("Enter password") + + Keys.onReturnPressed: { + if (connectWithPasswordButton.enabled) { + connectWithPasswordButton.clicked(); + } + } + Keys.onEnterPressed: { + if (connectWithPasswordButton.enabled) { + connectWithPasswordButton.clicked(); + } + } + } + } + + TextButton { + id: connectWithPasswordButton + text: qsTr("Connect") + enabled: ssidSelector.selectedSSID.length > 0 && passwordField.text.length > 0 + onClicked: { + if (ssidSelector.selectedSSID && passwordField.text) { + appendLog("Connecting to: " + ssidSelector.selectedSSID + " (with password)"); + // Find the network to get BSSID + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network ? network.bssid : ""; + Nmcli.connectWireless(ssidSelector.selectedSSID, passwordField.text, bssid, (result) => { + if (result.success) { + appendLog("Connection succeeded!"); + // Clear password field + passwordField.text = ""; + // Refresh network list after connection + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else { + appendLog("Connection failed: " + (result.error || "Unknown error")); + if (result.exitCode !== 0) { + appendLog("Exit code: " + result.exitCode); + } + // Refresh network list anyway to check status + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } + }); + appendLog("Connection initiated, tracking pending connection..."); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network && network.bssid ? network.bssid : "N/A"; + return qsTr("BSSID: %1").arg(bssid); + } + } + + Item { + Layout.fillWidth: true + } + } + } + } + + // Saved Connection Profiles Section + StyledRect { + Layout.fillWidth: true + implicitHeight: savedProfilesLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: savedProfilesLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Saved Connection Profiles") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Connections: %1").arg(Nmcli.savedConnections.length) + } + + StyledText { + text: qsTr("WiFi SSIDs: %1").arg(Nmcli.savedConnectionSsids.length) + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh") + onClicked: { + appendLog("Refreshing saved connections..."); + Nmcli.loadSavedConnections((ssids) => { + appendLog("Found " + Nmcli.savedConnections.length + " saved connections"); + appendLog("Found " + Nmcli.savedConnectionSsids.length + " WiFi SSIDs"); + }); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Selected SSID: %1").arg(ssidSelector.selectedSSID || "None") + } + + StyledText { + visible: ssidSelector.selectedSSID.length > 0 + text: { + if (!ssidSelector.selectedSSID) return ""; + const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + return hasProfile ? qsTr("[Saved Profile]") : qsTr("[Not Saved]"); + } + color: { + if (!ssidSelector.selectedSSID) return Colours.palette.m3onSurface; + const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + return hasProfile ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant; + } + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Check Profile") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + appendLog("Profile check for '" + ssidSelector.selectedSSID + "': " + (hasProfile ? "Saved" : "Not saved")); + } + } + } + + TextButton { + text: qsTr("Forget Network") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + appendLog("Forgetting network: " + ssidSelector.selectedSSID); + Nmcli.forgetNetwork(ssidSelector.selectedSSID, (result) => { + if (result.success) { + appendLog("Network forgotten successfully"); + } else { + appendLog("Failed to forget network: " + (result.error || "Unknown error")); + } + }); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + TextButton { + text: qsTr("List All Saved SSIDs") + onClicked: { + appendLog("Saved WiFi SSIDs:"); + if (Nmcli.savedConnectionSsids.length === 0) { + appendLog(" No saved SSIDs"); + } else { + for (let i = 0; i < Nmcli.savedConnectionSsids.length; i++) { + appendLog(" " + (i + 1) + ". " + Nmcli.savedConnectionSsids[i]); + } + } + } + } + + TextButton { + text: qsTr("List All Connections") + onClicked: { + appendLog("Saved Connections:"); + if (Nmcli.savedConnections.length === 0) { + appendLog(" No saved connections"); + } else { + for (let i = 0; i < Nmcli.savedConnections.length; i++) { + appendLog(" " + (i + 1) + ". " + Nmcli.savedConnections[i]); + } + } + } + } + } + } + } + + // Pending Connection Tracking Section + StyledRect { + Layout.fillWidth: true + implicitHeight: pendingConnectionLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: pendingConnectionLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Pending Connection Tracking") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Status: %1").arg(Nmcli.pendingConnection ? "Connecting..." : "No pending connection") + color: Nmcli.pendingConnection ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + StyledText { + visible: Nmcli.pendingConnection + text: qsTr("SSID: %1").arg(Nmcli.pendingConnection ? Nmcli.pendingConnection.ssid : "") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Check Status") + onClicked: { + if (Nmcli.pendingConnection) { + appendLog("Pending connection: " + Nmcli.pendingConnection.ssid); + appendLog("BSSID: " + (Nmcli.pendingConnection.bssid || "N/A")); + const connected = Nmcli.active && Nmcli.active.ssid === Nmcli.pendingConnection.ssid; + appendLog("Connected: " + (connected ? "Yes" : "No")); + if (connected) { + appendLog("Connection succeeded!"); + } else { + appendLog("Still connecting..."); + } + } else { + appendLog("No pending connection"); + } + } + } + + TextButton { + text: qsTr("Clear Pending") + enabled: Nmcli.pendingConnection !== null + onClicked: { + if (Nmcli.pendingConnection) { + appendLog("Clearing pending connection: " + Nmcli.pendingConnection.ssid); + Nmcli.pendingConnection = null; + appendLog("Pending connection cleared"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Active Network: %1").arg(Nmcli.active ? Nmcli.active.ssid : "None") + color: Nmcli.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Networks & Check") + onClicked: { + appendLog("Refreshing network list to check pending connection..."); + Nmcli.getNetworks((networks) => { + appendLog("Network list refreshed"); + if (Nmcli.pendingConnection) { + const connected = Nmcli.active && Nmcli.active.ssid === Nmcli.pendingConnection.ssid; + appendLog("Pending connection check: " + (connected ? "Connected!" : "Still connecting...")); + } + }); + } + } + } + } + } + + // Connection Failure Handling Section + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionFailureLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionFailureLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Connection Failure Handling") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Last Failed SSID: %1").arg(lastFailedSsid || "None") + color: lastFailedSsid ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Clear Failure") + enabled: lastFailedSsid.length > 0 + onClicked: { + lastFailedSsid = ""; + appendLog("Cleared failure status"); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Test Password Detection") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Secure Network (No Password)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && network.isSecure) { + appendLog("Testing connection to secure network without password (should detect password requirement)"); + const bssid = network ? network.bssid : ""; + Nmcli.connectWireless(ssidSelector.selectedSSID, "", bssid, (result) => { + if (result.needsPassword) { + appendLog("✓ Password requirement detected correctly!"); + appendLog("Error: " + (result.error || "N/A")); + } else if (result.success) { + appendLog("Connection succeeded (saved password used)"); + } else { + appendLog("Connection failed: " + (result.error || "Unknown error")); + } + }); + } else { + appendLog("Selected network is not secure, cannot test password detection"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Connection Retry Test") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Retry Logic") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + appendLog("Testing connection retry logic (will retry up to 2 times on failure)"); + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network ? network.bssid : ""; + // Use invalid password to trigger failure + Nmcli.connectWireless(ssidSelector.selectedSSID, "invalid_password_test", bssid, (result) => { + if (result.success) { + appendLog("Connection succeeded (unexpected)"); + } else { + appendLog("Connection failed after retries: " + (result.error || "Unknown error")); + } + }); + } + } + } + } + } + } + + // Password Callback Handling Section + StyledRect { + Layout.fillWidth: true + implicitHeight: passwordCallbackLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: passwordCallbackLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Password Callback Handling") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Selected SSID: %1").arg(ssidSelector.selectedSSID || "None") + } + + StyledText { + visible: ssidSelector.selectedSSID.length > 0 + text: { + if (!ssidSelector.selectedSSID) return ""; + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (!network) return ""; + return network.isSecure ? qsTr("[Secure]") : qsTr("[Open]"); + } + color: { + if (!ssidSelector.selectedSSID) return Colours.palette.m3onSurface; + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (!network) return Colours.palette.m3onSurface; + return network.isSecure ? Colours.palette.m3error : Colours.palette.m3primary; + } + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Password Check (Secure)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && network.isSecure) { + appendLog("Testing password check for secure network: " + ssidSelector.selectedSSID); + appendLog("This will try saved password first, then prompt if needed"); + const bssid = network ? network.bssid : ""; + Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, true, (result) => { + if (result.success) { + if (result.usedSavedPassword) { + appendLog("✓ Connection succeeded using saved password!"); + } else { + appendLog("✓ Connection succeeded!"); + } + // Refresh network list + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else if (result.needsPassword) { + appendLog("→ Password required - callback triggered"); + appendLog(" Error: " + (result.error || "N/A")); + appendLog(" (In real UI, this would show password dialog)"); + } else { + appendLog("✗ Connection failed: " + (result.error || "Unknown error")); + } + }, bssid); + } else { + appendLog("Selected network is not secure, cannot test password check"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Test Open Network") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Password Check (Open)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && !network.isSecure) { + appendLog("Testing password check for open network: " + ssidSelector.selectedSSID); + appendLog("Open networks should connect directly without password"); + const bssid = network ? network.bssid : ""; + Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, false, (result) => { + if (result.success) { + appendLog("✓ Connection succeeded!"); + // Refresh network list + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else { + appendLog("✗ Connection failed: " + (result.error || "Unknown error")); + } + }, bssid); + } else { + appendLog("Selected network is not open, cannot test open network handling"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Test with Saved Password") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Secure Network (Has Saved Password)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && network.isSecure) { + const hasSaved = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + appendLog("Testing password check for: " + ssidSelector.selectedSSID); + appendLog("Has saved profile: " + (hasSaved ? "Yes" : "No")); + if (hasSaved) { + appendLog("This should connect using saved password without prompting"); + } else { + appendLog("This should prompt for password since no saved profile exists"); + } + const bssid = network ? network.bssid : ""; + Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, true, (result) => { + if (result.success) { + if (result.usedSavedPassword) { + appendLog("✓ Connection succeeded using saved password!"); + } else { + appendLog("✓ Connection succeeded!"); + } + // Refresh network list + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else if (result.needsPassword) { + appendLog("→ Password required - callback triggered"); + appendLog(" (In real UI, this would show password dialog)"); + } else { + appendLog("✗ Connection failed: " + (result.error || "Unknown error")); + } + }, bssid); + } else { + appendLog("Selected network is not secure, cannot test saved password"); + } + } + } + } + } + } + } + + // Device Details Parsing Section + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceDetailsLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: deviceDetailsLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Device Details Parsing") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Wireless Device Details") + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Get Wireless Details") + onClicked: { + const activeInterface = interfaceSelector.selectedInterface; + if (activeInterface && activeInterface.length > 0) { + appendLog("Getting wireless device details for: " + activeInterface); + Nmcli.getWirelessDeviceDetails(activeInterface, (details) => { + if (details) { + appendLog("Wireless Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("Failed to get wireless device details"); + } + }); + } else { + appendLog("Getting wireless device details for active interface"); + Nmcli.getWirelessDeviceDetails("", (details) => { + if (details) { + appendLog("Wireless Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No active wireless interface or failed to get details"); + } + }); + } + } + } + + TextButton { + text: qsTr("Show Current") + onClicked: { + if (Nmcli.wirelessDeviceDetails) { + const details = Nmcli.wirelessDeviceDetails; + appendLog("Current Wireless Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No wireless device details available"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Ethernet Device Details") + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Get Ethernet Details") + onClicked: { + const activeInterface = interfaceSelector.selectedInterface; + if (activeInterface && activeInterface.length > 0) { + appendLog("Getting ethernet device details for: " + activeInterface); + Nmcli.getEthernetDeviceDetails(activeInterface, (details) => { + if (details) { + appendLog("Ethernet Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" Speed: " + (details.speed || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("Failed to get ethernet device details"); + } + }); + } else { + appendLog("Getting ethernet device details for active interface"); + Nmcli.getEthernetDeviceDetails("", (details) => { + if (details) { + appendLog("Ethernet Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" Speed: " + (details.speed || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No active ethernet interface or failed to get details"); + } + }); + } + } + } + + TextButton { + text: qsTr("Show Current") + onClicked: { + if (Nmcli.ethernetDeviceDetails) { + const details = Nmcli.ethernetDeviceDetails; + appendLog("Current Ethernet Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" Speed: " + (details.speed || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No ethernet device details available"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("CIDR to Subnet Mask Test") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test CIDR Conversion") + onClicked: { + appendLog("Testing CIDR to Subnet Mask conversion:"); + const testCases = ["8", "16", "24", "32", "0", "25", "30"]; + for (let i = 0; i < testCases.length; i++) { + const cidr = testCases[i]; + const subnet = Nmcli.cidrToSubnetMask(cidr); + appendLog(" /" + cidr + " -> " + (subnet || "Invalid")); + } + } + } + } + } + } + + // Connection Status Monitoring Section + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionMonitoringLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionMonitoringLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Connection Status Monitoring") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Active Network: %1").arg(Nmcli.active ? Nmcli.active.ssid : "None") + color: Nmcli.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + StyledText { + visible: Nmcli.active + text: Nmcli.active ? qsTr("Signal: %1%").arg(Nmcli.active.strength) : "" + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Networks") + onClicked: { + appendLog("Manually refreshing network list..."); + Nmcli.getNetworks((networks) => { + appendLog("Network list refreshed: " + networks.length + " networks"); + if (Nmcli.active) { + appendLog("Active network: " + Nmcli.active.ssid); + } else { + appendLog("No active network"); + } + }); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Monitor Status") + } + + StyledText { + text: qsTr("Monitoring connection changes (automatic refresh enabled)") + color: Colours.palette.m3primary + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Connection Change") + onClicked: { + appendLog("Testing connection change detection..."); + appendLog("This will trigger a manual refresh to simulate a connection change"); + Nmcli.refreshOnConnectionChange(); + appendLog("Refresh triggered - check if network list and device details updated"); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Device Details Auto-Refresh") + } + + StyledText { + text: { + if (Nmcli.wirelessDeviceDetails) { + return qsTr("Wireless: %1").arg(Nmcli.wirelessDeviceDetails.ipAddress || "N/A"); + } else if (Nmcli.ethernetDeviceDetails) { + return qsTr("Ethernet: %1").arg(Nmcli.ethernetDeviceDetails.ipAddress || "N/A"); + } else { + return qsTr("No device details"); + } + } + color: (Nmcli.wirelessDeviceDetails || Nmcli.ethernetDeviceDetails) ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Device Details") + onClicked: { + appendLog("Manually refreshing device details..."); + if (Nmcli.active && Nmcli.active.active) { + appendLog("Active network detected, refreshing device details..."); + // Refresh wireless device details + if (Nmcli.wirelessInterfaces.length > 0) { + const activeWireless = Nmcli.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + Nmcli.getWirelessDeviceDetails(activeWireless.device, (details) => { + if (details) { + appendLog("Wireless device details refreshed"); + } + }); + } + } + // Refresh ethernet device details + if (Nmcli.ethernetInterfaces.length > 0) { + const activeEthernet = Nmcli.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + Nmcli.getEthernetDeviceDetails(activeEthernet.device, (details) => { + if (details) { + appendLog("Ethernet device details refreshed"); + } + }); + } + } + } else { + appendLog("No active network, clearing device details"); + Nmcli.wirelessDeviceDetails = null; + Nmcli.ethernetDeviceDetails = null; + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Connection Events") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Show Active Network Info") + onClicked: { + if (Nmcli.active) { + appendLog("Active Network Information:"); + appendLog(" SSID: " + Nmcli.active.ssid); + appendLog(" BSSID: " + (Nmcli.active.bssid || "N/A")); + appendLog(" Signal: " + Nmcli.active.strength + "%"); + appendLog(" Frequency: " + Nmcli.active.frequency + " MHz"); + appendLog(" Security: " + (Nmcli.active.security || "Open")); + appendLog(" Is Secure: " + (Nmcli.active.isSecure ? "Yes" : "No")); + } else { + appendLog("No active network"); + } + } + } + } + } + } + + // Ethernet Device Management Section + StyledRect { + Layout.fillWidth: true + implicitHeight: ethernetManagementLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: ethernetManagementLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Ethernet Device Management") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Ethernet Devices: %1").arg(Nmcli.ethernetDevices.length) + } + + StyledText { + text: qsTr("Active: %1").arg(Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : "None") + color: Nmcli.activeEthernet ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Devices") + onClicked: { + appendLog("Refreshing ethernet devices..."); + Nmcli.getEthernetInterfaces((interfaces) => { + appendLog("Found " + Nmcli.ethernetDevices.length + " ethernet devices"); + for (let i = 0; i < Nmcli.ethernetDevices.length; i++) { + const dev = Nmcli.ethernetDevices[i]; + appendLog(" " + (i + 1) + ". " + dev.interface + " - " + dev.state + (dev.connected ? " [Connected]" : "")); + } + if (Nmcli.activeEthernet) { + appendLog("Active ethernet: " + Nmcli.activeEthernet.interface); + } + }); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Selected Interface: %1").arg(interfaceSelector.selectedInterface || "None") + } + + StyledText { + visible: interfaceSelector.selectedInterface.length > 0 + text: { + if (!interfaceSelector.selectedInterface) return ""; + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (!device) return ""; + return device.connected ? qsTr("[Connected]") : qsTr("[Disconnected]"); + } + color: { + if (!interfaceSelector.selectedInterface) return Colours.palette.m3onSurface; + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (!device) return Colours.palette.m3onSurface; + return device.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant; + } + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Connect Ethernet") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (device) { + appendLog("Connecting ethernet: " + interfaceSelector.selectedInterface); + appendLog("Connection name: " + (device.connection || "N/A")); + Nmcli.connectEthernet(device.connection || "", interfaceSelector.selectedInterface, (result) => { + if (result.success) { + appendLog("✓ Ethernet connection initiated"); + appendLog("Refreshing device list..."); + } else { + appendLog("✗ Failed to connect: " + (result.error || "Unknown error")); + } + }); + } else { + appendLog("Device not found in ethernet devices list"); + } + } + } + } + + TextButton { + text: qsTr("Disconnect Ethernet") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (device && device.connection) { + appendLog("Disconnecting ethernet: " + device.connection); + Nmcli.disconnectEthernet(device.connection, (result) => { + if (result.success) { + appendLog("✓ Ethernet disconnected"); + appendLog("Refreshing device list..."); + } else { + appendLog("✗ Failed to disconnect: " + (result.error || "Unknown error")); + } + }); + } else { + appendLog("No connection name available for this device"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("List All Ethernet Devices") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("List Devices") + onClicked: { + appendLog("Ethernet Devices:"); + if (Nmcli.ethernetDevices.length === 0) { + appendLog(" No ethernet devices found"); + } else { + for (let i = 0; i < Nmcli.ethernetDevices.length; i++) { + const dev = Nmcli.ethernetDevices[i]; + appendLog(" " + (i + 1) + ". " + dev.interface); + appendLog(" Type: " + dev.type); + appendLog(" State: " + dev.state); + appendLog(" Connection: " + (dev.connection || "None")); + appendLog(" Connected: " + (dev.connected ? "Yes" : "No")); + } + } + } + } + + TextButton { + text: qsTr("Show Active Device") + onClicked: { + if (Nmcli.activeEthernet) { + appendLog("Active Ethernet Device:"); + appendLog(" Interface: " + Nmcli.activeEthernet.interface); + appendLog(" State: " + Nmcli.activeEthernet.state); + appendLog(" Connection: " + (Nmcli.activeEthernet.connection || "None")); + } else { + appendLog("No active ethernet device"); + } + } + } } } } @@ -82,7 +1877,8 @@ Item { // Debug Output Section StyledRect { Layout.fillWidth: true - Layout.fillHeight: true + Layout.preferredHeight: 300 + Layout.minimumHeight: 200 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer @@ -116,17 +1912,17 @@ Item { } StyledFlickable { - id: flickable + id: debugOutputFlickable Layout.fillWidth: true Layout.fillHeight: true flickableDirection: Flickable.VerticalFlick - contentHeight: debugOutput.implicitHeight + clip: true TextEdit { id: debugOutput - width: flickable.width + width: debugOutputFlickable.width readOnly: true wrapMode: TextEdit.Wrap font.family: Appearance.font.family.mono @@ -143,19 +1939,25 @@ Item { onTextChanged: { // Ensure color stays set when text changes color = Colours.palette.m3primary; - if (flickable.contentHeight > flickable.height) { - flickable.contentY = flickable.contentHeight - flickable.height; - } + // Update content height + debugOutputFlickable.contentHeight = Math.max(implicitHeight, debugOutputFlickable.height); + // Auto-scroll to bottom + Qt.callLater(() => { + if (debugOutputFlickable.contentHeight > debugOutputFlickable.height) { + debugOutputFlickable.contentY = debugOutputFlickable.contentHeight - debugOutputFlickable.height; + } + }); } } } StyledScrollBar { - flickable: flickable + flickable: debugOutputFlickable policy: ScrollBar.AlwaysOn } } } + } } function appendLog(message: string): void { @@ -166,5 +1968,12 @@ Item { function log(message: string): void { appendLog(message); } + + Component.onCompleted: { + // Set up debug logger for Nmcli service + Nmcli.setDebugLogger((msg) => { + appendLog("[Nmcli] " + msg); + }); + } } @@ -0,0 +1,137 @@ +# Nmcli.qml Feature Completion Plan + +This document outlines the missing features needed in `Nmcli.qml` to replace `Network.qml` or rewrite the wireless panel in the control center. + +## Current Status + +`Nmcli.qml` currently has: +- ✅ Device status queries +- ✅ Wireless/Ethernet interface listing +- ✅ Interface connection status checking +- ✅ Basic wireless connection (SSID + password) +- ✅ Disconnect functionality +- ✅ Device details (basic) +- ✅ Interface up/down +- ✅ WiFi scanning +- ✅ SSID listing with signal/security (sorted) + +## Missing Features + +### 1. WiFi Radio Control +- [x] `enableWifi(enabled: bool)` - Turn WiFi radio on/off +- [x] `toggleWifi()` - Toggle WiFi radio state +- [x] `wifiEnabled` property - Current WiFi radio state +- [x] Monitor WiFi radio state changes + +**Implementation Notes:** +- Use `nmcli radio wifi on/off` +- Monitor state with `nmcli radio wifi` +- Update `wifiEnabled` property on state changes + +### 2. Network List Management +- [x] `networks` property - List of AccessPoint objects +- [x] `active` property - Currently active network +- [x] Real-time network list updates +- [x] Network grouping by SSID with signal prioritization +- [x] AccessPoint component/object with properties: + - `ssid`, `bssid`, `strength`, `frequency`, `active`, `security`, `isSecure` + +**Implementation Notes:** +- Use `nmcli -g ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY d w` +- Parse and group networks by SSID +- Prioritize active/connected networks +- Update network list on connection changes + +### 3. Connection Management - BSSID Support +- [x] BSSID support in `connectWireless()` function +- [x] Connection profile creation with BSSID (`createConnectionWithPassword`) +- [x] Handle BSSID in connection commands + +**Implementation Notes:** +- Use `nmcli connection add` with `802-11-wireless.bssid` for BSSID-based connections +- Fallback to SSID-only connection if BSSID not available +- Handle existing connection profiles when BSSID is provided + +### 4. Saved Connection Profile Management +- [x] `savedConnections` property - List of saved connection names +- [x] `savedConnectionSsids` property - List of saved SSIDs +- [x] `hasSavedProfile(ssid: string)` function - Check if profile exists +- [x] `forgetNetwork(ssid: string)` function - Delete connection profile +- [x] Load saved connections on startup +- [x] Update saved connections list after connection changes + +**Implementation Notes:** +- Use `nmcli -t -f NAME,TYPE connection show` to list connections +- Query SSIDs for WiFi connections: `nmcli -t -f 802-11-wireless.ssid connection show <name>` +- Use `nmcli connection delete <name>` to forget networks +- Case-insensitive SSID matching + +### 5. Pending Connection Tracking +- [x] `pendingConnection` property - Track connection in progress +- [x] Connection state tracking with timers +- [x] Connection success/failure detection +- [x] Automatic retry or callback on failure + +**Implementation Notes:** +- Track pending connection with SSID/BSSID +- Use timers to check connection status +- Monitor network list updates to detect successful connection +- Handle connection failures and trigger callbacks + +### 6. Connection Failure Handling +- [x] `connectionFailed(ssid: string)` signal +- [x] Password requirement detection from error messages +- [x] Connection retry logic +- [x] Error message parsing and reporting + +**Implementation Notes:** +- Parse stderr output for password requirements +- Detect specific error patterns (e.g., "Secrets were required") +- Emit signals for UI to handle password dialogs +- Provide meaningful error messages + +### 7. Password Callback Handling +- [x] `connectToNetworkWithPasswordCheck()` function +- [x] Try connection without password first (use saved password) +- [x] Callback on password requirement +- [x] Handle both secure and open networks + +**Implementation Notes:** +- Attempt connection without password for secure networks +- If connection fails with password error, trigger callback +- For open networks, connect directly +- Support callback pattern for password dialogs + +### 8. Device Details Parsing +- [x] Full parsing of `device show` output +- [x] `wirelessDeviceDetails` property with: + - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress` +- [x] `ethernetDeviceDetails` property with: + - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress`, `speed` +- [x] `cidrToSubnetMask()` helper function +- [x] Update device details on connection changes + +**Implementation Notes:** +- Parse `nmcli device show <interface>` output +- Extract IP4.ADDRESS, IP4.GATEWAY, IP4.DNS, etc. +- Convert CIDR notation to subnet mask +- Handle both wireless and ethernet device details + +### 9. Connection Status Monitoring +- [x] Automatic network list refresh on connection changes +- [x] Monitor connection state changes +- [x] Update active network property +- [x] Refresh device details on connection + +**Implementation Notes:** +- Use Process stdout SplitParser to monitor changes +- Trigger network list refresh on connection events +- Update `active` property when connection changes +- Refresh device details when connected + +### 10. Ethernet Device Management +- [x] `ethernetDevices` property - List of ethernet devices +- [x] `activeEthernet` property - Currently active ethernet device +- [x] `connectEthernet(connectionName, interfaceName)` function +- [x] `disconnectEthernet(connectionName)` function +- [x] Ethernet device details parsing diff --git a/services/Nmcli.qml b/services/Nmcli.qml new file mode 100644 index 0000000..4e45b41 --- /dev/null +++ b/services/Nmcli.qml @@ -0,0 +1,1246 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property var deviceStatus: null + property var wirelessInterfaces: [] + property var ethernetInterfaces: [] + property bool isConnected: false + property string activeInterface: "" + property string activeConnection: "" + property bool wifiEnabled: true + readonly property list<AccessPoint> networks: [] + readonly property AccessPoint active: networks.find(n => n.active) ?? null + property list<string> savedConnections: [] + property list<string> savedConnectionSsids: [] + + property var wifiConnectionQueue: [] + property int currentSsidQueryIndex: 0 + property var pendingConnection: null + signal connectionFailed(string ssid) + property var wirelessDeviceDetails: null + property var ethernetDeviceDetails: null + property list<var> ethernetDevices: [] + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null + + property list<var> activeProcesses: [] + property var debugLogger: null + + function setDebugLogger(logger: var): void { + root.debugLogger = logger; + } + + function log(message: string): void { + if (root.debugLogger) { + root.debugLogger(message); + } else { + console.log("[Nmcli]", message); + } + } + + function appendLog(message: string): void { + log(message); + } + + function executeCommand(args: list<string>, callback: var): void { + const proc = commandProc.createObject(root); + proc.command = ["nmcli", ...args]; + proc.callback = callback; + + activeProcesses.push(proc); + + proc.processFinished.connect(() => { + const index = activeProcesses.indexOf(proc); + if (index >= 0) { + activeProcesses.splice(index, 1); + } + }); + + Qt.callLater(() => { + proc.exec(proc.command); + }); + } + + function getDeviceStatus(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + if (callback) callback(result.output); + }); + } + + function getWirelessInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && parts[1] === "wifi") { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + root.wirelessInterfaces = interfaces; + if (callback) callback(interfaces); + }); + } + + function getEthernetInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const devices = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && parts[1] === "ethernet") { + const device = parts[0] || ""; + const type = parts[1] || ""; + const state = parts[2] || ""; + const connection = parts[3] || ""; + + const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); + + interfaces.push({ + device: device, + type: type, + state: state, + connection: connection + }); + + devices.push({ + interface: device, + type: type, + state: state, + connection: connection, + connected: connected, + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }); + } + } + root.ethernetInterfaces = interfaces; + root.ethernetDevices = devices; + if (callback) callback(interfaces); + }); + } + + function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { + if (connectionName && connectionName.length > 0) { + executeCommand(["connection", "up", connectionName], (result) => { + if (result.success) { + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + if (interfaceName && interfaceName.length > 0) { + Qt.callLater(() => { + getEthernetDeviceDetails(interfaceName, () => {}); + }, 1000); + } + }, 500); + } + if (callback) callback(result); + }); + } else if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "connect", interfaceName], (result) => { + if (result.success) { + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + Qt.callLater(() => { + getEthernetDeviceDetails(interfaceName, () => {}); + }, 1000); + }, 500); + } + if (callback) callback(result); + }); + } else { + if (callback) callback({ success: false, output: "", error: "No connection name or interface specified", exitCode: -1 }); + } + } + + function disconnectEthernet(connectionName: string, callback: var): void { + if (!connectionName || connectionName.length === 0) { + if (callback) callback({ success: false, output: "", error: "No connection name specified", exitCode: -1 }); + return; + } + + executeCommand(["connection", "down", connectionName], (result) => { + if (result.success) { + root.ethernetDeviceDetails = null; + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + }, 500); + } + if (callback) callback(result); + }); + } + + function getAllInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && (parts[1] === "wifi" || parts[1] === "ethernet")) { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + if (callback) callback(interfaces); + }); + } + + function isInterfaceConnected(interfaceName: string, callback: var): void { + executeCommand(["device", "status"], (result) => { + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(/\s+/); + if (parts.length >= 3 && parts[0] === interfaceName) { + const connected = parts[2] === "connected" || parts[2].startsWith("connected"); + if (callback) callback(connected); + return; + } + } + if (callback) callback(false); + }); + } + + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { + if (isSecure) { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + connectWireless(ssid, "", bssid, (result) => { + if (result.success) { + if (callback) callback({ success: true, usedSavedPassword: true, output: result.output, error: "", exitCode: 0 }); + } else if (result.needsPassword) { + if (callback) callback({ success: false, needsPassword: true, output: result.output, error: result.error, exitCode: result.exitCode }); + } else { + if (callback) callback(result); + } + }); + } else { + connectWireless(ssid, "", bssid, callback); + } + } + + function connectWireless(ssid: string, password: string, bssid: string, callback: var, retryCount: int): void { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + const retries = retryCount !== undefined ? retryCount : 0; + const maxRetries = 2; + + if (callback) { + root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback, retryCount: retries }; + connectionCheckTimer.start(); + immediateCheckTimer.checkCount = 0; + immediateCheckTimer.start(); + } + + if (password && password.length > 0 && hasBssid) { + const bssidUpper = bssid.toUpperCase(); + createConnectionWithPassword(ssid, bssidUpper, password, callback); + return; + } + + let cmd = ["device", "wifi", "connect", ssid]; + if (password && password.length > 0) { + cmd.push("password", password); + } + executeCommand(cmd, (result) => { + if (result.needsPassword && callback) { + if (callback) callback(result); + return; + } + + if (!result.success && root.pendingConnection && retries < maxRetries) { + log("Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + Qt.callLater(() => { + connectWireless(ssid, password, bssid, callback, retries + 1); + }, 1000); + } else if (!result.success && root.pendingConnection) { + } else if (result.success && callback) { + } else if (!result.success && !root.pendingConnection) { + if (callback) callback(result); + } + }); + } + + function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { + checkAndDeleteConnection(ssid, () => { + const cmd = ["connection", "add", + "type", "wifi", + "con-name", ssid, + "ifname", "*", + "ssid", ssid, + "802-11-wireless.bssid", bssidUpper, + "802-11-wireless-security.key-mgmt", "wpa-psk", + "802-11-wireless-security.psk", password]; + + executeCommand(cmd, (result) => { + if (result.success) { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { + const hasDuplicateWarning = result.error && ( + result.error.includes("another connection with the name") || + result.error.includes("Reference the connection by its uuid") + ); + + if (hasDuplicateWarning || (result.exitCode > 0 && result.exitCode < 10)) { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { + log("Connection profile creation failed, trying fallback..."); + let fallbackCmd = ["device", "wifi", "connect", ssid, "password", password]; + executeCommand(fallbackCmd, (fallbackResult) => { + if (callback) callback(fallbackResult); + }); + } + } + }); + }); + } + + function checkAndDeleteConnection(ssid: string, callback: var): void { + executeCommand(["connection", "show", ssid], (result) => { + if (result.success) { + executeCommand(["connection", "delete", ssid], (deleteResult) => { + Qt.callLater(() => { + if (callback) callback(); + }, 300); + }); + } else { + if (callback) callback(); + } + }); + } + + function activateConnection(connectionName: string, callback: var): void { + executeCommand(["connection", "up", connectionName], (result) => { + if (callback) callback(result); + }); + } + + function loadSavedConnections(callback: var): void { + executeCommand(["-t", "-f", "NAME,TYPE", "connection", "show"], (result) => { + if (!result.success) { + root.savedConnections = []; + root.savedConnectionSsids = []; + if (callback) callback([]); + return; + } + + parseConnectionList(result.output, callback); + }); + } + + function parseConnectionList(output: string, callback: var): void { + const lines = output.trim().split("\n").filter(line => line.length > 0); + const wifiConnections = []; + const connections = []; + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2) { + const name = parts[0]; + const type = parts[1]; + connections.push(name); + + if (type === "802-11-wireless") { + wifiConnections.push(name); + } + } + } + + root.savedConnections = connections; + + if (wifiConnections.length > 0) { + root.wifiConnectionQueue = wifiConnections; + root.currentSsidQueryIndex = 0; + root.savedConnectionSsids = []; + queryNextSsid(callback); + } else { + root.savedConnectionSsids = []; + root.wifiConnectionQueue = []; + if (callback) callback(root.savedConnectionSsids); + } + } + + function queryNextSsid(callback: var): void { + if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { + const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; + root.currentSsidQueryIndex++; + + executeCommand(["-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName], (result) => { + if (result.success) { + processSsidOutput(result.output); + } + queryNextSsid(callback); + }); + } else { + root.wifiConnectionQueue = []; + root.currentSsidQueryIndex = 0; + if (callback) callback(root.savedConnectionSsids); + } + } + + function processSsidOutput(output: string): void { + const lines = output.trim().split("\n"); + for (const line of lines) { + if (line.startsWith("802-11-wireless.ssid:")) { + const ssid = line.substring("802-11-wireless.ssid:".length).trim(); + if (ssid && ssid.length > 0) { + const ssidLower = ssid.toLowerCase(); + const exists = root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower); + if (!exists) { + const newList = root.savedConnectionSsids.slice(); + newList.push(ssid); + root.savedConnectionSsids = newList; + } + } + } + } + } + + function hasSavedProfile(ssid: string): bool { + if (!ssid || ssid.length === 0) { + return false; + } + const ssidLower = ssid.toLowerCase().trim(); + + if (root.active && root.active.ssid) { + const activeSsidLower = root.active.ssid.toLowerCase().trim(); + if (activeSsidLower === ssidLower) { + return true; + } + } + + const hasSsid = root.savedConnectionSsids.some(savedSsid => + savedSsid && savedSsid.toLowerCase().trim() === ssidLower + ); + + if (hasSsid) { + return true; + } + + const hasConnectionName = root.savedConnections.some(connName => + connName && connName.toLowerCase().trim() === ssidLower + ); + + return hasConnectionName; + } + + function forgetNetwork(ssid: string, callback: var): void { + if (!ssid || ssid.length === 0) { + if (callback) callback({ success: false, output: "", error: "No SSID specified", exitCode: -1 }); + return; + } + + const connectionName = root.savedConnections.find(conn => + conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() + ) || ssid; + + executeCommand(["connection", "delete", connectionName], (result) => { + if (result.success) { + Qt.callLater(() => { + loadSavedConnections(() => {}); + }, 500); + } + if (callback) callback(result); + }); + } + + function disconnect(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "disconnect", interfaceName], (result) => { + if (callback) callback(result.success ? result.output : ""); + }); + } else { + executeCommand(["device", "disconnect", "wifi"], (result) => { + if (callback) callback(result.success ? result.output : ""); + }); + } + } + + function getDeviceDetails(interfaceName: string, callback: var): void { + executeCommand(["device", "show", interfaceName], (result) => { + if (callback) callback(result.output); + }); + } + + function refreshStatus(callback: var): void { + getDeviceStatus((output) => { + const lines = output.trim().split("\n"); + let connected = false; + let activeIf = ""; + let activeConn = ""; + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 4) { + const state = parts[2] || ""; + if (state === "connected" || state.startsWith("connected")) { + connected = true; + activeIf = parts[0] || ""; + activeConn = parts[3] || ""; + break; + } + } + } + + root.isConnected = connected; + root.activeInterface = activeIf; + root.activeConnection = activeConn; + + if (callback) callback({ connected, interface: activeIf, connection: activeConn }); + }); + } + + function bringInterfaceUp(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "connect", interfaceName], (result) => { + if (callback) { + callback(result); + } + }); + } else { + if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + } + } + + function bringInterfaceDown(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "disconnect", interfaceName], (result) => { + if (callback) { + callback(result); + } + }); + } else { + if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + } + } + + function scanWirelessNetworks(interfaceName: string, callback: var): void { + let cmd = ["device", "wifi", "rescan"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push("ifname", interfaceName); + } + executeCommand(cmd, (result) => { + if (callback) { + callback(result); + } + }); + } + + function enableWifi(enabled: bool, callback: var): void { + const cmd = enabled ? "on" : "off"; + executeCommand(["radio", "wifi", cmd], (result) => { + if (result.success) { + getWifiStatus((status) => { + root.wifiEnabled = status; + if (callback) callback(result); + }); + } else { + if (callback) callback(result); + } + }); + } + + function toggleWifi(callback: var): void { + const newState = !root.wifiEnabled; + enableWifi(newState, callback); + } + + function getWifiStatus(callback: var): void { + executeCommand(["radio", "wifi"], (result) => { + if (result.success) { + const enabled = result.output.trim() === "enabled"; + root.wifiEnabled = enabled; + if (callback) callback(enabled); + } else { + if (callback) callback(root.wifiEnabled); + } + }); + } + + function getNetworks(callback: var): void { + executeCommand(["-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"], (result) => { + if (!result.success) { + if (callback) callback([]); + return; + } + + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const allNetworks = result.output.trim().split("\n") + .filter(line => line && line.length > 0) + .map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }) + .filter(n => n.ssid && n.ssid.length > 0); + + const networkMap = new Map(); + for (const network of allNetworks) { + const existing = networkMap.get(network.ssid); + if (!existing) { + networkMap.set(network.ssid, network); + } else { + if (network.active && !existing.active) { + networkMap.set(network.ssid, network); + } else if (!network.active && !existing.active) { + if (network.strength > existing.strength) { + networkMap.set(network.ssid, network); + } + } + } + } + + const networks = Array.from(networkMap.values()); + const rNetworks = root.networks; + + const destroyed = rNetworks.filter(rn => !networks.find(n => + n.frequency === rn.frequency && + n.ssid === rn.ssid && + n.bssid === rn.bssid + )); + for (const network of destroyed) { + const index = rNetworks.indexOf(network); + if (index >= 0) { + rNetworks.splice(index, 1); + network.destroy(); + } + } + + for (const network of networks) { + const match = rNetworks.find(n => + n.frequency === network.frequency && + n.ssid === network.ssid && + n.bssid === network.bssid + ); + if (match) { + match.lastIpcObject = network; + } else { + rNetworks.push(apComp.createObject(root, { + lastIpcObject: network + })); + } + } + + if (callback) callback(root.networks); + checkPendingConnection(); + }); + } + + function getWirelessSSIDs(interfaceName: string, callback: var): void { + let cmd = ["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push("ifname", interfaceName); + } + executeCommand(cmd, (result) => { + if (!result.success) { + if (callback) callback([]); + return; + } + + const ssids = []; + const lines = result.output.trim().split("\n"); + const seenSSIDs = new Set(); + + for (const line of lines) { + if (!line || line.length === 0) continue; + + const parts = line.split(":"); + if (parts.length >= 1) { + const ssid = parts[0].trim(); + if (ssid && ssid.length > 0 && !seenSSIDs.has(ssid)) { + seenSSIDs.add(ssid); + const signalStr = parts.length >= 2 ? parts[1].trim() : ""; + const signal = signalStr ? parseInt(signalStr, 10) : 0; + const security = parts.length >= 3 ? parts[2].trim() : ""; + ssids.push({ + ssid: ssid, + signal: signalStr, + signalValue: isNaN(signal) ? 0 : signal, + security: security + }); + } + } + } + + ssids.sort((a, b) => { + return b.signalValue - a.signalValue; + }); + + if (callback) callback(ssids); + }); + } + + component CommandProcess: Process { + id: proc + property var callback: null + property list<string> command: [] + property bool callbackCalled: false + property int exitCode: 0 + signal processFinished + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: StdioCollector { + id: stdoutCollector + onStreamFinished: { + } + } + + stderr: StdioCollector { + id: stderrCollector + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand && root.pendingConnection && root.pendingConnection.callback) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + } + } + } + } + } + + onExited: { + proc.exitCode = exitCode; + Qt.callLater(() => { + if (proc.callbackCalled) { + proc.processFinished(); + return; + } + + if (proc.callback) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; + const success = proc.exitCode === 0; + + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + const needsPassword = isConnectionCommand && error && error.length > 0 && + (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + proc.processFinished(); + return; + } else if (!success && isConnectionCommand && root.pendingConnection) { + const failedSsid = root.pendingConnection.ssid; + root.connectionFailed(failedSsid); + } + + proc.callbackCalled = true; + proc.callback({ + success: success, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: needsPassword || false + }); + proc.processFinished(); + } else { + proc.processFinished(); + } + }); + } + } + + Component { + id: commandProc + CommandProcess {} + } + + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } + + Component { + id: apComp + AccessPoint {} + } + + Timer { + id: connectionCheckTimer + interval: 4000 + onTriggered: { + if (root.pendingConnection) { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (!connected && root.pendingConnection.callback) { + let foundPasswordError = false; + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + foundPasswordError = true; + break; + } + } + } + } + } + + if (!foundPasswordError) { + const pending = root.pendingConnection; + const failedSsid = pending.ssid; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.connectionFailed(failedSsid); + pending.callback({ + success: false, + output: "", + error: "Connection timeout", + exitCode: -1, + needsPassword: false + }); + } + } else if (connected) { + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + } + + Timer { + id: immediateCheckTimer + interval: 500 + repeat: true + triggeredOnStart: false + property int checkCount: 0 + + onTriggered: { + if (root.pendingConnection) { + checkCount++; + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + } + root.pendingConnection = null; + } else { + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return; + } + } + } + } + } + + if (checkCount >= 6) { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + + function checkPendingConnection(): void { + if (root.pendingConnection) { + Qt.callLater(() => { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + } + root.pendingConnection = null; + } else { + if (!immediateCheckTimer.running) { + immediateCheckTimer.start(); + } + } + }); + } + } + + function cidrToSubnetMask(cidr: string): string { + const cidrNum = parseInt(cidr, 10); + if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { + return ""; + } + + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; + const octet1 = (mask >>> 24) & 0xff; + const octet2 = (mask >>> 16) & 0xff; + const octet3 = (mask >>> 8) & 0xff; + const octet4 = mask & 0xff; + + return `${octet1}.${octet2}.${octet3}.${octet4}`; + } + + function getWirelessDeviceDetails(interfaceName: string, callback: var): void { + if (!interfaceName || interfaceName.length === 0) { + const activeInterface = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeInterface && activeInterface.device) { + interfaceName = activeInterface.device; + } else { + if (callback) callback(null); + return; + } + } + + executeCommand(["device", "show", interfaceName], (result) => { + if (!result.success || !result.output) { + root.wirelessDeviceDetails = null; + if (callback) callback(null); + return; + } + + const details = parseDeviceDetails(result.output, false); + root.wirelessDeviceDetails = details; + if (callback) callback(details); + }); + } + + function getEthernetDeviceDetails(interfaceName: string, callback: var): void { + if (!interfaceName || interfaceName.length === 0) { + const activeInterface = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeInterface && activeInterface.device) { + interfaceName = activeInterface.device; + } else { + if (callback) callback(null); + return; + } + } + + executeCommand(["device", "show", interfaceName], (result) => { + if (!result.success || !result.output) { + root.ethernetDeviceDetails = null; + if (callback) callback(null); + return; + } + + const details = parseDeviceDetails(result.output, true); + root.ethernetDeviceDetails = details; + if (callback) callback(details); + }); + } + + function parseDeviceDetails(output: string, isEthernet: bool): var { + const details = { + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + + if (!output || output.length === 0) { + return details; + } + + const lines = output.trim().split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(":"); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join(":").trim(); + + if (key.startsWith("IP4.ADDRESS")) { + const ipParts = value.split("/"); + details.ipAddress = ipParts[0] || ""; + if (ipParts[1]) { + details.subnet = cidrToSubnetMask(ipParts[1]); + } else { + details.subnet = ""; + } + } else if (key === "IP4.GATEWAY") { + if (value !== "--") { + details.gateway = value; + } + } else if (key.startsWith("IP4.DNS")) { + if (value !== "--" && value.length > 0) { + details.dns.push(value); + } + } else if (isEthernet && key === "WIRED-PROPERTIES.MAC") { + details.macAddress = value; + } else if (isEthernet && key === "WIRED-PROPERTIES.SPEED") { + details.speed = value; + } else if (!isEthernet && key === "GENERAL.HWADDR") { + details.macAddress = value; + } + } + } + + return details; + } + + Process { + id: monitorProc + running: true + command: ["nmcli", "monitor"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: SplitParser { + onRead: { + log("Connection state change detected, refreshing..."); + root.refreshOnConnectionChange(); + } + } + + onExited: { + log("Monitor process exited, restarting..."); + Qt.callLater(() => { + monitorProc.running = true; + }, 2000); + } + } + + function refreshOnConnectionChange(): void { + getNetworks((networks) => { + const newActive = root.active; + + if (newActive && newActive.active) { + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 500); + } else { + root.wirelessDeviceDetails = null; + root.ethernetDeviceDetails = null; + } + + getWirelessInterfaces(() => {}); + getEthernetInterfaces(() => { + if (root.activeEthernet && root.activeEthernet.connected) { + Qt.callLater(() => { + getEthernetDeviceDetails(root.activeEthernet.interface, () => {}); + }, 500); + } + }); + }); + } + + Component.onCompleted: { + getWifiStatus(() => {}); + getNetworks(() => {}); + loadSavedConnections(() => {}); + getEthernetInterfaces(() => {}); + + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 2000); + } +} + |