From 5d7151e79e04d8a6073ecb4ea4a14c5c9bdcfc52 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 17:54:16 -0500 Subject: controlcenter: network and audio panels --- services/Network.qml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) (limited to 'services/Network.qml') diff --git a/services/Network.qml b/services/Network.qml index 2c31065..f2c403e 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -27,13 +27,20 @@ Singleton { } function connectToNetwork(ssid: string, password: string): void { - // TODO: Implement password - connectProc.exec(["nmcli", "conn", "up", ssid]); + // First try to connect to an existing connection + // If that fails, create a new connection + if (password && password.length > 0) { + connectProc.exec(["nmcli", "device", "wifi", "connect", ssid, "password", password]); + } else { + // Try to connect to existing connection first + connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); + } } function disconnectFromNetwork(): void { if (active) { - disconnectProc.exec(["nmcli", "connection", "down", active.ssid]); + // Find the device name first, then disconnect + disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); } } @@ -86,6 +93,10 @@ Singleton { Process { id: connectProc + onExited: { + // Refresh network list after connection attempt + getNetworks.running = true; + } stdout: SplitParser { onRead: getNetworks.running = true } @@ -97,6 +108,10 @@ Singleton { Process { id: disconnectProc + onExited: { + // Refresh network list after disconnection + getNetworks.running = true; + } stdout: SplitParser { onRead: getNetworks.running = true } -- cgit v1.2.3-freya From d8de7c7e4b1b57470297dc238b210670f87c23ba Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 21:43:51 -0500 Subject: controlcenter: wifi passphrase input fix: dependability issues with nmcli --- modules/controlcenter/network/Details.qml | 30 ++- modules/controlcenter/network/NetworkList.qml | 29 ++- modules/controlcenter/network/NetworkPane.qml | 6 + modules/controlcenter/network/PasswordDialog.qml | 241 +++++++++++++++++++++++ services/Network.qml | 158 ++++++++++++++- 5 files changed, 451 insertions(+), 13 deletions(-) create mode 100644 modules/controlcenter/network/PasswordDialog.qml (limited to 'services/Network.qml') diff --git a/modules/controlcenter/network/Details.qml b/modules/controlcenter/network/Details.qml index 86164f1..19e011f 100644 --- a/modules/controlcenter/network/Details.qml +++ b/modules/controlcenter/network/Details.qml @@ -79,17 +79,37 @@ Item { checked: root.network?.active ?? false toggle.onToggled: { if (checked) { - if (root.network.isSecure) { - // TODO: Show password dialog - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = root.network; + // If already connected to a different network, disconnect first + if (Network.active && Network.active.ssid !== root.network.ssid) { + Network.disconnectFromNetwork(); + // Wait a moment before connecting to new network + Qt.callLater(() => { + connectToNetwork(); + }); } else { - Network.connectToNetwork(root.network.ssid, ""); + connectToNetwork(); } } else { Network.disconnectFromNetwork(); } } + + function connectToNetwork(): void { + if (root.network.isSecure) { + // Try connecting without password first (in case it's saved) + Network.connectToNetworkWithPasswordCheck( + root.network.ssid, + root.network.isSecure, + () => { + // Callback: connection failed, show password dialog + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = root.network; + } + ); + } else { + Network.connectToNetwork(root.network.ssid, ""); + } + } } } } diff --git a/modules/controlcenter/network/NetworkList.qml b/modules/controlcenter/network/NetworkList.qml index 8dfebc7..df05de7 100644 --- a/modules/controlcenter/network/NetworkList.qml +++ b/modules/controlcenter/network/NetworkList.qml @@ -214,14 +214,35 @@ ColumnLayout { if (modelData.active) { Network.disconnectFromNetwork(); } else { - if (modelData.isSecure) { - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = modelData; + // If already connected to a different network, disconnect first + if (Network.active && Network.active.ssid !== modelData.ssid) { + Network.disconnectFromNetwork(); + // Wait a moment before connecting to new network + Qt.callLater(() => { + connectToNetwork(); + }); } else { - Network.connectToNetwork(modelData.ssid, ""); + connectToNetwork(); } } } + + function connectToNetwork(): void { + if (modelData.isSecure) { + // Try connecting without password first (in case it's saved) + Network.connectToNetworkWithPasswordCheck( + modelData.ssid, + modelData.isSecure, + () => { + // Callback: connection failed, show password dialog + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = modelData; + } + ); + } else { + Network.connectToNetwork(modelData.ssid, ""); + } + } } MaterialIcon { diff --git a/modules/controlcenter/network/NetworkPane.qml b/modules/controlcenter/network/NetworkPane.qml index f37eedd..5e8a75a 100644 --- a/modules/controlcenter/network/NetworkPane.qml +++ b/modules/controlcenter/network/NetworkPane.qml @@ -76,4 +76,10 @@ RowLayout { } } } + + PasswordDialog { + anchors.fill: parent + session: root.session + z: 1000 + } } diff --git a/modules/controlcenter/network/PasswordDialog.qml b/modules/controlcenter/network/PasswordDialog.qml new file mode 100644 index 0000000..fa4788c --- /dev/null +++ b/modules/controlcenter/network/PasswordDialog.qml @@ -0,0 +1,241 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + readonly property var network: session.network.pendingNetwork + + visible: session.network.showPasswordDialog + enabled: visible + focus: visible + + Keys.onEscapePressed: { + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.5) + opacity: root.visible ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + } + } + } + + StyledRect { + id: dialog + + anchors.centerIn: parent + + implicitWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surface + opacity: root.visible ? 1 : 0 + scale: root.visible ? 1 : 0.9 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + + Behavior on scale { + NumberAnimation { + duration: 200 + } + } + + Keys.onEscapePressed: { + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + } + + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "lock" + font.pointSize: Appearance.font.size.extraLarge * 2 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter password") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + Item { + Layout.topMargin: Appearance.spacing.large + Layout.fillWidth: true + implicitHeight: passwordField.implicitHeight + Appearance.padding.normal * 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("Password") + + Component.onCompleted: { + if (root.visible) { + forceActiveFocus(); + } + } + + Connections { + target: root + function onVisibleChanged(): void { + if (root.visible) { + passwordField.forceActiveFocus(); + passwordField.text = ""; + } + } + } + + Keys.onReturnPressed: { + if (connectButton.enabled) { + connectButton.clicked(); + } + } + Keys.onEnterPressed: { + if (connectButton.enabled) { + connectButton.clicked(); + } + } + } + } + + RowLayout { + Layout.topMargin: Appearance.spacing.normal + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + Button { + id: cancelButton + + Layout.fillWidth: true + color: Colours.palette.m3secondaryContainer + onColor: Colours.palette.m3onSecondaryContainer + text: qsTr("Cancel") + + function onClicked(): void { + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + } + } + + Button { + id: connectButton + + Layout.fillWidth: true + color: Colours.palette.m3primary + onColor: Colours.palette.m3onPrimary + text: qsTr("Connect") + enabled: passwordField.text.length > 0 + + function onClicked(): void { + if (root.network && passwordField.text.length > 0) { + Network.connectToNetwork(root.network.ssid, passwordField.text); + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + } + } + } + } + } + } + + component Button: StyledRect { + property color onColor: Colours.palette.m3onSurface + property alias disabled: stateLayer.disabled + property alias text: label.text + property alias enabled: stateLayer.enabled + + function onClicked(): void { + } + + radius: Appearance.rounding.small + implicitHeight: label.implicitHeight + Appearance.padding.small * 2 + opacity: enabled ? 1 : 0.5 + + StateLayer { + id: stateLayer + + enabled: parent.enabled + color: parent.onColor + + function onClicked(): void { + if (enabled) { + parent.onClicked(); + } + } + } + + StyledText { + id: label + + anchors.centerIn: parent + animate: true + color: parent.onColor + font.pointSize: Appearance.font.size.normal + } + } +} + diff --git a/services/Network.qml b/services/Network.qml index f2c403e..3ceadab 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -26,20 +26,42 @@ Singleton { rescanProc.running = true; } + property var pendingConnection: null + signal connectionFailed(string ssid) + function connectToNetwork(ssid: string, password: string): void { // First try to connect to an existing connection // If that fails, create a new connection if (password && password.length > 0) { connectProc.exec(["nmcli", "device", "wifi", "connect", ssid, "password", password]); } else { - // Try to connect to existing connection first + // Try to connect to existing connection first (will use saved password if available) + connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); + } + } + + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var): void { + // For secure networks, try connecting without password first + // If connection succeeds (saved password exists), we're done + // If it fails with password error, callback will be called to show password dialog + if (isSecure) { + root.pendingConnection = { ssid: ssid, callback: callback }; + // Try connecting without password - will use saved password if available connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); + // Start timer to check if connection succeeded + connectionCheckTimer.start(); + } else { + connectToNetwork(ssid, ""); } } function disconnectFromNetwork(): void { - if (active) { - // Find the device name first, then disconnect + // Try to disconnect - use connection name if available, otherwise use device + if (active && active.ssid) { + // First try to disconnect by connection name (more reliable) + disconnectByConnectionProc.exec(["nmcli", "connection", "down", active.ssid]); + } else { + // Fallback: disconnect by device disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); } } @@ -90,18 +112,102 @@ Singleton { } } + Timer { + id: connectionCheckTimer + interval: 4000 + onTriggered: { + if (root.pendingConnection) { + // Final check - if connection still hasn't succeeded, show password dialog + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + if (!connected && root.pendingConnection.callback) { + // Connection didn't succeed after multiple checks, show password dialog + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + pending.callback(); + } else if (connected) { + // Connection succeeded, clear pending + 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) { + // Connection succeeded, stop timers and clear pending + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.pendingConnection = null; + } else if (checkCount >= 6) { + // Checked 6 times (3 seconds total), connection likely failed + // Stop immediate check, let the main timer handle it + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + Process { id: connectProc onExited: { // Refresh network list after connection attempt getNetworks.running = true; + + // Check if connection succeeded after a short delay (network list needs to update) + if (root.pendingConnection) { + immediateCheckTimer.start(); + } } stdout: SplitParser { onRead: getNetworks.running = true } stderr: StdioCollector { - onStreamFinished: console.warn("Network connection error:", text) + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + // Check for specific errors that indicate password is needed + // Be careful not to match success messages + const needsPassword = (error.includes("Secrets were required") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + (error.includes("password") && !error.includes("Connection activated")) || + (error.includes("Secrets") && !error.includes("Connection activated")) || + (error.includes("802.11") && !error.includes("Connection activated"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { + // Connection failed because password is needed - show dialog immediately + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + const pending = root.pendingConnection; + root.pendingConnection = null; + pending.callback(); + } else if (error && error.length > 0 && !error.includes("Connection activated")) { + // Only log non-success messages + console.warn("Network connection error:", error); + } + } + } } } @@ -115,6 +221,36 @@ Singleton { stdout: SplitParser { onRead: getNetworks.running = true } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { + console.warn("Network device disconnect error:", error); + } + } + } + } + + Process { + id: disconnectByConnectionProc + + onExited: { + // Refresh network list after disconnection + getNetworks.running = true; + } + stdout: SplitParser { + onRead: getNetworks.running = true + } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { + console.warn("Network connection disconnect error:", error); + // If connection down failed, try device disconnect as fallback + disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); + } + } + } } Process { @@ -182,6 +318,20 @@ Singleton { })); } } + + // Check if pending connection succeeded after network list is fully updated + if (root.pendingConnection) { + Qt.callLater(() => { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + if (connected) { + // Connection succeeded, stop timers and clear pending + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.pendingConnection = null; + } + }); + } } } } -- cgit v1.2.3-freya From 1debf488ee1ed24763a01c2e1bf5c3f4119de18f Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 10 Nov 2025 10:00:04 -0500 Subject: controlcenter: ethernet panel (debug) --- modules/controlcenter/NavRail.qml | 9 +- modules/controlcenter/Panes.qml | 14 +- modules/controlcenter/Session.qml | 7 +- modules/controlcenter/ethernet/EthernetDetails.qml | 204 ++++++++++++++++ modules/controlcenter/ethernet/EthernetList.qml | 262 +++++++++++++++++++++ modules/controlcenter/ethernet/EthernetPane.qml | 156 ++++++++++++ .../controlcenter/ethernet/EthernetSettings.qml | 155 ++++++++++++ services/Network.qml | 183 +++++++++++++- 8 files changed, 983 insertions(+), 7 deletions(-) create mode 100644 modules/controlcenter/ethernet/EthernetDetails.qml create mode 100644 modules/controlcenter/ethernet/EthernetList.qml create mode 100644 modules/controlcenter/ethernet/EthernetPane.qml create mode 100644 modules/controlcenter/ethernet/EthernetSettings.qml (limited to 'services/Network.qml') diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 96bbb65..b4fbf94 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -158,8 +158,13 @@ Item { NavItem { Layout.topMargin: Appearance.spacing.large * 2 - icon: "network_manage" - label: "network" + icon: "cable" + label: "ethernet" + } + + NavItem { + icon: "wifi" + label: "wireless" } NavItem { diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 5b1039c..4f4a337 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -1,5 +1,6 @@ pragma ComponentBehavior: Bound +import "ethernet" import "bluetooth" import "network" import "audio" @@ -26,27 +27,34 @@ ClippingRectangle { Pane { index: 0 - sourceComponent: NetworkPane { + sourceComponent: EthernetPane { session: root.session } } Pane { index: 1 - sourceComponent: BtPane { + sourceComponent: NetworkPane { session: root.session } } Pane { index: 2 - sourceComponent: AudioPane { + sourceComponent: BtPane { session: root.session } } Pane { index: 3 + sourceComponent: AudioPane { + session: root.session + } + } + + Pane { + index: 4 sourceComponent: AppearancePane { session: root.session } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 4ac09a4..f7c07e4 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -2,7 +2,7 @@ import Quickshell.Bluetooth import QtQuick QtObject { - readonly property list panes: ["network", "bluetooth", "audio", "appearance"] + readonly property list panes: ["ethernet", "wireless", "bluetooth", "audio", "appearance"] required property var root property bool floating: false @@ -12,6 +12,7 @@ QtObject { readonly property Bt bt: Bt {} readonly property Network network: Network {} + readonly property Ethernet ethernet: Ethernet {} onActiveChanged: activeIndex = panes.indexOf(active) onActiveIndexChanged: active = panes[activeIndex] @@ -29,4 +30,8 @@ QtObject { property bool showPasswordDialog: false property var pendingNetwork } + + component Ethernet: QtObject { + property var active + } } diff --git a/modules/controlcenter/ethernet/EthernetDetails.qml b/modules/controlcenter/ethernet/EthernetDetails.qml new file mode 100644 index 0000000..9be3ddc --- /dev/null +++ b/modules/controlcenter/ethernet/EthernetDetails.qml @@ -0,0 +1,204 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + readonly property var device: session.ethernet.active + + StyledFlickable { + anchors.fill: parent + + flickableDirection: Flickable.VerticalFlick + contentHeight: layout.height + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + animate: true + text: "cable" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.device?.interface ?? qsTr("Unknown") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Connection status") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Connection settings for this device") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: deviceStatus + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.larger + + Toggle { + label: qsTr("Connected") + checked: root.device?.connected ?? false + toggle.onToggled: { + if (checked) { + if (root.device?.connection) { + Network.connectEthernet(root.device.connection); + } + } else { + if (root.device?.connection) { + Network.disconnectEthernet(root.device.connection); + } + } + } + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Device properties") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Additional information") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: deviceProps + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("Interface") + } + + StyledText { + text: root.device?.interface ?? qsTr("Unknown") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Connection") + } + + StyledText { + text: root.device?.connection || qsTr("Not connected") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("State") + } + + StyledText { + text: root.device?.state ?? qsTr("Unknown") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } + + } + } + + component Toggle: RowLayout { + required property string label + property alias checked: toggle.checked + property alias toggle: toggle + + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: parent.label + } + + StyledSwitch { + id: toggle + + cLayer: 2 + } + } +} + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/controlcenter/ethernet/EthernetList.qml b/modules/controlcenter/ethernet/EthernetList.qml new file mode 100644 index 0000000..d239fc6 --- /dev/null +++ b/modules/controlcenter/ethernet/EthernetList.qml @@ -0,0 +1,262 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + ToggleButton { + toggled: !root.session.ethernet.active + icon: "settings" + accent: "Primary" + + function onClicked(): void { + if (root.session.ethernet.active) + root.session.ethernet.active = null; + else { + root.session.ethernet.active = view.model.get(0)?.modelData ?? null; + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + + StyledText { + text: qsTr("All available ethernet devices") + color: Colours.palette.m3outline + } + + StyledListView { + id: view + + Layout.fillWidth: true + Layout.fillHeight: true + + model: Network.ethernetDevices + + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: view + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: root.session.ethernet.active === modelData ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.session.ethernet.active = modelData; + } + } + + RowLayout { + id: rowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: "cable" + font.pointSize: Appearance.font.size.large + fill: modelData.connected ? 1 : 0 + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + + StyledText { + Layout.fillWidth: true + + text: modelData.interface || qsTr("Unknown") + } + + StyledText { + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.connected ? 500 : 400 + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) + + StateLayer { + function onClicked(): void { + if (modelData.connected && modelData.connection) { + Network.disconnectEthernet(modelData.connection); + } else if (modelData.connection) { + Network.connectEthernet(modelData.connection); + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + text: modelData.connected ? "link_off" : "link" + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + + component ToggleButton: StyledRect { + id: toggleBtn + + required property bool toggled + property string icon + property string label + property string accent: "Secondary" + + function onClicked(): void { + } + + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) + implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 + implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) + color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] + + StateLayer { + id: toggleStateLayer + + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + + function onClicked(): void { + toggleBtn.onClicked(); + } + } + + RowLayout { + id: toggleBtnInner + + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + id: toggleBtnIcon + + visible: !!text + fill: toggleBtn.toggled ? 1 : 0 + text: toggleBtn.icon + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + font.pointSize: Appearance.font.size.large + + Behavior on fill { + Anim {} + } + } + + Loader { + asynchronous: true + active: !!toggleBtn.label + visible: active + + sourceComponent: StyledText { + text: toggleBtn.label + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + } + } + } + + Behavior on radius { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } +} + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/controlcenter/ethernet/EthernetPane.qml b/modules/controlcenter/ethernet/EthernetPane.qml new file mode 100644 index 0000000..fc3e1c0 --- /dev/null +++ b/modules/controlcenter/ethernet/EthernetPane.qml @@ -0,0 +1,156 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.effects +import qs.components.containers +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + anchors.fill: parent + + spacing: 0 + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + EthernetList { + anchors.fill: parent + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + + session: root.session + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ClippingRectangle { + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: rightBorder.innerRadius + color: "transparent" + + Loader { + id: loader + + property var pane: root.session.ethernet.active + property string paneId: pane ? (pane.interface || "") : "" + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: false + asynchronous: true + sourceComponent: pane ? details : settings + + Behavior on paneId { + SequentialAnimation { + ParallelAnimation { + Anim { + target: loader + property: "opacity" + to: 0 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + Anim { + target: loader + property: "scale" + to: 0.8 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + } + PropertyAction {} + ParallelAnimation { + Anim { + target: loader + property: "opacity" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + target: loader + property: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + } + } + + onPaneChanged: { + paneId = pane ? (pane.interface || "") : ""; + } + } + } + + InnerBorder { + id: rightBorder + + leftThickness: Appearance.padding.normal / 2 + } + + Component { + id: settings + + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + + EthernetSettings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } + } + } + + Component { + id: details + + EthernetDetails { + session: root.session + } + } + } + + component Anim: NumberAnimation { + target: loader + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + } +} + + + + + + + + diff --git a/modules/controlcenter/ethernet/EthernetSettings.qml b/modules/controlcenter/ethernet/EthernetSettings.qml new file mode 100644 index 0000000..b780b55 --- /dev/null +++ b/modules/controlcenter/ethernet/EthernetSettings.qml @@ -0,0 +1,155 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "cable" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Ethernet settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Ethernet devices") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Available ethernet devices") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: ethernetInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("Total devices") + } + + StyledText { + text: qsTr("%1").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Connected devices") + } + + StyledText { + text: qsTr("%1").arg(Network.ethernetDevices.filter(d => d.connected).length) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Debug Info") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: debugInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: debugInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("Process running: %1").arg(Network.ethernetProcessRunning ? "Yes" : "No") + font.pointSize: Appearance.font.size.small + } + + StyledText { + text: qsTr("List length: %1").arg(Network.ethernetDevices.length) + font.pointSize: Appearance.font.size.small + } + + StyledText { + text: qsTr("Device count: %1").arg(Network.ethernetDeviceCount) + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Debug: %1").arg(Network.ethernetDebugInfo || "No info") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + } + } + } +} + + + + + + + + + + + + + + + + + + + + + diff --git a/services/Network.qml b/services/Network.qml index 3ceadab..1dee367 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -7,11 +7,24 @@ import QtQuick Singleton { id: root + Component.onCompleted: { + // Trigger ethernet device detection after initialization + Qt.callLater(() => { + getEthernetDevices(); + }); + } + readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property bool wifiEnabled: true readonly property bool scanning: rescanProc.running + property list ethernetDevices: [] + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null + property int ethernetDeviceCount: 0 + property string ethernetDebugInfo: "" + property bool ethernetProcessRunning: false + function enableWifi(enabled: bool): void { const cmd = enabled ? "on" : "off"; enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]); @@ -70,11 +83,27 @@ Singleton { wifiStatusProc.running = true; } + function getEthernetDevices(): void { + getEthernetDevicesProc.running = true; + } + + + function connectEthernet(connectionName: string): void { + connectEthernetProc.exec(["nmcli", "connection", "up", connectionName]); + } + + function disconnectEthernet(connectionName: string): void { + disconnectEthernetProc.exec(["nmcli", "connection", "down", connectionName]); + } + Process { running: true command: ["nmcli", "m"] stdout: SplitParser { - onRead: getNetworks.running = true + onRead: { + getNetworks.running = true; + getEthernetDevices(); + } } } @@ -336,6 +365,158 @@ Singleton { } } + Process { + id: getEthernetDevicesProc + + running: false + command: ["nmcli", "-g", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + onRunningChanged: { + root.ethernetProcessRunning = running; + if (!running) { + // Process finished, update debug info + Qt.callLater(() => { + if (root.ethernetDebugInfo === "" || root.ethernetDebugInfo.includes("Process exited")) { + root.ethernetDebugInfo = "Process finished, waiting for output..."; + } + }); + } + } + onExited: { + Qt.callLater(() => { + const outputLength = ethernetStdout.text ? ethernetStdout.text.length : 0; + root.ethernetDebugInfo = "Process exited with code: " + exitCode + ", output length: " + outputLength; + if (outputLength > 0) { + // Output was captured, process it + const output = ethernetStdout.text.trim(); + root.ethernetDebugInfo = "Processing output from onExited, length: " + output.length + "\nOutput: " + output.substring(0, 200); + root.processEthernetOutput(output); + } else { + root.ethernetDebugInfo = "No output captured in onExited"; + } + }); + } + stdout: StdioCollector { + id: ethernetStdout + onStreamFinished: { + const output = text.trim(); + root.ethernetDebugInfo = "Output received in onStreamFinished! Length: " + output.length + ", First 100 chars: " + output.substring(0, 100); + + if (!output || output.length === 0) { + root.ethernetDebugInfo = "No output received (empty)"; + return; + } + + root.processEthernetOutput(output); + } + } + } + + function processEthernetOutput(output: string): void { + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const lines = output.split("\n"); + root.ethernetDebugInfo = "Processing " + lines.length + " lines"; + + const allDevices = lines.map(d => { + const dev = d.replace(rep, PLACEHOLDER).split(":"); + return { + interface: dev[0]?.replace(rep2, ":") ?? "", + type: dev[1]?.replace(rep2, ":") ?? "", + state: dev[2]?.replace(rep2, ":") ?? "", + connection: dev[3]?.replace(rep2, ":") ?? "" + }; + }); + + root.ethernetDebugInfo = "All devices: " + allDevices.length + ", Types: " + allDevices.map(d => d.type).join(", "); + + const ethernetOnly = allDevices.filter(d => d.type === "ethernet"); + root.ethernetDebugInfo = "Ethernet devices found: " + ethernetOnly.length; + + const ethernetDevices = ethernetOnly.map(d => { + const state = d.state || ""; + const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); + return { + interface: d.interface, + type: d.type, + state: state, + connection: d.connection, + connected: connected, + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + }); + + root.ethernetDebugInfo = "Ethernet devices processed: " + ethernetDevices.length + ", First device: " + (ethernetDevices[0]?.interface || "none"); + + // Update the list - replace the entire array to ensure QML detects the change + // Create a new array and assign it to the property + const newDevices = []; + for (let i = 0; i < ethernetDevices.length; i++) { + newDevices.push(ethernetDevices[i]); + } + + // Replace the entire list + root.ethernetDevices = newDevices; + + // Force QML to detect the change by updating a property + root.ethernetDeviceCount = ethernetDevices.length; + + // Force QML to re-evaluate the list by accessing it + Qt.callLater(() => { + const count = root.ethernetDevices.length; + root.ethernetDebugInfo = "Final: Found " + ethernetDevices.length + " devices, List length: " + count + ", Parsed all: " + allDevices.length + ", Output length: " + output.length; + }); + } + + + Process { + id: connectEthernetProc + + onExited: { + getEthernetDevices(); + } + stdout: SplitParser { + onRead: getEthernetDevices() + } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0 && !error.includes("successfully") && !error.includes("Connection activated")) { + console.warn("Ethernet connection error:", error); + } + } + } + } + + Process { + id: disconnectEthernetProc + + onExited: { + getEthernetDevices(); + } + stdout: SplitParser { + onRead: getEthernetDevices() + } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { + console.warn("Ethernet disconnection error:", error); + } + } + } + } + component AccessPoint: QtObject { required property var lastIpcObject readonly property string ssid: lastIpcObject.ssid -- cgit v1.2.3-freya From 817156aec079852141d52d484dd14eec3fa0a88e Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 10 Nov 2025 11:51:43 -0500 Subject: controlcenter: polished ethernet panel --- modules/controlcenter/ethernet/EthernetDetails.qml | 95 ++++++++++++++++- modules/controlcenter/ethernet/EthernetList.qml | 5 +- .../controlcenter/ethernet/EthernetSettings.qml | 50 --------- services/Network.qml | 112 ++++++++++++++++++++- 4 files changed, 205 insertions(+), 57 deletions(-) (limited to 'services/Network.qml') diff --git a/modules/controlcenter/ethernet/EthernetDetails.qml b/modules/controlcenter/ethernet/EthernetDetails.qml index 9be3ddc..1db3db0 100644 --- a/modules/controlcenter/ethernet/EthernetDetails.qml +++ b/modules/controlcenter/ethernet/EthernetDetails.qml @@ -16,6 +16,20 @@ Item { required property Session session readonly property var device: session.ethernet.active + Component.onCompleted: { + if (device && device.interface) { + Network.updateEthernetDeviceDetails(device.interface); + } + } + + onDeviceChanged: { + if (device && device.interface) { + Network.updateEthernetDeviceDetails(device.interface); + } else { + Network.ethernetDeviceDetails = null; + } + } + StyledFlickable { anchors.fill: parent @@ -79,9 +93,8 @@ Item { checked: root.device?.connected ?? false toggle.onToggled: { if (checked) { - if (root.device?.connection) { - Network.connectEthernet(root.device.connection); - } + // Use connection name if available, otherwise use interface + Network.connectEthernet(root.device?.connection || "", root.device?.interface || ""); } else { if (root.device?.connection) { Network.disconnectEthernet(root.device.connection); @@ -155,6 +168,82 @@ Item { } } + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Connection information") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Network connection details") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("IP Address") + } + + StyledText { + text: Network.ethernetDeviceDetails?.ipAddress || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Subnet Mask") + } + + StyledText { + text: Network.ethernetDeviceDetails?.subnet || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Gateway") + } + + StyledText { + text: Network.ethernetDeviceDetails?.gateway || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("DNS Servers") + } + + StyledText { + text: (Network.ethernetDeviceDetails && Network.ethernetDeviceDetails.dns && Network.ethernetDeviceDetails.dns.length > 0) ? Network.ethernetDeviceDetails.dns.join(", ") : qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + } + } + } + } } diff --git a/modules/controlcenter/ethernet/EthernetList.qml b/modules/controlcenter/ethernet/EthernetList.qml index d239fc6..6ed50fd 100644 --- a/modules/controlcenter/ethernet/EthernetList.qml +++ b/modules/controlcenter/ethernet/EthernetList.qml @@ -144,8 +144,9 @@ ColumnLayout { function onClicked(): void { if (modelData.connected && modelData.connection) { Network.disconnectEthernet(modelData.connection); - } else if (modelData.connection) { - Network.connectEthernet(modelData.connection); + } else { + // Use connection name if available, otherwise use interface + Network.connectEthernet(modelData.connection || "", modelData.interface || ""); } } } diff --git a/modules/controlcenter/ethernet/EthernetSettings.qml b/modules/controlcenter/ethernet/EthernetSettings.qml index b780b55..33b1449 100644 --- a/modules/controlcenter/ethernet/EthernetSettings.qml +++ b/modules/controlcenter/ethernet/EthernetSettings.qml @@ -81,56 +81,6 @@ ColumnLayout { } } } - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Debug Info") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: debugInfo.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: debugInfo - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.small / 2 - - StyledText { - text: qsTr("Process running: %1").arg(Network.ethernetProcessRunning ? "Yes" : "No") - font.pointSize: Appearance.font.size.small - } - - StyledText { - text: qsTr("List length: %1").arg(Network.ethernetDevices.length) - font.pointSize: Appearance.font.size.small - } - - StyledText { - text: qsTr("Device count: %1").arg(Network.ethernetDeviceCount) - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Debug: %1").arg(Network.ethernetDebugInfo || "No info") - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3outline - wrapMode: Text.Wrap - Layout.maximumWidth: parent.width - } - } - } } diff --git a/services/Network.qml b/services/Network.qml index 1dee367..acd4bcb 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -24,6 +24,7 @@ Singleton { property int ethernetDeviceCount: 0 property string ethernetDebugInfo: "" property bool ethernetProcessRunning: false + property var ethernetDeviceDetails: null function enableWifi(enabled: bool): void { const cmd = enabled ? "on" : "off"; @@ -88,14 +89,46 @@ Singleton { } - function connectEthernet(connectionName: string): void { - connectEthernetProc.exec(["nmcli", "connection", "up", connectionName]); + function connectEthernet(connectionName: string, interfaceName: string): void { + if (connectionName && connectionName.length > 0) { + // Use connection name if available + connectEthernetProc.exec(["nmcli", "connection", "up", connectionName]); + } else if (interfaceName && interfaceName.length > 0) { + // Fallback to device interface if no connection name + connectEthernetProc.exec(["nmcli", "device", "connect", interfaceName]); + } } function disconnectEthernet(connectionName: string): void { disconnectEthernetProc.exec(["nmcli", "connection", "down", connectionName]); } + function updateEthernetDeviceDetails(interfaceName: string): void { + if (interfaceName && interfaceName.length > 0) { + getEthernetDetailsProc.exec(["nmcli", "device", "show", interfaceName]); + } else { + ethernetDeviceDetails = null; + } + } + + function cidrToSubnetMask(cidr: string): string { + // Convert CIDR notation (e.g., "24") to subnet mask (e.g., "255.255.255.0") + const cidrNum = parseInt(cidr); + if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { + return ""; + } + + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; + const octets = [ + (mask >>> 24) & 0xff, + (mask >>> 16) & 0xff, + (mask >>> 8) & 0xff, + mask & 0xff + ]; + + return octets.join("."); + } + Process { running: true command: ["nmcli", "m"] @@ -484,6 +517,13 @@ Singleton { onExited: { getEthernetDevices(); + // Refresh device details after connection + Qt.callLater(() => { + const activeDevice = root.ethernetDevices.find(function(d) { return d.connected; }); + if (activeDevice && activeDevice.interface) { + updateEthernetDeviceDetails(activeDevice.interface); + } + }); } stdout: SplitParser { onRead: getEthernetDevices() @@ -503,6 +543,10 @@ Singleton { onExited: { getEthernetDevices(); + // Clear device details after disconnection + Qt.callLater(() => { + root.ethernetDeviceDetails = null; + }); } stdout: SplitParser { onRead: getEthernetDevices() @@ -517,6 +561,70 @@ Singleton { } } + Process { + id: getEthernetDetailsProc + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output || output.length === 0) { + root.ethernetDeviceDetails = null; + return; + } + + const lines = output.split("\n"); + const details = { + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + + 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")) { + // Extract IP and subnet from format like "10.13.1.45/24" + const ipParts = value.split("/"); + details.ipAddress = ipParts[0] || ""; + if (ipParts[1]) { + // Convert CIDR notation to subnet mask + details.subnet = root.cidrToSubnetMask(ipParts[1]); + } else { + details.subnet = ""; + } + } else if (key === "IP4.GATEWAY") { + details.gateway = value; + } else if (key.startsWith("IP4.DNS")) { + details.dns.push(value); + } else if (key === "WIRED-PROPERTIES.MAC") { + details.macAddress = value; + } else if (key === "WIRED-PROPERTIES.SPEED") { + details.speed = value; + } + } + } + + root.ethernetDeviceDetails = details; + } + } + onExited: { + if (exitCode !== 0) { + root.ethernetDeviceDetails = null; + } + } + } + component AccessPoint: QtObject { required property var lastIpcObject readonly property string ssid: lastIpcObject.ssid -- cgit v1.2.3-freya From 5af1e9222e2f15c84102dc1ffb46e8643959e74a Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 16:18:32 -0500 Subject: controlcenter: added connection information to wireless to match ethernet panel --- modules/controlcenter/network/Details.qml | 101 ++++++++++++++++++++++++++ services/Network.qml | 114 +++++++++++++++++++++++++++++- 2 files changed, 214 insertions(+), 1 deletion(-) (limited to 'services/Network.qml') diff --git a/modules/controlcenter/network/Details.qml b/modules/controlcenter/network/Details.qml index 19e011f..31d20bc 100644 --- a/modules/controlcenter/network/Details.qml +++ b/modules/controlcenter/network/Details.qml @@ -16,6 +16,31 @@ Item { required property Session session readonly property var network: session.network.active + Component.onCompleted: { + if (network && network.active) { + Network.updateWirelessDeviceDetails(); + } + } + + onNetworkChanged: { + if (network && network.active) { + Network.updateWirelessDeviceDetails(); + } else { + Network.wirelessDeviceDetails = null; + } + } + + Connections { + target: Network + function onActiveChanged() { + if (root.network && root.network.active && Network.active && Network.active.ssid === root.network.ssid) { + Network.updateWirelessDeviceDetails(); + } else if (!root.network || !root.network.active) { + Network.wirelessDeviceDetails = null; + } + } + } + StyledFlickable { anchors.fill: parent @@ -198,6 +223,82 @@ Item { } } } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Connection information") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Network connection details") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("IP Address") + } + + StyledText { + text: Network.wirelessDeviceDetails?.ipAddress || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Subnet Mask") + } + + StyledText { + text: Network.wirelessDeviceDetails?.subnet || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Gateway") + } + + StyledText { + text: Network.wirelessDeviceDetails?.gateway || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("DNS Servers") + } + + StyledText { + text: (Network.wirelessDeviceDetails && Network.wirelessDeviceDetails.dns && Network.wirelessDeviceDetails.dns.length > 0) ? Network.wirelessDeviceDetails.dns.join(", ") : qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + } + } + } } } diff --git a/services/Network.qml b/services/Network.qml index acd4bcb..c8ab264 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -25,6 +25,7 @@ Singleton { property string ethernetDebugInfo: "" property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null + property var wirelessDeviceDetails: null function enableWifi(enabled: bool): void { const cmd = enabled ? "on" : "off"; @@ -111,6 +112,11 @@ Singleton { } } + function updateWirelessDeviceDetails(): void { + // Find the wireless interface by looking for wifi devices + findWirelessInterfaceProc.exec(["nmcli", "device", "status"]); + } + function cidrToSubnetMask(cidr: string): string { // Convert CIDR notation (e.g., "24") to subnet mask (e.g., "255.255.255.0") const cidrNum = parseInt(cidr); @@ -587,7 +593,7 @@ Singleton { }; for (let i = 0; i < lines.length; i++) { - const line = lines[i]; +const line = lines[i]; const parts = line.split(":"); if (parts.length >= 2) { const key = parts[0].trim(); @@ -625,6 +631,112 @@ Singleton { } } + Process { + id: findWirelessInterfaceProc + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output || output.length === 0) { + root.wirelessDeviceDetails = null; + return; + } + + // Find the connected wifi interface from device status + const lines = output.split("\n"); + let wifiInterface = ""; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(/\s+/); + // Format: DEVICE TYPE STATE CONNECTION + // Look for wifi devices that are connected + if (parts.length >= 3 && parts[1] === "wifi" && parts[2] === "connected") { + wifiInterface = parts[0]; + break; + } + } + + if (wifiInterface && wifiInterface.length > 0) { + getWirelessDetailsProc.exec(["nmcli", "device", "show", wifiInterface]); + } else { + root.wirelessDeviceDetails = null; + } + } + } + onExited: { + if (exitCode !== 0) { + root.wirelessDeviceDetails = null; + } + } + } + + Process { + id: getWirelessDetailsProc + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output || output.length === 0) { + root.wirelessDeviceDetails = null; + return; + } + + const lines = output.split("\n"); + const details = { + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + + 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")) { + // Extract IP and subnet from format like "10.13.1.45/24" + const ipParts = value.split("/"); + details.ipAddress = ipParts[0] || ""; + if (ipParts[1]) { + // Convert CIDR notation to subnet mask + details.subnet = root.cidrToSubnetMask(ipParts[1]); + } else { + details.subnet = ""; + } + } else if (key === "IP4.GATEWAY") { + details.gateway = value; + } else if (key.startsWith("IP4.DNS")) { + details.dns.push(value); + } else if (key === "GENERAL.HWADDR") { + details.macAddress = value; + } + } + } + + root.wirelessDeviceDetails = details; + } + } + onExited: { + if (exitCode !== 0) { + root.wirelessDeviceDetails = null; + } + } + } + component AccessPoint: QtObject { required property var lastIpcObject readonly property string ssid: lastIpcObject.ssid -- cgit v1.2.3-freya From 7e6f3270911d9a3b7a73532b18670a5fa613ee92 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 19:57:42 -0500 Subject: conrolcenter: debug/rewrite of wireless panel --- modules/bar/popouts/Network.qml | 2 +- modules/controlcenter/network/Details.qml | 55 +++- modules/controlcenter/network/NetworkList.qml | 5 +- modules/controlcenter/network/PasswordDialog.qml | 283 +++++++++++++++++++- services/Network.qml | 316 ++++++++++++++++++++++- 5 files changed, 637 insertions(+), 24 deletions(-) (limited to 'services/Network.qml') diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index f21a92d..f040b6a 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -118,7 +118,7 @@ ColumnLayout { Network.disconnectFromNetwork(); } else { root.connectingToSsid = networkItem.modelData.ssid; - Network.connectToNetwork(networkItem.modelData.ssid, ""); + Network.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); } } } diff --git a/modules/controlcenter/network/Details.qml b/modules/controlcenter/network/Details.qml index a53f62e..5e636a2 100644 --- a/modules/controlcenter/network/Details.qml +++ b/modules/controlcenter/network/Details.qml @@ -95,10 +95,31 @@ Item { // Callback: connection failed, show password dialog root.session.network.showPasswordDialog = true; root.session.network.pendingNetwork = root.network; - } + }, + root.network.bssid ); } else { - Network.connectToNetwork(root.network.ssid, ""); + Network.connectToNetwork(root.network.ssid, "", root.network.bssid, null); + } + } + } + + Button { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + visible: root.network && root.network.ssid && Network.savedConnections.includes(root.network.ssid) + color: Colours.palette.m3errorContainer + onColor: Colours.palette.m3onErrorContainer + text: qsTr("Forget Network") + + onClicked: { + if (root.network && root.network.ssid) { + // Disconnect first if connected + if (root.network.active) { + Network.disconnectFromNetwork(); + } + // Delete the connection profile + Network.forgetNetwork(root.network.ssid); } } } @@ -152,9 +173,39 @@ Item { deviceDetails: Network.wirelessDeviceDetails } } + } } + component Button: StyledRect { + property color onColor: Colours.palette.m3onSurface + property alias disabled: stateLayer.disabled + property alias text: label.text + property alias enabled: stateLayer.enabled + + Layout.fillWidth: true + implicitHeight: label.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.normal + + StateLayer { + id: stateLayer + color: parent.onColor + function onClicked(): void { + if (parent.enabled !== false) { + parent.clicked(); + } + } + } + + StyledText { + id: label + anchors.centerIn: parent + color: parent.onColor + } + + signal clicked + } + } diff --git a/modules/controlcenter/network/NetworkList.qml b/modules/controlcenter/network/NetworkList.qml index 09d7352..6c4158c 100644 --- a/modules/controlcenter/network/NetworkList.qml +++ b/modules/controlcenter/network/NetworkList.qml @@ -203,10 +203,11 @@ ColumnLayout { // Callback: connection failed, show password dialog root.session.network.showPasswordDialog = true; root.session.network.pendingNetwork = modelData; - } + }, + modelData.bssid ); } else { - Network.connectToNetwork(modelData.ssid, ""); + Network.connectToNetwork(modelData.ssid, "", modelData.bssid, null); } } } diff --git a/modules/controlcenter/network/PasswordDialog.qml b/modules/controlcenter/network/PasswordDialog.qml index fa4788c..7aa698b 100644 --- a/modules/controlcenter/network/PasswordDialog.qml +++ b/modules/controlcenter/network/PasswordDialog.qml @@ -14,11 +14,37 @@ Item { id: root required property Session session - readonly property var network: session.network.pendingNetwork + readonly property var network: { + // Try pendingNetwork first, then fall back to active network selection + if (session.network.pendingNetwork) { + return session.network.pendingNetwork; + } + // Fallback to active network if available + if (session.network.active) { + return session.network.active; + } + return null; + } visible: session.network.showPasswordDialog enabled: visible focus: visible + + // Ensure network is set when dialog opens + Component.onCompleted: { + if (visible && !session.network.pendingNetwork && session.network.active) { + session.network.pendingNetwork = session.network.active; + } + } + + Connections { + target: root + function onVisibleChanged(): void { + if (visible && !session.network.pendingNetwork && session.network.active) { + session.network.pendingNetwork = session.network.active; + } + } + } Keys.onEscapePressed: { root.session.network.showPasswordDialog = false; @@ -105,6 +131,35 @@ Item { font.pointSize: Appearance.font.size.small } + StyledText { + id: statusText + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.spacing.small + visible: Network.connectionStatus.length > 0 || connectButton.connecting + text: { + if (Network.connectionStatus.length > 0) { + return Network.connectionStatus; + } else if (connectButton.connecting) { + return qsTr("Connecting..."); + } + return ""; + } + color: { + const status = Network.connectionStatus; + if (status.includes("Error") || status.includes("error") || status.includes("failed")) { + return Colours.palette.m3error; + } else if (status.includes("successful") || status.includes("Connected") || status.includes("success")) { + return Colours.palette.m3primary; + } + return Colours.palette.m3onSurfaceVariant; + } + font.pointSize: Appearance.font.size.small + font.weight: (Network.connectionStatus.includes("Error") || Network.connectionStatus.includes("error")) ? 500 : 400 + wrapMode: Text.WordWrap + Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + } + Item { Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true @@ -145,9 +200,17 @@ Item { if (root.visible) { passwordField.forceActiveFocus(); passwordField.text = ""; + Network.connectionStatus = ""; } } } + + Connections { + target: Network + function onConnectionStatusChanged(): void { + // Status updated, ensure it's visible + } + } Keys.onReturnPressed: { if (connectButton.enabled) { @@ -190,12 +253,222 @@ Item { text: qsTr("Connect") enabled: passwordField.text.length > 0 + property bool connecting: false + function onClicked(): void { - if (root.network && passwordField.text.length > 0) { - Network.connectToNetwork(root.network.ssid, passwordField.text); - root.session.network.showPasswordDialog = false; - passwordField.text = ""; + Network.connectionStatus = ""; + + // Get password first + const password = passwordField.text; + + // Try multiple ways to get the network + let networkToUse = null; + + // Try 1: root.network (computed property) + if (root.network) { + networkToUse = root.network; + } + + // Try 2: pendingNetwork + if (!networkToUse && root.session.network.pendingNetwork) { + networkToUse = root.session.network.pendingNetwork; + } + + // Try 3: active network + if (!networkToUse && root.session.network.active) { + networkToUse = root.session.network.active; + root.session.network.pendingNetwork = networkToUse; + } + + // Check all conditions + const hasNetwork = !!networkToUse; + const hasPassword = password && password.length > 0; + const notConnecting = !connecting; + + if (hasNetwork && hasPassword && notConnecting) { + // Set status immediately + Network.connectionStatus = qsTr("Preparing to connect..."); + + // Keep dialog open and track connection + connecting = true; + connectButton.enabled = false; + connectButton.text = qsTr("Connecting..."); + + // Force immediate UI update + statusText.visible = true; + + // Store target SSID for later comparison + const ssidToConnect = networkToUse.ssid || ""; + const bssidToConnect = networkToUse.bssid || ""; + + // Store the SSID we're connecting to so we can compare later + // even if root.network changes + content.connectingToSsid = ssidToConnect; + + // Execute connection immediately + Network.connectToNetwork( + ssidToConnect, + password, + bssidToConnect, + () => { + // Callback if connection fails - keep dialog open + connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + content.connectingToSsid = ""; // Clear on failure + } + ); + + // Start connection check timer immediately + connectionCheckTimer.checkCount = 0; + connectionCheckTimer.start(); + + // Also check immediately after a short delay to catch quick connections + Qt.callLater(() => { + if (root.visible) { + closeDialogIfConnected(); + } + }); + } else { + // Show error in status + Network.connectionStatus = qsTr("Error: Cannot connect - missing network or password"); + } + } + } + } + + // Store the SSID we're connecting to when connection starts + property string connectingToSsid: "" + + property string targetSsid: { + // Track the SSID we're trying to connect to + // Prefer explicitly stored connectingToSsid, then computed values + if (connectingToSsid && connectingToSsid.length > 0) { + return connectingToSsid; + } + if (root.network && root.network.ssid) { + return root.network.ssid; + } + if (root.session.network.pendingNetwork && root.session.network.pendingNetwork.ssid) { + return root.session.network.pendingNetwork.ssid; + } + return ""; + } + + function closeDialogIfConnected(): bool { + // Check if we're connected to the network we're trying to connect to + const ssid = targetSsid; + + if (!ssid || ssid.length === 0) { + return false; + } + + if (!Network.active) { + return false; + } + + const activeSsid = Network.active.ssid || ""; + + if (activeSsid === ssid) { + // Connection succeeded - close dialog + connectionCheckTimer.stop(); + aggressiveCheckTimer.stop(); + connectionCheckTimer.checkCount = 0; + connectButton.connecting = false; + Network.connectionStatus = ""; + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + content.connectingToSsid = ""; // Clear stored SSID + return true; + } + return false; + } + + Timer { + id: connectionCheckTimer + interval: 1000 // Check every 1 second for faster response + repeat: true + triggeredOnStart: false + property int checkCount: 0 + + onTriggered: { + checkCount++; + + // Try to close dialog if connected + const closed = content.closeDialogIfConnected(); + if (closed) { + return; + } + + if (connectButton.connecting) { + // Still connecting, check again + // Limit to 20 checks (20 seconds total) + if (checkCount >= 20) { + connectionCheckTimer.stop(); + connectionCheckTimer.checkCount = 0; + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + } + } else { + // Not connecting anymore, stop timer + connectionCheckTimer.stop(); + connectionCheckTimer.checkCount = 0; + } + } + } + + Connections { + target: Network + function onActiveChanged(): void { + // Check immediately when active network changes + if (root.visible) { + // Check immediately - if connected, close right away + if (content.closeDialogIfConnected()) { + return; + } + + // Also check after a delay in case the active network isn't fully updated yet + Qt.callLater(() => { + if (root.visible) { + content.closeDialogIfConnected(); + } + }); + } + } + } + + // Also check when dialog becomes visible + Connections { + target: root + function onVisibleChanged(): void { + if (root.visible) { + // Check immediately when dialog opens + Qt.callLater(() => { + if (root.visible) { + closeDialogIfConnected(); + } + }); + } + } + } + + // Aggressive polling timer - checks every 500ms when dialog is visible and connecting + // This ensures we catch the connection even if signals are missed + Timer { + id: aggressiveCheckTimer + interval: 500 + repeat: true + running: root.visible && connectButton.connecting + triggeredOnStart: true + + onTriggered: { + if (root.visible && connectButton.connecting) { + if (content.closeDialogIfConnected()) { + stop(); } + } else { + stop(); } } } diff --git a/services/Network.qml b/services/Network.qml index c8ab264..ea5c3e7 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -12,6 +12,8 @@ Singleton { Qt.callLater(() => { getEthernetDevices(); }); + // Load saved connections on startup + listConnectionsProc.running = true; } readonly property list networks: [] @@ -26,6 +28,34 @@ Singleton { property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null property var wirelessDeviceDetails: null + property string connectionStatus: "" + property string connectionDebug: "" + + function clearConnectionStatus(): void { + connectionStatus = ""; + // Don't clear debug - keep it for reference + // connectionDebug = ""; + } + + function setConnectionStatus(status: string): void { + connectionStatus = status; + } + + function addDebugInfo(info: string): void { + const timestamp = new Date().toLocaleTimeString(); + const newInfo = "[" + timestamp + "] " + info; + // CRITICAL: Always append - NEVER replace + // Get current value - NEVER allow it to be empty/cleared + let current = connectionDebug; + if (!current || current === undefined || current === null) { + current = ""; + } + // ALWAYS append - never replace + // If current is empty, just use newInfo, otherwise append with newline + const updated = (current.length > 0) ? (current + "\n" + newInfo) : newInfo; + // CRITICAL: Only assign if we're appending, never replace + connectionDebug = updated; + } function enableWifi(enabled: bool): void { const cmd = enabled ? "on" : "off"; @@ -44,30 +74,118 @@ Singleton { property var pendingConnection: null signal connectionFailed(string ssid) - function connectToNetwork(ssid: string, password: string): void { - // First try to connect to an existing connection - // If that fails, create a new connection + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { + // When password is provided, use BSSID for more reliable connection + // When no password, use SSID (will use saved password if available) + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + let cmd = []; + + // Set up pending connection tracking if callback provided + if (callback) { + root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; + } + if (password && password.length > 0) { - connectProc.exec(["nmcli", "device", "wifi", "connect", ssid, "password", password]); + // When password is provided, try BSSID first if available, otherwise use SSID + if (hasBssid) { + // Use BSSID when password is provided - ensure BSSID is uppercase + const bssidUpper = bssid.toUpperCase(); + // Create connection profile with all required properties for BSSID + password + // First remove any existing connection with this name + cmd = ["nmcli", "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]; + root.setConnectionStatus(qsTr("Connecting to %1 (BSSID: %2)...").arg(ssid).arg(bssidUpper)); + root.addDebugInfo(qsTr("Using BSSID: %1 for SSID: %2").arg(bssidUpper).arg(ssid)); + root.addDebugInfo(qsTr("Creating connection profile with password and key-mgmt")); + } else { + // Fallback to SSID if BSSID not available - use device wifi connect + cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; + root.setConnectionStatus(qsTr("Connecting to %1...").arg(ssid)); + root.addDebugInfo(qsTr("Using SSID only (no BSSID): %1").arg(ssid)); + } } else { // Try to connect to existing connection first (will use saved password if available) - connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); + cmd = ["nmcli", "device", "wifi", "connect", ssid]; + root.setConnectionStatus(qsTr("Connecting to %1 (using saved password)...").arg(ssid)); + root.addDebugInfo(qsTr("Using saved password for: %1").arg(ssid)); + } + + // Show the exact command being executed + const cmdStr = cmd.join(" "); + root.addDebugInfo(qsTr("=== COMMAND TO EXECUTE ===")); + root.addDebugInfo(qsTr("Command: %1").arg(cmdStr)); + root.addDebugInfo(qsTr("Command array: [%1]").arg(cmd.map((arg, i) => `"${arg}"`).join(", "))); + root.addDebugInfo(qsTr("Command array length: %1").arg(cmd.length)); + root.addDebugInfo(qsTr("===========================")); + + // Set command and start process + root.addDebugInfo(qsTr("Setting command property...")); + connectProc.command = cmd; + const setCmdStr = connectProc.command ? connectProc.command.join(" ") : "null"; + root.addDebugInfo(qsTr("Command property set, value: %1").arg(setCmdStr)); + root.addDebugInfo(qsTr("Command property verified: %1").arg(setCmdStr === cmdStr ? "Match" : "MISMATCH")); + + // If we're creating a connection profile, we need to activate it after creation + const isConnectionAdd = cmd.length > 0 && cmd[0] === "nmcli" && cmd[1] === "connection" && cmd[2] === "add"; + + // Wait a moment before starting to ensure command is set + Qt.callLater(() => { + root.addDebugInfo(qsTr("=== STARTING PROCESS ===")); + root.addDebugInfo(qsTr("Current running state: %1").arg(connectProc.running)); + root.addDebugInfo(qsTr("Command to run: %1").arg(connectProc.command ? connectProc.command.join(" ") : "NOT SET")); + root.addDebugInfo(qsTr("Is connection add command: %1").arg(isConnectionAdd)); + connectProc.running = true; + root.addDebugInfo(qsTr("Process running set to: %1").arg(connectProc.running)); + root.addDebugInfo(qsTr("========================")); + + // Check if process actually started after a short delay + Qt.callLater(() => { + root.addDebugInfo(qsTr("Process status check (100ms later):")); + root.addDebugInfo(qsTr(" Running: %1").arg(connectProc.running)); + root.addDebugInfo(qsTr(" Command: %1").arg(connectProc.command ? connectProc.command.join(" ") : "null")); + if (!connectProc.running) { + root.addDebugInfo(qsTr("WARNING: Process did not start!")); + root.setConnectionStatus(qsTr("Error: Process failed to start")); + } + }, 100); + }); + + // Start connection check timer if we have a callback + if (callback) { + root.addDebugInfo(qsTr("Starting connection check timer (4 second interval)")); + connectionCheckTimer.start(); + } else { + root.addDebugInfo(qsTr("No callback provided - not starting connection check timer")); } } - function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var): void { + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { + root.addDebugInfo(qsTr("=== connectToNetworkWithPasswordCheck ===")); + root.addDebugInfo(qsTr("SSID: %1, isSecure: %2").arg(ssid).arg(isSecure)); + // For secure networks, try connecting without password first // If connection succeeds (saved password exists), we're done // If it fails with password error, callback will be called to show password dialog if (isSecure) { - root.pendingConnection = { ssid: ssid, callback: callback }; + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; + root.addDebugInfo(qsTr("Trying to connect without password (will use saved if available)")); // Try connecting without password - will use saved password if available connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); // Start timer to check if connection succeeded + root.addDebugInfo(qsTr("Starting connection check timer")); connectionCheckTimer.start(); } else { - connectToNetwork(ssid, ""); + root.addDebugInfo(qsTr("Network is not secure, connecting directly")); + connectToNetwork(ssid, "", bssid, null); } + root.addDebugInfo(qsTr("=========================================")); } function disconnectFromNetwork(): void { @@ -80,6 +198,50 @@ Singleton { disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); } } + + function forgetNetwork(ssid: string): void { + // Delete the connection profile for this network + // This will remove the saved password and connection settings + if (ssid && ssid.length > 0) { + deleteConnectionProc.exec(["nmcli", "connection", "delete", ssid]); + // Also refresh network list after deletion + Qt.callLater(() => { + getNetworks.running = true; + }, 500); + } + } + + function hasConnectionProfile(ssid: string): bool { + // Check if a connection profile exists for this SSID + // This is synchronous check - returns true if connection exists + if (!ssid || ssid.length === 0) { + return false; + } + // Use nmcli to check if connection exists + // We'll use a Process to check, but for now return false + // The actual check will be done asynchronously + return false; + } + + property list savedConnections: [] + + Process { + id: listConnectionsProc + command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] + onExited: { + if (exitCode === 0) { + // Parse connection names from output + const connections = stdout.text.trim().split("\n").filter(name => name.length > 0); + root.savedConnections = connections; + } + } + stdout: StdioCollector { + onStreamFinished: { + const connections = text.trim().split("\n").filter(name => name.length > 0); + root.savedConnections = connections; + } + } + } function getWifiStatus(): void { wifiStatusProc.running = true; @@ -184,11 +346,17 @@ Singleton { id: connectionCheckTimer interval: 4000 onTriggered: { + root.addDebugInfo(qsTr("=== CONNECTION CHECK TIMER (4s) ===")); if (root.pendingConnection) { - // Final check - if connection still hasn't succeeded, show password dialog const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + root.addDebugInfo(qsTr("Checking connection status...")); + root.addDebugInfo(qsTr(" Pending SSID: %1").arg(root.pendingConnection.ssid)); + root.addDebugInfo(qsTr(" Active SSID: %1").arg(root.active ? root.active.ssid : "None")); + root.addDebugInfo(qsTr(" Connected: %1").arg(connected)); + if (!connected && root.pendingConnection.callback) { // Connection didn't succeed after multiple checks, show password dialog + root.addDebugInfo(qsTr("Connection failed - calling password dialog callback")); const pending = root.pendingConnection; root.pendingConnection = null; immediateCheckTimer.stop(); @@ -196,11 +364,19 @@ Singleton { pending.callback(); } else if (connected) { // Connection succeeded, clear pending + root.addDebugInfo(qsTr("Connection succeeded!")); + root.setConnectionStatus(qsTr("Connected successfully!")); root.pendingConnection = null; immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; + } else { + root.addDebugInfo(qsTr("Still connecting...")); + root.setConnectionStatus(qsTr("Still connecting...")); } + } else { + root.addDebugInfo(qsTr("No pending connection")); } + root.addDebugInfo(qsTr("================================")); } } @@ -210,23 +386,37 @@ Singleton { repeat: true triggeredOnStart: false property int checkCount: 0 + + onRunningChanged: { + if (running) { + root.addDebugInfo(qsTr("Immediate check timer started (checks every 500ms)")); + } + } + onTriggered: { if (root.pendingConnection) { checkCount++; const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + root.addDebugInfo(qsTr("Immediate check #%1: Connected=%2").arg(checkCount).arg(connected)); + if (connected) { // Connection succeeded, stop timers and clear pending + root.addDebugInfo(qsTr("Connection succeeded on check #%1!").arg(checkCount)); + root.setConnectionStatus(qsTr("Connected successfully!")); connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; root.pendingConnection = null; } else if (checkCount >= 6) { + root.addDebugInfo(qsTr("Checked %1 times (3 seconds) - connection taking longer").arg(checkCount)); + root.setConnectionStatus(qsTr("Connection taking longer than expected...")); // Checked 6 times (3 seconds total), connection likely failed // Stop immediate check, let the main timer handle it immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } } else { + root.addDebugInfo(qsTr("Immediate check: No pending connection, stopping timer")); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } @@ -236,22 +426,92 @@ Singleton { Process { id: connectProc + onRunningChanged: { + root.addDebugInfo(qsTr("Process running changed to: %1").arg(running)); + } + + onStarted: { + root.addDebugInfo(qsTr("Process started successfully")); + } + onExited: { + root.addDebugInfo(qsTr("=== PROCESS EXITED ===")); + root.addDebugInfo(qsTr("Exit code: %1").arg(exitCode)); + root.addDebugInfo(qsTr("(Exit code 0 = success, non-zero = error)")); + + // Check if this was a "connection add" command - if so, we need to activate it + const wasConnectionAdd = connectProc.command && connectProc.command.length > 0 + && connectProc.command[0] === "nmcli" + && connectProc.command[1] === "connection" + && connectProc.command[2] === "add"; + + if (wasConnectionAdd && exitCode === 0 && root.pendingConnection) { + // Connection profile was created successfully, now activate it + const ssid = root.pendingConnection.ssid; + root.addDebugInfo(qsTr("Connection profile created successfully, now activating: %1").arg(ssid)); + root.setConnectionStatus(qsTr("Activating connection...")); + + // Update saved connections list since we just created one + listConnectionsProc.running = true; + + // Activate the connection we just created + connectProc.command = ["nmcli", "connection", "up", ssid]; + Qt.callLater(() => { + connectProc.running = true; + }); + // Don't start timers yet - wait for activation to complete + return; + } + // Refresh network list after connection attempt getNetworks.running = true; // Check if connection succeeded after a short delay (network list needs to update) if (root.pendingConnection) { - immediateCheckTimer.start(); + if (exitCode === 0) { + // Process succeeded, start checking connection status + root.setConnectionStatus(qsTr("Connection command succeeded, verifying...")); + root.addDebugInfo(qsTr("Command succeeded, checking connection status...")); + root.addDebugInfo(qsTr("Starting immediate check timer (500ms intervals)")); + immediateCheckTimer.start(); + } else { + // Process failed, but wait a moment to see if connection still works + root.setConnectionStatus(qsTr("Connection command exited with code %1, checking status...").arg(exitCode)); + root.addDebugInfo(qsTr("Command exited with error code %1").arg(exitCode)); + root.addDebugInfo(qsTr("This usually means the command failed")); + root.addDebugInfo(qsTr("Checking connection status anyway...")); + root.addDebugInfo(qsTr("Starting immediate check timer (500ms intervals)")); + immediateCheckTimer.start(); + } + } else { + root.addDebugInfo(qsTr("No pending connection - not starting immediate check timer")); } + root.addDebugInfo(qsTr("======================")); } stdout: SplitParser { - onRead: getNetworks.running = true + onRead: { + getNetworks.running = true; + // Also log output for debugging + if (text && text.trim().length > 0) { + root.addDebugInfo(qsTr("STDOUT: %1").arg(text.trim())); + root.setConnectionStatus(qsTr("Status: %1").arg(text.trim())); + } + } } stderr: StdioCollector { onStreamFinished: { const error = text.trim(); + root.addDebugInfo(qsTr("=== STDERR OUTPUT ===")); if (error && error.length > 0) { + // Split error into lines and add each one + const errorLines = error.split("\n"); + for (let i = 0; i < errorLines.length; i++) { + const line = errorLines[i].trim(); + if (line.length > 0) { + root.addDebugInfo(qsTr("STDERR: %1").arg(line)); + } + } + // Check for specific errors that indicate password is needed // Be careful not to match success messages const needsPassword = (error.includes("Secrets were required") || @@ -270,11 +530,19 @@ Singleton { const pending = root.pendingConnection; root.pendingConnection = null; pending.callback(); - } else if (error && error.length > 0 && !error.includes("Connection activated")) { - // Only log non-success messages - console.warn("Network connection error:", error); + } else if (error && error.length > 0 && !error.includes("Connection activated") && !error.includes("successfully")) { + // Log all errors (except success messages) + root.setConnectionStatus(qsTr("Error: %1").arg(errorLines[0] || error)); + // Emit signal for UI to handle + root.connectionFailed(root.pendingConnection ? root.pendingConnection.ssid : ""); + } else if (error && (error.includes("Connection activated") || error.includes("successfully"))) { + root.addDebugInfo(qsTr("Connection successful!")); + root.setConnectionStatus(qsTr("Connection successful!")); } + } else { + root.addDebugInfo(qsTr("STDERR: (empty)")); } + root.addDebugInfo(qsTr("====================")); } } } @@ -321,6 +589,26 @@ Singleton { } } + Process { + id: deleteConnectionProc + + // Delete connection profile - refresh network list and saved connections after deletion + onExited: { + // Refresh network list and saved connections after deletion + getNetworks.running = true; + listConnectionsProc.running = true; + } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + // Log error but don't fail - connection might not exist + console.warn("Network connection delete error:", error); + } + } + } + } + Process { id: getNetworks -- cgit v1.2.3-freya From 6ae1313b6b61c965ccc5f2d9d61458d7a5ed21b8 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 22:31:11 -0500 Subject: controlcenter: wireless panel refactoring --- modules/controlcenter/network/WirelessDetails.qml | 24 +- modules/controlcenter/network/WirelessList.qml | 18 +- .../network/WirelessPasswordDialog.qml | 53 +++-- services/Network.qml | 257 ++++++++++++++++++--- 4 files changed, 300 insertions(+), 52 deletions(-) (limited to 'services/Network.qml') diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 418c463..3e48b55 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -19,10 +19,22 @@ Item { Component.onCompleted: { updateDeviceDetails(); + checkSavedProfile(); } onNetworkChanged: { updateDeviceDetails(); + checkSavedProfile(); + } + + function checkSavedProfile(): void { + // Refresh saved connections list to ensure it's up to date + // This ensures the "Forget Network" button visibility is accurate + if (network && network.ssid) { + // Always refresh to ensure we have the latest saved connections + // This is important when networks are selected or changed + Network.listConnectionsProc.running = true; + } } Connections { @@ -80,7 +92,13 @@ Item { SimpleButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal - visible: root.network && root.network.ssid && Network.savedConnections.includes(root.network.ssid) + visible: { + if (!root.network || !root.network.ssid) { + return false; + } + // Check if profile exists - this will update reactively when savedConnectionSsids changes + return Network.hasSavedProfile(root.network.ssid); + } color: Colours.palette.m3errorContainer onColor: Colours.palette.m3onErrorContainer text: qsTr("Forget Network") @@ -164,8 +182,8 @@ Item { function connectToNetwork(): void { if (root.network.isSecure) { - // Check if we have a saved connection profile for this network - const hasSavedProfile = Network.savedConnections.includes(root.network.ssid); + // Check if we have a saved connection profile for this network (by SSID) + const hasSavedProfile = Network.hasSavedProfile(root.network.ssid); if (hasSavedProfile) { // Try connecting with saved password - don't show dialog if it fails diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index c64c4be..aabfc4b 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -117,6 +117,10 @@ ColumnLayout { StateLayer { function onClicked(): void { root.session.network.active = modelData; + // Check if we need to refresh saved connections when selecting a network + if (modelData && modelData.ssid) { + root.checkSavedProfileForNetwork(modelData.ssid); + } } } @@ -200,6 +204,16 @@ ColumnLayout { } } + function checkSavedProfileForNetwork(ssid: string): void { + // Refresh saved connections list to ensure it's up to date + // This ensures accurate profile detection when selecting networks + if (ssid && ssid.length > 0) { + // Always refresh to ensure we have the latest saved connections + // This is important when a network is selected from the list + Network.listConnectionsProc.running = true; + } + } + function handleConnect(network): void { // If already connected to a different network, disconnect first if (Network.active && Network.active.ssid !== network.ssid) { @@ -214,8 +228,8 @@ ColumnLayout { function connectToNetwork(network): void { if (network.isSecure) { - // Check if we have a saved connection profile for this network - const hasSavedProfile = Network.savedConnections.includes(network.ssid); + // Check if we have a saved connection profile for this network (by SSID) + const hasSavedProfile = Network.hasSavedProfile(network.ssid); if (hasSavedProfile) { // Try connecting with saved password - don't show dialog if it fails diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index df8a8cf..5bcf33c 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -248,24 +248,47 @@ Item { return; } - // Check if we're connected to the target network - if (root.network && Network.active && Network.active.ssid === root.network.ssid) { - // Successfully connected - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - closeDialog(); + // Check connection status message for success indicators + const status = Network.connectionStatus; + const statusLower = status.toLowerCase(); + + // Check for success indicators in status message + const hasSuccessIndicator = statusLower.includes("connection activated") || + statusLower.includes("successfully") || + statusLower.includes("connected successfully") || + (statusLower.includes("connected") && !statusLower.includes("error") && !statusLower.includes("failed")); + + // Check if we're connected to the target network (case-insensitive SSID comparison) + const isConnected = root.network && Network.active && Network.active.ssid && + Network.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected || hasSuccessIndicator) { + // Successfully connected - give it a moment for network list to update + Qt.callLater(() => { + // Double-check connection is still active + if (root.visible && Network.active && Network.active.ssid) { + const stillConnected = Network.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected || hasSuccessIndicator) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + closeDialog(); + } + } + }, 500); return; } - // Check for connection errors - const status = Network.connectionStatus; - if (status.includes("Error") || status.includes("error") || status.includes("failed")) { - // Connection failed - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); + // Check for connection errors (but not warnings about duplicate names) + if (status.includes("Error") || (status.includes("error") && !status.includes("Warning"))) { + // Only treat as error if it's not just a warning about duplicate names + if (!status.includes("another connection with the name") && !status.includes("Reference the connection by its uuid")) { + // Connection failed + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + } } } diff --git a/services/Network.qml b/services/Network.qml index ea5c3e7..0b936b8 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -90,19 +90,26 @@ Singleton { if (hasBssid) { // Use BSSID when password is provided - ensure BSSID is uppercase const bssidUpper = bssid.toUpperCase(); - // Create connection profile with all required properties for BSSID + password - // First remove any existing connection with this name - cmd = ["nmcli", "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]; - root.setConnectionStatus(qsTr("Connecting to %1 (BSSID: %2)...").arg(ssid).arg(bssidUpper)); - root.addDebugInfo(qsTr("Using BSSID: %1 for SSID: %2").arg(bssidUpper).arg(ssid)); - root.addDebugInfo(qsTr("Creating connection profile with password and key-mgmt")); + + // Check if a connection with this SSID already exists + const existingConnection = root.savedConnections.find(conn => + conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() + ); + + if (existingConnection) { + // Connection already exists - delete it first, then create new one with updated password + root.addDebugInfo(qsTr("Connection '%1' already exists, deleting it first...").arg(existingConnection)); + deleteConnectionProc.exec(["nmcli", "connection", "delete", existingConnection]); + // Wait a moment for deletion to complete, then create new connection + Qt.callLater(() => { + createConnectionWithPassword(ssid, bssidUpper, password); + }, 300); + return; + } else { + // No existing connection, create new one + createConnectionWithPassword(ssid, bssidUpper, password); + return; + } } else { // Fallback to SSID if BSSID not available - use device wifi connect cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; @@ -164,6 +171,29 @@ Singleton { root.addDebugInfo(qsTr("No callback provided - not starting connection check timer")); } } + + function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string): void { + // Create connection profile with all required properties for BSSID + password + const cmd = ["nmcli", "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]; + + root.setConnectionStatus(qsTr("Connecting to %1 (BSSID: %2)...").arg(ssid).arg(bssidUpper)); + root.addDebugInfo(qsTr("Using BSSID: %1 for SSID: %2").arg(bssidUpper).arg(ssid)); + root.addDebugInfo(qsTr("Creating connection profile with password and key-mgmt")); + + // Set command and start process + connectProc.command = cmd; + + Qt.callLater(() => { + connectProc.running = true; + }); + } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { root.addDebugInfo(qsTr("=== connectToNetworkWithPasswordCheck ===")); @@ -224,23 +254,150 @@ Singleton { } property list savedConnections: [] + property list savedConnectionSsids: [] + property var wifiConnectionQueue: [] + property int currentSsidQueryIndex: 0 Process { id: listConnectionsProc - command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] + command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + onExited: { + if (exitCode === 0) { + parseConnectionList(stdout.text); + } + } + stdout: StdioCollector { + onStreamFinished: { + parseConnectionList(text); + } + } + } + + function parseConnectionList(output: string): void { + const lines = output.trim().split("\n").filter(line => line.length > 0); + const wifiConnections = []; + const connections = []; + + // First pass: identify WiFi 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; + + // Second pass: get SSIDs for WiFi connections + if (wifiConnections.length > 0) { + root.wifiConnectionQueue = wifiConnections; + root.currentSsidQueryIndex = 0; + root.savedConnectionSsids = []; + // Start querying SSIDs one by one + queryNextSsid(); + } else { + root.savedConnectionSsids = []; + root.wifiConnectionQueue = []; + } + } + + Process { + id: getSsidProc + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) onExited: { if (exitCode === 0) { - // Parse connection names from output - const connections = stdout.text.trim().split("\n").filter(name => name.length > 0); - root.savedConnections = connections; + processSsidOutput(stdout.text); + } else { + // Move to next connection even if this one failed + queryNextSsid(); } } stdout: StdioCollector { onStreamFinished: { - const connections = text.trim().split("\n").filter(name => name.length > 0); - root.savedConnections = connections; + processSsidOutput(text); + } + } + } + + function processSsidOutput(output: string): void { + // Parse "802-11-wireless.ssid:SSID_NAME" format + 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) { + // Add to list if not already present (case-insensitive) + const ssidLower = ssid.toLowerCase(); + if (!root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower)) { + // Create new array to trigger QML property change notification + const newList = root.savedConnectionSsids.slice(); + newList.push(ssid); + root.savedConnectionSsids = newList; + } + } + } + } + + // Query next connection + queryNextSsid(); + } + + function queryNextSsid(): void { + if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { + const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; + root.currentSsidQueryIndex++; + getSsidProc.command = ["nmcli", "-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName]; + getSsidProc.running = true; + } else { + // All SSIDs retrieved + root.wifiConnectionQueue = []; + root.currentSsidQueryIndex = 0; + } + } + + function hasSavedProfile(ssid: string): bool { + if (!ssid || ssid.length === 0) { + return false; + } + const ssidLower = ssid.toLowerCase().trim(); + + // If currently connected to this network, it definitely has a saved profile + if (root.active && root.active.ssid) { + const activeSsidLower = root.active.ssid.toLowerCase().trim(); + if (activeSsidLower === ssidLower) { + return true; } } + + // Check if SSID is in saved connections (case-insensitive comparison) + const hasSsid = root.savedConnectionSsids.some(savedSsid => + savedSsid && savedSsid.toLowerCase().trim() === ssidLower + ); + + if (hasSsid) { + return true; + } + + // Fallback: also check if connection name matches SSID (some connections use SSID as name) + const hasConnectionName = root.savedConnections.some(connName => + connName && connName.toLowerCase().trim() === ssidLower + ); + + return hasConnectionName; } function getWifiStatus(): void { @@ -445,22 +602,58 @@ Singleton { && connectProc.command[1] === "connection" && connectProc.command[2] === "add"; - if (wasConnectionAdd && exitCode === 0 && root.pendingConnection) { - // Connection profile was created successfully, now activate it + if (wasConnectionAdd && root.pendingConnection) { const ssid = root.pendingConnection.ssid; - root.addDebugInfo(qsTr("Connection profile created successfully, now activating: %1").arg(ssid)); - root.setConnectionStatus(qsTr("Activating connection...")); - // Update saved connections list since we just created one - listConnectionsProc.running = true; + // Check for duplicate connection warning in stderr text + const stderrText = connectProc.stderr ? connectProc.stderr.text : ""; + const hasDuplicateWarning = stderrText && ( + stderrText.includes("another connection with the name") || + stderrText.includes("Reference the connection by its uuid") + ); - // Activate the connection we just created - connectProc.command = ["nmcli", "connection", "up", ssid]; - Qt.callLater(() => { - connectProc.running = true; - }); - // Don't start timers yet - wait for activation to complete - return; + // Even with duplicate warning (or if connection already exists), we should try to activate it + // Also try if exit code is non-zero but small (might be a warning, not a real error) + if (exitCode === 0 || hasDuplicateWarning || (exitCode > 0 && exitCode < 10)) { + if (hasDuplicateWarning) { + root.addDebugInfo(qsTr("Connection with name '%1' already exists (warning), will try to activate it").arg(ssid)); + root.setConnectionStatus(qsTr("Activating existing connection...")); + } else { + root.addDebugInfo(qsTr("Connection profile created successfully, now activating: %1").arg(ssid)); + root.setConnectionStatus(qsTr("Activating connection...")); + } + + // Update saved connections list + listConnectionsProc.running = true; + + // Try to activate the connection by SSID (connection name) + connectProc.command = ["nmcli", "connection", "up", ssid]; + Qt.callLater(() => { + connectProc.running = true; + }); + // Don't start timers yet - wait for activation to complete + return; + } else { + // Connection add failed - try using device wifi connect as fallback + root.addDebugInfo(qsTr("Connection add failed (exit code %1), trying device wifi connect as fallback").arg(exitCode)); + // Extract password from the command if available + let password = ""; + if (connectProc.command) { + const pskIndex = connectProc.command.findIndex(arg => arg === "802-11-wireless-security.psk"); + if (pskIndex >= 0 && pskIndex + 1 < connectProc.command.length) { + password = connectProc.command[pskIndex + 1]; + } + } + + if (password && password.length > 0) { + root.addDebugInfo(qsTr("Using device wifi connect with password as fallback")); + connectProc.command = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; + Qt.callLater(() => { + connectProc.running = true; + }); + return; + } + } } // Refresh network list after connection attempt -- cgit v1.2.3-freya From 1da9c68be8f336a671f9514cf5feaaf5998da981 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 14:41:14 -0500 Subject: cleanup: trailing whitespace removeal (entire project) --- components/controls/CollapsibleSection.qml | 2 +- .../controlcenter/appearance/AppearancePane.qml | 50 +++---- modules/controlcenter/launcher/LauncherPane.qml | 10 +- modules/controlcenter/network/WirelessDetails.qml | 6 +- modules/controlcenter/network/WirelessList.qml | 2 +- .../network/WirelessPasswordDialog.qml | 10 +- modules/controlcenter/taskbar/TaskbarPane.qml | 8 +- modules/drawers/Interactions.qml | 22 +-- services/Network.qml | 154 ++++++++++----------- services/VPN.qml | 6 +- utils/Icons.qml | 4 +- 11 files changed, 137 insertions(+), 137 deletions(-) (limited to 'services/Network.qml') diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 945386c..cb6e62a 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -12,7 +12,7 @@ ColumnLayout { required property string title property string description: "" property bool expanded: false - + signal toggleRequested spacing: Appearance.spacing.small / 2 diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 68e2e2d..fc338f9 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -310,20 +310,20 @@ RowLayout { StateLayer { function onClicked(): void { const variant = modelData.variant; - + // Optimistic update - set immediately Schemes.currentVariant = variant; - + // Execute the command Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - + // Reload after a delay to confirm Qt.callLater(() => { reloadTimer.restart(); }); } } - + Timer { id: reloadTimer interval: 300 @@ -410,20 +410,20 @@ RowLayout { const name = modelData.name; const flavour = modelData.flavour; const schemeKey = `${name} ${flavour}`; - + // Optimistic update - set immediately Schemes.currentScheme = schemeKey; - + // Execute the command Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - + // Reload after a delay to confirm Qt.callLater(() => { reloadTimer.restart(); }); } } - + Timer { id: reloadTimer interval: 300 @@ -1053,7 +1053,7 @@ RowLayout { columns: Math.max(1, Math.floor(parent.width / 200)) rowSpacing: Appearance.spacing.normal columnSpacing: Appearance.spacing.normal - + // Center the grid content Layout.maximumWidth: { const cols = columns; @@ -1100,16 +1100,16 @@ RowLayout { path: modelData.path anchors.fill: parent - + // Ensure sourceSize is always set to valid dimensions sourceSize: Qt.size( Math.max(1, Math.floor(parent.width)), Math.max(1, Math.floor(parent.height)) ) - + // Show when ready, hide if fallback is showing opacity: status === Image.Ready && !fallbackImage.visible ? 1 : 0 - + Behavior on opacity { NumberAnimation { duration: 200 @@ -1129,11 +1129,11 @@ RowLayout { Math.max(1, Math.floor(parent.width)), Math.max(1, Math.floor(parent.height)) ) - + // Show if caching image hasn't loaded after a delay visible: opacity > 0 opacity: 0 - + Timer { id: fallbackTimer interval: 500 @@ -1144,7 +1144,7 @@ RowLayout { } } } - + // Also check status changes onStatusChanged: { if (status === Image.Ready && cachingImage.status !== Image.Ready) { @@ -1155,7 +1155,7 @@ RowLayout { }); } } - + Behavior on opacity { NumberAnimation { duration: 200 @@ -1182,26 +1182,26 @@ RowLayout { anchors.right: parent.right anchors.bottom: parent.bottom height: filenameText.implicitHeight + Appearance.padding.normal * 2 - + // Match the parent's rounded corners at the bottom radius: Appearance.rounding.normal - + gradient: Gradient { GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0) } GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) } GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.75) } GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.85) } } - + opacity: 0 - + Behavior on opacity { NumberAnimation { duration: 200 easing.type: Easing.OutCubic } } - + Component.onCompleted: { opacity = 1; } @@ -1228,20 +1228,20 @@ RowLayout { color: isCurrent ? Colours.palette.m3primary : "#FFFFFF" elide: Text.ElideMiddle maximumLineCount: 1 - + // Text shadow for better readability style: Text.Outline styleColor: Qt.rgba(0, 0, 0, 0.6) - + opacity: 0 - + Behavior on opacity { NumberAnimation { duration: 200 easing.type: Easing.OutCubic } } - + Component.onCompleted: { opacity = 1; } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 9b2570a..dd00877 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -53,7 +53,7 @@ RowLayout { try { const config = JSON.parse(configFile.text()); const appId = root.selectedApp.id || root.selectedApp.entry?.id; - + if (config.launcher && config.launcher.hiddenApps) { root.hideFromLauncherChecked = config.launcher.hiddenApps.includes(appId); } else { @@ -72,12 +72,12 @@ RowLayout { try { const config = JSON.parse(configFile.text()); const appId = root.selectedApp.id || root.selectedApp.entry?.id; - + if (!config.launcher) config.launcher = {}; if (!config.launcher.hiddenApps) config.launcher.hiddenApps = []; - + const hiddenApps = config.launcher.hiddenApps; - + if (isHidden) { // Add to hiddenApps if not already there if (!hiddenApps.includes(appId)) { @@ -90,7 +90,7 @@ RowLayout { hiddenApps.splice(index, 1); } } - + const jsonString = JSON.stringify(config, null, 4); configFile.setText(jsonString); } catch (e) { diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 7039720..d5abc9d 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -26,7 +26,7 @@ Item { updateDeviceDetails(); checkSavedProfile(); } - + function checkSavedProfile(): void { // Refresh saved connections list to ensure it's up to date // This ensures the "Forget Network" button visibility is accurate @@ -102,7 +102,7 @@ Item { color: Colours.palette.m3errorContainer onColor: Colours.palette.m3onErrorContainer text: qsTr("Forget Network") - + onClicked: { if (root.network && root.network.ssid) { // Disconnect first if connected @@ -184,7 +184,7 @@ Item { if (root.network.isSecure) { // Check if we have a saved connection profile for this network (by SSID) const hasSavedProfile = Network.hasSavedProfile(root.network.ssid); - + if (hasSavedProfile) { // Try connecting with saved password - don't show dialog if it fails // The saved password should work, but if connection fails for other reasons, diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index f861db4..ca6947a 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -230,7 +230,7 @@ ColumnLayout { if (network.isSecure) { // Check if we have a saved connection profile for this network (by SSID) const hasSavedProfile = Network.hasSavedProfile(network.ssid); - + if (hasSavedProfile) { // Try connecting with saved password - don't show dialog if it fails // The saved password should work, but if connection fails for other reasons, diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 2b33b43..8a71fa8 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -15,7 +15,7 @@ Item { id: root required property Session session - + readonly property var network: { // Prefer pendingNetwork, then active network if (session.network.pendingNetwork) { @@ -105,7 +105,7 @@ Item { StyledText { id: statusText - + Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.small visible: Network.connectionStatus.length > 0 || connectButton.connecting @@ -251,15 +251,15 @@ Item { // Check connection status message for success indicators const status = Network.connectionStatus; const statusLower = status.toLowerCase(); - + // Check for success indicators in status message - const hasSuccessIndicator = statusLower.includes("connection activated") || + const hasSuccessIndicator = statusLower.includes("connection activated") || statusLower.includes("successfully") || statusLower.includes("connected successfully") || (statusLower.includes("connected") && !statusLower.includes("error") && !statusLower.includes("failed")); // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Network.active && Network.active.ssid && + const isConnected = root.network && Network.active && Network.active.ssid && Network.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); if (isConnected || hasSuccessIndicator) { diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 2bb50d8..cf52fd3 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -120,13 +120,13 @@ RowLayout { if (!configFile.loaded) { return; } - + try { const config = JSON.parse(configFile.text()); - + // Ensure bar object exists if (!config.bar) config.bar = {}; - + // Update clock setting if (!config.bar.clock) config.bar.clock = {}; config.bar.clock.showIcon = clockShowIconSwitch.checked; @@ -163,7 +163,7 @@ RowLayout { // Update entries from the model (same approach as clock - use provided value if available) if (!config.bar.entries) config.bar.entries = []; config.bar.entries = []; - + for (let i = 0; i < entriesModel.count; i++) { const entry = entriesModel.get(i); // If this is the entry being updated, use the provided value (same as clock toggle reads from switch) diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 10190a4..2d0c115 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -204,18 +204,18 @@ CustomMouseArea { const panelWidth = panels.notifications.width || panels.notifications.implicitWidth || Config.notifs.sizes.width; const panelX = bar.implicitWidth + panels.notifications.x; const isPanelCollapsed = panelHeight < 10; // Consider collapsed if height is very small - + let showNotifications = inTopPanel(panels.notifications, x, y); - + // Only use fallback corner detection when panel is collapsed if (!showNotifications && isPanelCollapsed) { // Use panel's actual width and position for fallback, with some padding const cornerPadding = Config.border.rounding || 20; - showNotifications = x >= panelX - cornerPadding && - x <= panelX + panelWidth + cornerPadding && + showNotifications = x >= panelX - cornerPadding && + x <= panelX + panelWidth + cornerPadding && y < Config.border.thickness + cornerPadding; } - + // Check if mouse is over the clear all button area // Button is positioned to the left of the notification panel if (!showNotifications && panels.notifications.height > 0 && panels.clearAllButton && panels.clearAllButton.visible) { @@ -223,17 +223,17 @@ CustomMouseArea { const buttonY = Config.border.thickness + panels.clearAllButton.y; const buttonWidth = panels.clearAllButton.width; const buttonHeight = panels.clearAllButton.height; - - const inButtonArea = x >= buttonX && - x <= buttonX + buttonWidth && - y >= buttonY && + + const inButtonArea = x >= buttonX && + x <= buttonX + buttonWidth && + y >= buttonY && y <= buttonY + buttonHeight; - + if (inButtonArea) { showNotifications = true; } } - + // Show or hide notification panel based on hover if (panels.notifications.content) { if (showNotifications) { diff --git a/services/Network.qml b/services/Network.qml index 0b936b8..7732a1c 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -30,17 +30,17 @@ Singleton { property var wirelessDeviceDetails: null property string connectionStatus: "" property string connectionDebug: "" - + function clearConnectionStatus(): void { connectionStatus = ""; // Don't clear debug - keep it for reference // connectionDebug = ""; } - + function setConnectionStatus(status: string): void { connectionStatus = status; } - + function addDebugInfo(info: string): void { const timestamp = new Date().toLocaleTimeString(); const newInfo = "[" + timestamp + "] " + info; @@ -79,23 +79,23 @@ Singleton { // When no password, use SSID (will use saved password if available) const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; let cmd = []; - + // Set up pending connection tracking if callback provided if (callback) { root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; } - + if (password && password.length > 0) { // When password is provided, try BSSID first if available, otherwise use SSID if (hasBssid) { // Use BSSID when password is provided - ensure BSSID is uppercase const bssidUpper = bssid.toUpperCase(); - + // Check if a connection with this SSID already exists - const existingConnection = root.savedConnections.find(conn => + const existingConnection = root.savedConnections.find(conn => conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() ); - + if (existingConnection) { // Connection already exists - delete it first, then create new one with updated password root.addDebugInfo(qsTr("Connection '%1' already exists, deleting it first...").arg(existingConnection)); @@ -122,7 +122,7 @@ Singleton { root.setConnectionStatus(qsTr("Connecting to %1 (using saved password)...").arg(ssid)); root.addDebugInfo(qsTr("Using saved password for: %1").arg(ssid)); } - + // Show the exact command being executed const cmdStr = cmd.join(" "); root.addDebugInfo(qsTr("=== COMMAND TO EXECUTE ===")); @@ -130,17 +130,17 @@ Singleton { root.addDebugInfo(qsTr("Command array: [%1]").arg(cmd.map((arg, i) => `"${arg}"`).join(", "))); root.addDebugInfo(qsTr("Command array length: %1").arg(cmd.length)); root.addDebugInfo(qsTr("===========================")); - + // Set command and start process root.addDebugInfo(qsTr("Setting command property...")); connectProc.command = cmd; const setCmdStr = connectProc.command ? connectProc.command.join(" ") : "null"; root.addDebugInfo(qsTr("Command property set, value: %1").arg(setCmdStr)); root.addDebugInfo(qsTr("Command property verified: %1").arg(setCmdStr === cmdStr ? "Match" : "MISMATCH")); - + // If we're creating a connection profile, we need to activate it after creation const isConnectionAdd = cmd.length > 0 && cmd[0] === "nmcli" && cmd[1] === "connection" && cmd[2] === "add"; - + // Wait a moment before starting to ensure command is set Qt.callLater(() => { root.addDebugInfo(qsTr("=== STARTING PROCESS ===")); @@ -150,7 +150,7 @@ Singleton { connectProc.running = true; root.addDebugInfo(qsTr("Process running set to: %1").arg(connectProc.running)); root.addDebugInfo(qsTr("========================")); - + // Check if process actually started after a short delay Qt.callLater(() => { root.addDebugInfo(qsTr("Process status check (100ms later):")); @@ -162,7 +162,7 @@ Singleton { } }, 100); }); - + // Start connection check timer if we have a callback if (callback) { root.addDebugInfo(qsTr("Starting connection check timer (4 second interval)")); @@ -171,25 +171,25 @@ Singleton { root.addDebugInfo(qsTr("No callback provided - not starting connection check timer")); } } - + function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string): void { // Create connection profile with all required properties for BSSID + password - const cmd = ["nmcli", "connection", "add", - "type", "wifi", + const cmd = ["nmcli", "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]; - + root.setConnectionStatus(qsTr("Connecting to %1 (BSSID: %2)...").arg(ssid).arg(bssidUpper)); root.addDebugInfo(qsTr("Using BSSID: %1 for SSID: %2").arg(bssidUpper).arg(ssid)); root.addDebugInfo(qsTr("Creating connection profile with password and key-mgmt")); - + // Set command and start process connectProc.command = cmd; - + Qt.callLater(() => { connectProc.running = true; }); @@ -198,7 +198,7 @@ Singleton { function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { root.addDebugInfo(qsTr("=== connectToNetworkWithPasswordCheck ===")); root.addDebugInfo(qsTr("SSID: %1, isSecure: %2").arg(ssid).arg(isSecure)); - + // For secure networks, try connecting without password first // If connection succeeds (saved password exists), we're done // If it fails with password error, callback will be called to show password dialog @@ -228,7 +228,7 @@ Singleton { disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); } } - + function forgetNetwork(ssid: string): void { // Delete the connection profile for this network // This will remove the saved password and connection settings @@ -240,7 +240,7 @@ Singleton { }, 500); } } - + function hasConnectionProfile(ssid: string): bool { // Check if a connection profile exists for this SSID // This is synchronous check - returns true if connection exists @@ -252,12 +252,12 @@ Singleton { // The actual check will be done asynchronously return false; } - + property list savedConnections: [] property list savedConnectionSsids: [] property var wifiConnectionQueue: [] property int currentSsidQueryIndex: 0 - + Process { id: listConnectionsProc command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] @@ -276,12 +276,12 @@ Singleton { } } } - + function parseConnectionList(output: string): void { const lines = output.trim().split("\n").filter(line => line.length > 0); const wifiConnections = []; const connections = []; - + // First pass: identify WiFi connections for (const line of lines) { const parts = line.split(":"); @@ -289,15 +289,15 @@ Singleton { const name = parts[0]; const type = parts[1]; connections.push(name); - + if (type === "802-11-wireless") { wifiConnections.push(name); } } } - + root.savedConnections = connections; - + // Second pass: get SSIDs for WiFi connections if (wifiConnections.length > 0) { root.wifiConnectionQueue = wifiConnections; @@ -310,10 +310,10 @@ Singleton { root.wifiConnectionQueue = []; } } - + Process { id: getSsidProc - + environment: ({ LANG: "C.UTF-8", LC_ALL: "C.UTF-8" @@ -332,7 +332,7 @@ Singleton { } } } - + function processSsidOutput(output: string): void { // Parse "802-11-wireless.ssid:SSID_NAME" format const lines = output.trim().split("\n"); @@ -351,11 +351,11 @@ Singleton { } } } - + // Query next connection queryNextSsid(); } - + function queryNextSsid(): void { if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; @@ -368,13 +368,13 @@ Singleton { root.currentSsidQueryIndex = 0; } } - + function hasSavedProfile(ssid: string): bool { if (!ssid || ssid.length === 0) { return false; } const ssidLower = ssid.toLowerCase().trim(); - + // If currently connected to this network, it definitely has a saved profile if (root.active && root.active.ssid) { const activeSsidLower = root.active.ssid.toLowerCase().trim(); @@ -382,21 +382,21 @@ Singleton { return true; } } - + // Check if SSID is in saved connections (case-insensitive comparison) - const hasSsid = root.savedConnectionSsids.some(savedSsid => + const hasSsid = root.savedConnectionSsids.some(savedSsid => savedSsid && savedSsid.toLowerCase().trim() === ssidLower ); - + if (hasSsid) { return true; } - + // Fallback: also check if connection name matches SSID (some connections use SSID as name) - const hasConnectionName = root.savedConnections.some(connName => + const hasConnectionName = root.savedConnections.some(connName => connName && connName.toLowerCase().trim() === ssidLower ); - + return hasConnectionName; } @@ -442,7 +442,7 @@ Singleton { if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { return ""; } - + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; const octets = [ (mask >>> 24) & 0xff, @@ -450,7 +450,7 @@ Singleton { (mask >>> 8) & 0xff, mask & 0xff ]; - + return octets.join("."); } @@ -510,7 +510,7 @@ Singleton { root.addDebugInfo(qsTr(" Pending SSID: %1").arg(root.pendingConnection.ssid)); root.addDebugInfo(qsTr(" Active SSID: %1").arg(root.active ? root.active.ssid : "None")); root.addDebugInfo(qsTr(" Connected: %1").arg(connected)); - + if (!connected && root.pendingConnection.callback) { // Connection didn't succeed after multiple checks, show password dialog root.addDebugInfo(qsTr("Connection failed - calling password dialog callback")); @@ -543,19 +543,19 @@ Singleton { repeat: true triggeredOnStart: false property int checkCount: 0 - + onRunningChanged: { if (running) { root.addDebugInfo(qsTr("Immediate check timer started (checks every 500ms)")); } } - + onTriggered: { if (root.pendingConnection) { checkCount++; const connected = root.active && root.active.ssid === root.pendingConnection.ssid; root.addDebugInfo(qsTr("Immediate check #%1: Connected=%2").arg(checkCount).arg(connected)); - + if (connected) { // Connection succeeded, stop timers and clear pending root.addDebugInfo(qsTr("Connection succeeded on check #%1!").arg(checkCount)); @@ -586,32 +586,32 @@ Singleton { onRunningChanged: { root.addDebugInfo(qsTr("Process running changed to: %1").arg(running)); } - + onStarted: { root.addDebugInfo(qsTr("Process started successfully")); } - + onExited: { root.addDebugInfo(qsTr("=== PROCESS EXITED ===")); root.addDebugInfo(qsTr("Exit code: %1").arg(exitCode)); root.addDebugInfo(qsTr("(Exit code 0 = success, non-zero = error)")); - + // Check if this was a "connection add" command - if so, we need to activate it - const wasConnectionAdd = connectProc.command && connectProc.command.length > 0 - && connectProc.command[0] === "nmcli" - && connectProc.command[1] === "connection" + const wasConnectionAdd = connectProc.command && connectProc.command.length > 0 + && connectProc.command[0] === "nmcli" + && connectProc.command[1] === "connection" && connectProc.command[2] === "add"; - + if (wasConnectionAdd && root.pendingConnection) { const ssid = root.pendingConnection.ssid; - + // Check for duplicate connection warning in stderr text const stderrText = connectProc.stderr ? connectProc.stderr.text : ""; const hasDuplicateWarning = stderrText && ( stderrText.includes("another connection with the name") || stderrText.includes("Reference the connection by its uuid") ); - + // Even with duplicate warning (or if connection already exists), we should try to activate it // Also try if exit code is non-zero but small (might be a warning, not a real error) if (exitCode === 0 || hasDuplicateWarning || (exitCode > 0 && exitCode < 10)) { @@ -622,10 +622,10 @@ Singleton { root.addDebugInfo(qsTr("Connection profile created successfully, now activating: %1").arg(ssid)); root.setConnectionStatus(qsTr("Activating connection...")); } - + // Update saved connections list listConnectionsProc.running = true; - + // Try to activate the connection by SSID (connection name) connectProc.command = ["nmcli", "connection", "up", ssid]; Qt.callLater(() => { @@ -644,7 +644,7 @@ Singleton { password = connectProc.command[pskIndex + 1]; } } - + if (password && password.length > 0) { root.addDebugInfo(qsTr("Using device wifi connect with password as fallback")); connectProc.command = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; @@ -655,10 +655,10 @@ Singleton { } } } - + // Refresh network list after connection attempt getNetworks.running = true; - + // Check if connection succeeded after a short delay (network list needs to update) if (root.pendingConnection) { if (exitCode === 0) { @@ -704,10 +704,10 @@ Singleton { root.addDebugInfo(qsTr("STDERR: %1").arg(line)); } } - + // Check for specific errors that indicate password is needed // Be careful not to match success messages - const needsPassword = (error.includes("Secrets were required") || + const needsPassword = (error.includes("Secrets were required") || error.includes("No secrets provided") || error.includes("802-11-wireless-security.psk") || (error.includes("password") && !error.includes("Connection activated")) || @@ -715,7 +715,7 @@ Singleton { (error.includes("802.11") && !error.includes("Connection activated"))) && !error.includes("Connection activated") && !error.includes("successfully"); - + if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { // Connection failed because password is needed - show dialog immediately connectionCheckTimer.stop(); @@ -784,7 +784,7 @@ Singleton { Process { id: deleteConnectionProc - + // Delete connection profile - refresh network list and saved connections after deletion onExited: { // Refresh network list and saved connections after deletion @@ -924,12 +924,12 @@ Singleton { onStreamFinished: { const output = text.trim(); root.ethernetDebugInfo = "Output received in onStreamFinished! Length: " + output.length + ", First 100 chars: " + output.substring(0, 100); - + if (!output || output.length === 0) { root.ethernetDebugInfo = "No output received (empty)"; return; } - + root.processEthernetOutput(output); } } @@ -942,7 +942,7 @@ Singleton { const lines = output.split("\n"); root.ethernetDebugInfo = "Processing " + lines.length + " lines"; - + const allDevices = lines.map(d => { const dev = d.replace(rep, PLACEHOLDER).split(":"); return { @@ -952,9 +952,9 @@ Singleton { connection: dev[3]?.replace(rep2, ":") ?? "" }; }); - + root.ethernetDebugInfo = "All devices: " + allDevices.length + ", Types: " + allDevices.map(d => d.type).join(", "); - + const ethernetOnly = allDevices.filter(d => d.type === "ethernet"); root.ethernetDebugInfo = "Ethernet devices found: " + ethernetOnly.length; @@ -975,7 +975,7 @@ Singleton { speed: "" }; }); - + root.ethernetDebugInfo = "Ethernet devices processed: " + ethernetDevices.length + ", First device: " + (ethernetDevices[0]?.interface || "none"); // Update the list - replace the entire array to ensure QML detects the change @@ -984,13 +984,13 @@ Singleton { for (let i = 0; i < ethernetDevices.length; i++) { newDevices.push(ethernetDevices[i]); } - + // Replace the entire list root.ethernetDevices = newDevices; - + // Force QML to detect the change by updating a property root.ethernetDeviceCount = ethernetDevices.length; - + // Force QML to re-evaluate the list by accessing it Qt.callLater(() => { const count = root.ethernetDevices.length; @@ -1130,7 +1130,7 @@ const line = lines[i]; // Find the connected wifi interface from device status const lines = output.split("\n"); let wifiInterface = ""; - + for (let i = 0; i < lines.length; i++) { const line = lines[i]; const parts = line.split(/\s+/); diff --git a/services/VPN.qml b/services/VPN.qml index 10e5e7e..412bda4 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -21,7 +21,7 @@ Singleton { const name = providerName; const iface = interfaceName; const defaults = getBuiltinDefaults(name, iface); - + if (isCustomProvider) { const custom = providerInput; return { @@ -31,7 +31,7 @@ Singleton { displayName: custom.displayName || defaults.displayName }; } - + return defaults; } @@ -62,7 +62,7 @@ Singleton { displayName: "Tailscale" } }; - + return builtins[name] || { connectCmd: [name, "up"], disconnectCmd: [name, "down"], diff --git a/utils/Icons.qml b/utils/Icons.qml index e946c4f..389eca3 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -194,13 +194,13 @@ Singleton { function getSpecialWsIcon(name: string): string { name = name.toLowerCase().slice("special:".length); - + for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons) { if (iconConfig.name === name) { return iconConfig.icon; } } - + if (name === "special") return "star"; if (name === "communication") -- cgit v1.2.3-freya From 38c613a75e60f3b8a393356712117096c7e111c5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 15:08:01 -0500 Subject: controlcenter: wireless debug removal (preparing for rewrite) --- services/Network.qml | 209 +-------------------------------------------------- 1 file changed, 1 insertion(+), 208 deletions(-) (limited to 'services/Network.qml') diff --git a/services/Network.qml b/services/Network.qml index 7732a1c..2b79f12 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -24,38 +24,9 @@ Singleton { property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null property int ethernetDeviceCount: 0 - property string ethernetDebugInfo: "" property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null property var wirelessDeviceDetails: null - property string connectionStatus: "" - property string connectionDebug: "" - - function clearConnectionStatus(): void { - connectionStatus = ""; - // Don't clear debug - keep it for reference - // connectionDebug = ""; - } - - function setConnectionStatus(status: string): void { - connectionStatus = status; - } - - function addDebugInfo(info: string): void { - const timestamp = new Date().toLocaleTimeString(); - const newInfo = "[" + timestamp + "] " + info; - // CRITICAL: Always append - NEVER replace - // Get current value - NEVER allow it to be empty/cleared - let current = connectionDebug; - if (!current || current === undefined || current === null) { - current = ""; - } - // ALWAYS append - never replace - // If current is empty, just use newInfo, otherwise append with newline - const updated = (current.length > 0) ? (current + "\n" + newInfo) : newInfo; - // CRITICAL: Only assign if we're appending, never replace - connectionDebug = updated; - } function enableWifi(enabled: bool): void { const cmd = enabled ? "on" : "off"; @@ -98,7 +69,6 @@ Singleton { if (existingConnection) { // Connection already exists - delete it first, then create new one with updated password - root.addDebugInfo(qsTr("Connection '%1' already exists, deleting it first...").arg(existingConnection)); deleteConnectionProc.exec(["nmcli", "connection", "delete", existingConnection]); // Wait a moment for deletion to complete, then create new connection Qt.callLater(() => { @@ -113,62 +83,26 @@ Singleton { } else { // Fallback to SSID if BSSID not available - use device wifi connect cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; - root.setConnectionStatus(qsTr("Connecting to %1...").arg(ssid)); - root.addDebugInfo(qsTr("Using SSID only (no BSSID): %1").arg(ssid)); } } else { // Try to connect to existing connection first (will use saved password if available) cmd = ["nmcli", "device", "wifi", "connect", ssid]; - root.setConnectionStatus(qsTr("Connecting to %1 (using saved password)...").arg(ssid)); - root.addDebugInfo(qsTr("Using saved password for: %1").arg(ssid)); } - // Show the exact command being executed - const cmdStr = cmd.join(" "); - root.addDebugInfo(qsTr("=== COMMAND TO EXECUTE ===")); - root.addDebugInfo(qsTr("Command: %1").arg(cmdStr)); - root.addDebugInfo(qsTr("Command array: [%1]").arg(cmd.map((arg, i) => `"${arg}"`).join(", "))); - root.addDebugInfo(qsTr("Command array length: %1").arg(cmd.length)); - root.addDebugInfo(qsTr("===========================")); - // Set command and start process - root.addDebugInfo(qsTr("Setting command property...")); connectProc.command = cmd; - const setCmdStr = connectProc.command ? connectProc.command.join(" ") : "null"; - root.addDebugInfo(qsTr("Command property set, value: %1").arg(setCmdStr)); - root.addDebugInfo(qsTr("Command property verified: %1").arg(setCmdStr === cmdStr ? "Match" : "MISMATCH")); // If we're creating a connection profile, we need to activate it after creation const isConnectionAdd = cmd.length > 0 && cmd[0] === "nmcli" && cmd[1] === "connection" && cmd[2] === "add"; // Wait a moment before starting to ensure command is set Qt.callLater(() => { - root.addDebugInfo(qsTr("=== STARTING PROCESS ===")); - root.addDebugInfo(qsTr("Current running state: %1").arg(connectProc.running)); - root.addDebugInfo(qsTr("Command to run: %1").arg(connectProc.command ? connectProc.command.join(" ") : "NOT SET")); - root.addDebugInfo(qsTr("Is connection add command: %1").arg(isConnectionAdd)); connectProc.running = true; - root.addDebugInfo(qsTr("Process running set to: %1").arg(connectProc.running)); - root.addDebugInfo(qsTr("========================")); - - // Check if process actually started after a short delay - Qt.callLater(() => { - root.addDebugInfo(qsTr("Process status check (100ms later):")); - root.addDebugInfo(qsTr(" Running: %1").arg(connectProc.running)); - root.addDebugInfo(qsTr(" Command: %1").arg(connectProc.command ? connectProc.command.join(" ") : "null")); - if (!connectProc.running) { - root.addDebugInfo(qsTr("WARNING: Process did not start!")); - root.setConnectionStatus(qsTr("Error: Process failed to start")); - } - }, 100); }); // Start connection check timer if we have a callback if (callback) { - root.addDebugInfo(qsTr("Starting connection check timer (4 second interval)")); connectionCheckTimer.start(); - } else { - root.addDebugInfo(qsTr("No callback provided - not starting connection check timer")); } } @@ -183,10 +117,6 @@ Singleton { "802-11-wireless-security.key-mgmt", "wpa-psk", "802-11-wireless-security.psk", password]; - root.setConnectionStatus(qsTr("Connecting to %1 (BSSID: %2)...").arg(ssid).arg(bssidUpper)); - root.addDebugInfo(qsTr("Using BSSID: %1 for SSID: %2").arg(bssidUpper).arg(ssid)); - root.addDebugInfo(qsTr("Creating connection profile with password and key-mgmt")); - // Set command and start process connectProc.command = cmd; @@ -196,26 +126,19 @@ Singleton { } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { - root.addDebugInfo(qsTr("=== connectToNetworkWithPasswordCheck ===")); - root.addDebugInfo(qsTr("SSID: %1, isSecure: %2").arg(ssid).arg(isSecure)); - // For secure networks, try connecting without password first // If connection succeeds (saved password exists), we're done // If it fails with password error, callback will be called to show password dialog if (isSecure) { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; - root.addDebugInfo(qsTr("Trying to connect without password (will use saved if available)")); // Try connecting without password - will use saved password if available connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); // Start timer to check if connection succeeded - root.addDebugInfo(qsTr("Starting connection check timer")); connectionCheckTimer.start(); } else { - root.addDebugInfo(qsTr("Network is not secure, connecting directly")); connectToNetwork(ssid, "", bssid, null); } - root.addDebugInfo(qsTr("=========================================")); } function disconnectFromNetwork(): void { @@ -503,17 +426,11 @@ Singleton { id: connectionCheckTimer interval: 4000 onTriggered: { - root.addDebugInfo(qsTr("=== CONNECTION CHECK TIMER (4s) ===")); if (root.pendingConnection) { const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - root.addDebugInfo(qsTr("Checking connection status...")); - root.addDebugInfo(qsTr(" Pending SSID: %1").arg(root.pendingConnection.ssid)); - root.addDebugInfo(qsTr(" Active SSID: %1").arg(root.active ? root.active.ssid : "None")); - root.addDebugInfo(qsTr(" Connected: %1").arg(connected)); if (!connected && root.pendingConnection.callback) { // Connection didn't succeed after multiple checks, show password dialog - root.addDebugInfo(qsTr("Connection failed - calling password dialog callback")); const pending = root.pendingConnection; root.pendingConnection = null; immediateCheckTimer.stop(); @@ -521,19 +438,11 @@ Singleton { pending.callback(); } else if (connected) { // Connection succeeded, clear pending - root.addDebugInfo(qsTr("Connection succeeded!")); - root.setConnectionStatus(qsTr("Connected successfully!")); root.pendingConnection = null; immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; - } else { - root.addDebugInfo(qsTr("Still connecting...")); - root.setConnectionStatus(qsTr("Still connecting...")); } - } else { - root.addDebugInfo(qsTr("No pending connection")); } - root.addDebugInfo(qsTr("================================")); } } @@ -544,36 +453,24 @@ Singleton { triggeredOnStart: false property int checkCount: 0 - onRunningChanged: { - if (running) { - root.addDebugInfo(qsTr("Immediate check timer started (checks every 500ms)")); - } - } - onTriggered: { if (root.pendingConnection) { checkCount++; const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - root.addDebugInfo(qsTr("Immediate check #%1: Connected=%2").arg(checkCount).arg(connected)); if (connected) { // Connection succeeded, stop timers and clear pending - root.addDebugInfo(qsTr("Connection succeeded on check #%1!").arg(checkCount)); - root.setConnectionStatus(qsTr("Connected successfully!")); connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; root.pendingConnection = null; } else if (checkCount >= 6) { - root.addDebugInfo(qsTr("Checked %1 times (3 seconds) - connection taking longer").arg(checkCount)); - root.setConnectionStatus(qsTr("Connection taking longer than expected...")); // Checked 6 times (3 seconds total), connection likely failed // Stop immediate check, let the main timer handle it immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } } else { - root.addDebugInfo(qsTr("Immediate check: No pending connection, stopping timer")); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } @@ -583,18 +480,7 @@ Singleton { Process { id: connectProc - onRunningChanged: { - root.addDebugInfo(qsTr("Process running changed to: %1").arg(running)); - } - - onStarted: { - root.addDebugInfo(qsTr("Process started successfully")); - } - onExited: { - root.addDebugInfo(qsTr("=== PROCESS EXITED ===")); - root.addDebugInfo(qsTr("Exit code: %1").arg(exitCode)); - root.addDebugInfo(qsTr("(Exit code 0 = success, non-zero = error)")); // Check if this was a "connection add" command - if so, we need to activate it const wasConnectionAdd = connectProc.command && connectProc.command.length > 0 @@ -615,13 +501,6 @@ Singleton { // Even with duplicate warning (or if connection already exists), we should try to activate it // Also try if exit code is non-zero but small (might be a warning, not a real error) if (exitCode === 0 || hasDuplicateWarning || (exitCode > 0 && exitCode < 10)) { - if (hasDuplicateWarning) { - root.addDebugInfo(qsTr("Connection with name '%1' already exists (warning), will try to activate it").arg(ssid)); - root.setConnectionStatus(qsTr("Activating existing connection...")); - } else { - root.addDebugInfo(qsTr("Connection profile created successfully, now activating: %1").arg(ssid)); - root.setConnectionStatus(qsTr("Activating connection...")); - } // Update saved connections list listConnectionsProc.running = true; @@ -635,7 +514,6 @@ Singleton { return; } else { // Connection add failed - try using device wifi connect as fallback - root.addDebugInfo(qsTr("Connection add failed (exit code %1), trying device wifi connect as fallback").arg(exitCode)); // Extract password from the command if available let password = ""; if (connectProc.command) { @@ -646,7 +524,6 @@ Singleton { } if (password && password.length > 0) { - root.addDebugInfo(qsTr("Using device wifi connect with password as fallback")); connectProc.command = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; Qt.callLater(() => { connectProc.running = true; @@ -661,50 +538,18 @@ Singleton { // Check if connection succeeded after a short delay (network list needs to update) if (root.pendingConnection) { - if (exitCode === 0) { - // Process succeeded, start checking connection status - root.setConnectionStatus(qsTr("Connection command succeeded, verifying...")); - root.addDebugInfo(qsTr("Command succeeded, checking connection status...")); - root.addDebugInfo(qsTr("Starting immediate check timer (500ms intervals)")); - immediateCheckTimer.start(); - } else { - // Process failed, but wait a moment to see if connection still works - root.setConnectionStatus(qsTr("Connection command exited with code %1, checking status...").arg(exitCode)); - root.addDebugInfo(qsTr("Command exited with error code %1").arg(exitCode)); - root.addDebugInfo(qsTr("This usually means the command failed")); - root.addDebugInfo(qsTr("Checking connection status anyway...")); - root.addDebugInfo(qsTr("Starting immediate check timer (500ms intervals)")); - immediateCheckTimer.start(); - } - } else { - root.addDebugInfo(qsTr("No pending connection - not starting immediate check timer")); + immediateCheckTimer.start(); } - root.addDebugInfo(qsTr("======================")); } stdout: SplitParser { onRead: { getNetworks.running = true; - // Also log output for debugging - if (text && text.trim().length > 0) { - root.addDebugInfo(qsTr("STDOUT: %1").arg(text.trim())); - root.setConnectionStatus(qsTr("Status: %1").arg(text.trim())); - } } } stderr: StdioCollector { onStreamFinished: { const error = text.trim(); - root.addDebugInfo(qsTr("=== STDERR OUTPUT ===")); if (error && error.length > 0) { - // Split error into lines and add each one - const errorLines = error.split("\n"); - for (let i = 0; i < errorLines.length; i++) { - const line = errorLines[i].trim(); - if (line.length > 0) { - root.addDebugInfo(qsTr("STDERR: %1").arg(line)); - } - } - // Check for specific errors that indicate password is needed // Be careful not to match success messages const needsPassword = (error.includes("Secrets were required") || @@ -724,18 +569,10 @@ Singleton { root.pendingConnection = null; pending.callback(); } else if (error && error.length > 0 && !error.includes("Connection activated") && !error.includes("successfully")) { - // Log all errors (except success messages) - root.setConnectionStatus(qsTr("Error: %1").arg(errorLines[0] || error)); // Emit signal for UI to handle root.connectionFailed(root.pendingConnection ? root.pendingConnection.ssid : ""); - } else if (error && (error.includes("Connection activated") || error.includes("successfully"))) { - root.addDebugInfo(qsTr("Connection successful!")); - root.setConnectionStatus(qsTr("Connection successful!")); } - } else { - root.addDebugInfo(qsTr("STDERR: (empty)")); } - root.addDebugInfo(qsTr("====================")); } } } @@ -752,10 +589,6 @@ Singleton { } stderr: StdioCollector { onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { - console.warn("Network device disconnect error:", error); - } } } } @@ -774,7 +607,6 @@ Singleton { onStreamFinished: { const error = text.trim(); if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { - console.warn("Network connection disconnect error:", error); // If connection down failed, try device disconnect as fallback disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); } @@ -793,11 +625,6 @@ Singleton { } stderr: StdioCollector { onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0) { - // Log error but don't fail - connection might not exist - console.warn("Network connection delete error:", error); - } } } } @@ -896,26 +723,14 @@ Singleton { }) onRunningChanged: { root.ethernetProcessRunning = running; - if (!running) { - // Process finished, update debug info - Qt.callLater(() => { - if (root.ethernetDebugInfo === "" || root.ethernetDebugInfo.includes("Process exited")) { - root.ethernetDebugInfo = "Process finished, waiting for output..."; - } - }); - } } onExited: { Qt.callLater(() => { const outputLength = ethernetStdout.text ? ethernetStdout.text.length : 0; - root.ethernetDebugInfo = "Process exited with code: " + exitCode + ", output length: " + outputLength; if (outputLength > 0) { // Output was captured, process it const output = ethernetStdout.text.trim(); - root.ethernetDebugInfo = "Processing output from onExited, length: " + output.length + "\nOutput: " + output.substring(0, 200); root.processEthernetOutput(output); - } else { - root.ethernetDebugInfo = "No output captured in onExited"; } }); } @@ -923,10 +738,8 @@ Singleton { id: ethernetStdout onStreamFinished: { const output = text.trim(); - root.ethernetDebugInfo = "Output received in onStreamFinished! Length: " + output.length + ", First 100 chars: " + output.substring(0, 100); if (!output || output.length === 0) { - root.ethernetDebugInfo = "No output received (empty)"; return; } @@ -941,7 +754,6 @@ Singleton { const rep2 = new RegExp(PLACEHOLDER, "g"); const lines = output.split("\n"); - root.ethernetDebugInfo = "Processing " + lines.length + " lines"; const allDevices = lines.map(d => { const dev = d.replace(rep, PLACEHOLDER).split(":"); @@ -953,10 +765,7 @@ Singleton { }; }); - root.ethernetDebugInfo = "All devices: " + allDevices.length + ", Types: " + allDevices.map(d => d.type).join(", "); - const ethernetOnly = allDevices.filter(d => d.type === "ethernet"); - root.ethernetDebugInfo = "Ethernet devices found: " + ethernetOnly.length; const ethernetDevices = ethernetOnly.map(d => { const state = d.state || ""; @@ -976,8 +785,6 @@ Singleton { }; }); - root.ethernetDebugInfo = "Ethernet devices processed: " + ethernetDevices.length + ", First device: " + (ethernetDevices[0]?.interface || "none"); - // Update the list - replace the entire array to ensure QML detects the change // Create a new array and assign it to the property const newDevices = []; @@ -990,12 +797,6 @@ Singleton { // Force QML to detect the change by updating a property root.ethernetDeviceCount = ethernetDevices.length; - - // Force QML to re-evaluate the list by accessing it - Qt.callLater(() => { - const count = root.ethernetDevices.length; - root.ethernetDebugInfo = "Final: Found " + ethernetDevices.length + " devices, List length: " + count + ", Parsed all: " + allDevices.length + ", Output length: " + output.length; - }); } @@ -1017,10 +818,6 @@ Singleton { } stderr: StdioCollector { onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0 && !error.includes("successfully") && !error.includes("Connection activated")) { - console.warn("Ethernet connection error:", error); - } } } } @@ -1040,10 +837,6 @@ Singleton { } stderr: StdioCollector { onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { - console.warn("Ethernet disconnection error:", error); - } } } } -- cgit v1.2.3-freya From ba5dfbd4c48352856865687fa90aba1b1cdd6fb7 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 09:00:05 -0500 Subject: network: removed all execs from Network.qml, now relies only on Nmcli.qml --- services/Network.qml | 1086 +++++++++----------------------------------------- 1 file changed, 186 insertions(+), 900 deletions(-) (limited to 'services/Network.qml') diff --git a/services/Network.qml b/services/Network.qml index 2b79f12..fc16915 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -3,6 +3,7 @@ pragma Singleton import Quickshell import Quickshell.Io import QtQuick +import qs.services Singleton { id: root @@ -13,13 +14,24 @@ Singleton { getEthernetDevices(); }); // Load saved connections on startup - listConnectionsProc.running = true; + Nmcli.loadSavedConnections(() => { + root.savedConnections = Nmcli.savedConnections; + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + }); + // Get initial WiFi status + Nmcli.getWifiStatus((enabled) => { + root.wifiEnabled = enabled; + }); + // Sync networks from Nmcli on startup + Qt.callLater(() => { + syncNetworksFromNmcli(); + }, 100); } readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property bool wifiEnabled: true - readonly property bool scanning: rescanProc.running + readonly property bool scanning: Nmcli.scanning property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null @@ -29,334 +41,245 @@ Singleton { property var wirelessDeviceDetails: null function enableWifi(enabled: bool): void { - const cmd = enabled ? "on" : "off"; - enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]); + Nmcli.enableWifi(enabled, (result) => { + if (result.success) { + root.getWifiStatus(); + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + } + }); } function toggleWifi(): void { - const cmd = wifiEnabled ? "off" : "on"; - enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]); + Nmcli.toggleWifi((result) => { + if (result.success) { + root.getWifiStatus(); + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + } + }); } function rescanWifi(): void { - rescanProc.running = true; + Nmcli.rescanWifi(); } property var pendingConnection: null signal connectionFailed(string ssid) function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { - // When password is provided, use BSSID for more reliable connection - // When no password, use SSID (will use saved password if available) - const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; - let cmd = []; - // Set up pending connection tracking if callback provided if (callback) { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; } - - if (password && password.length > 0) { - // When password is provided, try BSSID first if available, otherwise use SSID - if (hasBssid) { - // Use BSSID when password is provided - ensure BSSID is uppercase - const bssidUpper = bssid.toUpperCase(); - - // Check if a connection with this SSID already exists - const existingConnection = root.savedConnections.find(conn => - conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() - ); - - if (existingConnection) { - // Connection already exists - delete it first, then create new one with updated password - deleteConnectionProc.exec(["nmcli", "connection", "delete", existingConnection]); - // Wait a moment for deletion to complete, then create new connection - Qt.callLater(() => { - createConnectionWithPassword(ssid, bssidUpper, password); - }, 300); - return; - } else { - // No existing connection, create new one - createConnectionWithPassword(ssid, bssidUpper, password); - return; - } + + Nmcli.connectToNetwork(ssid, password, bssid, (result) => { + if (result && result.success) { + // Connection successful + if (callback) callback(result); + root.pendingConnection = null; + } else if (result && result.needsPassword) { + // Password needed - callback will handle showing dialog + if (callback) callback(result); } else { - // Fallback to SSID if BSSID not available - use device wifi connect - cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; + // Connection failed + if (result && result.error) { + root.connectionFailed(ssid); + } + if (callback) callback(result); + root.pendingConnection = null; } - } else { - // Try to connect to existing connection first (will use saved password if available) - cmd = ["nmcli", "device", "wifi", "connect", ssid]; - } - - // Set command and start process - connectProc.command = cmd; - - // If we're creating a connection profile, we need to activate it after creation - const isConnectionAdd = cmd.length > 0 && cmd[0] === "nmcli" && cmd[1] === "connection" && cmd[2] === "add"; - - // Wait a moment before starting to ensure command is set - Qt.callLater(() => { - connectProc.running = true; - }); - - // Start connection check timer if we have a callback - if (callback) { - connectionCheckTimer.start(); - } - } - - function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string): void { - // Create connection profile with all required properties for BSSID + password - const cmd = ["nmcli", "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]; - - // Set command and start process - connectProc.command = cmd; - - Qt.callLater(() => { - connectProc.running = true; }); } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { - // For secure networks, try connecting without password first - // If connection succeeds (saved password exists), we're done - // If it fails with password error, callback will be called to show password dialog - if (isSecure) { - const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; - root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; - // Try connecting without password - will use saved password if available - connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); - // Start timer to check if connection succeeded - connectionCheckTimer.start(); - } else { - connectToNetwork(ssid, "", bssid, null); - } + // Set up pending connection tracking + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; + + Nmcli.connectToNetworkWithPasswordCheck(ssid, isSecure, (result) => { + if (result && result.success) { + // Connection successful + if (callback) callback(result); + root.pendingConnection = null; + } else if (result && result.needsPassword) { + // Password needed - callback will handle showing dialog + if (callback) callback(result); + } else { + // Connection failed + if (result && result.error) { + root.connectionFailed(ssid); + } + if (callback) callback(result); + root.pendingConnection = null; + } + }, bssid); } function disconnectFromNetwork(): void { // Try to disconnect - use connection name if available, otherwise use device - if (active && active.ssid) { - // First try to disconnect by connection name (more reliable) - disconnectByConnectionProc.exec(["nmcli", "connection", "down", active.ssid]); - } else { - // Fallback: disconnect by device - disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); - } + Nmcli.disconnectFromNetwork(); + // Refresh network list after disconnection + Qt.callLater(() => { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + }, 500); } function forgetNetwork(ssid: string): void { // Delete the connection profile for this network // This will remove the saved password and connection settings - if (ssid && ssid.length > 0) { - deleteConnectionProc.exec(["nmcli", "connection", "delete", ssid]); - // Also refresh network list after deletion - Qt.callLater(() => { - getNetworks.running = true; - }, 500); - } + Nmcli.forgetNetwork(ssid, (result) => { + if (result.success) { + // Refresh network list after deletion + Qt.callLater(() => { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + }, 500); + } + }); } - function hasConnectionProfile(ssid: string): bool { - // Check if a connection profile exists for this SSID - // This is synchronous check - returns true if connection exists - if (!ssid || ssid.length === 0) { - return false; - } - // Use nmcli to check if connection exists - // We'll use a Process to check, but for now return false - // The actual check will be done asynchronously - return false; - } property list savedConnections: [] property list savedConnectionSsids: [] - property var wifiConnectionQueue: [] - property int currentSsidQueryIndex: 0 - - Process { - id: listConnectionsProc - command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - onExited: { - if (exitCode === 0) { - parseConnectionList(stdout.text); - } - } - stdout: StdioCollector { - onStreamFinished: { - parseConnectionList(text); - } - } - } - - function parseConnectionList(output: string): void { - const lines = output.trim().split("\n").filter(line => line.length > 0); - const wifiConnections = []; - const connections = []; - - // First pass: identify WiFi 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); + // Sync saved connections from Nmcli when they're updated + Connections { + target: Nmcli + function onSavedConnectionsChanged() { + root.savedConnections = Nmcli.savedConnections; + } + function onSavedConnectionSsidsChanged() { + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + } + } + + function syncNetworksFromNmcli(): void { + const rNetworks = root.networks; + const nNetworks = Nmcli.networks; + + // Build a map of existing networks by key + const existingMap = new Map(); + for (const rn of rNetworks) { + const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`; + existingMap.set(key, rn); + } + + // Build a map of new networks by key + const newMap = new Map(); + for (const nn of nNetworks) { + const key = `${nn.frequency}:${nn.ssid}:${nn.bssid}`; + newMap.set(key, nn); + } + + // Remove networks that no longer exist + for (const [key, network] of existingMap) { + if (!newMap.has(key)) { + const index = rNetworks.indexOf(network); + if (index >= 0) { + rNetworks.splice(index, 1); + network.destroy(); } } } - - root.savedConnections = connections; - - // Second pass: get SSIDs for WiFi connections - if (wifiConnections.length > 0) { - root.wifiConnectionQueue = wifiConnections; - root.currentSsidQueryIndex = 0; - root.savedConnectionSsids = []; - // Start querying SSIDs one by one - queryNextSsid(); - } else { - root.savedConnectionSsids = []; - root.wifiConnectionQueue = []; - } - } - - Process { - id: getSsidProc - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - onExited: { - if (exitCode === 0) { - processSsidOutput(stdout.text); + + // Add or update networks from Nmcli + for (const [key, nNetwork] of newMap) { + const existing = existingMap.get(key); + if (existing) { + // Update existing network's lastIpcObject + existing.lastIpcObject = nNetwork.lastIpcObject; } else { - // Move to next connection even if this one failed - queryNextSsid(); - } - } - stdout: StdioCollector { - onStreamFinished: { - processSsidOutput(text); + // Create new AccessPoint from Nmcli's data + rNetworks.push(apComp.createObject(root, { + lastIpcObject: nNetwork.lastIpcObject + })); } } } - function processSsidOutput(output: string): void { - // Parse "802-11-wireless.ssid:SSID_NAME" format - 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) { - // Add to list if not already present (case-insensitive) - const ssidLower = ssid.toLowerCase(); - if (!root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower)) { - // Create new array to trigger QML property change notification - const newList = root.savedConnectionSsids.slice(); - newList.push(ssid); - root.savedConnectionSsids = newList; - } - } - } - } - - // Query next connection - queryNextSsid(); + 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 } - function queryNextSsid(): void { - if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { - const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; - root.currentSsidQueryIndex++; - getSsidProc.command = ["nmcli", "-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName]; - getSsidProc.running = true; - } else { - // All SSIDs retrieved - root.wifiConnectionQueue = []; - root.currentSsidQueryIndex = 0; - } + Component { + id: apComp + AccessPoint {} } function hasSavedProfile(ssid: string): bool { - if (!ssid || ssid.length === 0) { - return false; - } - const ssidLower = ssid.toLowerCase().trim(); - - // If currently connected to this network, it definitely has a saved profile - if (root.active && root.active.ssid) { - const activeSsidLower = root.active.ssid.toLowerCase().trim(); - if (activeSsidLower === ssidLower) { - return true; - } - } - - // Check if SSID is in saved connections (case-insensitive comparison) - const hasSsid = root.savedConnectionSsids.some(savedSsid => - savedSsid && savedSsid.toLowerCase().trim() === ssidLower - ); - - if (hasSsid) { - return true; - } - - // Fallback: also check if connection name matches SSID (some connections use SSID as name) - const hasConnectionName = root.savedConnections.some(connName => - connName && connName.toLowerCase().trim() === ssidLower - ); - - return hasConnectionName; + // Use Nmcli's hasSavedProfile which has the same logic + return Nmcli.hasSavedProfile(ssid); } function getWifiStatus(): void { - wifiStatusProc.running = true; + Nmcli.getWifiStatus((enabled) => { + root.wifiEnabled = enabled; + }); } function getEthernetDevices(): void { - getEthernetDevicesProc.running = true; + root.ethernetProcessRunning = true; + Nmcli.getEthernetInterfaces((interfaces) => { + root.ethernetDevices = Nmcli.ethernetDevices; + root.ethernetDeviceCount = Nmcli.ethernetDevices.length; + root.ethernetProcessRunning = false; + }); } function connectEthernet(connectionName: string, interfaceName: string): void { - if (connectionName && connectionName.length > 0) { - // Use connection name if available - connectEthernetProc.exec(["nmcli", "connection", "up", connectionName]); - } else if (interfaceName && interfaceName.length > 0) { - // Fallback to device interface if no connection name - connectEthernetProc.exec(["nmcli", "device", "connect", interfaceName]); - } + Nmcli.connectEthernet(connectionName, interfaceName, (result) => { + if (result.success) { + getEthernetDevices(); + // Refresh device details after connection + Qt.callLater(() => { + const activeDevice = root.ethernetDevices.find(function(d) { return d.connected; }); + if (activeDevice && activeDevice.interface) { + updateEthernetDeviceDetails(activeDevice.interface); + } + }, 1000); + } + }); } function disconnectEthernet(connectionName: string): void { - disconnectEthernetProc.exec(["nmcli", "connection", "down", connectionName]); + Nmcli.disconnectEthernet(connectionName, (result) => { + if (result.success) { + getEthernetDevices(); + // Clear device details after disconnection + Qt.callLater(() => { + root.ethernetDeviceDetails = null; + }); + } + }); } function updateEthernetDeviceDetails(interfaceName: string): void { - if (interfaceName && interfaceName.length > 0) { - getEthernetDetailsProc.exec(["nmcli", "device", "show", interfaceName]); - } else { - ethernetDeviceDetails = null; - } + Nmcli.getEthernetDeviceDetails(interfaceName, (details) => { + root.ethernetDeviceDetails = details; + }); } function updateWirelessDeviceDetails(): void { // Find the wireless interface by looking for wifi devices - findWirelessInterfaceProc.exec(["nmcli", "device", "status"]); + // Pass empty string to let Nmcli find the active interface automatically + Nmcli.getWirelessDeviceDetails("", (details) => { + root.wirelessDeviceDetails = details; + }); } function cidrToSubnetMask(cidr: string): string { @@ -382,649 +305,12 @@ Singleton { command: ["nmcli", "m"] stdout: SplitParser { onRead: { - getNetworks.running = true; + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); getEthernetDevices(); } } } - Process { - id: wifiStatusProc - - running: true - command: ["nmcli", "radio", "wifi"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - root.wifiEnabled = text.trim() === "enabled"; - } - } - } - - Process { - id: enableWifiProc - - onExited: { - root.getWifiStatus(); - getNetworks.running = true; - } - } - - Process { - id: rescanProc - - command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"] - onExited: { - getNetworks.running = true; - } - } - - Timer { - id: connectionCheckTimer - interval: 4000 - onTriggered: { - if (root.pendingConnection) { - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (!connected && root.pendingConnection.callback) { - // Connection didn't succeed after multiple checks, show password dialog - const pending = root.pendingConnection; - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - pending.callback(); - } else if (connected) { - // Connection succeeded, clear pending - 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) { - // Connection succeeded, stop timers and clear pending - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - root.pendingConnection = null; - } else if (checkCount >= 6) { - // Checked 6 times (3 seconds total), connection likely failed - // Stop immediate check, let the main timer handle it - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } else { - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - - Process { - id: connectProc - - onExited: { - - // Check if this was a "connection add" command - if so, we need to activate it - const wasConnectionAdd = connectProc.command && connectProc.command.length > 0 - && connectProc.command[0] === "nmcli" - && connectProc.command[1] === "connection" - && connectProc.command[2] === "add"; - - if (wasConnectionAdd && root.pendingConnection) { - const ssid = root.pendingConnection.ssid; - - // Check for duplicate connection warning in stderr text - const stderrText = connectProc.stderr ? connectProc.stderr.text : ""; - const hasDuplicateWarning = stderrText && ( - stderrText.includes("another connection with the name") || - stderrText.includes("Reference the connection by its uuid") - ); - - // Even with duplicate warning (or if connection already exists), we should try to activate it - // Also try if exit code is non-zero but small (might be a warning, not a real error) - if (exitCode === 0 || hasDuplicateWarning || (exitCode > 0 && exitCode < 10)) { - - // Update saved connections list - listConnectionsProc.running = true; - - // Try to activate the connection by SSID (connection name) - connectProc.command = ["nmcli", "connection", "up", ssid]; - Qt.callLater(() => { - connectProc.running = true; - }); - // Don't start timers yet - wait for activation to complete - return; - } else { - // Connection add failed - try using device wifi connect as fallback - // Extract password from the command if available - let password = ""; - if (connectProc.command) { - const pskIndex = connectProc.command.findIndex(arg => arg === "802-11-wireless-security.psk"); - if (pskIndex >= 0 && pskIndex + 1 < connectProc.command.length) { - password = connectProc.command[pskIndex + 1]; - } - } - - if (password && password.length > 0) { - connectProc.command = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; - Qt.callLater(() => { - connectProc.running = true; - }); - return; - } - } - } - - // Refresh network list after connection attempt - getNetworks.running = true; - - // Check if connection succeeded after a short delay (network list needs to update) - if (root.pendingConnection) { - immediateCheckTimer.start(); - } - } - stdout: SplitParser { - onRead: { - getNetworks.running = true; - } - } - stderr: StdioCollector { - onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0) { - // Check for specific errors that indicate password is needed - // Be careful not to match success messages - const needsPassword = (error.includes("Secrets were required") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - (error.includes("password") && !error.includes("Connection activated")) || - (error.includes("Secrets") && !error.includes("Connection activated")) || - (error.includes("802.11") && !error.includes("Connection activated"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); - - if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { - // Connection failed because password is needed - show dialog immediately - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - const pending = root.pendingConnection; - root.pendingConnection = null; - pending.callback(); - } else if (error && error.length > 0 && !error.includes("Connection activated") && !error.includes("successfully")) { - // Emit signal for UI to handle - root.connectionFailed(root.pendingConnection ? root.pendingConnection.ssid : ""); - } - } - } - } - } - - Process { - id: disconnectProc - - onExited: { - // Refresh network list after disconnection - getNetworks.running = true; - } - stdout: SplitParser { - onRead: getNetworks.running = true - } - stderr: StdioCollector { - onStreamFinished: { - } - } - } - - Process { - id: disconnectByConnectionProc - - onExited: { - // Refresh network list after disconnection - getNetworks.running = true; - } - stdout: SplitParser { - onRead: getNetworks.running = true - } - stderr: StdioCollector { - onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { - // If connection down failed, try device disconnect as fallback - disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); - } - } - } - } - - Process { - id: deleteConnectionProc - - // Delete connection profile - refresh network list and saved connections after deletion - onExited: { - // Refresh network list and saved connections after deletion - getNetworks.running = true; - listConnectionsProc.running = true; - } - stderr: StdioCollector { - onStreamFinished: { - } - } - } - - Process { - id: getNetworks - - running: true - command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; - const rep = new RegExp("\\\\:", "g"); - const rep2 = new RegExp(PLACEHOLDER, "g"); - - const allNetworks = text.trim().split("\n").map(n => { - const net = n.replace(rep, PLACEHOLDER).split(":"); - return { - active: net[0] === "yes", - strength: parseInt(net[1]), - frequency: parseInt(net[2]), - ssid: net[3]?.replace(rep2, ":") ?? "", - bssid: net[4]?.replace(rep2, ":") ?? "", - security: net[5] ?? "" - }; - }).filter(n => n.ssid && n.ssid.length > 0); - - // Group networks by SSID and prioritize connected ones - const networkMap = new Map(); - for (const network of allNetworks) { - const existing = networkMap.get(network.ssid); - if (!existing) { - networkMap.set(network.ssid, network); - } else { - // Prioritize active/connected networks - if (network.active && !existing.active) { - networkMap.set(network.ssid, network); - } else if (!network.active && !existing.active) { - // If both are inactive, keep the one with better signal - if (network.strength > existing.strength) { - networkMap.set(network.ssid, network); - } - } - // If existing is active and new is not, keep existing - } - } - - 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) - rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.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 - })); - } - } - - // Check if pending connection succeeded after network list is fully updated - if (root.pendingConnection) { - Qt.callLater(() => { - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - if (connected) { - // Connection succeeded, stop timers and clear pending - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - root.pendingConnection = null; - } - }); - } - } - } - } - - Process { - id: getEthernetDevicesProc - - running: false - command: ["nmcli", "-g", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - onRunningChanged: { - root.ethernetProcessRunning = running; - } - onExited: { - Qt.callLater(() => { - const outputLength = ethernetStdout.text ? ethernetStdout.text.length : 0; - if (outputLength > 0) { - // Output was captured, process it - const output = ethernetStdout.text.trim(); - root.processEthernetOutput(output); - } - }); - } - stdout: StdioCollector { - id: ethernetStdout - onStreamFinished: { - const output = text.trim(); - - if (!output || output.length === 0) { - return; - } - - root.processEthernetOutput(output); - } - } - } - - function processEthernetOutput(output: string): void { - const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; - const rep = new RegExp("\\\\:", "g"); - const rep2 = new RegExp(PLACEHOLDER, "g"); - - const lines = output.split("\n"); - - const allDevices = lines.map(d => { - const dev = d.replace(rep, PLACEHOLDER).split(":"); - return { - interface: dev[0]?.replace(rep2, ":") ?? "", - type: dev[1]?.replace(rep2, ":") ?? "", - state: dev[2]?.replace(rep2, ":") ?? "", - connection: dev[3]?.replace(rep2, ":") ?? "" - }; - }); - - const ethernetOnly = allDevices.filter(d => d.type === "ethernet"); - - const ethernetDevices = ethernetOnly.map(d => { - const state = d.state || ""; - const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); - return { - interface: d.interface, - type: d.type, - state: state, - connection: d.connection, - connected: connected, - ipAddress: "", - gateway: "", - dns: [], - subnet: "", - macAddress: "", - speed: "" - }; - }); - - // Update the list - replace the entire array to ensure QML detects the change - // Create a new array and assign it to the property - const newDevices = []; - for (let i = 0; i < ethernetDevices.length; i++) { - newDevices.push(ethernetDevices[i]); - } - - // Replace the entire list - root.ethernetDevices = newDevices; - - // Force QML to detect the change by updating a property - root.ethernetDeviceCount = ethernetDevices.length; - } - - - Process { - id: connectEthernetProc - - onExited: { - getEthernetDevices(); - // Refresh device details after connection - Qt.callLater(() => { - const activeDevice = root.ethernetDevices.find(function(d) { return d.connected; }); - if (activeDevice && activeDevice.interface) { - updateEthernetDeviceDetails(activeDevice.interface); - } - }); - } - stdout: SplitParser { - onRead: getEthernetDevices() - } - stderr: StdioCollector { - onStreamFinished: { - } - } - } - - Process { - id: disconnectEthernetProc - - onExited: { - getEthernetDevices(); - // Clear device details after disconnection - Qt.callLater(() => { - root.ethernetDeviceDetails = null; - }); - } - stdout: SplitParser { - onRead: getEthernetDevices() - } - stderr: StdioCollector { - onStreamFinished: { - } - } - } - - Process { - id: getEthernetDetailsProc - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - const output = text.trim(); - if (!output || output.length === 0) { - root.ethernetDeviceDetails = null; - return; - } - - const lines = output.split("\n"); - const details = { - ipAddress: "", - gateway: "", - dns: [], - subnet: "", - macAddress: "", - speed: "" - }; - - 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")) { - // Extract IP and subnet from format like "10.13.1.45/24" - const ipParts = value.split("/"); - details.ipAddress = ipParts[0] || ""; - if (ipParts[1]) { - // Convert CIDR notation to subnet mask - details.subnet = root.cidrToSubnetMask(ipParts[1]); - } else { - details.subnet = ""; - } - } else if (key === "IP4.GATEWAY") { - details.gateway = value; - } else if (key.startsWith("IP4.DNS")) { - details.dns.push(value); - } else if (key === "WIRED-PROPERTIES.MAC") { - details.macAddress = value; - } else if (key === "WIRED-PROPERTIES.SPEED") { - details.speed = value; - } - } - } - - root.ethernetDeviceDetails = details; - } - } - onExited: { - if (exitCode !== 0) { - root.ethernetDeviceDetails = null; - } - } - } - - Process { - id: findWirelessInterfaceProc - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - const output = text.trim(); - if (!output || output.length === 0) { - root.wirelessDeviceDetails = null; - return; - } - - // Find the connected wifi interface from device status - const lines = output.split("\n"); - let wifiInterface = ""; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const parts = line.split(/\s+/); - // Format: DEVICE TYPE STATE CONNECTION - // Look for wifi devices that are connected - if (parts.length >= 3 && parts[1] === "wifi" && parts[2] === "connected") { - wifiInterface = parts[0]; - break; - } - } - - if (wifiInterface && wifiInterface.length > 0) { - getWirelessDetailsProc.exec(["nmcli", "device", "show", wifiInterface]); - } else { - root.wirelessDeviceDetails = null; - } - } - } - onExited: { - if (exitCode !== 0) { - root.wirelessDeviceDetails = null; - } - } - } - - Process { - id: getWirelessDetailsProc - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - const output = text.trim(); - if (!output || output.length === 0) { - root.wirelessDeviceDetails = null; - return; - } - - const lines = output.split("\n"); - const details = { - ipAddress: "", - gateway: "", - dns: [], - subnet: "", - macAddress: "", - speed: "" - }; - - 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")) { - // Extract IP and subnet from format like "10.13.1.45/24" - const ipParts = value.split("/"); - details.ipAddress = ipParts[0] || ""; - if (ipParts[1]) { - // Convert CIDR notation to subnet mask - details.subnet = root.cidrToSubnetMask(ipParts[1]); - } else { - details.subnet = ""; - } - } else if (key === "IP4.GATEWAY") { - details.gateway = value; - } else if (key.startsWith("IP4.DNS")) { - details.dns.push(value); - } else if (key === "GENERAL.HWADDR") { - details.macAddress = value; - } - } - } - - root.wirelessDeviceDetails = details; - } - } - onExited: { - if (exitCode !== 0) { - root.wirelessDeviceDetails = null; - } - } - } - - 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 {} - } } -- cgit v1.2.3-freya