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') 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') 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') 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') 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') 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 c1510b547645de5e8f70f6be99a0ba894b797241 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 14:51:22 -0500 Subject: notifs/toasts: reworked notifications and toasts and how they display and work together. see pull request comment. --- modules/drawers/Drawers.qml | 1 + modules/drawers/Interactions.qml | 46 +++++++ modules/drawers/Panels.qml | 88 +++++++++++++ modules/lock/NotifDock.qml | 4 +- modules/lock/NotifGroup.qml | 2 +- modules/notifications/Content.qml | 58 ++++++++- modules/notifications/Notification.qml | 67 +++++++--- modules/notifications/NotificationToast.qml | 154 ++++++++++++++++++++++ modules/notifications/NotificationToasts.qml | 186 +++++++++++++++++++++++++++ modules/notifications/Wrapper.qml | 2 + modules/utilities/toasts/ToastItem.qml | 9 +- services/Notifs.qml | 37 +++++- 12 files changed, 627 insertions(+), 27 deletions(-) create mode 100644 modules/notifications/NotificationToast.qml create mode 100644 modules/notifications/NotificationToasts.qml (limited to 'services') diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 2ba79a4..5337917 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -140,6 +140,7 @@ Variants { property bool dashboard property bool utilities property bool sidebar + property bool notifications Component.onCompleted: Visibilities.load(scope.modelData, this) } diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 9579b15..10190a4 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -198,6 +198,52 @@ CustomMouseArea { utilitiesShortcutActive = false; } + // Show notifications panel on hover + // Try using inTopPanel first (works when panel is visible), fallback to corner detection only when panel is collapsed + const panelHeight = panels.notifications.height || panels.notifications.implicitHeight || 0; + 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 && + 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) { + const buttonX = bar.implicitWidth + panels.clearAllButton.x; + 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 && + y <= buttonY + buttonHeight; + + if (inButtonArea) { + showNotifications = true; + } + } + + // Show or hide notification panel based on hover + if (panels.notifications.content) { + if (showNotifications) { + panels.notifications.content.show(); + } else { + // Hide if not hovering over panel or button + panels.notifications.content.shouldShow = false; + } + } + // Show popouts on hover if (x < bar.implicitWidth) { bar.checkPopout(y); diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 4ce1182..8b5a251 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -8,6 +8,10 @@ import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities as Utilities import qs.modules.utilities.toasts as Toasts import qs.modules.sidebar as Sidebar +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services import Quickshell import QtQuick @@ -27,6 +31,7 @@ Item { readonly property alias utilities: utilities readonly property alias toasts: toasts readonly property alias sidebar: sidebar + readonly property alias clearAllButton: clearAllButton anchors.fill: parent anchors.margins: Config.border.thickness @@ -54,6 +59,89 @@ Item { anchors.right: parent.right } + // Clear all notifications button - positioned to the left of the notification panel + Item { + id: clearAllButton + + readonly property bool hasNotifications: Notifs.notClosed.length > 0 + readonly property bool panelVisible: notifications.height > 0 || notifications.implicitHeight > 0 + readonly property bool shouldShow: hasNotifications && panelVisible + + anchors.top: notifications.top + anchors.right: notifications.left + anchors.rightMargin: Appearance.padding.normal + anchors.topMargin: Appearance.padding.large + + width: button.implicitWidth + height: button.implicitHeight + enabled: shouldShow + + IconButton { + id: button + + icon: "clear_all" + radius: Appearance.rounding.normal + padding: Appearance.padding.normal + font.pointSize: Math.round(Appearance.font.size.large * 1.2) + + onClicked: { + // Clear all notifications + for (const notif of Notifs.list.slice()) + notif.close(); + } + + Elevation { + anchors.fill: parent + radius: parent.radius + z: -1 + level: button.stateLayer.containsMouse ? 4 : 3 + } + } + + // Keep notification panel visible when hovering over the button + MouseArea { + anchors.fill: button + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: { + if (notifications.content && Notifs.notClosed.length > 0) { + notifications.content.show(); + } + } + onExited: { + // Panel will be hidden by Interactions.qml if mouse is not over panel or button + } + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + opacity: shouldShow ? 1 : 0 + scale: shouldShow ? 1 : 0.5 + } + + Notifications.NotificationToasts { + id: notificationToasts + + panels: root + + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Config.border.thickness + anchors.rightMargin: Config.border.thickness + } + Session.Wrapper { id: session diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 7551e68..db087bd 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -22,7 +22,7 @@ ColumnLayout { StyledText { Layout.fillWidth: true - text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") + text: Notifs.notClosed.length > 0 ? qsTr("%1 notification%2").arg(Notifs.notClosed.length).arg(Notifs.notClosed.length === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline font.family: Appearance.font.family.mono font.weight: 500 @@ -42,7 +42,7 @@ ColumnLayout { anchors.centerIn: parent asynchronous: true active: opacity > 0 - opacity: Notifs.list.length > 0 ? 0 : 1 + opacity: Notifs.notClosed.length > 0 ? 0 : 1 sourceComponent: ColumnLayout { spacing: Appearance.spacing.large diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 2a08c26..50b14ae 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -16,7 +16,7 @@ StyledRect { required property string modelData - readonly property list notifs: Notifs.list.filter(notif => notif.appName === modelData) + readonly property list notifs: Notifs.notClosed.filter(notif => notif.appName === modelData) readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? "" readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low" diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 2d4590e..035a228 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -13,6 +13,8 @@ Item { required property Item panels readonly property int padding: Appearance.padding.large + property bool shouldShow: false + anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right @@ -20,13 +22,16 @@ Item { implicitWidth: Config.notifs.sizes.width + padding * 2 implicitHeight: { const count = list.count; - if (count === 0) + if (count === 0 || !shouldShow) return 0; let height = (count - 1) * Appearance.spacing.smaller; for (let i = 0; i < count; i++) height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; + const screenHeight = QsWindow.window?.screen?.height ?? 0; + const maxHeight = Math.floor(screenHeight * 0.45); + if (visibilities && panels) { if (visibilities.osd) { const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; @@ -41,7 +46,8 @@ Item { } } - return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); + const availableHeight = Math.min(maxHeight, screenHeight - Config.border.thickness * 2); + return Math.min(availableHeight, height + padding * 2); } ClippingWrapperRectangle { @@ -55,7 +61,7 @@ Item { id: list model: ScriptModel { - values: Notifs.popups.filter(n => !n.closed) + values: [...Notifs.notClosed] } anchors.fill: parent @@ -192,6 +198,52 @@ Item { } } + Timer { + id: hideTimer + + interval: 5000 + onTriggered: { + if (list.count > 0) + root.shouldShow = false; + } + } + + function show(): void { + if (list.count > 0) { + shouldShow = true; + hideTimer.restart(); + } + } + + Connections { + target: list + + function onCountChanged(): void { + if (list.count === 0) { + root.shouldShow = false; + hideTimer.stop(); + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: { + if (list.count > 0) { + root.shouldShow = true; + hideTimer.restart(); + } + } + onExited: { + if (list.count > 0) { + root.shouldShow = false; + hideTimer.stop(); + } + } + } + Behavior on implicitHeight { Anim {} } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 95507fc..091da2c 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -17,22 +17,35 @@ StyledRect { required property Notifs.Notif modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 - readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 + readonly property bool isCritical: modelData.urgency === NotificationUrgency.Critical + readonly property bool isLow: modelData.urgency === NotificationUrgency.Low + readonly property int nonAnimHeight: { + const baseHeight = summary.implicitHeight + inner.anchors.margins * 2; + return root.expanded + ? baseHeight + appName.height + body.height + actions.height + actions.anchors.topMargin + : baseHeight + bodyPreview.height; + } property bool expanded + property bool disableSlideIn: false - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer + color: root.isCritical + ? Colours.palette.m3secondaryContainer + : Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal implicitWidth: Config.notifs.sizes.width implicitHeight: inner.implicitHeight - x: Config.notifs.sizes.width + x: disableSlideIn ? 0 : Config.notifs.sizes.width Component.onCompleted: { - x = 0; + if (!root.disableSlideIn) { + x = 0; + } modelData.lock(this); } Component.onDestruction: modelData.unlock(this) Behavior on x { + enabled: !disableSlideIn Anim { easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } @@ -134,8 +147,8 @@ StyledRect { Loader { id: appIcon - active: root.hasAppIcon || !root.hasImage - asynchronous: true + active: !root.hasImage || root.hasAppIcon + asynchronous: false anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter @@ -144,7 +157,11 @@ StyledRect { sourceComponent: StyledRect { radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer + color: { + if (root.isCritical) return Colours.palette.m3error; + if (root.isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2); + return Colours.palette.m3secondaryContainer; + } implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image @@ -152,7 +169,8 @@ StyledRect { id: icon active: root.hasAppIcon - asynchronous: true + asynchronous: false + visible: active anchors.centerIn: parent @@ -162,14 +180,19 @@ StyledRect { sourceComponent: ColouredIcon { anchors.fill: parent source: Quickshell.iconPath(root.modelData.appIcon) - colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + colour: { + if (root.isCritical) return Colours.palette.m3onError; + if (root.isLow) return Colours.palette.m3onSurface; + return Colours.palette.m3onSecondaryContainer; + } layer.enabled: root.modelData.appIcon.endsWith("symbolic") } } Loader { active: !root.hasAppIcon - asynchronous: true + asynchronous: false + visible: active anchors.centerIn: parent anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 @@ -177,7 +200,11 @@ StyledRect { sourceComponent: MaterialIcon { text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + color: { + if (root.isCritical) return Colours.palette.m3onError; + if (root.isLow) return Colours.palette.m3onSurface; + return Colours.palette.m3onSecondaryContainer; + } font.pointSize: Appearance.font.size.large } } @@ -322,7 +349,9 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + color: root.isCritical + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurface function onClicked() { root.expanded = !root.expanded; @@ -442,8 +471,12 @@ StyledRect { required property var modelData + readonly property bool isCritical: root.isCritical + radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + color: isCritical + ? Colours.palette.m3secondary + : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 @@ -452,7 +485,9 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface + color: isCritical + ? Colours.palette.m3onSecondary + : Colours.palette.m3onSurface function onClicked(): void { action.modelData.invoke(); @@ -464,7 +499,9 @@ StyledRect { anchors.centerIn: parent text: actionTextMetrics.elidedText - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant + color: isCritical + ? Colours.palette.m3onSecondary + : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } diff --git a/modules/notifications/NotificationToast.qml b/modules/notifications/NotificationToast.qml new file mode 100644 index 0000000..1ce334b --- /dev/null +++ b/modules/notifications/NotificationToast.qml @@ -0,0 +1,154 @@ +import qs.components +import qs.components.effects +import qs.services +import qs.config +import qs.utils +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property Notifs.Notif modelData + + readonly property bool hasImage: modelData.image.length > 0 + readonly property bool hasAppIcon: modelData.appIcon.length > 0 + + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.normal + color: Colours.palette.m3surface + + border.width: 1 + border.color: Colours.palette.m3outlineVariant + + Elevation { + anchors.fill: parent + radius: parent.radius + opacity: parent.opacity + z: -1 + level: 3 + } + + RowLayout { + id: layout + + anchors.fill: parent + anchors.margins: Appearance.padding.smaller + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + spacing: Appearance.spacing.normal + + Item { + Layout.preferredWidth: Config.notifs.sizes.image + Layout.preferredHeight: Config.notifs.sizes.image + + Loader { + id: imageLoader + + active: root.hasImage + asynchronous: true + anchors.fill: parent + + sourceComponent: ClippingRectangle { + radius: Appearance.rounding.full + implicitWidth: Config.notifs.sizes.image + implicitHeight: Config.notifs.sizes.image + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(root.modelData.image) + fillMode: Image.PreserveAspectCrop + cache: false + asynchronous: true + } + } + } + + Loader { + id: appIconLoader + + active: root.hasAppIcon || !root.hasImage + asynchronous: true + + anchors.horizontalCenter: root.hasImage ? undefined : parent.horizontalCenter + anchors.verticalCenter: root.hasImage ? undefined : parent.verticalCenter + anchors.right: root.hasImage ? parent.right : undefined + anchors.bottom: root.hasImage ? parent.bottom : undefined + + sourceComponent: StyledRect { + radius: Appearance.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer + implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + + Loader { + id: appIcon + + active: root.hasAppIcon + asynchronous: true + + anchors.centerIn: parent + + width: Math.round(parent.width * 0.6) + height: Math.round(parent.width * 0.6) + + sourceComponent: ColouredIcon { + anchors.fill: parent + source: Quickshell.iconPath(root.modelData.appIcon) + colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + layer.enabled: root.modelData.appIcon.endsWith("symbolic") + } + } + + Loader { + active: !root.hasAppIcon + asynchronous: true + anchors.centerIn: parent + anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 + anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 + + sourceComponent: MaterialIcon { + text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + font.pointSize: Appearance.font.size.large + } + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + id: title + + Layout.fillWidth: true + text: root.modelData.summary + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.normal + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + textFormat: Text.StyledText + text: root.modelData.body + color: Colours.palette.m3onSurface + opacity: 0.8 + elide: Text.ElideRight + } + } + } + + Behavior on border.color { + CAnim {} + } +} diff --git a/modules/notifications/NotificationToasts.qml b/modules/notifications/NotificationToasts.qml new file mode 100644 index 0000000..96fe817 --- /dev/null +++ b/modules/notifications/NotificationToasts.qml @@ -0,0 +1,186 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.config +import qs.services +import Quickshell +import Quickshell.Widgets +import QtQuick + +Item { + id: root + + required property Item panels + + readonly property int spacing: Appearance.spacing.small + readonly property int maxToasts: 5 + readonly property bool listVisible: panels.notifications.content.shouldShow + + property bool flag + property var activeToasts: new Set() + + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Appearance.padding.normal + + implicitWidth: Config.notifs.sizes.width + implicitHeight: { + if (listVisible) + return 0; + + let height = -spacing; + for (let i = 0; i < repeater.count; i++) { + const item = repeater.itemAt(i) as ToastWrapper; + if (item && !item.modelData.closed && !item.previewHidden) + height += item.implicitHeight + spacing; + } + return height; + } + + opacity: listVisible ? 0 : 1 + visible: opacity > 0 + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + } + } + + Repeater { + id: repeater + + model: ScriptModel { + values: { + const toasts = []; + let visibleCount = 0; + + for (const notif of Notifs.list) { + if (notif.showAsToast) { + root.activeToasts.add(notif); + } + if (notif.closed) { + root.activeToasts.delete(notif); + } + } + + for (const notif of Notifs.list) { + if (root.activeToasts.has(notif)) { + toasts.push(notif); + if (notif.showAsToast && !notif.closed) { + visibleCount++; + if (visibleCount > root.maxToasts) + break; + } + } + } + return toasts; + } + onValuesChanged: root.flagChanged() + } + + ToastWrapper {} + } + + component ToastWrapper: MouseArea { + id: toast + + required property int index + required property Notifs.Notif modelData + + readonly property bool previewHidden: { + let extraHidden = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i); + if (item && item.modelData.closed) + extraHidden++; + } + return index >= root.maxToasts + extraHidden; + } + + opacity: modelData.closed || previewHidden || !modelData.showAsToast ? 0 : 1 + scale: modelData.closed || previewHidden || !modelData.showAsToast ? 0.7 : 1 + + anchors.topMargin: { + root.flag; + let margin = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i) as ToastWrapper; + if (item && !item.modelData.closed && !item.previewHidden) + margin += item.implicitHeight + root.spacing; + } + return margin; + } + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + implicitHeight: toastInner.implicitHeight + + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + onClicked: { + modelData.showAsToast = false; + modelData.close(); + } + + Component.onCompleted: modelData.lock(this) + + onPreviewHiddenChanged: { + if (initAnim.running && previewHidden) + initAnim.stop(); + } + + Anim { + id: initAnim + + Component.onCompleted: running = !toast.previewHidden + + target: toast + properties: "opacity,scale" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + + ParallelAnimation { + running: toast.modelData.closed || (!toast.modelData.showAsToast && !toast.modelData.closed) + onStarted: toast.anchors.topMargin = toast.anchors.topMargin + onFinished: { + if (toast.modelData.closed) + toast.modelData.unlock(toast); + } + + Anim { + target: toast + property: "opacity" + to: 0 + } + Anim { + target: toast + property: "scale" + to: 0.7 + } + } + + NotificationToast { + id: toastInner + + modelData: toast.modelData + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + Behavior on anchors.topMargin { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + } +} diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 61acc56..4b54883 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -8,6 +8,8 @@ Item { required property var visibilities required property Item panels + readonly property alias content: content + visible: height > 0 implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) implicitHeight: content.implicitHeight diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index f475500..477a23c 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -28,14 +28,13 @@ StyledRect { border.width: 1 border.color: { - let colour = Colours.palette.m3outlineVariant; if (root.modelData.type === Toast.Success) - colour = Colours.palette.m3success; + return Colours.palette.m3success; if (root.modelData.type === Toast.Warning) - colour = Colours.palette.m3secondaryContainer; + return Colours.palette.m3secondaryContainer; if (root.modelData.type === Toast.Error) - colour = Colours.palette.m3error; - return Qt.alpha(colour, 0.3); + return Colours.palette.m3error; + return Colours.palette.m3outlineVariant; } Elevation { diff --git a/services/Notifs.qml b/services/Notifs.qml index 4a89c7f..ea0c52a 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -77,8 +77,10 @@ Singleton { onNotification: notif => { notif.tracked = true; + const shouldShowAsToast = !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar); const comp = notifComp.createObject(root, { - popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), + popup: shouldShowAsToast, + showAsToast: shouldShowAsToast, notification: notif }); root.list = [comp, ...root.list]; @@ -143,6 +145,7 @@ Singleton { property bool popup property bool closed + property bool showAsToast: false property var locks: new Set() property date time: new Date() @@ -177,6 +180,38 @@ Singleton { property list actions readonly property Timer timer: Timer { + id: toastTimer + + running: notif.showAsToast + interval: { + let timeout = notif.expireTimeout; + if (timeout <= 0) { + switch (notif.urgency) { + case NotificationUrgency.Critical: + timeout = 10000; + break; + case NotificationUrgency.Normal: + timeout = 5000; + break; + case NotificationUrgency.Low: + timeout = 5000; + break; + default: + timeout = 5000; + } + } + return timeout; + } + onTriggered: { + if (Config.notifs.expire) + notif.popup = false; + if (notif.showAsToast) { + notif.showAsToast = false; + } + } + } + + readonly property Timer popupTimer: Timer { running: true interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout onTriggered: { -- cgit v1.2.3-freya From 84e839cf55fe745185c33b2d597501bcababa547 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 16:50:20 -0500 Subject: notif/toasts: refactoring colors --- modules/notifications/AppIconBadge.qml | 21 +++-------------- modules/notifications/Notification.qml | 26 +++++---------------- modules/notifications/NotificationToast.qml | 2 -- services/Notifs.qml | 35 +++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 41 deletions(-) (limited to 'services') diff --git a/modules/notifications/AppIconBadge.qml b/modules/notifications/AppIconBadge.qml index 8bbae89..286522f 100644 --- a/modules/notifications/AppIconBadge.qml +++ b/modules/notifications/AppIconBadge.qml @@ -13,15 +13,9 @@ StyledRect { required property Notifs.Notif modelData required property bool hasImage required property bool hasAppIcon - required property bool isCritical - required property bool isLow radius: Appearance.rounding.full - color: { - if (root.isCritical) return Colours.palette.m3error; - if (root.isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2); - return Colours.palette.m3secondaryContainer; - } + color: modelData.getBadgeBackgroundColor() implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image @@ -40,11 +34,7 @@ StyledRect { sourceComponent: ColouredIcon { anchors.fill: parent source: Quickshell.iconPath(root.modelData.appIcon) - colour: { - if (root.isCritical) return Colours.palette.m3onError; - if (root.isLow) return Colours.palette.m3onSurface; - return Colours.palette.m3onSecondaryContainer; - } + colour: root.modelData.getIconColor() layer.enabled: root.modelData.appIcon.endsWith("symbolic") } } @@ -59,12 +49,7 @@ StyledRect { sourceComponent: MaterialIcon { text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - - color: { - if (root.isCritical) return Colours.palette.m3onError; - if (root.isLow) return Colours.palette.m3onSurface; - return Colours.palette.m3onSecondaryContainer; - } + color: root.modelData.getIconColor() font.pointSize: Appearance.font.size.large } } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 75defb6..bc5c086 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -17,8 +17,6 @@ StyledRect { required property Notifs.Notif modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 - readonly property bool isCritical: modelData.urgency === NotificationUrgency.Critical - readonly property bool isLow: modelData.urgency === NotificationUrgency.Low readonly property int nonAnimHeight: { const baseHeight = summary.implicitHeight + inner.anchors.margins * 2; return root.expanded @@ -28,9 +26,7 @@ StyledRect { property bool expanded property bool disableSlideIn: false - color: root.isCritical - ? Colours.palette.m3secondaryContainer - : Colours.tPalette.m3surfaceContainer + color: modelData.getBackgroundColor() radius: Appearance.rounding.normal implicitWidth: Config.notifs.sizes.width implicitHeight: inner.implicitHeight @@ -159,8 +155,6 @@ StyledRect { modelData: root.modelData hasImage: root.hasImage hasAppIcon: root.hasAppIcon - isCritical: root.isCritical - isLow: root.isLow } } @@ -302,9 +296,7 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: root.isCritical - ? Colours.palette.m3onSecondaryContainer - : Colours.palette.m3onSurface + color: root.modelData.getStateLayerColor() function onClicked() { root.expanded = !root.expanded; @@ -424,12 +416,8 @@ StyledRect { required property var modelData - readonly property bool isCritical: root.isCritical - radius: Appearance.rounding.full - color: isCritical - ? Colours.palette.m3secondary - : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + color: root.modelData.getActionBackgroundColor() Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 @@ -438,9 +426,7 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: isCritical - ? Colours.palette.m3onSecondary - : Colours.palette.m3onSurface + color: root.modelData.getStateLayerColor() function onClicked(): void { action.modelData.invoke(); @@ -452,9 +438,7 @@ StyledRect { anchors.centerIn: parent text: actionTextMetrics.elidedText - color: isCritical - ? Colours.palette.m3onSecondary - : Colours.palette.m3onSurfaceVariant + color: root.modelData.getActionTextColor() font.pointSize: Appearance.font.size.small } diff --git a/modules/notifications/NotificationToast.qml b/modules/notifications/NotificationToast.qml index f8b830a..90414fe 100644 --- a/modules/notifications/NotificationToast.qml +++ b/modules/notifications/NotificationToast.qml @@ -85,8 +85,6 @@ StyledRect { modelData: root.modelData hasImage: root.hasImage hasAppIcon: root.hasAppIcon - isCritical: root.modelData.urgency === NotificationUrgency.Critical - isLow: root.modelData.urgency === NotificationUrgency.Low } } } diff --git a/services/Notifs.qml b/services/Notifs.qml index ea0c52a..82ed8c4 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -179,6 +179,41 @@ Singleton { property bool hasActionIcons property list actions + readonly property bool isCritical: urgency === NotificationUrgency.Critical + readonly property bool isLow: urgency === NotificationUrgency.Low + + function getBackgroundColor(): color { + if (isCritical) return Colours.palette.m3secondaryContainer; + return Colours.tPalette.m3surfaceContainer; + } + + function getBadgeBackgroundColor(): color { + if (isCritical) return Colours.palette.m3error; + if (isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2); + return Colours.palette.m3secondaryContainer; + } + + function getIconColor(): color { + if (isCritical) return Colours.palette.m3onError; + if (isLow) return Colours.palette.m3onSurface; + return Colours.palette.m3onSecondaryContainer; + } + + function getStateLayerColor(): color { + if (isCritical) return Colours.palette.m3onSecondaryContainer; + return Colours.palette.m3onSurface; + } + + function getActionBackgroundColor(): color { + if (isCritical) return Colours.palette.m3secondary; + return Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); + } + + function getActionTextColor(): color { + if (isCritical) return Colours.palette.m3onSecondary; + return Colours.palette.m3onSurfaceVariant; + } + readonly property Timer timer: Timer { id: toastTimer -- 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') 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 ffe14748a2cf5bc5710fe24d0ccae80b8437f35d Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 20:57:38 -0500 Subject: notifs/toasts: reverted all changes to notifications to c0ea060f --- modules/drawers/Panels.qml | 88 ------------- modules/lock/NotifDock.qml | 4 +- modules/lock/NotifGroup.qml | 2 +- modules/notifications/AppIconBadge.qml | 57 -------- modules/notifications/Content.qml | 58 +-------- modules/notifications/Notification.qml | 72 +++++++---- modules/notifications/NotificationToast.qml | 120 ----------------- modules/notifications/NotificationToasts.qml | 186 --------------------------- modules/notifications/Wrapper.qml | 2 - modules/utilities/toasts/ToastItem.qml | 9 +- services/Notifs.qml | 72 +---------- 11 files changed, 61 insertions(+), 609 deletions(-) delete mode 100644 modules/notifications/AppIconBadge.qml delete mode 100644 modules/notifications/NotificationToast.qml delete mode 100644 modules/notifications/NotificationToasts.qml (limited to 'services') diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 8b5a251..4ce1182 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -8,10 +8,6 @@ import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities as Utilities import qs.modules.utilities.toasts as Toasts import qs.modules.sidebar as Sidebar -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services import Quickshell import QtQuick @@ -31,7 +27,6 @@ Item { readonly property alias utilities: utilities readonly property alias toasts: toasts readonly property alias sidebar: sidebar - readonly property alias clearAllButton: clearAllButton anchors.fill: parent anchors.margins: Config.border.thickness @@ -59,89 +54,6 @@ Item { anchors.right: parent.right } - // Clear all notifications button - positioned to the left of the notification panel - Item { - id: clearAllButton - - readonly property bool hasNotifications: Notifs.notClosed.length > 0 - readonly property bool panelVisible: notifications.height > 0 || notifications.implicitHeight > 0 - readonly property bool shouldShow: hasNotifications && panelVisible - - anchors.top: notifications.top - anchors.right: notifications.left - anchors.rightMargin: Appearance.padding.normal - anchors.topMargin: Appearance.padding.large - - width: button.implicitWidth - height: button.implicitHeight - enabled: shouldShow - - IconButton { - id: button - - icon: "clear_all" - radius: Appearance.rounding.normal - padding: Appearance.padding.normal - font.pointSize: Math.round(Appearance.font.size.large * 1.2) - - onClicked: { - // Clear all notifications - for (const notif of Notifs.list.slice()) - notif.close(); - } - - Elevation { - anchors.fill: parent - radius: parent.radius - z: -1 - level: button.stateLayer.containsMouse ? 4 : 3 - } - } - - // Keep notification panel visible when hovering over the button - MouseArea { - anchors.fill: button - hoverEnabled: true - acceptedButtons: Qt.NoButton - onEntered: { - if (notifications.content && Notifs.notClosed.length > 0) { - notifications.content.show(); - } - } - onExited: { - // Panel will be hidden by Interactions.qml if mouse is not over panel or button - } - } - - Behavior on opacity { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on scale { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - opacity: shouldShow ? 1 : 0 - scale: shouldShow ? 1 : 0.5 - } - - Notifications.NotificationToasts { - id: notificationToasts - - panels: root - - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Config.border.thickness - anchors.rightMargin: Config.border.thickness - } - Session.Wrapper { id: session diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index db087bd..7551e68 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -22,7 +22,7 @@ ColumnLayout { StyledText { Layout.fillWidth: true - text: Notifs.notClosed.length > 0 ? qsTr("%1 notification%2").arg(Notifs.notClosed.length).arg(Notifs.notClosed.length === 1 ? "" : "s") : qsTr("Notifications") + text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline font.family: Appearance.font.family.mono font.weight: 500 @@ -42,7 +42,7 @@ ColumnLayout { anchors.centerIn: parent asynchronous: true active: opacity > 0 - opacity: Notifs.notClosed.length > 0 ? 0 : 1 + opacity: Notifs.list.length > 0 ? 0 : 1 sourceComponent: ColumnLayout { spacing: Appearance.spacing.large diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 50b14ae..2a08c26 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -16,7 +16,7 @@ StyledRect { required property string modelData - readonly property list notifs: Notifs.notClosed.filter(notif => notif.appName === modelData) + readonly property list notifs: Notifs.list.filter(notif => notif.appName === modelData) readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? "" readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low" diff --git a/modules/notifications/AppIconBadge.qml b/modules/notifications/AppIconBadge.qml deleted file mode 100644 index 286522f..0000000 --- a/modules/notifications/AppIconBadge.qml +++ /dev/null @@ -1,57 +0,0 @@ -import qs.components -import qs.components.effects -import qs.services -import qs.config -import qs.utils -import Quickshell -import Quickshell.Services.Notifications -import QtQuick - -StyledRect { - id: root - - required property Notifs.Notif modelData - required property bool hasImage - required property bool hasAppIcon - - radius: Appearance.rounding.full - color: modelData.getBadgeBackgroundColor() - implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - - Loader { - id: icon - - active: root.hasAppIcon - asynchronous: false - visible: active - - anchors.centerIn: parent - - width: Math.round(parent.width * 0.6) - height: Math.round(parent.width * 0.6) - - sourceComponent: ColouredIcon { - anchors.fill: parent - source: Quickshell.iconPath(root.modelData.appIcon) - colour: root.modelData.getIconColor() - layer.enabled: root.modelData.appIcon.endsWith("symbolic") - } - } - - Loader { - active: !root.hasAppIcon - asynchronous: false - visible: active - anchors.centerIn: parent - anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 - anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 - - sourceComponent: MaterialIcon { - text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - color: root.modelData.getIconColor() - font.pointSize: Appearance.font.size.large - } - } -} - diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 035a228..2d4590e 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -13,8 +13,6 @@ Item { required property Item panels readonly property int padding: Appearance.padding.large - property bool shouldShow: false - anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right @@ -22,16 +20,13 @@ Item { implicitWidth: Config.notifs.sizes.width + padding * 2 implicitHeight: { const count = list.count; - if (count === 0 || !shouldShow) + if (count === 0) return 0; let height = (count - 1) * Appearance.spacing.smaller; for (let i = 0; i < count; i++) height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; - const screenHeight = QsWindow.window?.screen?.height ?? 0; - const maxHeight = Math.floor(screenHeight * 0.45); - if (visibilities && panels) { if (visibilities.osd) { const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; @@ -46,8 +41,7 @@ Item { } } - const availableHeight = Math.min(maxHeight, screenHeight - Config.border.thickness * 2); - return Math.min(availableHeight, height + padding * 2); + return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); } ClippingWrapperRectangle { @@ -61,7 +55,7 @@ Item { id: list model: ScriptModel { - values: [...Notifs.notClosed] + values: Notifs.popups.filter(n => !n.closed) } anchors.fill: parent @@ -198,52 +192,6 @@ Item { } } - Timer { - id: hideTimer - - interval: 5000 - onTriggered: { - if (list.count > 0) - root.shouldShow = false; - } - } - - function show(): void { - if (list.count > 0) { - shouldShow = true; - hideTimer.restart(); - } - } - - Connections { - target: list - - function onCountChanged(): void { - if (list.count === 0) { - root.shouldShow = false; - hideTimer.stop(); - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.NoButton - onEntered: { - if (list.count > 0) { - root.shouldShow = true; - hideTimer.restart(); - } - } - onExited: { - if (list.count > 0) { - root.shouldShow = false; - hideTimer.stop(); - } - } - } - Behavior on implicitHeight { Anim {} } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index bc5c086..95507fc 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -17,31 +17,22 @@ StyledRect { required property Notifs.Notif modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 - readonly property int nonAnimHeight: { - const baseHeight = summary.implicitHeight + inner.anchors.margins * 2; - return root.expanded - ? baseHeight + appName.height + body.height + actions.height + actions.anchors.topMargin - : baseHeight + bodyPreview.height; - } + readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 property bool expanded - property bool disableSlideIn: false - color: modelData.getBackgroundColor() + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal implicitWidth: Config.notifs.sizes.width implicitHeight: inner.implicitHeight - x: disableSlideIn ? 0 : Config.notifs.sizes.width + x: Config.notifs.sizes.width Component.onCompleted: { - if (!root.disableSlideIn) { - x = 0; - } + x = 0; modelData.lock(this); } Component.onDestruction: modelData.unlock(this) Behavior on x { - enabled: !disableSlideIn Anim { easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } @@ -143,18 +134,53 @@ StyledRect { Loader { id: appIcon - active: !root.hasImage || root.hasAppIcon - asynchronous: false + active: root.hasAppIcon || !root.hasImage + asynchronous: true anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter anchors.right: root.hasImage ? image.right : undefined anchors.bottom: root.hasImage ? image.bottom : undefined - sourceComponent: AppIconBadge { - modelData: root.modelData - hasImage: root.hasImage - hasAppIcon: root.hasAppIcon + sourceComponent: StyledRect { + radius: Appearance.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer + implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + + Loader { + id: icon + + active: root.hasAppIcon + asynchronous: true + + anchors.centerIn: parent + + width: Math.round(parent.width * 0.6) + height: Math.round(parent.width * 0.6) + + sourceComponent: ColouredIcon { + anchors.fill: parent + source: Quickshell.iconPath(root.modelData.appIcon) + colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + layer.enabled: root.modelData.appIcon.endsWith("symbolic") + } + } + + Loader { + active: !root.hasAppIcon + asynchronous: true + anchors.centerIn: parent + anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 + anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 + + sourceComponent: MaterialIcon { + text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) + + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + font.pointSize: Appearance.font.size.large + } + } } } @@ -296,7 +322,7 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: root.modelData.getStateLayerColor() + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface function onClicked() { root.expanded = !root.expanded; @@ -417,7 +443,7 @@ StyledRect { required property var modelData radius: Appearance.rounding.full - color: root.modelData.getActionBackgroundColor() + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 @@ -426,7 +452,7 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: root.modelData.getStateLayerColor() + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface function onClicked(): void { action.modelData.invoke(); @@ -438,7 +464,7 @@ StyledRect { anchors.centerIn: parent text: actionTextMetrics.elidedText - color: root.modelData.getActionTextColor() + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } diff --git a/modules/notifications/NotificationToast.qml b/modules/notifications/NotificationToast.qml deleted file mode 100644 index 90414fe..0000000 --- a/modules/notifications/NotificationToast.qml +++ /dev/null @@ -1,120 +0,0 @@ -import qs.components -import qs.components.effects -import qs.services -import qs.config -import qs.utils -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications -import QtQuick -import QtQuick.Layouts - -StyledRect { - id: root - - required property Notifs.Notif modelData - - readonly property bool hasImage: modelData.image.length > 0 - readonly property bool hasAppIcon: modelData.appIcon.length > 0 - - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 - - radius: Appearance.rounding.normal - color: Colours.palette.m3surface - - border.width: 1 - border.color: Colours.palette.m3outlineVariant - - Elevation { - anchors.fill: parent - radius: parent.radius - opacity: parent.opacity - z: -1 - level: 3 - } - - RowLayout { - id: layout - - anchors.fill: parent - anchors.margins: Appearance.padding.smaller - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - spacing: Appearance.spacing.normal - - Item { - Layout.preferredWidth: Config.notifs.sizes.image - Layout.preferredHeight: Config.notifs.sizes.image - - Loader { - id: imageLoader - - active: root.hasImage - asynchronous: true - anchors.fill: parent - - sourceComponent: ClippingRectangle { - radius: Appearance.rounding.full - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image - - Image { - anchors.fill: parent - source: Qt.resolvedUrl(root.modelData.image) - fillMode: Image.PreserveAspectCrop - cache: false - asynchronous: true - } - } - } - - Loader { - id: appIconLoader - - active: root.hasAppIcon || !root.hasImage - asynchronous: true - - anchors.horizontalCenter: root.hasImage ? undefined : parent.horizontalCenter - anchors.verticalCenter: root.hasImage ? undefined : parent.verticalCenter - anchors.right: root.hasImage ? parent.right : undefined - anchors.bottom: root.hasImage ? parent.bottom : undefined - - sourceComponent: AppIconBadge { - modelData: root.modelData - hasImage: root.hasImage - hasAppIcon: root.hasAppIcon - } - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - StyledText { - id: title - - Layout.fillWidth: true - text: root.modelData.summary - color: Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.normal - elide: Text.ElideRight - } - - StyledText { - Layout.fillWidth: true - textFormat: Text.StyledText - text: root.modelData.body - color: Colours.palette.m3onSurface - opacity: 0.8 - elide: Text.ElideRight - } - } - } - - Behavior on border.color { - CAnim {} - } -} diff --git a/modules/notifications/NotificationToasts.qml b/modules/notifications/NotificationToasts.qml deleted file mode 100644 index 96fe817..0000000 --- a/modules/notifications/NotificationToasts.qml +++ /dev/null @@ -1,186 +0,0 @@ -pragma ComponentBehavior: Bound - -import qs.components -import qs.config -import qs.services -import Quickshell -import Quickshell.Widgets -import QtQuick - -Item { - id: root - - required property Item panels - - readonly property int spacing: Appearance.spacing.small - readonly property int maxToasts: 5 - readonly property bool listVisible: panels.notifications.content.shouldShow - - property bool flag - property var activeToasts: new Set() - - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Appearance.padding.normal - - implicitWidth: Config.notifs.sizes.width - implicitHeight: { - if (listVisible) - return 0; - - let height = -spacing; - for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i) as ToastWrapper; - if (item && !item.modelData.closed && !item.previewHidden) - height += item.implicitHeight + spacing; - } - return height; - } - - opacity: listVisible ? 0 : 1 - visible: opacity > 0 - - Behavior on opacity { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - } - } - - Repeater { - id: repeater - - model: ScriptModel { - values: { - const toasts = []; - let visibleCount = 0; - - for (const notif of Notifs.list) { - if (notif.showAsToast) { - root.activeToasts.add(notif); - } - if (notif.closed) { - root.activeToasts.delete(notif); - } - } - - for (const notif of Notifs.list) { - if (root.activeToasts.has(notif)) { - toasts.push(notif); - if (notif.showAsToast && !notif.closed) { - visibleCount++; - if (visibleCount > root.maxToasts) - break; - } - } - } - return toasts; - } - onValuesChanged: root.flagChanged() - } - - ToastWrapper {} - } - - component ToastWrapper: MouseArea { - id: toast - - required property int index - required property Notifs.Notif modelData - - readonly property bool previewHidden: { - let extraHidden = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i); - if (item && item.modelData.closed) - extraHidden++; - } - return index >= root.maxToasts + extraHidden; - } - - opacity: modelData.closed || previewHidden || !modelData.showAsToast ? 0 : 1 - scale: modelData.closed || previewHidden || !modelData.showAsToast ? 0.7 : 1 - - anchors.topMargin: { - root.flag; - let margin = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i) as ToastWrapper; - if (item && !item.modelData.closed && !item.previewHidden) - margin += item.implicitHeight + root.spacing; - } - return margin; - } - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - implicitHeight: toastInner.implicitHeight - - acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton - onClicked: { - modelData.showAsToast = false; - modelData.close(); - } - - Component.onCompleted: modelData.lock(this) - - onPreviewHiddenChanged: { - if (initAnim.running && previewHidden) - initAnim.stop(); - } - - Anim { - id: initAnim - - Component.onCompleted: running = !toast.previewHidden - - target: toast - properties: "opacity,scale" - from: 0 - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - - ParallelAnimation { - running: toast.modelData.closed || (!toast.modelData.showAsToast && !toast.modelData.closed) - onStarted: toast.anchors.topMargin = toast.anchors.topMargin - onFinished: { - if (toast.modelData.closed) - toast.modelData.unlock(toast); - } - - Anim { - target: toast - property: "opacity" - to: 0 - } - Anim { - target: toast - property: "scale" - to: 0.7 - } - } - - NotificationToast { - id: toastInner - - modelData: toast.modelData - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - Behavior on anchors.topMargin { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - } -} diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 4b54883..61acc56 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -8,8 +8,6 @@ Item { required property var visibilities required property Item panels - readonly property alias content: content - visible: height > 0 implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) implicitHeight: content.implicitHeight diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index 477a23c..f475500 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -28,13 +28,14 @@ StyledRect { border.width: 1 border.color: { + let colour = Colours.palette.m3outlineVariant; if (root.modelData.type === Toast.Success) - return Colours.palette.m3success; + colour = Colours.palette.m3success; if (root.modelData.type === Toast.Warning) - return Colours.palette.m3secondaryContainer; + colour = Colours.palette.m3secondaryContainer; if (root.modelData.type === Toast.Error) - return Colours.palette.m3error; - return Colours.palette.m3outlineVariant; + colour = Colours.palette.m3error; + return Qt.alpha(colour, 0.3); } Elevation { diff --git a/services/Notifs.qml b/services/Notifs.qml index 82ed8c4..4a89c7f 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -77,10 +77,8 @@ Singleton { onNotification: notif => { notif.tracked = true; - const shouldShowAsToast = !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar); const comp = notifComp.createObject(root, { - popup: shouldShowAsToast, - showAsToast: shouldShowAsToast, + popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), notification: notif }); root.list = [comp, ...root.list]; @@ -145,7 +143,6 @@ Singleton { property bool popup property bool closed - property bool showAsToast: false property var locks: new Set() property date time: new Date() @@ -179,74 +176,7 @@ Singleton { property bool hasActionIcons property list actions - readonly property bool isCritical: urgency === NotificationUrgency.Critical - readonly property bool isLow: urgency === NotificationUrgency.Low - - function getBackgroundColor(): color { - if (isCritical) return Colours.palette.m3secondaryContainer; - return Colours.tPalette.m3surfaceContainer; - } - - function getBadgeBackgroundColor(): color { - if (isCritical) return Colours.palette.m3error; - if (isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2); - return Colours.palette.m3secondaryContainer; - } - - function getIconColor(): color { - if (isCritical) return Colours.palette.m3onError; - if (isLow) return Colours.palette.m3onSurface; - return Colours.palette.m3onSecondaryContainer; - } - - function getStateLayerColor(): color { - if (isCritical) return Colours.palette.m3onSecondaryContainer; - return Colours.palette.m3onSurface; - } - - function getActionBackgroundColor(): color { - if (isCritical) return Colours.palette.m3secondary; - return Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); - } - - function getActionTextColor(): color { - if (isCritical) return Colours.palette.m3onSecondary; - return Colours.palette.m3onSurfaceVariant; - } - readonly property Timer timer: Timer { - id: toastTimer - - running: notif.showAsToast - interval: { - let timeout = notif.expireTimeout; - if (timeout <= 0) { - switch (notif.urgency) { - case NotificationUrgency.Critical: - timeout = 10000; - break; - case NotificationUrgency.Normal: - timeout = 5000; - break; - case NotificationUrgency.Low: - timeout = 5000; - break; - default: - timeout = 5000; - } - } - return timeout; - } - onTriggered: { - if (Config.notifs.expire) - notif.popup = false; - if (notif.showAsToast) { - notif.showAsToast = false; - } - } - } - - readonly property Timer popupTimer: Timer { running: true interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout onTriggered: { -- 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') 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') 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') 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 617e686238d3c7155112196043f0883ccf6a7012 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 18:59:48 -0500 Subject: service: Nmcli.qml --- modules/controlcenter/dev/DevDebugPane.qml | 1863 +++++++++++++++++++++++++++- plan.md | 137 ++ services/Nmcli.qml | 1246 +++++++++++++++++++ 3 files changed, 3219 insertions(+), 27 deletions(-) create mode 100644 plan.md create mode 100644 services/Nmcli.qml (limited to 'services') diff --git a/modules/controlcenter/dev/DevDebugPane.qml b/modules/controlcenter/dev/DevDebugPane.qml index 88d6542..1150f35 100644 --- a/modules/controlcenter/dev/DevDebugPane.qml +++ b/modules/controlcenter/dev/DevDebugPane.qml @@ -5,7 +5,9 @@ import ".." import qs.components import qs.components.controls import qs.components.containers +import qs.components.effects import qs.config +import qs.services import Quickshell import Quickshell.Widgets import QtQuick @@ -18,61 +20,1854 @@ Item { anchors.fill: parent - ColumnLayout { + // Track last failed connection + property string lastFailedSsid: "" + + // Connect to connection failure signal + Connections { + target: Nmcli + function onConnectionFailed(ssid: string) { + root.lastFailedSsid = ssid; + appendLog("Connection failed signal received for: " + ssid); + } + } + + StyledFlickable { + id: flickable + anchors.fill: parent anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + flickableDirection: Flickable.VerticalFlick + contentWidth: width + contentHeight: contentLayout.implicitHeight + + StyledScrollBar.vertical: StyledScrollBar { + flickable: flickable + } + + ColumnLayout { + id: contentLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Debug Panel") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + // Action Buttons Section + StyledRect { + Layout.fillWidth: true + implicitHeight: buttonsLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: buttonsLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Actions") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + Flow { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + TextButton { + text: qsTr("Clear Log") + onClicked: { + debugOutput.text = ""; + appendLog("Debug log cleared"); + } + } + + TextButton { + text: qsTr("Test Action") + onClicked: { + appendLog("Test action executed at " + new Date().toLocaleTimeString()); + } + } + + TextButton { + text: qsTr("Log Network State") + onClicked: { + appendLog("Network state:"); + appendLog(" Active: " + (root.session.network.active ? "Yes" : "No")); + } + } + + TextButton { + text: qsTr("Get Device Status") + onClicked: { + appendLog("Getting device status..."); + try { + Nmcli.getDeviceStatus((output) => { + if (!output) { + appendLog(" Error: No output received"); + return; + } + appendLog("Device Status:"); + const lines = output.trim().split("\n"); + if (lines.length === 0 || (lines.length === 1 && lines[0].length === 0)) { + appendLog(" No devices found"); + } else { + for (const line of lines) { + if (line.length > 0) { + appendLog(" " + line); + } + } + } + }); + } catch (e) { + appendLog("Error: " + e); + } + } + } + + TextButton { + text: qsTr("Get Wireless Interfaces") + onClicked: { + appendLog("Getting wireless interfaces..."); + Nmcli.getWirelessInterfaces((interfaces) => { + appendLog("Wireless Interfaces: " + interfaces.length); + for (const iface of interfaces) { + appendLog(` ${iface.device}: ${iface.state} (${iface.connection})`); + } + }); + } + } + + TextButton { + text: qsTr("Get Ethernet Interfaces") + onClicked: { + appendLog("Getting ethernet interfaces..."); + Nmcli.getEthernetInterfaces((interfaces) => { + appendLog("Ethernet Interfaces: " + interfaces.length); + for (const iface of interfaces) { + appendLog(` ${iface.device}: ${iface.state} (${iface.connection})`); + } + }); + } + } + + TextButton { + text: qsTr("Refresh Status") + onClicked: { + appendLog("Refreshing connection status..."); + Nmcli.refreshStatus((status) => { + appendLog("Connection Status:"); + appendLog(" Connected: " + (status.connected ? "Yes" : "No")); + appendLog(" Interface: " + (status.interface || "None")); + appendLog(" Connection: " + (status.connection || "None")); + }); + } + } + + TextButton { + text: qsTr("Check Interface") + onClicked: { + appendLog("Checking interface connection status..."); + // Check first wireless interface if available + if (Nmcli.wirelessInterfaces.length > 0) { + const iface = Nmcli.wirelessInterfaces[0].device; + appendLog("Checking: " + iface); + Nmcli.isInterfaceConnected(iface, (connected) => { + appendLog(` ${iface}: ${connected ? "Connected" : "Disconnected"}`); + }); + } else { + appendLog("No wireless interfaces found"); + } + } + } + } + } + } + + // WiFi Radio Control Section + StyledRect { + Layout.fillWidth: true + implicitHeight: wifiRadioLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: wifiRadioLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("WiFi Radio Control") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Status: ") + (Nmcli.wifiEnabled ? qsTr("Enabled") : qsTr("Disabled")) + color: Nmcli.wifiEnabled ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Toggle WiFi") + onClicked: { + appendLog("Toggling WiFi radio..."); + Nmcli.toggleWifi((result) => { + if (result.success) { + appendLog("WiFi radio toggled: " + (Nmcli.wifiEnabled ? "Enabled" : "Disabled")); + } else { + appendLog("Failed to toggle WiFi: " + (result.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Enable") + onClicked: { + appendLog("Enabling WiFi radio..."); + Nmcli.enableWifi(true, (result) => { + if (result.success) { + appendLog("WiFi radio enabled"); + } else { + appendLog("Failed to enable WiFi: " + (result.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Disable") + onClicked: { + appendLog("Disabling WiFi radio..."); + Nmcli.enableWifi(false, (result) => { + if (result.success) { + appendLog("WiFi radio disabled"); + } else { + appendLog("Failed to disable WiFi: " + (result.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Check Status") + onClicked: { + appendLog("Checking WiFi radio status..."); + Nmcli.getWifiStatus((enabled) => { + appendLog("WiFi radio status: " + (enabled ? "Enabled" : "Disabled")); + }); + } + } + } + } + } + + // Network List Management Section + StyledRect { + Layout.fillWidth: true + implicitHeight: networkListLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: networkListLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Network List Management") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Networks: %1").arg(Nmcli.networks.length) + } + + StyledText { + visible: Nmcli.active + text: qsTr("Active: %1").arg(Nmcli.active.ssid) + color: Colours.palette.m3primary + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Networks") + onClicked: { + appendLog("Refreshing network list..."); + Nmcli.getNetworks((networks) => { + appendLog("Found " + networks.length + " networks"); + if (Nmcli.active) { + appendLog("Active network: " + Nmcli.active.ssid + " (Signal: " + Nmcli.active.strength + "%, Security: " + (Nmcli.active.isSecure ? Nmcli.active.security : "Open") + ")"); + } else { + appendLog("No active network"); + } + }); + } + } + + TextButton { + text: qsTr("List All Networks") + onClicked: { + appendLog("Network list:"); + if (Nmcli.networks.length === 0) { + appendLog(" No networks found"); + } else { + for (let i = 0; i < Nmcli.networks.length; i++) { + const net = Nmcli.networks[i]; + const activeMark = net.active ? " [ACTIVE]" : ""; + appendLog(` ${i + 1}. ${net.ssid}${activeMark}`); + appendLog(` Signal: ${net.strength}%, Freq: ${net.frequency}MHz, Security: ${net.isSecure ? net.security : "Open"}`); + if (net.bssid) { + appendLog(` BSSID: ${net.bssid}`); + } + } + } + } + } + } + } + } + + // Interface Selector Section (for future features) + Item { + Layout.fillWidth: true + implicitHeight: interfaceSelectorContainer.implicitHeight + z: 10 // Ensure dropdown menu appears above other elements + + StyledRect { + id: interfaceSelectorContainer + + anchors.fill: parent + implicitHeight: interfaceSelectorLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: interfaceSelectorLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Interface Selector") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + SplitButton { + id: interfaceSelector + + type: SplitButton.Tonal + fallbackText: qsTr("Select Interface") + fallbackIcon: "settings_ethernet" + menuItems: interfaceList.instances + menuOnTop: true // Position menu above button to avoid being covered + + property string selectedInterface: "" + + menu.onItemSelected: (item) => { + interfaceSelector.selectedInterface = item.modelData.device; + appendLog("Selected interface: " + item.modelData.device + " (" + item.modelData.type + ")"); + } + + Variants { + id: interfaceList + + model: interfaceSelector.interfaces + + MenuItem { + required property var modelData + + text: modelData.device + " (" + modelData.type + ")" + icon: modelData.type === "wifi" ? "wifi" : "settings_ethernet" + } + } + + property list interfaces: [] + + function refreshInterfaces(): void { + appendLog("Refreshing interface list..."); + Nmcli.getAllInterfaces((interfaces) => { + interfaceSelector.interfaces = interfaces; + if (interfaces.length > 0) { + // Wait for Variants to create instances, then set active + Qt.callLater(() => { + if (interfaceList.instances.length > 0) { + interfaceSelector.active = interfaceList.instances[0]; + interfaceSelector.selectedInterface = interfaces[0].device; + } + }); + appendLog("Found " + interfaces.length + " interfaces"); + } else { + interfaceSelector.selectedInterface = ""; + appendLog("No interfaces found"); + } + }); + } + + Component.onCompleted: { + // Ensure menu appears above other elements + menu.z = 100; + } + } + + TextButton { + text: qsTr("Refresh") + onClicked: { + interfaceSelector.refreshInterfaces(); + } + } + + TextButton { + text: qsTr("Up") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + appendLog("Bringing interface up: " + interfaceSelector.selectedInterface); + Nmcli.bringInterfaceUp(interfaceSelector.selectedInterface, (result) => { + if (result.success) { + appendLog("Interface up: Success"); + } else { + appendLog("Interface up: Failed (exit code: " + result.exitCode + ")"); + if (result.error && result.error.length > 0) { + appendLog("Error: " + result.error); + } + } + // Refresh interface list after bringing up + Qt.callLater(() => { + interfaceSelector.refreshInterfaces(); + }, 500); + }); + } + } + } + + TextButton { + text: qsTr("Down") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + appendLog("Bringing interface down: " + interfaceSelector.selectedInterface); + Nmcli.bringInterfaceDown(interfaceSelector.selectedInterface, (result) => { + if (result.success) { + appendLog("Interface down: Success"); + } else { + appendLog("Interface down: Failed (exit code: " + result.exitCode + ")"); + if (result.error && result.error.length > 0) { + appendLog("Error: " + result.error); + } + } + // Refresh interface list after bringing down + Qt.callLater(() => { + interfaceSelector.refreshInterfaces(); + }, 500); + }); + } + } + } + } + } + } + } + + // Wireless SSID Selector Section + Item { + Layout.fillWidth: true + implicitHeight: ssidSelectorContainer.implicitHeight + z: 10 // Ensure dropdown menu appears above other elements + + StyledRect { + id: ssidSelectorContainer + + anchors.fill: parent + implicitHeight: ssidSelectorLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: ssidSelectorLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Wireless SSID Selector") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + SplitButton { + id: ssidSelector + + type: SplitButton.Tonal + fallbackText: qsTr("Select SSID") + fallbackIcon: "wifi" + menuItems: ssidList.instances + menuOnTop: true + + property string selectedSSID: "" + + menu.onItemSelected: (item) => { + ssidSelector.selectedSSID = item.modelData.ssid; + appendLog("Selected SSID: " + item.modelData.ssid + " (Signal: " + item.modelData.signal + ", Security: " + item.modelData.security + ")"); + } + + Component.onCompleted: { + // Ensure menu appears above other elements + menu.z = 100; + } + + Variants { + id: ssidList + + model: ssidSelector.ssids + + MenuItem { + required property var modelData + + text: modelData.ssid + (modelData.signal ? " (" + modelData.signal + "%)" : "") + icon: "wifi" + } + } + + property list ssids: [] + + function scanForSSIDs(): void { + appendLog("Scanning for wireless networks..."); + // Use first wireless interface if available, or let nmcli choose + let iface = ""; + if (interfaceSelector.selectedInterface) { + // Check if selected interface is wireless + for (const i of interfaceSelector.interfaces) { + if (i.device === interfaceSelector.selectedInterface && i.type === "wifi") { + iface = interfaceSelector.selectedInterface; + break; + } + } + } + + // If no wireless interface selected, use first available + if (!iface && Nmcli.wirelessInterfaces.length > 0) { + iface = Nmcli.wirelessInterfaces[0].device; + } + + Nmcli.scanWirelessNetworks(iface, (scanResult) => { + if (scanResult.success) { + appendLog("Scan completed, fetching SSID list..."); + // Wait a moment for scan results to be available + Qt.callLater(() => { + Nmcli.getWirelessSSIDs(iface, (ssids) => { + ssidSelector.ssids = ssids; + if (ssids.length > 0) { + Qt.callLater(() => { + if (ssidList.instances.length > 0) { + ssidSelector.active = ssidList.instances[0]; + ssidSelector.selectedSSID = ssids[0].ssid; + } + }); + appendLog("Found " + ssids.length + " SSIDs"); + } else { + appendLog("No SSIDs found"); + } + }); + }, 1000); + } else { + appendLog("Scan failed: " + (scanResult.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Scan") + onClicked: { + ssidSelector.scanForSSIDs(); + } + } + } + } + } + } + + // Wireless Connection Test Section + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionTestLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionTestLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Wireless Connection Test") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("SSID: %1").arg(ssidSelector.selectedSSID || "None selected") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Connect (No Password)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + appendLog("Connecting to: " + ssidSelector.selectedSSID + " (no password)"); + // Find the network to get BSSID + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network ? network.bssid : ""; + Nmcli.connectWireless(ssidSelector.selectedSSID, "", bssid, (result) => { + if (result.success) { + appendLog("Connection succeeded!"); + // Refresh network list after connection + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else { + appendLog("Connection failed: " + (result.error || "Unknown error")); + // Refresh network list anyway to check status + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } + }); + appendLog("Connection initiated, tracking pending connection..."); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Password:") + } + + Item { + Layout.fillWidth: true + implicitHeight: passwordField.implicitHeight + Appearance.padding.small * 2 + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + border.width: passwordField.activeFocus ? 2 : 1 + border.color: passwordField.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + + Behavior on border.color { + CAnim {} + } + } + + StyledTextField { + id: passwordField + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + echoMode: TextField.Password + placeholderText: qsTr("Enter password") + + Keys.onReturnPressed: { + if (connectWithPasswordButton.enabled) { + connectWithPasswordButton.clicked(); + } + } + Keys.onEnterPressed: { + if (connectWithPasswordButton.enabled) { + connectWithPasswordButton.clicked(); + } + } + } + } + + TextButton { + id: connectWithPasswordButton + text: qsTr("Connect") + enabled: ssidSelector.selectedSSID.length > 0 && passwordField.text.length > 0 + onClicked: { + if (ssidSelector.selectedSSID && passwordField.text) { + appendLog("Connecting to: " + ssidSelector.selectedSSID + " (with password)"); + // Find the network to get BSSID + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network ? network.bssid : ""; + Nmcli.connectWireless(ssidSelector.selectedSSID, passwordField.text, bssid, (result) => { + if (result.success) { + appendLog("Connection succeeded!"); + // Clear password field + passwordField.text = ""; + // Refresh network list after connection + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else { + appendLog("Connection failed: " + (result.error || "Unknown error")); + if (result.exitCode !== 0) { + appendLog("Exit code: " + result.exitCode); + } + // Refresh network list anyway to check status + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } + }); + appendLog("Connection initiated, tracking pending connection..."); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network && network.bssid ? network.bssid : "N/A"; + return qsTr("BSSID: %1").arg(bssid); + } + } + + Item { + Layout.fillWidth: true + } + } + } + } + + // Saved Connection Profiles Section + StyledRect { + Layout.fillWidth: true + implicitHeight: savedProfilesLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: savedProfilesLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Saved Connection Profiles") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Connections: %1").arg(Nmcli.savedConnections.length) + } + + StyledText { + text: qsTr("WiFi SSIDs: %1").arg(Nmcli.savedConnectionSsids.length) + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh") + onClicked: { + appendLog("Refreshing saved connections..."); + Nmcli.loadSavedConnections((ssids) => { + appendLog("Found " + Nmcli.savedConnections.length + " saved connections"); + appendLog("Found " + Nmcli.savedConnectionSsids.length + " WiFi SSIDs"); + }); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Selected SSID: %1").arg(ssidSelector.selectedSSID || "None") + } + + StyledText { + visible: ssidSelector.selectedSSID.length > 0 + text: { + if (!ssidSelector.selectedSSID) return ""; + const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + return hasProfile ? qsTr("[Saved Profile]") : qsTr("[Not Saved]"); + } + color: { + if (!ssidSelector.selectedSSID) return Colours.palette.m3onSurface; + const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + return hasProfile ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant; + } + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Check Profile") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + appendLog("Profile check for '" + ssidSelector.selectedSSID + "': " + (hasProfile ? "Saved" : "Not saved")); + } + } + } + + TextButton { + text: qsTr("Forget Network") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + appendLog("Forgetting network: " + ssidSelector.selectedSSID); + Nmcli.forgetNetwork(ssidSelector.selectedSSID, (result) => { + if (result.success) { + appendLog("Network forgotten successfully"); + } else { + appendLog("Failed to forget network: " + (result.error || "Unknown error")); + } + }); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + TextButton { + text: qsTr("List All Saved SSIDs") + onClicked: { + appendLog("Saved WiFi SSIDs:"); + if (Nmcli.savedConnectionSsids.length === 0) { + appendLog(" No saved SSIDs"); + } else { + for (let i = 0; i < Nmcli.savedConnectionSsids.length; i++) { + appendLog(" " + (i + 1) + ". " + Nmcli.savedConnectionSsids[i]); + } + } + } + } + + TextButton { + text: qsTr("List All Connections") + onClicked: { + appendLog("Saved Connections:"); + if (Nmcli.savedConnections.length === 0) { + appendLog(" No saved connections"); + } else { + for (let i = 0; i < Nmcli.savedConnections.length; i++) { + appendLog(" " + (i + 1) + ". " + Nmcli.savedConnections[i]); + } + } + } + } + } + } + } + + // Pending Connection Tracking Section + StyledRect { + Layout.fillWidth: true + implicitHeight: pendingConnectionLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: pendingConnectionLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Pending Connection Tracking") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Status: %1").arg(Nmcli.pendingConnection ? "Connecting..." : "No pending connection") + color: Nmcli.pendingConnection ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + StyledText { + visible: Nmcli.pendingConnection + text: qsTr("SSID: %1").arg(Nmcli.pendingConnection ? Nmcli.pendingConnection.ssid : "") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Check Status") + onClicked: { + if (Nmcli.pendingConnection) { + appendLog("Pending connection: " + Nmcli.pendingConnection.ssid); + appendLog("BSSID: " + (Nmcli.pendingConnection.bssid || "N/A")); + const connected = Nmcli.active && Nmcli.active.ssid === Nmcli.pendingConnection.ssid; + appendLog("Connected: " + (connected ? "Yes" : "No")); + if (connected) { + appendLog("Connection succeeded!"); + } else { + appendLog("Still connecting..."); + } + } else { + appendLog("No pending connection"); + } + } + } + + TextButton { + text: qsTr("Clear Pending") + enabled: Nmcli.pendingConnection !== null + onClicked: { + if (Nmcli.pendingConnection) { + appendLog("Clearing pending connection: " + Nmcli.pendingConnection.ssid); + Nmcli.pendingConnection = null; + appendLog("Pending connection cleared"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Active Network: %1").arg(Nmcli.active ? Nmcli.active.ssid : "None") + color: Nmcli.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Networks & Check") + onClicked: { + appendLog("Refreshing network list to check pending connection..."); + Nmcli.getNetworks((networks) => { + appendLog("Network list refreshed"); + if (Nmcli.pendingConnection) { + const connected = Nmcli.active && Nmcli.active.ssid === Nmcli.pendingConnection.ssid; + appendLog("Pending connection check: " + (connected ? "Connected!" : "Still connecting...")); + } + }); + } + } + } + } + } + + // Connection Failure Handling Section + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionFailureLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionFailureLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Connection Failure Handling") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Last Failed SSID: %1").arg(lastFailedSsid || "None") + color: lastFailedSsid ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Clear Failure") + enabled: lastFailedSsid.length > 0 + onClicked: { + lastFailedSsid = ""; + appendLog("Cleared failure status"); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Test Password Detection") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Secure Network (No Password)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && network.isSecure) { + appendLog("Testing connection to secure network without password (should detect password requirement)"); + const bssid = network ? network.bssid : ""; + Nmcli.connectWireless(ssidSelector.selectedSSID, "", bssid, (result) => { + if (result.needsPassword) { + appendLog("✓ Password requirement detected correctly!"); + appendLog("Error: " + (result.error || "N/A")); + } else if (result.success) { + appendLog("Connection succeeded (saved password used)"); + } else { + appendLog("Connection failed: " + (result.error || "Unknown error")); + } + }); + } else { + appendLog("Selected network is not secure, cannot test password detection"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Connection Retry Test") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Retry Logic") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + appendLog("Testing connection retry logic (will retry up to 2 times on failure)"); + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network ? network.bssid : ""; + // Use invalid password to trigger failure + Nmcli.connectWireless(ssidSelector.selectedSSID, "invalid_password_test", bssid, (result) => { + if (result.success) { + appendLog("Connection succeeded (unexpected)"); + } else { + appendLog("Connection failed after retries: " + (result.error || "Unknown error")); + } + }); + } + } + } + } + } + } + + // Password Callback Handling Section + StyledRect { + Layout.fillWidth: true + implicitHeight: passwordCallbackLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: passwordCallbackLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Password Callback Handling") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Selected SSID: %1").arg(ssidSelector.selectedSSID || "None") + } + + StyledText { + visible: ssidSelector.selectedSSID.length > 0 + text: { + if (!ssidSelector.selectedSSID) return ""; + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (!network) return ""; + return network.isSecure ? qsTr("[Secure]") : qsTr("[Open]"); + } + color: { + if (!ssidSelector.selectedSSID) return Colours.palette.m3onSurface; + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (!network) return Colours.palette.m3onSurface; + return network.isSecure ? Colours.palette.m3error : Colours.palette.m3primary; + } + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Password Check (Secure)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && network.isSecure) { + appendLog("Testing password check for secure network: " + ssidSelector.selectedSSID); + appendLog("This will try saved password first, then prompt if needed"); + const bssid = network ? network.bssid : ""; + Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, true, (result) => { + if (result.success) { + if (result.usedSavedPassword) { + appendLog("✓ Connection succeeded using saved password!"); + } else { + appendLog("✓ Connection succeeded!"); + } + // Refresh network list + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else if (result.needsPassword) { + appendLog("→ Password required - callback triggered"); + appendLog(" Error: " + (result.error || "N/A")); + appendLog(" (In real UI, this would show password dialog)"); + } else { + appendLog("✗ Connection failed: " + (result.error || "Unknown error")); + } + }, bssid); + } else { + appendLog("Selected network is not secure, cannot test password check"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Test Open Network") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Password Check (Open)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && !network.isSecure) { + appendLog("Testing password check for open network: " + ssidSelector.selectedSSID); + appendLog("Open networks should connect directly without password"); + const bssid = network ? network.bssid : ""; + Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, false, (result) => { + if (result.success) { + appendLog("✓ Connection succeeded!"); + // Refresh network list + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else { + appendLog("✗ Connection failed: " + (result.error || "Unknown error")); + } + }, bssid); + } else { + appendLog("Selected network is not open, cannot test open network handling"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Test with Saved Password") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Secure Network (Has Saved Password)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && network.isSecure) { + const hasSaved = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + appendLog("Testing password check for: " + ssidSelector.selectedSSID); + appendLog("Has saved profile: " + (hasSaved ? "Yes" : "No")); + if (hasSaved) { + appendLog("This should connect using saved password without prompting"); + } else { + appendLog("This should prompt for password since no saved profile exists"); + } + const bssid = network ? network.bssid : ""; + Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, true, (result) => { + if (result.success) { + if (result.usedSavedPassword) { + appendLog("✓ Connection succeeded using saved password!"); + } else { + appendLog("✓ Connection succeeded!"); + } + // Refresh network list + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else if (result.needsPassword) { + appendLog("→ Password required - callback triggered"); + appendLog(" (In real UI, this would show password dialog)"); + } else { + appendLog("✗ Connection failed: " + (result.error || "Unknown error")); + } + }, bssid); + } else { + appendLog("Selected network is not secure, cannot test saved password"); + } + } + } + } + } + } + } + + // Device Details Parsing Section + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceDetailsLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: deviceDetailsLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Device Details Parsing") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Wireless Device Details") + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Get Wireless Details") + onClicked: { + const activeInterface = interfaceSelector.selectedInterface; + if (activeInterface && activeInterface.length > 0) { + appendLog("Getting wireless device details for: " + activeInterface); + Nmcli.getWirelessDeviceDetails(activeInterface, (details) => { + if (details) { + appendLog("Wireless Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("Failed to get wireless device details"); + } + }); + } else { + appendLog("Getting wireless device details for active interface"); + Nmcli.getWirelessDeviceDetails("", (details) => { + if (details) { + appendLog("Wireless Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No active wireless interface or failed to get details"); + } + }); + } + } + } + + TextButton { + text: qsTr("Show Current") + onClicked: { + if (Nmcli.wirelessDeviceDetails) { + const details = Nmcli.wirelessDeviceDetails; + appendLog("Current Wireless Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No wireless device details available"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Ethernet Device Details") + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Get Ethernet Details") + onClicked: { + const activeInterface = interfaceSelector.selectedInterface; + if (activeInterface && activeInterface.length > 0) { + appendLog("Getting ethernet device details for: " + activeInterface); + Nmcli.getEthernetDeviceDetails(activeInterface, (details) => { + if (details) { + appendLog("Ethernet Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" Speed: " + (details.speed || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("Failed to get ethernet device details"); + } + }); + } else { + appendLog("Getting ethernet device details for active interface"); + Nmcli.getEthernetDeviceDetails("", (details) => { + if (details) { + appendLog("Ethernet Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" Speed: " + (details.speed || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No active ethernet interface or failed to get details"); + } + }); + } + } + } + + TextButton { + text: qsTr("Show Current") + onClicked: { + if (Nmcli.ethernetDeviceDetails) { + const details = Nmcli.ethernetDeviceDetails; + appendLog("Current Ethernet Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" Speed: " + (details.speed || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No ethernet device details available"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - StyledText { - text: qsTr("Debug Panel") - font.pointSize: Appearance.font.size.larger - font.weight: 500 + StyledText { + text: qsTr("CIDR to Subnet Mask Test") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test CIDR Conversion") + onClicked: { + appendLog("Testing CIDR to Subnet Mask conversion:"); + const testCases = ["8", "16", "24", "32", "0", "25", "30"]; + for (let i = 0; i < testCases.length; i++) { + const cidr = testCases[i]; + const subnet = Nmcli.cidrToSubnetMask(cidr); + appendLog(" /" + cidr + " -> " + (subnet || "Invalid")); + } + } + } + } + } } - // Action Buttons Section + // Connection Status Monitoring Section StyledRect { Layout.fillWidth: true - implicitHeight: buttonsLayout.implicitHeight + Appearance.padding.large * 2 + implicitHeight: connectionMonitoringLayout.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { - id: buttonsLayout + id: connectionMonitoringLayout anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { - text: qsTr("Actions") + text: qsTr("Connection Status Monitoring") font.pointSize: Appearance.font.size.normal font.weight: 500 } - Flow { + RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.small + StyledText { + text: qsTr("Active Network: %1").arg(Nmcli.active ? Nmcli.active.ssid : "None") + color: Nmcli.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + StyledText { + visible: Nmcli.active + text: Nmcli.active ? qsTr("Signal: %1%").arg(Nmcli.active.strength) : "" + } + + Item { + Layout.fillWidth: true + } + TextButton { - text: qsTr("Clear Log") + text: qsTr("Refresh Networks") onClicked: { - debugOutput.text = ""; - appendLog("Debug log cleared"); + appendLog("Manually refreshing network list..."); + Nmcli.getNetworks((networks) => { + appendLog("Network list refreshed: " + networks.length + " networks"); + if (Nmcli.active) { + appendLog("Active network: " + Nmcli.active.ssid); + } else { + appendLog("No active network"); + } + }); } } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Monitor Status") + } + + StyledText { + text: qsTr("Monitoring connection changes (automatic refresh enabled)") + color: Colours.palette.m3primary + } + + Item { + Layout.fillWidth: true + } TextButton { - text: qsTr("Test Action") + text: qsTr("Test Connection Change") onClicked: { - appendLog("Test action executed at " + new Date().toLocaleTimeString()); + appendLog("Testing connection change detection..."); + appendLog("This will trigger a manual refresh to simulate a connection change"); + Nmcli.refreshOnConnectionChange(); + appendLog("Refresh triggered - check if network list and device details updated"); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Device Details Auto-Refresh") + } + + StyledText { + text: { + if (Nmcli.wirelessDeviceDetails) { + return qsTr("Wireless: %1").arg(Nmcli.wirelessDeviceDetails.ipAddress || "N/A"); + } else if (Nmcli.ethernetDeviceDetails) { + return qsTr("Ethernet: %1").arg(Nmcli.ethernetDeviceDetails.ipAddress || "N/A"); + } else { + return qsTr("No device details"); + } } + color: (Nmcli.wirelessDeviceDetails || Nmcli.ethernetDeviceDetails) ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true } TextButton { - text: qsTr("Log Network State") + text: qsTr("Refresh Device Details") onClicked: { - appendLog("Network state:"); - appendLog(" Active: " + (root.session.network.active ? "Yes" : "No")); + appendLog("Manually refreshing device details..."); + if (Nmcli.active && Nmcli.active.active) { + appendLog("Active network detected, refreshing device details..."); + // Refresh wireless device details + if (Nmcli.wirelessInterfaces.length > 0) { + const activeWireless = Nmcli.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + Nmcli.getWirelessDeviceDetails(activeWireless.device, (details) => { + if (details) { + appendLog("Wireless device details refreshed"); + } + }); + } + } + // Refresh ethernet device details + if (Nmcli.ethernetInterfaces.length > 0) { + const activeEthernet = Nmcli.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + Nmcli.getEthernetDeviceDetails(activeEthernet.device, (details) => { + if (details) { + appendLog("Ethernet device details refreshed"); + } + }); + } + } + } else { + appendLog("No active network, clearing device details"); + Nmcli.wirelessDeviceDetails = null; + Nmcli.ethernetDeviceDetails = null; + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Connection Events") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Show Active Network Info") + onClicked: { + if (Nmcli.active) { + appendLog("Active Network Information:"); + appendLog(" SSID: " + Nmcli.active.ssid); + appendLog(" BSSID: " + (Nmcli.active.bssid || "N/A")); + appendLog(" Signal: " + Nmcli.active.strength + "%"); + appendLog(" Frequency: " + Nmcli.active.frequency + " MHz"); + appendLog(" Security: " + (Nmcli.active.security || "Open")); + appendLog(" Is Secure: " + (Nmcli.active.isSecure ? "Yes" : "No")); + } else { + appendLog("No active network"); + } + } + } + } + } + } + + // Ethernet Device Management Section + StyledRect { + Layout.fillWidth: true + implicitHeight: ethernetManagementLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: ethernetManagementLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Ethernet Device Management") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Ethernet Devices: %1").arg(Nmcli.ethernetDevices.length) + } + + StyledText { + text: qsTr("Active: %1").arg(Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : "None") + color: Nmcli.activeEthernet ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Devices") + onClicked: { + appendLog("Refreshing ethernet devices..."); + Nmcli.getEthernetInterfaces((interfaces) => { + appendLog("Found " + Nmcli.ethernetDevices.length + " ethernet devices"); + for (let i = 0; i < Nmcli.ethernetDevices.length; i++) { + const dev = Nmcli.ethernetDevices[i]; + appendLog(" " + (i + 1) + ". " + dev.interface + " - " + dev.state + (dev.connected ? " [Connected]" : "")); + } + if (Nmcli.activeEthernet) { + appendLog("Active ethernet: " + Nmcli.activeEthernet.interface); + } + }); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Selected Interface: %1").arg(interfaceSelector.selectedInterface || "None") + } + + StyledText { + visible: interfaceSelector.selectedInterface.length > 0 + text: { + if (!interfaceSelector.selectedInterface) return ""; + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (!device) return ""; + return device.connected ? qsTr("[Connected]") : qsTr("[Disconnected]"); + } + color: { + if (!interfaceSelector.selectedInterface) return Colours.palette.m3onSurface; + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (!device) return Colours.palette.m3onSurface; + return device.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant; + } + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Connect Ethernet") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (device) { + appendLog("Connecting ethernet: " + interfaceSelector.selectedInterface); + appendLog("Connection name: " + (device.connection || "N/A")); + Nmcli.connectEthernet(device.connection || "", interfaceSelector.selectedInterface, (result) => { + if (result.success) { + appendLog("✓ Ethernet connection initiated"); + appendLog("Refreshing device list..."); + } else { + appendLog("✗ Failed to connect: " + (result.error || "Unknown error")); + } + }); + } else { + appendLog("Device not found in ethernet devices list"); + } + } + } + } + + TextButton { + text: qsTr("Disconnect Ethernet") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (device && device.connection) { + appendLog("Disconnecting ethernet: " + device.connection); + Nmcli.disconnectEthernet(device.connection, (result) => { + if (result.success) { + appendLog("✓ Ethernet disconnected"); + appendLog("Refreshing device list..."); + } else { + appendLog("✗ Failed to disconnect: " + (result.error || "Unknown error")); + } + }); + } else { + appendLog("No connection name available for this device"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("List All Ethernet Devices") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("List Devices") + onClicked: { + appendLog("Ethernet Devices:"); + if (Nmcli.ethernetDevices.length === 0) { + appendLog(" No ethernet devices found"); + } else { + for (let i = 0; i < Nmcli.ethernetDevices.length; i++) { + const dev = Nmcli.ethernetDevices[i]; + appendLog(" " + (i + 1) + ". " + dev.interface); + appendLog(" Type: " + dev.type); + appendLog(" State: " + dev.state); + appendLog(" Connection: " + (dev.connection || "None")); + appendLog(" Connected: " + (dev.connected ? "Yes" : "No")); + } + } + } + } + + TextButton { + text: qsTr("Show Active Device") + onClicked: { + if (Nmcli.activeEthernet) { + appendLog("Active Ethernet Device:"); + appendLog(" Interface: " + Nmcli.activeEthernet.interface); + appendLog(" State: " + Nmcli.activeEthernet.state); + appendLog(" Connection: " + (Nmcli.activeEthernet.connection || "None")); + } else { + appendLog("No active ethernet device"); + } } } } @@ -82,7 +1877,8 @@ Item { // Debug Output Section StyledRect { Layout.fillWidth: true - Layout.fillHeight: true + Layout.preferredHeight: 300 + Layout.minimumHeight: 200 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer @@ -116,17 +1912,17 @@ Item { } StyledFlickable { - id: flickable + id: debugOutputFlickable Layout.fillWidth: true Layout.fillHeight: true flickableDirection: Flickable.VerticalFlick - contentHeight: debugOutput.implicitHeight + clip: true TextEdit { id: debugOutput - width: flickable.width + width: debugOutputFlickable.width readOnly: true wrapMode: TextEdit.Wrap font.family: Appearance.font.family.mono @@ -143,19 +1939,25 @@ Item { onTextChanged: { // Ensure color stays set when text changes color = Colours.palette.m3primary; - if (flickable.contentHeight > flickable.height) { - flickable.contentY = flickable.contentHeight - flickable.height; - } + // Update content height + debugOutputFlickable.contentHeight = Math.max(implicitHeight, debugOutputFlickable.height); + // Auto-scroll to bottom + Qt.callLater(() => { + if (debugOutputFlickable.contentHeight > debugOutputFlickable.height) { + debugOutputFlickable.contentY = debugOutputFlickable.contentHeight - debugOutputFlickable.height; + } + }); } } } StyledScrollBar { - flickable: flickable + flickable: debugOutputFlickable policy: ScrollBar.AlwaysOn } } } + } } function appendLog(message: string): void { @@ -166,5 +1968,12 @@ Item { function log(message: string): void { appendLog(message); } + + Component.onCompleted: { + // Set up debug logger for Nmcli service + Nmcli.setDebugLogger((msg) => { + appendLog("[Nmcli] " + msg); + }); + } } diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..4762ef0 --- /dev/null +++ b/plan.md @@ -0,0 +1,137 @@ +# Nmcli.qml Feature Completion Plan + +This document outlines the missing features needed in `Nmcli.qml` to replace `Network.qml` or rewrite the wireless panel in the control center. + +## Current Status + +`Nmcli.qml` currently has: +- ✅ Device status queries +- ✅ Wireless/Ethernet interface listing +- ✅ Interface connection status checking +- ✅ Basic wireless connection (SSID + password) +- ✅ Disconnect functionality +- ✅ Device details (basic) +- ✅ Interface up/down +- ✅ WiFi scanning +- ✅ SSID listing with signal/security (sorted) + +## Missing Features + +### 1. WiFi Radio Control +- [x] `enableWifi(enabled: bool)` - Turn WiFi radio on/off +- [x] `toggleWifi()` - Toggle WiFi radio state +- [x] `wifiEnabled` property - Current WiFi radio state +- [x] Monitor WiFi radio state changes + +**Implementation Notes:** +- Use `nmcli radio wifi on/off` +- Monitor state with `nmcli radio wifi` +- Update `wifiEnabled` property on state changes + +### 2. Network List Management +- [x] `networks` property - List of AccessPoint objects +- [x] `active` property - Currently active network +- [x] Real-time network list updates +- [x] Network grouping by SSID with signal prioritization +- [x] AccessPoint component/object with properties: + - `ssid`, `bssid`, `strength`, `frequency`, `active`, `security`, `isSecure` + +**Implementation Notes:** +- Use `nmcli -g ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY d w` +- Parse and group networks by SSID +- Prioritize active/connected networks +- Update network list on connection changes + +### 3. Connection Management - BSSID Support +- [x] BSSID support in `connectWireless()` function +- [x] Connection profile creation with BSSID (`createConnectionWithPassword`) +- [x] Handle BSSID in connection commands + +**Implementation Notes:** +- Use `nmcli connection add` with `802-11-wireless.bssid` for BSSID-based connections +- Fallback to SSID-only connection if BSSID not available +- Handle existing connection profiles when BSSID is provided + +### 4. Saved Connection Profile Management +- [x] `savedConnections` property - List of saved connection names +- [x] `savedConnectionSsids` property - List of saved SSIDs +- [x] `hasSavedProfile(ssid: string)` function - Check if profile exists +- [x] `forgetNetwork(ssid: string)` function - Delete connection profile +- [x] Load saved connections on startup +- [x] Update saved connections list after connection changes + +**Implementation Notes:** +- Use `nmcli -t -f NAME,TYPE connection show` to list connections +- Query SSIDs for WiFi connections: `nmcli -t -f 802-11-wireless.ssid connection show ` +- Use `nmcli connection delete ` to forget networks +- Case-insensitive SSID matching + +### 5. Pending Connection Tracking +- [x] `pendingConnection` property - Track connection in progress +- [x] Connection state tracking with timers +- [x] Connection success/failure detection +- [x] Automatic retry or callback on failure + +**Implementation Notes:** +- Track pending connection with SSID/BSSID +- Use timers to check connection status +- Monitor network list updates to detect successful connection +- Handle connection failures and trigger callbacks + +### 6. Connection Failure Handling +- [x] `connectionFailed(ssid: string)` signal +- [x] Password requirement detection from error messages +- [x] Connection retry logic +- [x] Error message parsing and reporting + +**Implementation Notes:** +- Parse stderr output for password requirements +- Detect specific error patterns (e.g., "Secrets were required") +- Emit signals for UI to handle password dialogs +- Provide meaningful error messages + +### 7. Password Callback Handling +- [x] `connectToNetworkWithPasswordCheck()` function +- [x] Try connection without password first (use saved password) +- [x] Callback on password requirement +- [x] Handle both secure and open networks + +**Implementation Notes:** +- Attempt connection without password for secure networks +- If connection fails with password error, trigger callback +- For open networks, connect directly +- Support callback pattern for password dialogs + +### 8. Device Details Parsing +- [x] Full parsing of `device show` output +- [x] `wirelessDeviceDetails` property with: + - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress` +- [x] `ethernetDeviceDetails` property with: + - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress`, `speed` +- [x] `cidrToSubnetMask()` helper function +- [x] Update device details on connection changes + +**Implementation Notes:** +- Parse `nmcli device show ` output +- Extract IP4.ADDRESS, IP4.GATEWAY, IP4.DNS, etc. +- Convert CIDR notation to subnet mask +- Handle both wireless and ethernet device details + +### 9. Connection Status Monitoring +- [x] Automatic network list refresh on connection changes +- [x] Monitor connection state changes +- [x] Update active network property +- [x] Refresh device details on connection + +**Implementation Notes:** +- Use Process stdout SplitParser to monitor changes +- Trigger network list refresh on connection events +- Update `active` property when connection changes +- Refresh device details when connected + +### 10. Ethernet Device Management +- [x] `ethernetDevices` property - List of ethernet devices +- [x] `activeEthernet` property - Currently active ethernet device +- [x] `connectEthernet(connectionName, interfaceName)` function +- [x] `disconnectEthernet(connectionName)` function +- [x] Ethernet device details parsing diff --git a/services/Nmcli.qml b/services/Nmcli.qml new file mode 100644 index 0000000..4e45b41 --- /dev/null +++ b/services/Nmcli.qml @@ -0,0 +1,1246 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property var deviceStatus: null + property var wirelessInterfaces: [] + property var ethernetInterfaces: [] + property bool isConnected: false + property string activeInterface: "" + property string activeConnection: "" + property bool wifiEnabled: true + readonly property list networks: [] + readonly property AccessPoint active: networks.find(n => n.active) ?? null + property list savedConnections: [] + property list savedConnectionSsids: [] + + property var wifiConnectionQueue: [] + property int currentSsidQueryIndex: 0 + property var pendingConnection: null + signal connectionFailed(string ssid) + property var wirelessDeviceDetails: null + property var ethernetDeviceDetails: null + property list ethernetDevices: [] + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null + + property list activeProcesses: [] + property var debugLogger: null + + function setDebugLogger(logger: var): void { + root.debugLogger = logger; + } + + function log(message: string): void { + if (root.debugLogger) { + root.debugLogger(message); + } else { + console.log("[Nmcli]", message); + } + } + + function appendLog(message: string): void { + log(message); + } + + function executeCommand(args: list, callback: var): void { + const proc = commandProc.createObject(root); + proc.command = ["nmcli", ...args]; + proc.callback = callback; + + activeProcesses.push(proc); + + proc.processFinished.connect(() => { + const index = activeProcesses.indexOf(proc); + if (index >= 0) { + activeProcesses.splice(index, 1); + } + }); + + Qt.callLater(() => { + proc.exec(proc.command); + }); + } + + function getDeviceStatus(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + if (callback) callback(result.output); + }); + } + + function getWirelessInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && parts[1] === "wifi") { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + root.wirelessInterfaces = interfaces; + if (callback) callback(interfaces); + }); + } + + function getEthernetInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const devices = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && parts[1] === "ethernet") { + const device = parts[0] || ""; + const type = parts[1] || ""; + const state = parts[2] || ""; + const connection = parts[3] || ""; + + const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); + + interfaces.push({ + device: device, + type: type, + state: state, + connection: connection + }); + + devices.push({ + interface: device, + type: type, + state: state, + connection: connection, + connected: connected, + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }); + } + } + root.ethernetInterfaces = interfaces; + root.ethernetDevices = devices; + if (callback) callback(interfaces); + }); + } + + function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { + if (connectionName && connectionName.length > 0) { + executeCommand(["connection", "up", connectionName], (result) => { + if (result.success) { + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + if (interfaceName && interfaceName.length > 0) { + Qt.callLater(() => { + getEthernetDeviceDetails(interfaceName, () => {}); + }, 1000); + } + }, 500); + } + if (callback) callback(result); + }); + } else if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "connect", interfaceName], (result) => { + if (result.success) { + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + Qt.callLater(() => { + getEthernetDeviceDetails(interfaceName, () => {}); + }, 1000); + }, 500); + } + if (callback) callback(result); + }); + } else { + if (callback) callback({ success: false, output: "", error: "No connection name or interface specified", exitCode: -1 }); + } + } + + function disconnectEthernet(connectionName: string, callback: var): void { + if (!connectionName || connectionName.length === 0) { + if (callback) callback({ success: false, output: "", error: "No connection name specified", exitCode: -1 }); + return; + } + + executeCommand(["connection", "down", connectionName], (result) => { + if (result.success) { + root.ethernetDeviceDetails = null; + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + }, 500); + } + if (callback) callback(result); + }); + } + + function getAllInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && (parts[1] === "wifi" || parts[1] === "ethernet")) { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + if (callback) callback(interfaces); + }); + } + + function isInterfaceConnected(interfaceName: string, callback: var): void { + executeCommand(["device", "status"], (result) => { + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(/\s+/); + if (parts.length >= 3 && parts[0] === interfaceName) { + const connected = parts[2] === "connected" || parts[2].startsWith("connected"); + if (callback) callback(connected); + return; + } + } + if (callback) callback(false); + }); + } + + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { + if (isSecure) { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + connectWireless(ssid, "", bssid, (result) => { + if (result.success) { + if (callback) callback({ success: true, usedSavedPassword: true, output: result.output, error: "", exitCode: 0 }); + } else if (result.needsPassword) { + if (callback) callback({ success: false, needsPassword: true, output: result.output, error: result.error, exitCode: result.exitCode }); + } else { + if (callback) callback(result); + } + }); + } else { + connectWireless(ssid, "", bssid, callback); + } + } + + function connectWireless(ssid: string, password: string, bssid: string, callback: var, retryCount: int): void { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + const retries = retryCount !== undefined ? retryCount : 0; + const maxRetries = 2; + + if (callback) { + root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback, retryCount: retries }; + connectionCheckTimer.start(); + immediateCheckTimer.checkCount = 0; + immediateCheckTimer.start(); + } + + if (password && password.length > 0 && hasBssid) { + const bssidUpper = bssid.toUpperCase(); + createConnectionWithPassword(ssid, bssidUpper, password, callback); + return; + } + + let cmd = ["device", "wifi", "connect", ssid]; + if (password && password.length > 0) { + cmd.push("password", password); + } + executeCommand(cmd, (result) => { + if (result.needsPassword && callback) { + if (callback) callback(result); + return; + } + + if (!result.success && root.pendingConnection && retries < maxRetries) { + log("Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + Qt.callLater(() => { + connectWireless(ssid, password, bssid, callback, retries + 1); + }, 1000); + } else if (!result.success && root.pendingConnection) { + } else if (result.success && callback) { + } else if (!result.success && !root.pendingConnection) { + if (callback) callback(result); + } + }); + } + + function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { + checkAndDeleteConnection(ssid, () => { + const cmd = ["connection", "add", + "type", "wifi", + "con-name", ssid, + "ifname", "*", + "ssid", ssid, + "802-11-wireless.bssid", bssidUpper, + "802-11-wireless-security.key-mgmt", "wpa-psk", + "802-11-wireless-security.psk", password]; + + executeCommand(cmd, (result) => { + if (result.success) { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { + const hasDuplicateWarning = result.error && ( + result.error.includes("another connection with the name") || + result.error.includes("Reference the connection by its uuid") + ); + + if (hasDuplicateWarning || (result.exitCode > 0 && result.exitCode < 10)) { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { + log("Connection profile creation failed, trying fallback..."); + let fallbackCmd = ["device", "wifi", "connect", ssid, "password", password]; + executeCommand(fallbackCmd, (fallbackResult) => { + if (callback) callback(fallbackResult); + }); + } + } + }); + }); + } + + function checkAndDeleteConnection(ssid: string, callback: var): void { + executeCommand(["connection", "show", ssid], (result) => { + if (result.success) { + executeCommand(["connection", "delete", ssid], (deleteResult) => { + Qt.callLater(() => { + if (callback) callback(); + }, 300); + }); + } else { + if (callback) callback(); + } + }); + } + + function activateConnection(connectionName: string, callback: var): void { + executeCommand(["connection", "up", connectionName], (result) => { + if (callback) callback(result); + }); + } + + function loadSavedConnections(callback: var): void { + executeCommand(["-t", "-f", "NAME,TYPE", "connection", "show"], (result) => { + if (!result.success) { + root.savedConnections = []; + root.savedConnectionSsids = []; + if (callback) callback([]); + return; + } + + parseConnectionList(result.output, callback); + }); + } + + function parseConnectionList(output: string, callback: var): void { + const lines = output.trim().split("\n").filter(line => line.length > 0); + const wifiConnections = []; + const connections = []; + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2) { + const name = parts[0]; + const type = parts[1]; + connections.push(name); + + if (type === "802-11-wireless") { + wifiConnections.push(name); + } + } + } + + root.savedConnections = connections; + + if (wifiConnections.length > 0) { + root.wifiConnectionQueue = wifiConnections; + root.currentSsidQueryIndex = 0; + root.savedConnectionSsids = []; + queryNextSsid(callback); + } else { + root.savedConnectionSsids = []; + root.wifiConnectionQueue = []; + if (callback) callback(root.savedConnectionSsids); + } + } + + function queryNextSsid(callback: var): void { + if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { + const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; + root.currentSsidQueryIndex++; + + executeCommand(["-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName], (result) => { + if (result.success) { + processSsidOutput(result.output); + } + queryNextSsid(callback); + }); + } else { + root.wifiConnectionQueue = []; + root.currentSsidQueryIndex = 0; + if (callback) callback(root.savedConnectionSsids); + } + } + + function processSsidOutput(output: string): void { + const lines = output.trim().split("\n"); + for (const line of lines) { + if (line.startsWith("802-11-wireless.ssid:")) { + const ssid = line.substring("802-11-wireless.ssid:".length).trim(); + if (ssid && ssid.length > 0) { + const ssidLower = ssid.toLowerCase(); + const exists = root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower); + if (!exists) { + const newList = root.savedConnectionSsids.slice(); + newList.push(ssid); + root.savedConnectionSsids = newList; + } + } + } + } + } + + function hasSavedProfile(ssid: string): bool { + if (!ssid || ssid.length === 0) { + return false; + } + const ssidLower = ssid.toLowerCase().trim(); + + if (root.active && root.active.ssid) { + const activeSsidLower = root.active.ssid.toLowerCase().trim(); + if (activeSsidLower === ssidLower) { + return true; + } + } + + const hasSsid = root.savedConnectionSsids.some(savedSsid => + savedSsid && savedSsid.toLowerCase().trim() === ssidLower + ); + + if (hasSsid) { + return true; + } + + const hasConnectionName = root.savedConnections.some(connName => + connName && connName.toLowerCase().trim() === ssidLower + ); + + return hasConnectionName; + } + + function forgetNetwork(ssid: string, callback: var): void { + if (!ssid || ssid.length === 0) { + if (callback) callback({ success: false, output: "", error: "No SSID specified", exitCode: -1 }); + return; + } + + const connectionName = root.savedConnections.find(conn => + conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() + ) || ssid; + + executeCommand(["connection", "delete", connectionName], (result) => { + if (result.success) { + Qt.callLater(() => { + loadSavedConnections(() => {}); + }, 500); + } + if (callback) callback(result); + }); + } + + function disconnect(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "disconnect", interfaceName], (result) => { + if (callback) callback(result.success ? result.output : ""); + }); + } else { + executeCommand(["device", "disconnect", "wifi"], (result) => { + if (callback) callback(result.success ? result.output : ""); + }); + } + } + + function getDeviceDetails(interfaceName: string, callback: var): void { + executeCommand(["device", "show", interfaceName], (result) => { + if (callback) callback(result.output); + }); + } + + function refreshStatus(callback: var): void { + getDeviceStatus((output) => { + const lines = output.trim().split("\n"); + let connected = false; + let activeIf = ""; + let activeConn = ""; + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 4) { + const state = parts[2] || ""; + if (state === "connected" || state.startsWith("connected")) { + connected = true; + activeIf = parts[0] || ""; + activeConn = parts[3] || ""; + break; + } + } + } + + root.isConnected = connected; + root.activeInterface = activeIf; + root.activeConnection = activeConn; + + if (callback) callback({ connected, interface: activeIf, connection: activeConn }); + }); + } + + function bringInterfaceUp(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "connect", interfaceName], (result) => { + if (callback) { + callback(result); + } + }); + } else { + if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + } + } + + function bringInterfaceDown(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "disconnect", interfaceName], (result) => { + if (callback) { + callback(result); + } + }); + } else { + if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + } + } + + function scanWirelessNetworks(interfaceName: string, callback: var): void { + let cmd = ["device", "wifi", "rescan"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push("ifname", interfaceName); + } + executeCommand(cmd, (result) => { + if (callback) { + callback(result); + } + }); + } + + function enableWifi(enabled: bool, callback: var): void { + const cmd = enabled ? "on" : "off"; + executeCommand(["radio", "wifi", cmd], (result) => { + if (result.success) { + getWifiStatus((status) => { + root.wifiEnabled = status; + if (callback) callback(result); + }); + } else { + if (callback) callback(result); + } + }); + } + + function toggleWifi(callback: var): void { + const newState = !root.wifiEnabled; + enableWifi(newState, callback); + } + + function getWifiStatus(callback: var): void { + executeCommand(["radio", "wifi"], (result) => { + if (result.success) { + const enabled = result.output.trim() === "enabled"; + root.wifiEnabled = enabled; + if (callback) callback(enabled); + } else { + if (callback) callback(root.wifiEnabled); + } + }); + } + + function getNetworks(callback: var): void { + executeCommand(["-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"], (result) => { + if (!result.success) { + if (callback) callback([]); + return; + } + + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const allNetworks = result.output.trim().split("\n") + .filter(line => line && line.length > 0) + .map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }) + .filter(n => n.ssid && n.ssid.length > 0); + + const networkMap = new Map(); + for (const network of allNetworks) { + const existing = networkMap.get(network.ssid); + if (!existing) { + networkMap.set(network.ssid, network); + } else { + if (network.active && !existing.active) { + networkMap.set(network.ssid, network); + } else if (!network.active && !existing.active) { + if (network.strength > existing.strength) { + networkMap.set(network.ssid, network); + } + } + } + } + + const networks = Array.from(networkMap.values()); + const rNetworks = root.networks; + + const destroyed = rNetworks.filter(rn => !networks.find(n => + n.frequency === rn.frequency && + n.ssid === rn.ssid && + n.bssid === rn.bssid + )); + for (const network of destroyed) { + const index = rNetworks.indexOf(network); + if (index >= 0) { + rNetworks.splice(index, 1); + network.destroy(); + } + } + + for (const network of networks) { + const match = rNetworks.find(n => + n.frequency === network.frequency && + n.ssid === network.ssid && + n.bssid === network.bssid + ); + if (match) { + match.lastIpcObject = network; + } else { + rNetworks.push(apComp.createObject(root, { + lastIpcObject: network + })); + } + } + + if (callback) callback(root.networks); + checkPendingConnection(); + }); + } + + function getWirelessSSIDs(interfaceName: string, callback: var): void { + let cmd = ["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push("ifname", interfaceName); + } + executeCommand(cmd, (result) => { + if (!result.success) { + if (callback) callback([]); + return; + } + + const ssids = []; + const lines = result.output.trim().split("\n"); + const seenSSIDs = new Set(); + + for (const line of lines) { + if (!line || line.length === 0) continue; + + const parts = line.split(":"); + if (parts.length >= 1) { + const ssid = parts[0].trim(); + if (ssid && ssid.length > 0 && !seenSSIDs.has(ssid)) { + seenSSIDs.add(ssid); + const signalStr = parts.length >= 2 ? parts[1].trim() : ""; + const signal = signalStr ? parseInt(signalStr, 10) : 0; + const security = parts.length >= 3 ? parts[2].trim() : ""; + ssids.push({ + ssid: ssid, + signal: signalStr, + signalValue: isNaN(signal) ? 0 : signal, + security: security + }); + } + } + } + + ssids.sort((a, b) => { + return b.signalValue - a.signalValue; + }); + + if (callback) callback(ssids); + }); + } + + component CommandProcess: Process { + id: proc + property var callback: null + property list command: [] + property bool callbackCalled: false + property int exitCode: 0 + signal processFinished + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: StdioCollector { + id: stdoutCollector + onStreamFinished: { + } + } + + stderr: StdioCollector { + id: stderrCollector + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand && root.pendingConnection && root.pendingConnection.callback) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + } + } + } + } + } + + onExited: { + proc.exitCode = exitCode; + Qt.callLater(() => { + if (proc.callbackCalled) { + proc.processFinished(); + return; + } + + if (proc.callback) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; + const success = proc.exitCode === 0; + + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + const needsPassword = isConnectionCommand && error && error.length > 0 && + (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + proc.processFinished(); + return; + } else if (!success && isConnectionCommand && root.pendingConnection) { + const failedSsid = root.pendingConnection.ssid; + root.connectionFailed(failedSsid); + } + + proc.callbackCalled = true; + proc.callback({ + success: success, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: needsPassword || false + }); + proc.processFinished(); + } else { + proc.processFinished(); + } + }); + } + } + + Component { + id: commandProc + CommandProcess {} + } + + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } + + Component { + id: apComp + AccessPoint {} + } + + Timer { + id: connectionCheckTimer + interval: 4000 + onTriggered: { + if (root.pendingConnection) { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (!connected && root.pendingConnection.callback) { + let foundPasswordError = false; + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + foundPasswordError = true; + break; + } + } + } + } + } + + if (!foundPasswordError) { + const pending = root.pendingConnection; + const failedSsid = pending.ssid; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.connectionFailed(failedSsid); + pending.callback({ + success: false, + output: "", + error: "Connection timeout", + exitCode: -1, + needsPassword: false + }); + } + } else if (connected) { + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + } + + Timer { + id: immediateCheckTimer + interval: 500 + repeat: true + triggeredOnStart: false + property int checkCount: 0 + + onTriggered: { + if (root.pendingConnection) { + checkCount++; + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + } + root.pendingConnection = null; + } else { + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return; + } + } + } + } + } + + if (checkCount >= 6) { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + + function checkPendingConnection(): void { + if (root.pendingConnection) { + Qt.callLater(() => { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + } + root.pendingConnection = null; + } else { + if (!immediateCheckTimer.running) { + immediateCheckTimer.start(); + } + } + }); + } + } + + function cidrToSubnetMask(cidr: string): string { + const cidrNum = parseInt(cidr, 10); + if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { + return ""; + } + + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; + const octet1 = (mask >>> 24) & 0xff; + const octet2 = (mask >>> 16) & 0xff; + const octet3 = (mask >>> 8) & 0xff; + const octet4 = mask & 0xff; + + return `${octet1}.${octet2}.${octet3}.${octet4}`; + } + + function getWirelessDeviceDetails(interfaceName: string, callback: var): void { + if (!interfaceName || interfaceName.length === 0) { + const activeInterface = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeInterface && activeInterface.device) { + interfaceName = activeInterface.device; + } else { + if (callback) callback(null); + return; + } + } + + executeCommand(["device", "show", interfaceName], (result) => { + if (!result.success || !result.output) { + root.wirelessDeviceDetails = null; + if (callback) callback(null); + return; + } + + const details = parseDeviceDetails(result.output, false); + root.wirelessDeviceDetails = details; + if (callback) callback(details); + }); + } + + function getEthernetDeviceDetails(interfaceName: string, callback: var): void { + if (!interfaceName || interfaceName.length === 0) { + const activeInterface = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeInterface && activeInterface.device) { + interfaceName = activeInterface.device; + } else { + if (callback) callback(null); + return; + } + } + + executeCommand(["device", "show", interfaceName], (result) => { + if (!result.success || !result.output) { + root.ethernetDeviceDetails = null; + if (callback) callback(null); + return; + } + + const details = parseDeviceDetails(result.output, true); + root.ethernetDeviceDetails = details; + if (callback) callback(details); + }); + } + + function parseDeviceDetails(output: string, isEthernet: bool): var { + const details = { + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + + if (!output || output.length === 0) { + return details; + } + + const lines = output.trim().split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(":"); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join(":").trim(); + + if (key.startsWith("IP4.ADDRESS")) { + const ipParts = value.split("/"); + details.ipAddress = ipParts[0] || ""; + if (ipParts[1]) { + details.subnet = cidrToSubnetMask(ipParts[1]); + } else { + details.subnet = ""; + } + } else if (key === "IP4.GATEWAY") { + if (value !== "--") { + details.gateway = value; + } + } else if (key.startsWith("IP4.DNS")) { + if (value !== "--" && value.length > 0) { + details.dns.push(value); + } + } else if (isEthernet && key === "WIRED-PROPERTIES.MAC") { + details.macAddress = value; + } else if (isEthernet && key === "WIRED-PROPERTIES.SPEED") { + details.speed = value; + } else if (!isEthernet && key === "GENERAL.HWADDR") { + details.macAddress = value; + } + } + } + + return details; + } + + Process { + id: monitorProc + running: true + command: ["nmcli", "monitor"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: SplitParser { + onRead: { + log("Connection state change detected, refreshing..."); + root.refreshOnConnectionChange(); + } + } + + onExited: { + log("Monitor process exited, restarting..."); + Qt.callLater(() => { + monitorProc.running = true; + }, 2000); + } + } + + function refreshOnConnectionChange(): void { + getNetworks((networks) => { + const newActive = root.active; + + if (newActive && newActive.active) { + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 500); + } else { + root.wirelessDeviceDetails = null; + root.ethernetDeviceDetails = null; + } + + getWirelessInterfaces(() => {}); + getEthernetInterfaces(() => { + if (root.activeEthernet && root.activeEthernet.connected) { + Qt.callLater(() => { + getEthernetDeviceDetails(root.activeEthernet.interface, () => {}); + }, 500); + } + }); + }); + } + + Component.onCompleted: { + getWifiStatus(() => {}); + getNetworks(() => {}); + loadSavedConnections(() => {}); + getEthernetInterfaces(() => {}); + + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 2000); + } +} + -- cgit v1.2.3-freya From 36a91213b14f0dfd000761aa0e7be76db0609101 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 19:05:23 -0500 Subject: network: migrated to nmcli.qml --- modules/bar/popouts/Network.qml | 33 +++++----- plan.md | 137 ---------------------------------------- services/Nmcli.qml | 33 ++++++++++ 3 files changed, 49 insertions(+), 154 deletions(-) delete mode 100644 plan.md (limited to 'services') diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index f040b6a..cb012bf 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -20,27 +20,27 @@ ColumnLayout { StyledText { Layout.topMargin: Appearance.padding.normal Layout.rightMargin: Appearance.padding.small - text: qsTr("Wifi %1").arg(Network.wifiEnabled ? "enabled" : "disabled") + text: qsTr("Wifi %1").arg(Nmcli.wifiEnabled ? "enabled" : "disabled") font.weight: 500 } Toggle { label: qsTr("Enabled") - checked: Network.wifiEnabled - toggle.onToggled: Network.enableWifi(checked) + checked: Nmcli.wifiEnabled + toggle.onToggled: Nmcli.enableWifi(checked) } StyledText { Layout.topMargin: Appearance.spacing.small Layout.rightMargin: Appearance.padding.small - text: qsTr("%1 networks available").arg(Network.networks.length) + text: qsTr("%1 networks available").arg(Nmcli.networks.length) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } Repeater { model: ScriptModel { - values: [...Network.networks].sort((a, b) => { + values: [...Nmcli.networks].sort((a, b) => { if (a.active !== b.active) return b.active - a.active; return b.strength - a.strength; @@ -50,7 +50,7 @@ ColumnLayout { RowLayout { id: networkItem - required property Network.AccessPoint modelData + required property Nmcli.AccessPoint modelData readonly property bool isConnecting: root.connectingToSsid === modelData.ssid readonly property bool loading: networkItem.isConnecting @@ -111,14 +111,14 @@ ColumnLayout { StateLayer { color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: networkItem.loading || !Network.wifiEnabled + disabled: networkItem.loading || !Nmcli.wifiEnabled function onClicked(): void { if (networkItem.modelData.active) { - Network.disconnectFromNetwork(); + Nmcli.disconnectFromNetwork(); } else { root.connectingToSsid = networkItem.modelData.ssid; - Network.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); + Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); } } } @@ -151,10 +151,10 @@ ColumnLayout { StateLayer { color: Colours.palette.m3onPrimaryContainer - disabled: Network.scanning || !Network.wifiEnabled + disabled: Nmcli.scanning || !Nmcli.wifiEnabled function onClicked(): void { - Network.rescanWifi(); + Nmcli.rescanWifi(); } } @@ -163,7 +163,7 @@ ColumnLayout { anchors.centerIn: parent spacing: Appearance.spacing.small - opacity: Network.scanning ? 0 : 1 + opacity: Nmcli.scanning ? 0 : 1 MaterialIcon { id: scanIcon @@ -188,22 +188,21 @@ ColumnLayout { strokeWidth: Appearance.padding.small / 2 bgColour: "transparent" implicitHeight: parent.implicitHeight - Appearance.padding.smaller * 2 - running: Network.scanning + running: Nmcli.scanning } } - // Reset connecting state when network changes Connections { - target: Network + target: Nmcli function onActiveChanged(): void { - if (Network.active && root.connectingToSsid === Network.active.ssid) { + if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; } } function onScanningChanged(): void { - if (!Network.scanning) + if (!Nmcli.scanning) scanIcon.rotation = 0; } } diff --git a/plan.md b/plan.md deleted file mode 100644 index 4762ef0..0000000 --- a/plan.md +++ /dev/null @@ -1,137 +0,0 @@ -# Nmcli.qml Feature Completion Plan - -This document outlines the missing features needed in `Nmcli.qml` to replace `Network.qml` or rewrite the wireless panel in the control center. - -## Current Status - -`Nmcli.qml` currently has: -- ✅ Device status queries -- ✅ Wireless/Ethernet interface listing -- ✅ Interface connection status checking -- ✅ Basic wireless connection (SSID + password) -- ✅ Disconnect functionality -- ✅ Device details (basic) -- ✅ Interface up/down -- ✅ WiFi scanning -- ✅ SSID listing with signal/security (sorted) - -## Missing Features - -### 1. WiFi Radio Control -- [x] `enableWifi(enabled: bool)` - Turn WiFi radio on/off -- [x] `toggleWifi()` - Toggle WiFi radio state -- [x] `wifiEnabled` property - Current WiFi radio state -- [x] Monitor WiFi radio state changes - -**Implementation Notes:** -- Use `nmcli radio wifi on/off` -- Monitor state with `nmcli radio wifi` -- Update `wifiEnabled` property on state changes - -### 2. Network List Management -- [x] `networks` property - List of AccessPoint objects -- [x] `active` property - Currently active network -- [x] Real-time network list updates -- [x] Network grouping by SSID with signal prioritization -- [x] AccessPoint component/object with properties: - - `ssid`, `bssid`, `strength`, `frequency`, `active`, `security`, `isSecure` - -**Implementation Notes:** -- Use `nmcli -g ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY d w` -- Parse and group networks by SSID -- Prioritize active/connected networks -- Update network list on connection changes - -### 3. Connection Management - BSSID Support -- [x] BSSID support in `connectWireless()` function -- [x] Connection profile creation with BSSID (`createConnectionWithPassword`) -- [x] Handle BSSID in connection commands - -**Implementation Notes:** -- Use `nmcli connection add` with `802-11-wireless.bssid` for BSSID-based connections -- Fallback to SSID-only connection if BSSID not available -- Handle existing connection profiles when BSSID is provided - -### 4. Saved Connection Profile Management -- [x] `savedConnections` property - List of saved connection names -- [x] `savedConnectionSsids` property - List of saved SSIDs -- [x] `hasSavedProfile(ssid: string)` function - Check if profile exists -- [x] `forgetNetwork(ssid: string)` function - Delete connection profile -- [x] Load saved connections on startup -- [x] Update saved connections list after connection changes - -**Implementation Notes:** -- Use `nmcli -t -f NAME,TYPE connection show` to list connections -- Query SSIDs for WiFi connections: `nmcli -t -f 802-11-wireless.ssid connection show ` -- Use `nmcli connection delete ` to forget networks -- Case-insensitive SSID matching - -### 5. Pending Connection Tracking -- [x] `pendingConnection` property - Track connection in progress -- [x] Connection state tracking with timers -- [x] Connection success/failure detection -- [x] Automatic retry or callback on failure - -**Implementation Notes:** -- Track pending connection with SSID/BSSID -- Use timers to check connection status -- Monitor network list updates to detect successful connection -- Handle connection failures and trigger callbacks - -### 6. Connection Failure Handling -- [x] `connectionFailed(ssid: string)` signal -- [x] Password requirement detection from error messages -- [x] Connection retry logic -- [x] Error message parsing and reporting - -**Implementation Notes:** -- Parse stderr output for password requirements -- Detect specific error patterns (e.g., "Secrets were required") -- Emit signals for UI to handle password dialogs -- Provide meaningful error messages - -### 7. Password Callback Handling -- [x] `connectToNetworkWithPasswordCheck()` function -- [x] Try connection without password first (use saved password) -- [x] Callback on password requirement -- [x] Handle both secure and open networks - -**Implementation Notes:** -- Attempt connection without password for secure networks -- If connection fails with password error, trigger callback -- For open networks, connect directly -- Support callback pattern for password dialogs - -### 8. Device Details Parsing -- [x] Full parsing of `device show` output -- [x] `wirelessDeviceDetails` property with: - - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress` -- [x] `ethernetDeviceDetails` property with: - - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress`, `speed` -- [x] `cidrToSubnetMask()` helper function -- [x] Update device details on connection changes - -**Implementation Notes:** -- Parse `nmcli device show ` output -- Extract IP4.ADDRESS, IP4.GATEWAY, IP4.DNS, etc. -- Convert CIDR notation to subnet mask -- Handle both wireless and ethernet device details - -### 9. Connection Status Monitoring -- [x] Automatic network list refresh on connection changes -- [x] Monitor connection state changes -- [x] Update active network property -- [x] Refresh device details on connection - -**Implementation Notes:** -- Use Process stdout SplitParser to monitor changes -- Trigger network list refresh on connection events -- Update `active` property when connection changes -- Refresh device details when connected - -### 10. Ethernet Device Management -- [x] `ethernetDevices` property - List of ethernet devices -- [x] `activeEthernet` property - Currently active ethernet device -- [x] `connectEthernet(connectionName, interfaceName)` function -- [x] `disconnectEthernet(connectionName)` function -- [x] Ethernet device details parsing diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 4e45b41..5fb0c6c 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -14,6 +14,7 @@ Singleton { property string activeInterface: "" property string activeConnection: "" property bool wifiEnabled: true + readonly property bool scanning: rescanProc.running readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property list savedConnections: [] @@ -235,6 +236,10 @@ Singleton { } } + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { + connectWireless(ssid, password, bssid, callback); + } + function connectWireless(ssid: string, password: string, bssid: string, callback: var, retryCount: int): void { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; const retries = retryCount !== undefined ? retryCount : 0; @@ -473,6 +478,22 @@ Singleton { } } + function disconnectFromNetwork(): void { + if (active && active.ssid) { + executeCommand(["connection", "down", active.ssid], (result) => { + if (result.success) { + getNetworks(() => {}); + } + }); + } else { + executeCommand(["device", "disconnect", "wifi"], (result) => { + if (result.success) { + getNetworks(() => {}); + } + }); + } + } + function getDeviceDetails(interfaceName: string, callback: var): void { executeCommand(["device", "show", interfaceName], (result) => { if (callback) callback(result.output); @@ -543,6 +564,10 @@ Singleton { }); } + function rescanWifi(): void { + rescanProc.running = true; + } + function enableWifi(enabled: bool, callback: var): void { const cmd = enabled ? "on" : "off"; executeCommand(["radio", "wifi", cmd], (result) => { @@ -1152,6 +1177,14 @@ Singleton { return details; } + Process { + id: rescanProc + command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"] + onExited: { + getNetworks(() => {}); + } + } + Process { id: monitorProc running: true -- 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') 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 From 3f58823762ba6a894bb96fae4e9f06480714e460 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 11:12:07 -0500 Subject: nmcli: refactor to be readable/extensible --- services/Nmcli.qml | 511 ++++++++++++++++++++++++++++------------------------- 1 file changed, 267 insertions(+), 244 deletions(-) (limited to 'services') diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 5fb0c6c..a9f9e8e 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -32,6 +32,29 @@ Singleton { property list activeProcesses: [] property var debugLogger: null + // Constants + readonly property string deviceTypeWifi: "wifi" + readonly property string deviceTypeEthernet: "ethernet" + readonly property string connectionTypeWireless: "802-11-wireless" + readonly property string nmcliCommandDevice: "device" + readonly property string nmcliCommandConnection: "connection" + readonly property string nmcliCommandWifi: "wifi" + readonly property string nmcliCommandRadio: "radio" + readonly property string deviceStatusFields: "DEVICE,TYPE,STATE,CONNECTION" + readonly property string connectionListFields: "NAME,TYPE" + readonly property string wirelessSsidField: "802-11-wireless.ssid" + readonly property string networkListFields: "SSID,SIGNAL,SECURITY" + readonly property string networkDetailFields: "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY" + readonly property string securityKeyMgmt: "802-11-wireless-security.key-mgmt" + readonly property string securityPsk: "802-11-wireless-security.psk" + readonly property string keyMgmtWpaPsk: "wpa-psk" + readonly property string connectionParamType: "type" + readonly property string connectionParamConName: "con-name" + readonly property string connectionParamIfname: "ifname" + readonly property string connectionParamSsid: "ssid" + readonly property string connectionParamPassword: "password" + readonly property string connectionParamBssid: "802-11-wireless.bssid" + function setDebugLogger(logger: var): void { root.debugLogger = logger; } @@ -48,6 +71,128 @@ Singleton { log(message); } + function detectPasswordRequired(error: string): bool { + if (!error || error.length === 0) { + return false; + } + + return (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + } + + function parseNetworkOutput(output: string): list { + if (!output || output.length === 0) { + return []; + } + + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const allNetworks = output.trim().split("\n") + .filter(line => line && line.length > 0) + .map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }) + .filter(n => n.ssid && n.ssid.length > 0); + + return allNetworks; + } + + function deduplicateNetworks(networks: list): list { + if (!networks || networks.length === 0) { + return []; + } + + const networkMap = new Map(); + for (const network of networks) { + const existing = networkMap.get(network.ssid); + if (!existing) { + networkMap.set(network.ssid, network); + } else { + if (network.active && !existing.active) { + networkMap.set(network.ssid, network); + } else if (!network.active && !existing.active) { + if (network.strength > existing.strength) { + networkMap.set(network.ssid, network); + } + } + } + } + + return Array.from(networkMap.values()); + } + + function isConnectionCommand(command: list): bool { + if (!command || command.length === 0) { + return false; + } + + return command.includes(root.nmcliCommandWifi) || command.includes(root.nmcliCommandConnection); + } + + function parseDeviceStatusOutput(output: string, filterType: string): list { + if (!output || output.length === 0) { + return []; + } + + const interfaces = []; + const lines = output.trim().split("\n"); + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2) { + const deviceType = parts[1]; + let shouldInclude = false; + + if (filterType === root.deviceTypeWifi && deviceType === root.deviceTypeWifi) { + shouldInclude = true; + } else if (filterType === root.deviceTypeEthernet && deviceType === root.deviceTypeEthernet) { + shouldInclude = true; + } else if (filterType === "both" && (deviceType === root.deviceTypeWifi || deviceType === root.deviceTypeEthernet)) { + shouldInclude = true; + } + + if (shouldInclude) { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + } + + return interfaces; + } + + function isConnectedState(state: string): bool { + if (!state || state.length === 0) { + return false; + } + + return state === "100 (connected)" || + state === "connected" || + state.startsWith("connected"); + } + function executeCommand(args: list, callback: var): void { const proc = commandProc.createObject(root); proc.command = ["nmcli", ...args]; @@ -68,68 +213,42 @@ Singleton { } function getDeviceStatus(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { if (callback) callback(result.output); }); } function getWirelessInterfaces(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { - const interfaces = []; - const lines = result.output.trim().split("\n"); - for (const line of lines) { - const parts = line.split(":"); - if (parts.length >= 2 && parts[1] === "wifi") { - interfaces.push({ - device: parts[0] || "", - type: parts[1] || "", - state: parts[2] || "", - connection: parts[3] || "" - }); - } - } + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeWifi); root.wirelessInterfaces = interfaces; if (callback) callback(interfaces); }); } function getEthernetInterfaces(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { - const interfaces = []; + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeEthernet); const devices = []; - const lines = result.output.trim().split("\n"); - for (const line of lines) { - const parts = line.split(":"); - if (parts.length >= 2 && parts[1] === "ethernet") { - const device = parts[0] || ""; - const type = parts[1] || ""; - const state = parts[2] || ""; - const connection = parts[3] || ""; - - const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); - - interfaces.push({ - device: device, - type: type, - state: state, - connection: connection - }); - - devices.push({ - interface: device, - type: type, - state: state, - connection: connection, - connected: connected, - ipAddress: "", - gateway: "", - dns: [], - subnet: "", - macAddress: "", - speed: "" - }); - } + + for (const iface of interfaces) { + const connected = isConnectedState(iface.state); + + devices.push({ + interface: iface.device, + type: iface.type, + state: iface.state, + connection: iface.connection, + connected: connected, + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }); } + root.ethernetInterfaces = interfaces; root.ethernetDevices = devices; if (callback) callback(interfaces); @@ -138,7 +257,7 @@ Singleton { function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { if (connectionName && connectionName.length > 0) { - executeCommand(["connection", "up", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -152,7 +271,7 @@ Singleton { if (callback) callback(result); }); } else if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -174,7 +293,7 @@ Singleton { return; } - executeCommand(["connection", "down", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "down", connectionName], (result) => { if (result.success) { root.ethernetDeviceDetails = null; Qt.callLater(() => { @@ -186,31 +305,19 @@ Singleton { } function getAllInterfaces(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { - const interfaces = []; - const lines = result.output.trim().split("\n"); - for (const line of lines) { - const parts = line.split(":"); - if (parts.length >= 2 && (parts[1] === "wifi" || parts[1] === "ethernet")) { - interfaces.push({ - device: parts[0] || "", - type: parts[1] || "", - state: parts[2] || "", - connection: parts[3] || "" - }); - } - } + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + const interfaces = parseDeviceStatusOutput(result.output, "both"); if (callback) callback(interfaces); }); } function isInterfaceConnected(interfaceName: string, callback: var): void { - executeCommand(["device", "status"], (result) => { + executeCommand([root.nmcliCommandDevice, "status"], (result) => { const lines = result.output.trim().split("\n"); for (const line of lines) { const parts = line.split(/\s+/); if (parts.length >= 3 && parts[0] === interfaceName) { - const connected = parts[2] === "connected" || parts[2].startsWith("connected"); + const connected = isConnectedState(parts[2]); if (callback) callback(connected); return; } @@ -258,9 +365,9 @@ Singleton { return; } - let cmd = ["device", "wifi", "connect", ssid]; + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid]; if (password && password.length > 0) { - cmd.push("password", password); + cmd.push(root.connectionParamPassword, password); } executeCommand(cmd, (result) => { if (result.needsPassword && callback) { @@ -283,14 +390,14 @@ Singleton { function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { checkAndDeleteConnection(ssid, () => { - const cmd = ["connection", "add", - "type", "wifi", - "con-name", ssid, - "ifname", "*", - "ssid", ssid, - "802-11-wireless.bssid", bssidUpper, - "802-11-wireless-security.key-mgmt", "wpa-psk", - "802-11-wireless-security.psk", password]; + const cmd = [root.nmcliCommandConnection, "add", + root.connectionParamType, root.deviceTypeWifi, + root.connectionParamConName, ssid, + root.connectionParamIfname, "*", + root.connectionParamSsid, ssid, + root.connectionParamBssid, bssidUpper, + root.securityKeyMgmt, root.keyMgmtWpaPsk, + root.securityPsk, password]; executeCommand(cmd, (result) => { if (result.success) { @@ -307,7 +414,7 @@ Singleton { activateConnection(ssid, callback); } else { log("Connection profile creation failed, trying fallback..."); - let fallbackCmd = ["device", "wifi", "connect", ssid, "password", password]; + let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; executeCommand(fallbackCmd, (fallbackResult) => { if (callback) callback(fallbackResult); }); @@ -318,9 +425,9 @@ Singleton { } function checkAndDeleteConnection(ssid: string, callback: var): void { - executeCommand(["connection", "show", ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "show", ssid], (result) => { if (result.success) { - executeCommand(["connection", "delete", ssid], (deleteResult) => { + executeCommand([root.nmcliCommandConnection, "delete", ssid], (deleteResult) => { Qt.callLater(() => { if (callback) callback(); }, 300); @@ -332,13 +439,13 @@ Singleton { } function activateConnection(connectionName: string, callback: var): void { - executeCommand(["connection", "up", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { if (callback) callback(result); }); } function loadSavedConnections(callback: var): void { - executeCommand(["-t", "-f", "NAME,TYPE", "connection", "show"], (result) => { + executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], (result) => { if (!result.success) { root.savedConnections = []; root.savedConnectionSsids = []; @@ -362,7 +469,7 @@ Singleton { const type = parts[1]; connections.push(name); - if (type === "802-11-wireless") { + if (type === root.connectionTypeWireless) { wifiConnections.push(name); } } @@ -387,7 +494,7 @@ Singleton { const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; root.currentSsidQueryIndex++; - executeCommand(["-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName], (result) => { + executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], (result) => { if (result.success) { processSsidOutput(result.output); } @@ -456,7 +563,7 @@ Singleton { conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() ) || ssid; - executeCommand(["connection", "delete", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "delete", connectionName], (result) => { if (result.success) { Qt.callLater(() => { loadSavedConnections(() => {}); @@ -468,11 +575,11 @@ Singleton { function disconnect(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "disconnect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { if (callback) callback(result.success ? result.output : ""); }); } else { - executeCommand(["device", "disconnect", "wifi"], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { if (callback) callback(result.success ? result.output : ""); }); } @@ -480,13 +587,13 @@ Singleton { function disconnectFromNetwork(): void { if (active && active.ssid) { - executeCommand(["connection", "down", active.ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "down", active.ssid], (result) => { if (result.success) { getNetworks(() => {}); } }); } else { - executeCommand(["device", "disconnect", "wifi"], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { if (result.success) { getNetworks(() => {}); } @@ -495,7 +602,7 @@ Singleton { } function getDeviceDetails(interfaceName: string, callback: var): void { - executeCommand(["device", "show", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "show", interfaceName], (result) => { if (callback) callback(result.output); }); } @@ -511,7 +618,7 @@ Singleton { const parts = line.split(":"); if (parts.length >= 4) { const state = parts[2] || ""; - if (state === "connected" || state.startsWith("connected")) { + if (isConnectedState(state)) { connected = true; activeIf = parts[0] || ""; activeConn = parts[3] || ""; @@ -530,7 +637,7 @@ Singleton { function bringInterfaceUp(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { if (callback) { callback(result); } @@ -542,7 +649,7 @@ Singleton { function bringInterfaceDown(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "disconnect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { if (callback) { callback(result); } @@ -553,9 +660,9 @@ Singleton { } function scanWirelessNetworks(interfaceName: string, callback: var): void { - let cmd = ["device", "wifi", "rescan"]; + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "rescan"]; if (interfaceName && interfaceName.length > 0) { - cmd.push("ifname", interfaceName); + cmd.push(root.connectionParamIfname, interfaceName); } executeCommand(cmd, (result) => { if (callback) { @@ -570,7 +677,7 @@ Singleton { function enableWifi(enabled: bool, callback: var): void { const cmd = enabled ? "on" : "off"; - executeCommand(["radio", "wifi", cmd], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], (result) => { if (result.success) { getWifiStatus((status) => { root.wifiEnabled = status; @@ -588,7 +695,7 @@ Singleton { } function getWifiStatus(callback: var): void { - executeCommand(["radio", "wifi"], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], (result) => { if (result.success) { const enabled = result.output.trim() === "enabled"; root.wifiEnabled = enabled; @@ -600,48 +707,14 @@ Singleton { } function getNetworks(callback: var): void { - executeCommand(["-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"], (result) => { + executeCommand(["-g", root.networkDetailFields, "d", "w"], (result) => { if (!result.success) { if (callback) callback([]); return; } - const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; - const rep = new RegExp("\\\\:", "g"); - const rep2 = new RegExp(PLACEHOLDER, "g"); - - const allNetworks = result.output.trim().split("\n") - .filter(line => line && line.length > 0) - .map(n => { - const net = n.replace(rep, PLACEHOLDER).split(":"); - return { - active: net[0] === "yes", - strength: parseInt(net[1] || "0", 10) || 0, - frequency: parseInt(net[2] || "0", 10) || 0, - ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), - bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), - security: (net[5] ?? "").trim() - }; - }) - .filter(n => n.ssid && n.ssid.length > 0); - - const networkMap = new Map(); - for (const network of allNetworks) { - const existing = networkMap.get(network.ssid); - if (!existing) { - networkMap.set(network.ssid, network); - } else { - if (network.active && !existing.active) { - networkMap.set(network.ssid, network); - } else if (!network.active && !existing.active) { - if (network.strength > existing.strength) { - networkMap.set(network.ssid, network); - } - } - } - } - - const networks = Array.from(networkMap.values()); + const allNetworks = parseNetworkOutput(result.output); + const networks = deduplicateNetworks(allNetworks); const rNetworks = root.networks; const destroyed = rNetworks.filter(rn => !networks.find(n => @@ -678,9 +751,9 @@ Singleton { } function getWirelessSSIDs(interfaceName: string, callback: var): void { - let cmd = ["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list"]; + let cmd = ["-t", "-f", root.networkListFields, root.nmcliCommandDevice, root.nmcliCommandWifi, "list"]; if (interfaceName && interfaceName.length > 0) { - cmd.push("ifname", interfaceName); + cmd.push(root.connectionParamIfname, interfaceName); } executeCommand(cmd, (result) => { if (!result.success) { @@ -721,6 +794,43 @@ Singleton { }); } + function handlePasswordRequired(proc: var, error: string, output: string, exitCode: int): bool { + if (!proc || !error || error.length === 0) { + return false; + } + + if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { + return false; + } + + const needsPassword = detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: output || "", + error: error, + exitCode: exitCode, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return true; + } + + return false; + } + component CommandProcess: Process { id: proc property var callback: null @@ -745,43 +855,8 @@ Singleton { onStreamFinished: { const error = text.trim(); if (error && error.length > 0) { - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - - if (isConnectionCommand && root.pendingConnection && root.pendingConnection.callback) { - const needsPassword = (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - const pending = root.pendingConnection; - root.pendingConnection = null; - proc.callbackCalled = true; - const result = { - success: false, - output: (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - } - } + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + handlePasswordRequired(proc, error, output, -1); } } } @@ -798,44 +873,16 @@ Singleton { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; const success = proc.exitCode === 0; + const cmdIsConnection = isConnectionCommand(proc.command); - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - const needsPassword = isConnectionCommand && error && error.length > 0 && - (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); - - if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - const pending = root.pendingConnection; - root.pendingConnection = null; - proc.callbackCalled = true; - const result = { - success: false, - output: output, - error: error, - exitCode: proc.exitCode, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } + if (handlePasswordRequired(proc, error, output, proc.exitCode)) { proc.processFinished(); return; - } else if (!success && isConnectionCommand && root.pendingConnection) { + } + + const needsPassword = cmdIsConnection && detectPasswordRequired(error); + + if (!success && cmdIsConnection && root.pendingConnection) { const failedSsid = root.pendingConnection.ssid; root.connectionFailed(failedSsid); } @@ -891,20 +938,8 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - - if (isConnectionCommand) { - const needsPassword = (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); + if (isConnectionCommand(proc.command)) { + const needsPassword = detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection) { const pending = root.pendingConnection; @@ -983,20 +1018,8 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - - if (isConnectionCommand) { - const needsPassword = (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); + if (isConnectionCommand(proc.command)) { + const needsPassword = detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { connectionCheckTimer.stop(); @@ -1076,7 +1099,7 @@ Singleton { function getWirelessDeviceDetails(interfaceName: string, callback: var): void { if (!interfaceName || interfaceName.length === 0) { const activeInterface = root.wirelessInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; @@ -1102,7 +1125,7 @@ Singleton { function getEthernetDeviceDetails(interfaceName: string, callback: var): void { if (!interfaceName || interfaceName.length === 0) { const activeInterface = root.ethernetInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; @@ -1179,7 +1202,7 @@ Singleton { Process { id: rescanProc - command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"] + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] onExited: { getNetworks(() => {}); } @@ -1217,7 +1240,7 @@ Singleton { Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeWireless && activeWireless.device) { getWirelessDeviceDetails(activeWireless.device, () => {}); @@ -1226,7 +1249,7 @@ Singleton { if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeEthernet && activeEthernet.device) { getEthernetDeviceDetails(activeEthernet.device, () => {}); @@ -1258,7 +1281,7 @@ Singleton { Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeWireless && activeWireless.device) { getWirelessDeviceDetails(activeWireless.device, () => {}); @@ -1267,7 +1290,7 @@ Singleton { if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeEthernet && activeEthernet.device) { getEthernetDeviceDetails(activeEthernet.device, () => {}); -- cgit v1.2.3-freya From 9825ad4d3102130ec40bb9324c3e37e1622c9c57 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:53:30 +1100 Subject: nmcli: fix errors + disable most logs --- services/Nmcli.qml | 624 ++++++++++++++++++++++++++++------------------------- 1 file changed, 333 insertions(+), 291 deletions(-) (limited to 'services') diff --git a/services/Nmcli.qml b/services/Nmcli.qml index a9f9e8e..24a93da 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1,4 +1,5 @@ pragma Singleton +pragma ComponentBehavior: Bound import Quickshell import Quickshell.Io @@ -19,7 +20,7 @@ Singleton { readonly property AccessPoint active: networks.find(n => n.active) ?? null property list savedConnections: [] property list savedConnectionSsids: [] - + property var wifiConnectionQueue: [] property int currentSsidQueryIndex: 0 property var pendingConnection: null @@ -28,9 +29,8 @@ Singleton { property var ethernetDeviceDetails: null property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null - + property list activeProcesses: [] - property var debugLogger: null // Constants readonly property string deviceTypeWifi: "wifi" @@ -55,63 +55,35 @@ Singleton { readonly property string connectionParamPassword: "password" readonly property string connectionParamBssid: "802-11-wireless.bssid" - function setDebugLogger(logger: var): void { - root.debugLogger = logger; - } - - function log(message: string): void { - if (root.debugLogger) { - root.debugLogger(message); - } else { - console.log("[Nmcli]", message); - } - } - - function appendLog(message: string): void { - log(message); - } - function detectPasswordRequired(error: string): bool { if (!error || error.length === 0) { return false; } - - return (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); + + return (error.includes("Secrets were required") || error.includes("Secrets were required, but not provided") || error.includes("No secrets provided") || error.includes("802-11-wireless-security.psk") || error.includes("password for") || (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && !error.includes("Connection activated") && !error.includes("successfully"); } function parseNetworkOutput(output: string): list { if (!output || output.length === 0) { return []; } - + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; const rep = new RegExp("\\\\:", "g"); const rep2 = new RegExp(PLACEHOLDER, "g"); - - const allNetworks = output.trim().split("\n") - .filter(line => line && line.length > 0) - .map(n => { - const net = n.replace(rep, PLACEHOLDER).split(":"); - return { - active: net[0] === "yes", - strength: parseInt(net[1] || "0", 10) || 0, - frequency: parseInt(net[2] || "0", 10) || 0, - ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), - bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), - security: (net[5] ?? "").trim() - }; - }) - .filter(n => n.ssid && n.ssid.length > 0); - + + const allNetworks = output.trim().split("\n").filter(line => line && line.length > 0).map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }).filter(n => n.ssid && n.ssid.length > 0); + return allNetworks; } @@ -119,7 +91,7 @@ Singleton { if (!networks || networks.length === 0) { return []; } - + const networkMap = new Map(); for (const network of networks) { const existing = networkMap.get(network.ssid); @@ -135,7 +107,7 @@ Singleton { } } } - + return Array.from(networkMap.values()); } @@ -143,7 +115,7 @@ Singleton { if (!command || command.length === 0) { return false; } - + return command.includes(root.nmcliCommandWifi) || command.includes(root.nmcliCommandConnection); } @@ -151,16 +123,16 @@ Singleton { if (!output || output.length === 0) { return []; } - + const interfaces = []; const lines = output.trim().split("\n"); - + for (const line of lines) { const parts = line.split(":"); if (parts.length >= 2) { const deviceType = parts[1]; let shouldInclude = false; - + if (filterType === root.deviceTypeWifi && deviceType === root.deviceTypeWifi) { shouldInclude = true; } else if (filterType === root.deviceTypeEthernet && deviceType === root.deviceTypeEthernet) { @@ -168,7 +140,7 @@ Singleton { } else if (filterType === "both" && (deviceType === root.deviceTypeWifi || deviceType === root.deviceTypeEthernet)) { shouldInclude = true; } - + if (shouldInclude) { interfaces.push({ device: parts[0] || "", @@ -179,7 +151,7 @@ Singleton { } } } - + return interfaces; } @@ -187,53 +159,53 @@ Singleton { if (!state || state.length === 0) { return false; } - - return state === "100 (connected)" || - state === "connected" || - state.startsWith("connected"); + + return state === "100 (connected)" || state === "connected" || state.startsWith("connected"); } function executeCommand(args: list, callback: var): void { const proc = commandProc.createObject(root); proc.command = ["nmcli", ...args]; proc.callback = callback; - + activeProcesses.push(proc); - + proc.processFinished.connect(() => { const index = activeProcesses.indexOf(proc); if (index >= 0) { activeProcesses.splice(index, 1); } }); - + Qt.callLater(() => { proc.exec(proc.command); }); } function getDeviceStatus(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { - if (callback) callback(result.output); + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { + if (callback) + callback(result.output); }); } function getWirelessInterfaces(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeWifi); root.wirelessInterfaces = interfaces; - if (callback) callback(interfaces); + if (callback) + callback(interfaces); }); } function getEthernetInterfaces(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeEthernet); const devices = []; - + for (const iface of interfaces) { const connected = isConnectedState(iface.state); - + devices.push({ interface: iface.device, type: iface.type, @@ -248,16 +220,17 @@ Singleton { speed: "" }); } - + root.ethernetInterfaces = interfaces; root.ethernetDevices = devices; - if (callback) callback(interfaces); + if (callback) + callback(interfaces); }); } function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { if (connectionName && connectionName.length > 0) { - executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -268,10 +241,11 @@ Singleton { } }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } else if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], result => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -280,62 +254,94 @@ Singleton { }, 1000); }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } else { - if (callback) callback({ success: false, output: "", error: "No connection name or interface specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No connection name or interface specified", + exitCode: -1 + }); } } function disconnectEthernet(connectionName: string, callback: var): void { if (!connectionName || connectionName.length === 0) { - if (callback) callback({ success: false, output: "", error: "No connection name specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No connection name specified", + exitCode: -1 + }); return; } - - executeCommand([root.nmcliCommandConnection, "down", connectionName], (result) => { + + executeCommand([root.nmcliCommandConnection, "down", connectionName], result => { if (result.success) { root.ethernetDeviceDetails = null; Qt.callLater(() => { getEthernetInterfaces(() => {}); }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } function getAllInterfaces(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, "both"); - if (callback) callback(interfaces); + if (callback) + callback(interfaces); }); } function isInterfaceConnected(interfaceName: string, callback: var): void { - executeCommand([root.nmcliCommandDevice, "status"], (result) => { + executeCommand([root.nmcliCommandDevice, "status"], result => { const lines = result.output.trim().split("\n"); for (const line of lines) { const parts = line.split(/\s+/); if (parts.length >= 3 && parts[0] === interfaceName) { const connected = isConnectedState(parts[2]); - if (callback) callback(connected); + if (callback) + callback(connected); return; } } - if (callback) callback(false); + if (callback) + callback(false); }); } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { if (isSecure) { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; - connectWireless(ssid, "", bssid, (result) => { + connectWireless(ssid, "", bssid, result => { if (result.success) { - if (callback) callback({ success: true, usedSavedPassword: true, output: result.output, error: "", exitCode: 0 }); + if (callback) + callback({ + success: true, + usedSavedPassword: true, + output: result.output, + error: "", + exitCode: 0 + }); } else if (result.needsPassword) { - if (callback) callback({ success: false, needsPassword: true, output: result.output, error: result.error, exitCode: result.exitCode }); + if (callback) + callback({ + success: false, + needsPassword: true, + output: result.output, + error: result.error, + exitCode: result.exitCode + }); } else { - if (callback) callback(result); + if (callback) + callback(result); } }); } else { @@ -351,72 +357,68 @@ Singleton { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; const retries = retryCount !== undefined ? retryCount : 0; const maxRetries = 2; - + if (callback) { - root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback, retryCount: retries }; + root.pendingConnection = { + ssid: ssid, + bssid: hasBssid ? bssid : "", + callback: callback, + retryCount: retries + }; connectionCheckTimer.start(); immediateCheckTimer.checkCount = 0; immediateCheckTimer.start(); } - + if (password && password.length > 0 && hasBssid) { const bssidUpper = bssid.toUpperCase(); createConnectionWithPassword(ssid, bssidUpper, password, callback); return; } - + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid]; if (password && password.length > 0) { cmd.push(root.connectionParamPassword, password); } - executeCommand(cmd, (result) => { + executeCommand(cmd, result => { if (result.needsPassword && callback) { - if (callback) callback(result); + if (callback) + callback(result); return; } - + if (!result.success && root.pendingConnection && retries < maxRetries) { - log("Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + console.warn("[NMCLI] Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); Qt.callLater(() => { connectWireless(ssid, password, bssid, callback, retries + 1); }, 1000); - } else if (!result.success && root.pendingConnection) { - } else if (result.success && callback) { - } else if (!result.success && !root.pendingConnection) { - if (callback) callback(result); + } else if (!result.success && root.pendingConnection) {} else if (result.success && callback) {} else if (!result.success && !root.pendingConnection) { + if (callback) + callback(result); } }); } function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { checkAndDeleteConnection(ssid, () => { - const cmd = [root.nmcliCommandConnection, "add", - root.connectionParamType, root.deviceTypeWifi, - root.connectionParamConName, ssid, - root.connectionParamIfname, "*", - root.connectionParamSsid, ssid, - root.connectionParamBssid, bssidUpper, - root.securityKeyMgmt, root.keyMgmtWpaPsk, - root.securityPsk, password]; - - executeCommand(cmd, (result) => { + const cmd = [root.nmcliCommandConnection, "add", root.connectionParamType, root.deviceTypeWifi, root.connectionParamConName, ssid, root.connectionParamIfname, "*", root.connectionParamSsid, ssid, root.connectionParamBssid, bssidUpper, root.securityKeyMgmt, root.keyMgmtWpaPsk, root.securityPsk, password]; + + executeCommand(cmd, result => { if (result.success) { loadSavedConnections(() => {}); activateConnection(ssid, callback); } else { - const hasDuplicateWarning = result.error && ( - result.error.includes("another connection with the name") || - result.error.includes("Reference the connection by its uuid") - ); - + const hasDuplicateWarning = result.error && (result.error.includes("another connection with the name") || result.error.includes("Reference the connection by its uuid")); + if (hasDuplicateWarning || (result.exitCode > 0 && result.exitCode < 10)) { loadSavedConnections(() => {}); activateConnection(ssid, callback); } else { - log("Connection profile creation failed, trying fallback..."); + console.warn("[NMCLI] Connection profile creation failed, trying fallback..."); let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; - executeCommand(fallbackCmd, (fallbackResult) => { - if (callback) callback(fallbackResult); + executeCommand(fallbackCmd, fallbackResult => { + if (callback) + callback(fallbackResult); }); } } @@ -425,34 +427,38 @@ Singleton { } function checkAndDeleteConnection(ssid: string, callback: var): void { - executeCommand([root.nmcliCommandConnection, "show", ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "show", ssid], result => { if (result.success) { - executeCommand([root.nmcliCommandConnection, "delete", ssid], (deleteResult) => { + executeCommand([root.nmcliCommandConnection, "delete", ssid], deleteResult => { Qt.callLater(() => { - if (callback) callback(); + if (callback) + callback(); }, 300); }); } else { - if (callback) callback(); + if (callback) + callback(); } }); } function activateConnection(connectionName: string, callback: var): void { - executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { - if (callback) callback(result); + executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { + if (callback) + callback(result); }); } function loadSavedConnections(callback: var): void { - executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], (result) => { + executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], result => { if (!result.success) { root.savedConnections = []; root.savedConnectionSsids = []; - if (callback) callback([]); + if (callback) + callback([]); return; } - + parseConnectionList(result.output, callback); }); } @@ -461,22 +467,22 @@ Singleton { const lines = output.trim().split("\n").filter(line => line.length > 0); const wifiConnections = []; const connections = []; - + for (const line of lines) { const parts = line.split(":"); if (parts.length >= 2) { const name = parts[0]; const type = parts[1]; connections.push(name); - + if (type === root.connectionTypeWireless) { wifiConnections.push(name); } } } - + root.savedConnections = connections; - + if (wifiConnections.length > 0) { root.wifiConnectionQueue = wifiConnections; root.currentSsidQueryIndex = 0; @@ -485,7 +491,8 @@ Singleton { } else { root.savedConnectionSsids = []; root.wifiConnectionQueue = []; - if (callback) callback(root.savedConnectionSsids); + if (callback) + callback(root.savedConnectionSsids); } } @@ -493,8 +500,8 @@ Singleton { if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; root.currentSsidQueryIndex++; - - executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], (result) => { + + executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], result => { if (result.success) { processSsidOutput(result.output); } @@ -503,7 +510,8 @@ Singleton { } else { root.wifiConnectionQueue = []; root.currentSsidQueryIndex = 0; - if (callback) callback(root.savedConnectionSsids); + if (callback) + callback(root.savedConnectionSsids); } } @@ -530,70 +538,73 @@ Singleton { return false; } const ssidLower = ssid.toLowerCase().trim(); - + if (root.active && root.active.ssid) { const activeSsidLower = root.active.ssid.toLowerCase().trim(); if (activeSsidLower === ssidLower) { return true; } } - - const hasSsid = root.savedConnectionSsids.some(savedSsid => - savedSsid && savedSsid.toLowerCase().trim() === ssidLower - ); - + + const hasSsid = root.savedConnectionSsids.some(savedSsid => savedSsid && savedSsid.toLowerCase().trim() === ssidLower); + if (hasSsid) { return true; } - - const hasConnectionName = root.savedConnections.some(connName => - connName && connName.toLowerCase().trim() === ssidLower - ); - + + const hasConnectionName = root.savedConnections.some(connName => connName && connName.toLowerCase().trim() === ssidLower); + return hasConnectionName; } function forgetNetwork(ssid: string, callback: var): void { if (!ssid || ssid.length === 0) { - if (callback) callback({ success: false, output: "", error: "No SSID specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No SSID specified", + exitCode: -1 + }); return; } - - const connectionName = root.savedConnections.find(conn => - conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() - ) || ssid; - - executeCommand([root.nmcliCommandConnection, "delete", connectionName], (result) => { + + const connectionName = root.savedConnections.find(conn => conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim()) || ssid; + + executeCommand([root.nmcliCommandConnection, "delete", connectionName], result => { if (result.success) { Qt.callLater(() => { loadSavedConnections(() => {}); }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } function disconnect(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { - if (callback) callback(result.success ? result.output : ""); + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], result => { + if (callback) + callback(result.success ? result.output : ""); }); } else { - executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { - if (callback) callback(result.success ? result.output : ""); + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { + if (callback) + callback(result.success ? result.output : ""); }); } } function disconnectFromNetwork(): void { if (active && active.ssid) { - executeCommand([root.nmcliCommandConnection, "down", active.ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "down", active.ssid], result => { if (result.success) { getNetworks(() => {}); } }); } else { - executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { if (result.success) { getNetworks(() => {}); } @@ -602,13 +613,14 @@ Singleton { } function getDeviceDetails(interfaceName: string, callback: var): void { - executeCommand([root.nmcliCommandDevice, "show", interfaceName], (result) => { - if (callback) callback(result.output); + executeCommand([root.nmcliCommandDevice, "show", interfaceName], result => { + if (callback) + callback(result.output); }); } function refreshStatus(callback: var): void { - getDeviceStatus((output) => { + getDeviceStatus(output => { const lines = output.trim().split("\n"); let connected = false; let activeIf = ""; @@ -631,31 +643,48 @@ Singleton { root.activeInterface = activeIf; root.activeConnection = activeConn; - if (callback) callback({ connected, interface: activeIf, connection: activeConn }); + if (callback) + callback({ + connected, + interface: activeIf, + connection: activeConn + }); }); } function bringInterfaceUp(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], result => { if (callback) { callback(result); } }); } else { - if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No interface specified", + exitCode: -1 + }); } } function bringInterfaceDown(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], result => { if (callback) { callback(result); } }); } else { - if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No interface specified", + exitCode: -1 + }); } } @@ -664,7 +693,7 @@ Singleton { if (interfaceName && interfaceName.length > 0) { cmd.push(root.connectionParamIfname, interfaceName); } - executeCommand(cmd, (result) => { + executeCommand(cmd, result => { if (callback) { callback(result); } @@ -677,14 +706,16 @@ Singleton { function enableWifi(enabled: bool, callback: var): void { const cmd = enabled ? "on" : "off"; - executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], result => { if (result.success) { - getWifiStatus((status) => { + getWifiStatus(status => { root.wifiEnabled = status; - if (callback) callback(result); + if (callback) + callback(result); }); } else { - if (callback) callback(result); + if (callback) + callback(result); } }); } @@ -695,33 +726,32 @@ Singleton { } function getWifiStatus(callback: var): void { - executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], result => { if (result.success) { const enabled = result.output.trim() === "enabled"; root.wifiEnabled = enabled; - if (callback) callback(enabled); + if (callback) + callback(enabled); } else { - if (callback) callback(root.wifiEnabled); + if (callback) + callback(root.wifiEnabled); } }); } function getNetworks(callback: var): void { - executeCommand(["-g", root.networkDetailFields, "d", "w"], (result) => { + executeCommand(["-g", root.networkDetailFields, "d", "w"], result => { if (!result.success) { - if (callback) callback([]); + if (callback) + callback([]); return; } - + const allNetworks = parseNetworkOutput(result.output); const networks = deduplicateNetworks(allNetworks); const rNetworks = root.networks; - - const destroyed = rNetworks.filter(rn => !networks.find(n => - n.frequency === rn.frequency && - n.ssid === rn.ssid && - n.bssid === rn.bssid - )); + + const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); for (const network of destroyed) { const index = rNetworks.indexOf(network); if (index >= 0) { @@ -729,13 +759,9 @@ Singleton { network.destroy(); } } - + for (const network of networks) { - const match = rNetworks.find(n => - n.frequency === network.frequency && - n.ssid === network.ssid && - n.bssid === network.bssid - ); + const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); if (match) { match.lastIpcObject = network; } else { @@ -744,8 +770,9 @@ Singleton { })); } } - - if (callback) callback(root.networks); + + if (callback) + callback(root.networks); checkPendingConnection(); }); } @@ -755,19 +782,21 @@ Singleton { if (interfaceName && interfaceName.length > 0) { cmd.push(root.connectionParamIfname, interfaceName); } - executeCommand(cmd, (result) => { + executeCommand(cmd, result => { if (!result.success) { - if (callback) callback([]); + if (callback) + callback([]); return; } - + const ssids = []; const lines = result.output.trim().split("\n"); const seenSSIDs = new Set(); - + for (const line of lines) { - if (!line || line.length === 0) continue; - + if (!line || line.length === 0) + continue; + const parts = line.split(":"); if (parts.length >= 1) { const ssid = parts[0].trim(); @@ -785,12 +814,13 @@ Singleton { } } } - + ssids.sort((a, b) => { return b.signalValue - a.signalValue; }); - - if (callback) callback(ssids); + + if (callback) + callback(ssids); }); } @@ -798,13 +828,13 @@ Singleton { if (!proc || !error || error.length === 0) { return false; } - + if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { return false; } - + const needsPassword = detectPasswordRequired(error); - + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); @@ -827,77 +857,79 @@ Singleton { } return true; } - + return false; } component CommandProcess: Process { id: proc + property var callback: null property list command: [] property bool callbackCalled: false property int exitCode: 0 + signal processFinished environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) stdout: StdioCollector { id: stdoutCollector - onStreamFinished: { - } } stderr: StdioCollector { id: stderrCollector + onStreamFinished: { const error = text.trim(); if (error && error.length > 0) { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - handlePasswordRequired(proc, error, output, -1); + root.handlePasswordRequired(proc, error, output, -1); } } } - onExited: { - proc.exitCode = exitCode; + onExited: code => { + exitCode = code; + Qt.callLater(() => { - if (proc.callbackCalled) { - proc.processFinished(); + if (callbackCalled) { + processFinished(); return; } - + if (proc.callback) { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; - const success = proc.exitCode === 0; + const success = exitCode === 0; const cmdIsConnection = isConnectionCommand(proc.command); - - if (handlePasswordRequired(proc, error, output, proc.exitCode)) { - proc.processFinished(); + + if (root.handlePasswordRequired(proc, error, output, exitCode)) { + processFinished(); return; } - - const needsPassword = cmdIsConnection && detectPasswordRequired(error); - + + const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); + if (!success && cmdIsConnection && root.pendingConnection) { const failedSsid = root.pendingConnection.ssid; root.connectionFailed(failedSsid); } - - proc.callbackCalled = true; - proc.callback({ + + callbackCalled = true; + callback({ success: success, output: output, error: error, exitCode: proc.exitCode, needsPassword: needsPassword || false }); - proc.processFinished(); + processFinished(); } else { - proc.processFinished(); + processFinished(); } }); } @@ -905,6 +937,7 @@ Singleton { Component { id: commandProc + CommandProcess {} } @@ -921,16 +954,18 @@ Singleton { Component { id: apComp + AccessPoint {} } Timer { id: connectionCheckTimer + interval: 4000 onTriggered: { if (root.pendingConnection) { const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - + if (!connected && root.pendingConnection.callback) { let foundPasswordError = false; for (let i = 0; i < root.activeProcesses.length; i++) { @@ -938,9 +973,9 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - if (isConnectionCommand(proc.command)) { - const needsPassword = detectPasswordRequired(error); - + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { const pending = root.pendingConnection; root.pendingConnection = null; @@ -967,7 +1002,7 @@ Singleton { } } } - + if (!foundPasswordError) { const pending = root.pendingConnection; const failedSsid = pending.ssid; @@ -975,10 +1010,10 @@ Singleton { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; root.connectionFailed(failedSsid); - pending.callback({ - success: false, - output: "", - error: "Connection timeout", + pending.callback({ + success: false, + output: "", + error: "Connection timeout", exitCode: -1, needsPassword: false }); @@ -994,22 +1029,29 @@ Singleton { Timer { id: immediateCheckTimer + + property int checkCount: 0 + interval: 500 repeat: true triggeredOnStart: false - property int checkCount: 0 onTriggered: { if (root.pendingConnection) { checkCount++; const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - + if (connected) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; if (root.pendingConnection.callback) { - root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); } root.pendingConnection = null; } else { @@ -1018,9 +1060,9 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - if (isConnectionCommand(proc.command)) { - const needsPassword = detectPasswordRequired(error); - + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); @@ -1047,7 +1089,7 @@ Singleton { } } } - + if (checkCount >= 6) { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; @@ -1069,7 +1111,12 @@ Singleton { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; if (root.pendingConnection.callback) { - root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); } root.pendingConnection = null; } else { @@ -1086,13 +1133,13 @@ Singleton { if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { return ""; } - + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; const octet1 = (mask >>> 24) & 0xff; const octet2 = (mask >>> 16) & 0xff; const octet3 = (mask >>> 8) & 0xff; const octet4 = mask & 0xff; - + return `${octet1}.${octet2}.${octet3}.${octet4}`; } @@ -1104,21 +1151,24 @@ Singleton { if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; } else { - if (callback) callback(null); + if (callback) + callback(null); return; } } - - executeCommand(["device", "show", interfaceName], (result) => { + + executeCommand(["device", "show", interfaceName], result => { if (!result.success || !result.output) { root.wirelessDeviceDetails = null; - if (callback) callback(null); + if (callback) + callback(null); return; } - + const details = parseDeviceDetails(result.output, false); root.wirelessDeviceDetails = details; - if (callback) callback(details); + if (callback) + callback(details); }); } @@ -1130,21 +1180,24 @@ Singleton { if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; } else { - if (callback) callback(null); + if (callback) + callback(null); return; } } - - executeCommand(["device", "show", interfaceName], (result) => { + + executeCommand(["device", "show", interfaceName], result => { if (!result.success || !result.output) { root.ethernetDeviceDetails = null; - if (callback) callback(null); + if (callback) + callback(null); return; } - + const details = parseDeviceDetails(result.output, true); root.ethernetDeviceDetails = details; - if (callback) callback(details); + if (callback) + callback(details); }); } @@ -1157,20 +1210,20 @@ Singleton { macAddress: "", speed: "" }; - + if (!output || output.length === 0) { return details; } - + const lines = output.trim().split("\n"); - + for (let i = 0; i < lines.length; i++) { const line = lines[i]; const parts = line.split(":"); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(":").trim(); - + if (key.startsWith("IP4.ADDRESS")) { const ipParts = value.split("/"); details.ipAddress = ipParts[0] || ""; @@ -1196,46 +1249,36 @@ Singleton { } } } - + return details; } Process { id: rescanProc + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] - onExited: { - getNetworks(() => {}); - } + onExited: root.getNetworks() } Process { id: monitorProc + running: true command: ["nmcli", "monitor"] environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) stdout: SplitParser { - onRead: { - log("Connection state change detected, refreshing..."); - root.refreshOnConnectionChange(); - } - } - - onExited: { - log("Monitor process exited, restarting..."); - Qt.callLater(() => { - monitorProc.running = true; - }, 2000); + onRead: root.refreshOnConnectionChange() } + onExited: Qt.callLater(() => monitorProc.running = true, 2000) } function refreshOnConnectionChange(): void { - getNetworks((networks) => { + getNetworks(networks => { const newActive = root.active; - + if (newActive && newActive.active) { Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { @@ -1246,7 +1289,7 @@ Singleton { getWirelessDeviceDetails(activeWireless.device, () => {}); } } - + if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { return isConnectedState(iface.state); @@ -1260,7 +1303,7 @@ Singleton { root.wirelessDeviceDetails = null; root.ethernetDeviceDetails = null; } - + getWirelessInterfaces(() => {}); getEthernetInterfaces(() => { if (root.activeEthernet && root.activeEthernet.connected) { @@ -1277,7 +1320,7 @@ Singleton { getNetworks(() => {}); loadSavedConnections(() => {}); getEthernetInterfaces(() => {}); - + Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { @@ -1287,7 +1330,7 @@ Singleton { getWirelessDeviceDetails(activeWireless.device, () => {}); } } - + if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { return isConnectedState(iface.state); @@ -1299,4 +1342,3 @@ Singleton { }, 2000); } } - -- cgit v1.2.3-freya From 05b0660627586dc7624380e82b818b53004771f5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 01:31:53 -0500 Subject: controlcenter: password input errors/wrong pass --- modules/bar/popouts/WirelessPasswordPopout.qml | 85 ++- modules/controlcenter/network/NetworkingPane.qml | 691 +++++++++++---------- .../network/WirelessPasswordDialog.qml | 115 +++- services/Nmcli.qml | 10 +- 4 files changed, 502 insertions(+), 399 deletions(-) (limited to 'services') diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml index aa7f40f..59a15b9 100644 --- a/modules/bar/popouts/WirelessPasswordPopout.qml +++ b/modules/bar/popouts/WirelessPasswordPopout.qml @@ -32,14 +32,22 @@ ColumnLayout { } } // Force focus to password container when popout becomes active - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - }, 100); - }, 100); + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + }); } } } + Timer { + id: focusTimer + interval: 150 + onTriggered: { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); + } + } + spacing: Appearance.spacing.normal implicitWidth: 400 @@ -51,19 +59,15 @@ ColumnLayout { Component.onCompleted: { if (shouldBeVisible) { - Qt.callLater(() => { - root.forceActiveFocus(); - passwordContainer.forceActiveFocus(); - }, 150); + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); } } onShouldBeVisibleChanged: { if (shouldBeVisible) { - Qt.callLater(() => { - root.forceActiveFocus(); - passwordContainer.forceActiveFocus(); - }, 150); + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); } } @@ -243,20 +247,26 @@ ColumnLayout { target: root function onShouldBeVisibleChanged(): void { if (root.shouldBeVisible) { - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - }, 50); + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); passwordContainer.passwordBuffer = ""; connectButton.hasError = false; } } } + Timer { + id: passwordFocusTimer + interval: 50 + onTriggered: { + passwordContainer.forceActiveFocus(); + } + } + Component.onCompleted: { if (root.shouldBeVisible) { - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - }, 100); + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); } } @@ -489,22 +499,8 @@ ColumnLayout { if (isConnected) { // Successfully connected - give it a moment for network list to update - Qt.callLater(() => { - // Double-check connection is still active - if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { - const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (stillConnected) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - // Return to network popout on successful connection - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - closeDialog(); - } - } - }, 500); + // Use Timer for actual delay + connectionSuccessTimer.start(); return; } @@ -545,6 +541,27 @@ ColumnLayout { } } + Timer { + id: connectionSuccessTimer + interval: 500 + onTriggered: { + // Double-check connection is still active + if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + // Return to network popout on successful connection + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + closeDialog(); + } + } + } + } + Connections { target: Nmcli function onActiveChanged() { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 56ab7f1..74e0034 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -15,500 +15,507 @@ import Quickshell.Widgets import QtQuick import QtQuick.Layouts -RowLayout { +Item { id: root required property Session session anchors.fill: parent - spacing: 0 + RowLayout { + id: contentLayout - Item { - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true + anchors.fill: parent + spacing: 0 - // Left pane - networking list with collapsible sections - StyledFlickable { - id: leftFlickable + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true - 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 - flickableDirection: Flickable.VerticalFlick - contentHeight: leftContent.height - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: leftFlickable - } + // Left pane - networking list with collapsible sections + StyledFlickable { + id: leftFlickable - ColumnLayout { - id: leftContent + 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 + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height + clip: true - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.normal + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } - // Settings header above the collapsible sections - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.smaller + ColumnLayout { + id: leftContent - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal - Item { + // Settings header above the collapsible sections + RowLayout { Layout.fillWidth: true - } + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } - ToggleButton { - toggled: Nmcli.wifiEnabled - icon: "wifi" - accent: "Tertiary" + Item { + Layout.fillWidth: true + } - onClicked: { - Nmcli.toggleWifi(null); + ToggleButton { + toggled: Nmcli.wifiEnabled + icon: "wifi" + accent: "Tertiary" + + onClicked: { + Nmcli.toggleWifi(null); + } } - } - ToggleButton { - toggled: Nmcli.scanning - icon: "wifi_find" - accent: "Secondary" + ToggleButton { + toggled: Nmcli.scanning + icon: "wifi_find" + accent: "Secondary" - onClicked: { - Nmcli.rescanWifi(); + onClicked: { + Nmcli.rescanWifi(); + } } - } - ToggleButton { - toggled: !root.session.ethernet.active && !root.session.network.active - icon: "settings" - accent: "Primary" - - onClicked: { - if (root.session.ethernet.active || root.session.network.active) { - root.session.ethernet.active = null; - root.session.network.active = null; - } else { - // Toggle to show settings - prefer ethernet if available, otherwise wireless - if (Nmcli.ethernetDevices.length > 0) { - root.session.ethernet.active = Nmcli.ethernetDevices[0]; - } else if (Nmcli.networks.length > 0) { - root.session.network.active = Nmcli.networks[0]; + ToggleButton { + toggled: !root.session.ethernet.active && !root.session.network.active + icon: "settings" + accent: "Primary" + + onClicked: { + if (root.session.ethernet.active || root.session.network.active) { + root.session.ethernet.active = null; + root.session.network.active = null; + } else { + // Toggle to show settings - prefer ethernet if available, otherwise wireless + if (Nmcli.ethernetDevices.length > 0) { + root.session.ethernet.active = Nmcli.ethernetDevices[0]; + } else if (Nmcli.networks.length > 0) { + root.session.network.active = Nmcli.networks[0]; + } } } } } - } - - CollapsibleSection { - id: ethernetListSection - Layout.fillWidth: true - title: qsTr("Ethernet") - expanded: true + CollapsibleSection { + id: ethernetListSection - ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + title: qsTr("Ethernet") + expanded: true - RowLayout { + ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small - StyledText { - text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) - font.pointSize: Appearance.font.size.normal - font.weight: 500 + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } } - } - StyledText { - Layout.fillWidth: true - text: qsTr("All available ethernet devices") - color: Colours.palette.m3outline - } + StyledText { + Layout.fillWidth: true + text: qsTr("All available ethernet devices") + color: Colours.palette.m3outline + } - Repeater { - id: ethernetRepeater + Repeater { + id: ethernetRepeater - Layout.fillWidth: true - model: Nmcli.ethernetDevices + Layout.fillWidth: true + model: Nmcli.ethernetDevices - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - Layout.fillWidth: true + Layout.fillWidth: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal - StateLayer { - function onClicked(): void { - root.session.network.active = null; - root.session.ethernet.active = modelData; + StateLayer { + function onClicked(): void { + root.session.network.active = null; + root.session.ethernet.active = modelData; + } } - } - RowLayout { - id: rowLayout + RowLayout { + id: rowLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal - color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - MaterialIcon { - id: icon + 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 + 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 - elide: Text.ElideRight - maximumLineCount: 1 + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 - text: modelData.interface || qsTr("Unknown") - } + 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 - } + 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 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) - StateLayer { - function onClicked(): void { - if (modelData.connected && modelData.connection) { - Nmcli.disconnectEthernet(modelData.connection, () => {}); - } else { - Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + StateLayer { + function onClicked(): void { + if (modelData.connected && modelData.connection) { + Nmcli.disconnectEthernet(modelData.connection, () => {}); + } else { + Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + } } } - } - MaterialIcon { - id: connectIcon + MaterialIcon { + id: connectIcon - anchors.centerIn: parent - text: modelData.connected ? "link_off" : "link" - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + 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 + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } } } } - } - - CollapsibleSection { - id: wirelessListSection - Layout.fillWidth: true - title: qsTr("Wireless") - expanded: true + CollapsibleSection { + id: wirelessListSection - ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + title: qsTr("Wireless") + expanded: true - RowLayout { + ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small - StyledText { - text: qsTr("Networks (%1)").arg(Nmcli.networks.length) - font.pointSize: Appearance.font.size.normal - font.weight: 500 + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Networks (%1)").arg(Nmcli.networks.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + StyledText { + visible: Nmcli.scanning + text: qsTr("Scanning...") + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.small + } } StyledText { - visible: Nmcli.scanning - text: qsTr("Scanning...") - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.small + Layout.fillWidth: true + text: qsTr("All available WiFi networks") + color: Colours.palette.m3outline } - } - - StyledText { - Layout.fillWidth: true - text: qsTr("All available WiFi networks") - color: Colours.palette.m3outline - } - Repeater { - id: wirelessRepeater + Repeater { + id: wirelessRepeater - Layout.fillWidth: true - model: ScriptModel { - values: [...Nmcli.networks].sort((a, b) => { - // Put active/connected network first - if (a.active !== b.active) - return b.active - a.active; - // Then sort by signal strength - return b.strength - a.strength; - }) - } + Layout.fillWidth: true + model: ScriptModel { + values: [...Nmcli.networks].sort((a, b) => { + // Put active/connected network first + if (a.active !== b.active) + return b.active - a.active; + // Then sort by signal strength + return b.strength - a.strength; + }) + } - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - Layout.fillWidth: true + Layout.fillWidth: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (modelData && root.session.network.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal StateLayer { function onClicked(): void { + if (!modelData) { + return; + } root.session.ethernet.active = null; root.session.network.active = modelData; // Check if we need to refresh saved connections when selecting a network - if (modelData && modelData.ssid) { + if (modelData.ssid) { checkSavedProfileForNetwork(modelData.ssid); } } } - RowLayout { - id: wirelessRowLayout + RowLayout { + id: wirelessRowLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal - color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + color: (modelData && modelData.active) ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - MaterialIcon { - id: wirelessIcon + MaterialIcon { + id: wirelessIcon - anchors.centerIn: parent - text: Icons.getNetworkIcon(modelData.strength) - font.pointSize: Appearance.font.size.large - fill: modelData.active ? 1 : 0 - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + anchors.centerIn: parent + text: Icons.getNetworkIcon(modelData && modelData.strength !== undefined ? modelData.strength : 0) + font.pointSize: Appearance.font.size.large + fill: (modelData && modelData.active) ? 1 : 0 + color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } } - } - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 - text: modelData.ssid || qsTr("Unknown") - } + text: (modelData && modelData.ssid) ? modelData.ssid : qsTr("Unknown") + } - StyledText { - text: modelData.active ? qsTr("Connected") : (modelData.isSecure ? qsTr("Secured") : qsTr("Open")) - color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.active ? 500 : 400 - } + StyledText { + text: (modelData && modelData.active) ? qsTr("Connected") : ((modelData && modelData.isSecure) ? qsTr("Secured") : qsTr("Open")) + color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: (modelData && modelData.active) ? 500 : 400 + } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, (modelData && modelData.active) ? 1 : 0) - StateLayer { - function onClicked(): void { - if (modelData.active) { - Nmcli.disconnectFromNetwork(); - } else { - handleWirelessConnect(modelData); + StateLayer { + function onClicked(): void { + if (modelData && modelData.active) { + Nmcli.disconnectFromNetwork(); + } else if (modelData) { + handleWirelessConnect(modelData); + } } } - } - MaterialIcon { - id: wirelessConnectIcon + MaterialIcon { + id: wirelessConnectIcon - anchors.centerIn: parent - text: modelData.active ? "link_off" : "link" - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + anchors.centerIn: parent + text: (modelData && modelData.active) ? "link_off" : "link" + color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } } } - } - implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2 + } } } } } } - } - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 + 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" - clip: true - - // Right pane - networking details/settings - Loader { - id: loader - property var ethernetPane: root.session.ethernet.active - property var wirelessPane: root.session.network.active - property var pane: ethernetPane || wirelessPane - property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "") + Item { + Layout.fillWidth: true + Layout.fillHeight: true + ClippingRectangle { anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - opacity: 1 - scale: 1 - transformOrigin: Item.Center + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + radius: rightBorder.innerRadius + color: "transparent" clip: true - asynchronous: true - sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : 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 + + // Right pane - networking details/settings + Loader { + id: loader + + property var ethernetPane: root.session.ethernet.active + property var wirelessPane: root.session.network.active + property var pane: ethernetPane || wirelessPane + property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "") + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: true + asynchronous: true + sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : 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 + } } - Anim { - target: loader - property: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + 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 = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + onPaneChanged: { + paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + } } } - } - InnerBorder { - id: rightBorder + InnerBorder { + id: rightBorder - leftThickness: Appearance.padding.normal / 2 - } + leftThickness: Appearance.padding.normal / 2 + } - Component { - id: settings + Component { + id: settings - StyledFlickable { - id: settingsFlickable + StyledFlickable { + id: settingsFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - clip: true + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + clip: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } - NetworkSettings { - id: settingsInner + NetworkSettings { + id: settingsInner - anchors.left: parent.left - anchors.right: parent.right - session: root.session + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } } } - } - Component { - id: ethernetDetails + Component { + id: ethernetDetails - EthernetDetails { - session: root.session + EthernetDetails { + session: root.session + } } - } - Component { - id: wirelessDetails + Component { + id: wirelessDetails - WirelessDetails { - session: root.session + WirelessDetails { + session: root.session + } } } } WirelessPasswordDialog { - Layout.fillWidth: true - Layout.fillHeight: true + anchors.fill: parent session: root.session z: 1000 } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index f3381b7..4b350be 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -131,14 +131,17 @@ Item { Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.small - visible: connectButton.connecting + visible: connectButton.connecting || connectButton.hasError text: { + if (connectButton.hasError) { + return qsTr("Connection failed. Please check your password and try again."); + } if (connectButton.connecting) { return qsTr("Connecting..."); } return ""; } - color: Colours.palette.m3onSurfaceVariant + color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small font.weight: 400 wrapMode: Text.WordWrap @@ -153,18 +156,31 @@ Item { focus: true Keys.onPressed: event => { + // Ensure we have focus when receiving keyboard input + if (!activeFocus) { + forceActiveFocus(); + } + + // Clear error when user starts typing + if (connectButton.hasError && event.text && event.text.length > 0) { + connectButton.hasError = false; + } + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { if (connectButton.enabled) { connectButton.clicked(); } + event.accepted = true; } else if (event.key === Qt.Key_Backspace) { if (event.modifiers & Qt.ControlModifier) { passwordBuffer = ""; } else { passwordBuffer = passwordBuffer.slice(0, -1); } + event.accepted = true; } else if (event.text && event.text.length > 0) { passwordBuffer += event.text; + event.accepted = true; } } @@ -178,6 +194,7 @@ Item { Qt.callLater(() => { passwordContainer.forceActiveFocus(); passwordContainer.passwordBuffer = ""; + connectButton.hasError = false; }); } } @@ -198,13 +215,29 @@ Item { StyledRect { anchors.fill: parent radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - border.width: passwordContainer.activeFocus ? 2 : 1 - border.color: passwordContainer.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) + border.color: { + if (connectButton.hasError) { + return Colours.palette.m3error; + } + if (passwordContainer.activeFocus) { + return Colours.palette.m3primary; + } + return root.visible ? Colours.palette.m3outline : "transparent"; + } Behavior on border.color { CAnim {} } + + Behavior on border.width { + CAnim {} + } + + Behavior on color { + CAnim {} + } } StateLayer { @@ -329,6 +362,9 @@ Item { TextButton { id: connectButton + property bool connecting: false + property bool hasError: false + Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 inactiveColour: Colours.palette.m3primary @@ -336,8 +372,6 @@ Item { text: qsTr("Connect") enabled: passwordContainer.passwordBuffer.length > 0 && !connecting - property bool connecting: false - onClicked: { if (!root.network || connecting) { return; @@ -348,6 +382,9 @@ Item { return; } + // Clear any previous error + hasError = false; + // Set connecting state connecting = true; enabled = false; @@ -361,11 +398,27 @@ Item { // Shouldn't happen since we provided password connectionMonitor.stop(); connecting = false; + hasError = true; enabled = true; text = qsTr("Connect"); - } else - // Connection failed, monitor will handle timeout - {} + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } else { + // Connection failed immediately - show error + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } }); // Start monitoring connection @@ -386,18 +439,8 @@ Item { if (isConnected) { // Successfully connected - give it a moment for network list to update - Qt.callLater(() => { - // Double-check connection is still active - if (root.visible && Nmcli.active && Nmcli.active.ssid) { - const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (stillConnected) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - closeDialog(); - } - } - }, 500); + // Use Timer for actual delay + connectionSuccessTimer.start(); return; } @@ -407,8 +450,14 @@ Item { if (connectionMonitor.repeatCount > 10) { connectionMonitor.stop(); connectButton.connecting = false; + connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } } } } @@ -432,6 +481,23 @@ Item { } } + Timer { + id: connectionSuccessTimer + interval: 500 + onTriggered: { + // Double-check connection is still active + if (root.visible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + closeDialog(); + } + } + } + } + Connections { target: Nmcli function onActiveChanged() { @@ -443,8 +509,12 @@ Item { if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) { connectionMonitor.stop(); connectButton.connecting = false; + connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + Nmcli.forgetNetwork(ssid); } } } @@ -457,6 +527,7 @@ Item { isClosing = true; passwordContainer.passwordBuffer = ""; connectButton.connecting = false; + connectButton.hasError = false; connectButton.text = qsTr("Connect"); connectionMonitor.stop(); } diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 24a93da..36bd3e6 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1272,7 +1272,15 @@ Singleton { stdout: SplitParser { onRead: root.refreshOnConnectionChange() } - onExited: Qt.callLater(() => monitorProc.running = true, 2000) + onExited: monitorRestartTimer.start() + } + + Timer { + id: monitorRestartTimer + interval: 2000 + onTriggered: { + monitorProc.running = true; + } } function refreshOnConnectionChange(): void { -- cgit v1.2.3-freya