summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorATMDA <atdma2600@gmail.com>2025-11-13 18:59:48 -0500
committerATMDA <atdma2600@gmail.com>2025-11-13 18:59:48 -0500
commit617e686238d3c7155112196043f0883ccf6a7012 (patch)
treeaa18b05efd2e088a37f9099fb225f93658d07127
parentcontrolcenter: created dev panel for wireless testing (diff)
downloadcaelestia-shell-617e686238d3c7155112196043f0883ccf6a7012.tar.gz
caelestia-shell-617e686238d3c7155112196043f0883ccf6a7012.tar.bz2
caelestia-shell-617e686238d3c7155112196043f0883ccf6a7012.zip
service: Nmcli.qml
-rw-r--r--modules/controlcenter/dev/DevDebugPane.qml1837
-rw-r--r--plan.md137
-rw-r--r--services/Nmcli.qml1246
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);
+ });
+ }
}
diff --git a/plan.md b/plan.md
new file mode 100644
index 0000000..4762ef0
--- /dev/null
+++ b/plan.md
@@ -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);
+ }
+}
+