From f2ff8e17d93b7b7be0d8b1868d269b702ae98abb Mon Sep 17 00:00:00 2001 From: RDXFGXY1 Date: Fri, 18 Sep 2026 14:25:33 +0100 Subject: [PATCH] feat(network): add hidden Wi-Fi connection flow --- .../qsbar/panels/NetworkManagerAdapter.qml | 133 ++++++ .../barstyles/qsbar/panels/NetworkPanel.qml | 381 +++++++++++++++++- 2 files changed, 513 insertions(+), 1 deletion(-) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/NetworkManagerAdapter.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/NetworkManagerAdapter.qml index fdef5d90b..24d8a7743 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/NetworkManagerAdapter.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/NetworkManagerAdapter.qml @@ -21,6 +21,7 @@ Item { property bool profilesLoaded: false property bool scanning: false + function findDevice(type) { var devices = networkDevices || [] for (var i = 0; i < devices.length; i++) { @@ -279,6 +280,53 @@ Item { refreshAfterAction.restart() } + + // Create and activate a hidden Wi-Fi profile through NetworkManager. + // security: "open", "wpa-psk", or "sae". + function connectHidden(connectionName, ssid, password, security) { + connectionName = (connectionName || "").trim() + ssid = (ssid || "").trim() + password = password || "" + security = security || "wpa-psk" + + if (connectionName === "" || ssid === "" || hiddenCreate.running || hiddenActivate.running || hiddenCleanup.running) + return + if (security !== "open" && security !== "wpa-psk" && security !== "sae") + return + if (security !== "open" && password === "") + return + + if (panel) { + panel.networkActionError = "" + panel.hiddenConnectionError = "" + } + + hiddenCreate.connectionName = connectionName + hiddenCreate.hiddenSsid = ssid + hiddenCreate.hiddenPassword = password + hiddenCreate.hiddenSecurity = security + + var args = [ + "nmcli", "--wait", "20", "connection", "add", + "type", "wifi", + "ifname", "*", + "con-name", connectionName, + "ssid", ssid, + "802-11-wireless.hidden", "yes" + ] + + if (security === "wpa-psk") { + args.push("wifi-sec.key-mgmt", "wpa-psk") + args.push("wifi-sec.psk", password) + } else if (security === "sae") { + args.push("wifi-sec.key-mgmt", "sae") + args.push("wifi-sec.psk", password) + } + + hiddenCreate.command = args + hiddenCreate.running = true + } + function refresh() { if (wifiDevice) wifiDevice.scannerEnabled = panelOpen && !wifiBlocked @@ -300,6 +348,9 @@ Item { wifiDevice.scannerEnabled = false profileList.running = false profileAction.running = false + hiddenCreate.running = false + hiddenActivate.running = false + hiddenCleanup.running = false } Timer { @@ -379,6 +430,88 @@ Item { } } + + Process { + id: hiddenCreate + property string connectionName: "" + property string hiddenSsid: "" + property string hiddenPassword: "" + property string hiddenSecurity: "wpa-psk" + + stdout: StdioCollector { id: hiddenCreateOut; waitForEnd: true } + stderr: StdioCollector { id: hiddenCreateErr; waitForEnd: true } + + onExited: function(exitCode, exitStatus) { + if (exitCode !== 0) { + if (adapter.panel) { + adapter.panel.hiddenConnecting = false + var message = hiddenCreateErr.text.trim() + adapter.panel.hiddenConnectionError = message !== "" + ? message.split("\n")[0] + : I18n.tr("Could not create hidden network") + } + return + } + + // Activate the exact profile name supplied by the user. + hiddenActivate.connectionName = hiddenCreate.connectionName + hiddenActivate.command = [ + "nmcli", "--wait", "25", + "connection", "up", "id", hiddenCreate.connectionName + ] + hiddenActivate.running = true + } + } + + Process { + id: hiddenActivate + property string connectionName: "" + + stdout: StdioCollector { id: hiddenActivateOut; waitForEnd: true } + stderr: StdioCollector { id: hiddenActivateErr; waitForEnd: true } + + onExited: function(exitCode, exitStatus) { + if (adapter.panel) { + adapter.panel.hiddenConnecting = false + + if (exitCode === 0) { + if (typeof adapter.panel.clearHiddenNetwork === "function") + adapter.panel.clearHiddenNetwork() + } else { + var message = hiddenActivateErr.text.trim() + adapter.panel.hiddenConnectionError = message !== "" + ? message.split("\n")[0] + : I18n.tr("Connection failed") + + // Do not leave a bad/typo hidden profile in Saved connections. + hiddenCleanup.command = [ + "nmcli", "--wait", "10", + "connection", "delete", "id", hiddenActivate.connectionName + ] + hiddenCleanup.running = true + } + } + + if (adapter.panelOpen) { + profileList.running = false + profileList.running = true + } + refreshAfterAction.restart() + } + } + + Process { + id: hiddenCleanup + stdout: StdioCollector { waitForEnd: true } + stderr: StdioCollector { waitForEnd: true } + + onExited: function(exitCode, exitStatus) { + if (adapter.panelOpen && !profileList.running) + profileList.running = true + refreshAfterAction.restart() + } + } + Timer { id: refreshAfterAction interval: 1500 diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/NetworkPanel.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/NetworkPanel.qml index f3b025c3f..af9c442ae 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/NetworkPanel.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/NetworkPanel.qml @@ -61,6 +61,14 @@ PanelWindow { property string networkActionError: "" property bool nmProfilesLoaded: false + property bool hiddenNetworkOpen: false + property string hiddenName: "" + property string hiddenSsid: "" + property string hiddenPassword: "" + property string hiddenSecurity: "wpa-psk" + property string hiddenConnectionError: "" + property bool hiddenConnecting: false + // ── wifi radio ── property bool wifiBlocked: false @@ -291,6 +299,70 @@ PanelWindow { onSavedOnlyChanged: resetNetworkSelection() + + function openHiddenNetwork() { + clearNmPassword() + networkActionError = "" + hiddenNetworkOpen = true + hiddenName = "" + hiddenSsid = "" + hiddenPassword = "" + hiddenSecurity = "wpa-psk" + hiddenConnectionError = "" + hiddenConnecting = false + Qt.callLater(function() { + if (hiddenNameInput.visible) + hiddenNameInput.forceActiveFocus() + }) + } + + function clearHiddenNetwork() { + hiddenNetworkOpen = false + hiddenName = "" + hiddenSsid = "" + hiddenPassword = "" + hiddenSecurity = "wpa-psk" + hiddenConnectionError = "" + hiddenConnecting = false + } + + function cycleHiddenSecurity() { + if (hiddenSecurity === "open") + hiddenSecurity = "wpa-psk" + else if (hiddenSecurity === "wpa-psk") + hiddenSecurity = "sae" + else + hiddenSecurity = "open" + hiddenConnectionError = "" + } + + function hiddenSecurityLabel() { + switch (hiddenSecurity) { + case "open": return I18n.tr("Open") + case "sae": return I18n.tr("WPA3 Personal") + default: return I18n.tr("WPA/WPA2 Personal") + } + } + + function submitHiddenNetwork() { + if (!nmAdapterReady || hiddenConnecting) + return + + var connectionName = hiddenName.trim() + var ssid = hiddenSsid.trim() + if (connectionName === "" || ssid === "") + return + + if (hiddenSecurity !== "open" && hiddenPassword === "") { + hiddenConnectionError = I18n.tr("Password is required") + return + } + + hiddenConnectionError = "" + hiddenConnecting = true + nmAdapter.item.connectHidden(connectionName, ssid, hiddenPassword, hiddenSecurity) + } + function openWifiSettings() { wifiRunner.running = false wifiRunner.running = true @@ -409,7 +481,7 @@ PanelWindow { return } - if (netPanel.nmPasswordSsid !== "") + if (netPanel.nmPasswordSsid !== "" || netPanel.hiddenNetworkOpen) return var entries = netPanel.shownNetworks @@ -1058,6 +1130,313 @@ PanelWindow { } } + + Rectangle { + width: parent.width + height: 28 + visible: root.useNM && netPanel.nmAdapterReady && !netPanel.wifiBlocked + && !netPanel.hiddenNetworkOpen && netPanel.nmPasswordSsid === "" + radius: root.panelButtonRadius + color: hiddenNetworkMa.containsMouse ? root.fillHover : root.fillIdle + border.color: hiddenNetworkMa.containsMouse ? root.seal : root.sep + border.width: 1 + Behavior on color { ColorAnimation { duration: 120 } } + + UiText { + anchors.centerIn: parent + text: I18n.tr("+ Connect to hidden network") + color: hiddenNetworkMa.containsMouse ? root.seal : root.ink + font.family: root.mono + font.pixelSize: 10 + } + + MouseArea { + id: hiddenNetworkMa + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: netPanel.openHiddenNetwork() + } + } + + Rectangle { + width: parent.width + height: visible ? hiddenForm.implicitHeight + 16 : 0 + visible: root.useNM && netPanel.nmAdapterReady && netPanel.hiddenNetworkOpen + radius: root.panelButtonRadius + color: root.fillIdle + border.color: netPanel.hiddenConnectionError !== "" ? root.sealRaw : root.seal + border.width: 1 + clip: true + + Column { + id: hiddenForm + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 8 + spacing: 6 + + UiText { + width: parent.width + text: netPanel.hiddenConnectionError !== "" + ? netPanel.hiddenConnectionError + : I18n.tr("Connect to hidden network") + color: netPanel.hiddenConnectionError !== "" ? root.sealRaw : root.ink + font.family: root.mono + font.pixelSize: 10 + elide: Text.ElideRight + } + + Rectangle { + width: parent.width + height: 24 + radius: root.panelButtonRadius + color: root.bg + border.color: hiddenNameInput.activeFocus ? root.seal : root.sep + border.width: 1 + + TextInput { + id: hiddenNameInput + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + verticalAlignment: TextInput.AlignVCenter + text: netPanel.hiddenName + color: root.ink + selectionColor: root.seal + selectedTextColor: root.paper + font.family: root.mono + font.pixelSize: 11 + clip: true + enabled: !netPanel.hiddenConnecting + onTextChanged: netPanel.hiddenName = text + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + hiddenSsidInput.forceActiveFocus() + event.accepted = true + } else if (event.key === Qt.Key_Escape) { + netPanel.clearHiddenNetwork() + event.accepted = true + } + } + } + + UiText { + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + visible: hiddenNameInput.text === "" && !hiddenNameInput.activeFocus + text: I18n.tr("Network name") + color: root.sumi + font.family: root.mono + font.pixelSize: 10 + } + } + + Rectangle { + width: parent.width + height: 24 + radius: root.panelButtonRadius + color: root.bg + border.color: hiddenSsidInput.activeFocus ? root.seal : root.sep + border.width: 1 + + TextInput { + id: hiddenSsidInput + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + verticalAlignment: TextInput.AlignVCenter + text: netPanel.hiddenSsid + color: root.ink + selectionColor: root.seal + selectedTextColor: root.paper + font.family: root.mono + font.pixelSize: 11 + clip: true + enabled: !netPanel.hiddenConnecting + onTextChanged: netPanel.hiddenSsid = text + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (netPanel.hiddenSecurity === "open") + netPanel.submitHiddenNetwork() + else + hiddenPasswordInput.forceActiveFocus() + event.accepted = true + } else if (event.key === Qt.Key_Escape) { + netPanel.clearHiddenNetwork() + event.accepted = true + } + } + } + + UiText { + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + visible: hiddenSsidInput.text === "" && !hiddenSsidInput.activeFocus + text: I18n.tr("SSID") + color: root.sumi + font.family: root.mono + font.pixelSize: 10 + } + } + + Rectangle { + width: parent.width + height: 26 + radius: root.panelButtonRadius + color: hiddenSecurityMa.containsMouse ? root.fillHover : root.bg + border.color: hiddenSecurityMa.containsMouse ? root.seal : root.sep + border.width: 1 + Behavior on color { ColorAnimation { duration: 120 } } + + UiText { + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Security") + color: root.sumiHi + font.family: root.mono + font.pixelSize: 10 + } + + UiText { + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + text: netPanel.hiddenSecurityLabel() + " ▾" + color: hiddenSecurityMa.containsMouse ? root.seal : root.ink + font.family: root.mono + font.pixelSize: 10 + } + + MouseArea { + id: hiddenSecurityMa + anchors.fill: parent + enabled: !netPanel.hiddenConnecting + hoverEnabled: true + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: netPanel.cycleHiddenSecurity() + } + } + + Rectangle { + width: parent.width + height: netPanel.hiddenSecurity === "open" ? 0 : 24 + visible: netPanel.hiddenSecurity !== "open" + radius: root.panelButtonRadius + color: root.bg + border.color: hiddenPasswordInput.activeFocus ? root.seal : root.sep + border.width: 1 + + TextInput { + id: hiddenPasswordInput + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + verticalAlignment: TextInput.AlignVCenter + text: netPanel.hiddenPassword + echoMode: TextInput.Password + color: root.ink + selectionColor: root.seal + selectedTextColor: root.paper + font.family: root.mono + font.pixelSize: 11 + clip: true + enabled: !netPanel.hiddenConnecting + onTextChanged: netPanel.hiddenPassword = text + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + netPanel.submitHiddenNetwork() + event.accepted = true + } else if (event.key === Qt.Key_Escape) { + netPanel.clearHiddenNetwork() + event.accepted = true + } + } + } + + UiText { + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + visible: hiddenPasswordInput.text === "" && !hiddenPasswordInput.activeFocus + text: I18n.tr("Password") + color: root.sumi + font.family: root.mono + font.pixelSize: 10 + } + } + + Row { + width: parent.width + height: 22 + spacing: 6 + + Rectangle { + width: (parent.width - 6) / 2 + height: parent.height + radius: root.panelButtonRadius + color: hiddenSubmitMa.enabled + ? (hiddenSubmitMa.containsMouse ? root.fillPrimaryHover : root.seal) + : root.fillIdle + border.color: hiddenSubmitMa.enabled ? root.seal : root.sep + border.width: 1 + Behavior on color { ColorAnimation { duration: 120 } } + + UiText { + anchors.centerIn: parent + text: netPanel.hiddenConnecting ? I18n.tr("connecting…") : I18n.tr("connect") + color: hiddenSubmitMa.enabled ? root.paper : root.sumi + font.family: root.mono + font.pixelSize: 10 + } + + MouseArea { + id: hiddenSubmitMa + anchors.fill: parent + enabled: netPanel.hiddenName.trim() !== "" + && netPanel.hiddenSsid.trim() !== "" + && (netPanel.hiddenSecurity === "open" || netPanel.hiddenPassword !== "") + && !netPanel.hiddenConnecting + hoverEnabled: true + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: netPanel.submitHiddenNetwork() + } + } + + Rectangle { + width: (parent.width - 6) / 2 + height: parent.height + radius: root.panelButtonRadius + color: hiddenCancelMa.containsMouse ? root.fillHover : root.fillIdle + border.color: hiddenCancelMa.containsMouse ? root.seal : root.sep + border.width: 1 + Behavior on color { ColorAnimation { duration: 120 } } + + UiText { + anchors.centerIn: parent + text: I18n.tr("cancel") + color: hiddenCancelMa.containsMouse ? root.seal : root.sumi + font.family: root.mono + font.pixelSize: 10 + } + + MouseArea { + id: hiddenCancelMa + anchors.fill: parent + enabled: !netPanel.hiddenConnecting + hoverEnabled: true + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: netPanel.clearHiddenNetwork() + } + } + } + } + } + Rectangle { width: parent.width height: visible ? 92 : 0