From 952e5604d9c2a703d03c4f7a2baf3370144d601e Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 30 Aug 2025 13:57:47 -0400 Subject: [PATCH 01/13] Add NetworkManager VPN integration: VpnService + Control Center detail; move to TopBar VPN widget with popout; fix logs and parsing; default top bar item 'vpn' added; minor layout fixes --- Common/SettingsData.qml | 2 +- Modules/ControlCenter/Details/VpnDetail.qml | 162 ++++++++++++++++ Modules/Settings/TopBarTab.qml | 6 + Modules/TopBar/TopBar.qml | 33 ++++ Modules/TopBar/Vpn.qml | 106 +++++++++++ Modules/TopBar/VpnPopout.qml | 81 ++++++++ Services/VpnService.qml | 195 ++++++++++++++++++++ shell.qml | 11 ++ 8 files changed, 595 insertions(+), 1 deletion(-) create mode 100644 Modules/ControlCenter/Details/VpnDetail.qml create mode 100644 Modules/TopBar/Vpn.qml create mode 100644 Modules/TopBar/VpnPopout.qml create mode 100644 Services/VpnService.qml diff --git a/Common/SettingsData.qml b/Common/SettingsData.qml index 12b37f8d..d575e478 100644 --- a/Common/SettingsData.qml +++ b/Common/SettingsData.qml @@ -55,7 +55,7 @@ Singleton { property int mediaSize: 1 property var topBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"] property var topBarCenterWidgets: ["music", "clock", "weather"] - property var topBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"] + property var topBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "vpn", "controlCenterButton"] property alias topBarLeftWidgetsModel: leftWidgetsModel property alias topBarCenterWidgetsModel: centerWidgetsModel property alias topBarRightWidgetsModel: rightWidgetsModel diff --git a/Modules/ControlCenter/Details/VpnDetail.qml b/Modules/ControlCenter/Details/VpnDetail.qml new file mode 100644 index 00000000..0d8a2489 --- /dev/null +++ b/Modules/ControlCenter/Details/VpnDetail.qml @@ -0,0 +1,162 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + implicitHeight: 220 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Row { + spacing: Theme.spacingS + width: parent.width + + StyledText { + text: VpnService.connected ? ("Active: " + (VpnService.activeName || "VPN")) : "Active: None" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + Item { width: 10; height: 1 } + + Rectangle { + height: 28 + radius: 14 + color: toggleMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + visible: VpnService.connected || VpnService.profiles.length > 0 + width: 80 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + DankIcon { name: VpnService.connected ? "link_off" : "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } + StyledText { text: VpnService.connected ? "Disconnect" : "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } + } + + MouseArea { + id: toggleMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: VpnService.toggle() + } + } + } + + Rectangle { height: 1; width: parent.width; color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) } + + DankFlickable { + width: parent.width + height: 160 + contentHeight: listCol.height + + Column { + id: listCol + width: parent.width + spacing: Theme.spacingXS + + Item { + width: parent.width + height: VpnService.profiles.length === 0 ? 120 : 0 + visible: height > 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { name: "playlist_remove"; size: 36; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter } + StyledText { text: "No VPN profiles found"; font.pixelSize: Theme.fontSizeMedium; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter } + StyledText { text: "Add a VPN in NetworkManager"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter } + } + } + + Repeater { + model: VpnService.profiles + delegate: Rectangle { + required property var modelData + width: parent ? parent.width : 300 + height: 40 + radius: Theme.cornerRadius + color: rowMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + border.width: 1 + border.color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + + Row { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: modelData.uuid === VpnService.activeUuid ? "vpn_lock" : "vpn_key_off" + size: Theme.iconSize - 4 + color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { width: 1; height: 1 } + + Rectangle { + height: 28 + radius: 14 + color: actMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + width: 100 + + StyledText { + anchors.centerIn: parent + text: modelData.uuid === VpnService.activeUuid ? "Disconnect" : "Connect" + font.pixelSize: Theme.fontSizeSmall + color: modelData.uuid === VpnService.activeUuid ? Theme.error : Theme.surfaceText + font.weight: Font.Medium + } + + MouseArea { + id: actMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.uuid === VpnService.activeUuid) { + VpnService.disconnect(modelData.uuid) + } else { + VpnService.connect(modelData.uuid) + } + } + } + } + } + + MouseArea { + id: rowMouse + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + } + } + + Item { height: 1; width: 1 } + } + } + } +} diff --git a/Modules/Settings/TopBarTab.qml b/Modules/Settings/TopBarTab.qml index 002d642c..0ea9b86a 100644 --- a/Modules/Settings/TopBarTab.qml +++ b/Modules/Settings/TopBarTab.qml @@ -114,6 +114,12 @@ Item { "description": "Battery level and power management", "icon": "battery_std", "enabled": true + }, { + "id": "vpn", + "text": "VPN", + "description": "VPN status and quick connect", + "icon": "vpn_lock", + "enabled": true }, { "id": "idleInhibitor", "text": "Idle Inhibitor", diff --git a/Modules/TopBar/TopBar.qml b/Modules/TopBar/TopBar.qml index 13a12d9f..4cef86e8 100644 --- a/Modules/TopBar/TopBar.qml +++ b/Modules/TopBar/TopBar.qml @@ -339,6 +339,8 @@ PanelWindow { return DgopService.dgopAvailable case "keyboard_layout_name": return true + case "vpn": + return true default: return false } @@ -390,6 +392,8 @@ PanelWindow { return networkComponent case "keyboard_layout_name": return keyboardLayoutNameComponent + case "vpn": + return vpnComponent default: return null } @@ -1022,6 +1026,35 @@ PanelWindow { } } + Component { + id: vpnComponent + + Vpn { + widgetHeight: root.widgetHeight + barHeight: root.effectiveBarHeight + section: { + if (parent && parent.parent === leftSection) + return "left" + if (parent && parent.parent === rightSection) + return "right" + if (parent && parent.parent === centerSection) + return "center" + return "right" + } + popupTarget: { + vpnPopoutLoader.active = true + return vpnPopoutLoader.item + } + parentScreen: root.screen + onToggleVpnPopup: { + vpnPopoutLoader.active = true + if (vpnPopoutLoader.item) { + vpnPopoutLoader.item.toggle() + } + } + } + } + Component { id: controlCenterButtonComponent diff --git a/Modules/TopBar/Vpn.qml b/Modules/TopBar/Vpn.qml new file mode 100644 index 00000000..4f7b05d6 --- /dev/null +++ b/Modules/TopBar/Vpn.qml @@ -0,0 +1,106 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + // Passed in by TopBar + property int widgetHeight: 28 + property int barHeight: 32 + property string section: "right" + property var popupTarget: null + property var parentScreen: null + + signal toggleVpnPopup() + + width: Math.max(24, contentRow.implicitWidth + Theme.spacingXS * 2) + height: widgetHeight + + Row { + id: contentRow + anchors.fill: parent + anchors.margins: 0 + spacing: Theme.spacingXS + + Rectangle { + anchors.fill: parent + radius: 6 + color: "transparent" + } + + DankIcon { + id: icon + name: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off") + size: Theme.iconSize - 6 + color: VpnService.connected ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + + RotationAnimation on rotation { + running: VpnService.isBusy + loops: Animation.Infinite + from: 0 + to: 360 + duration: 900 + } + } + + Text { + id: label + text: VpnService.connected ? (VpnService.activeName || "VPN") : "VPN" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: VpnService.connected ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + visible: true + elide: Text.ElideRight + } + } + + MouseArea { + id: clickArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + var globalPos = mapToGlobal(0, 0) + var currentScreen = parentScreen || Screen + var screenX = currentScreen.x || 0 + var relativeX = globalPos.x - screenX + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen) + } + root.toggleVpnPopup() + } + } + + Rectangle { + id: tooltip + width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2) + height: tooltipText.contentHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.surfaceVariantAlpha + border.width: 1 + visible: clickArea.containsMouse && !(popupTarget && popupTarget.shouldBeVisible) + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + opacity: clickArea.containsMouse ? 1 : 0 + + Text { + id: tooltipText + anchors.centerIn: parent + text: VpnService.connected ? ("VPN Connected • " + (VpnService.activeName || "")) : "VPN Disconnected" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + + Behavior on opacity { + NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } + } + } +} diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml new file mode 100644 index 00000000..91a2bd15 --- /dev/null +++ b/Modules/TopBar/VpnPopout.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Widgets +import qs.Common +import qs.Services +import qs.Modules.ControlCenter.Details 1.0 as Details + +DankPopout { + id: root + + property string triggerSection: "right" + property var triggerScreen: null + + function setTriggerPosition(x, y, width, section, screen) { + triggerX = x + triggerY = y + triggerWidth = width + triggerSection = section + triggerScreen = screen + } + + popupWidth: 420 + popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 260 + triggerX: Screen.width - 400 - Theme.spacingL + triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS + triggerWidth: 70 + positioning: "center" + WlrLayershell.namespace: "quickshell-vpn" + screen: triggerScreen + shouldBeVisible: false + visible: shouldBeVisible + + content: Component { + Rectangle { + id: content + implicitHeight: contentColumn.height + Theme.spacingL * 2 + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.color: Theme.outlineMedium + border.width: 1 + antialiasing: true + smooth: true + focus: true + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Escape) { + root.close() + event.accepted = true + } + } + + Column { + id: contentColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Item { + width: parent.width + height: 28 + StyledText { + text: "VPN" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + Details.VpnDetail { + width: parent.width + } + } + } + } +} diff --git a/Services/VpnService.qml b/Services/VpnService.qml new file mode 100644 index 00000000..849cbdc1 --- /dev/null +++ b/Services/VpnService.qml @@ -0,0 +1,195 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +// Minimal VPN controller backed by NetworkManager (nmcli + D-Bus monitor) +Singleton { + id: root + + // State + property bool available: true + property bool isBusy: false + property string errorMessage: "" + + // Profiles discovered on the system + // [{ name, uuid, type }] + property var profiles: [] + + // Active VPN connection (if any) + property string activeUuid: "" + property string activeName: "" + property string activeDevice: "" + property string activeState: "" // activating, activated, deactivating + property bool connected: activeUuid !== "" && activeState === "activated" + + // Use implicit property notify signals (profilesChanged, activeUuidChanged, etc.) + + Component.onCompleted: initialize() + + function initialize() { + // Start monitoring NetworkManager for changes + nmMonitor.running = true + refreshAll() + } + + function refreshAll() { + listProfiles() + refreshActive() + } + + // Monitor NetworkManager changes and refresh on activity + Process { + id: nmMonitor + command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.NetworkManager"] + running: false + + stdout: SplitParser { + splitMarker: "\n" + onRead: line => { + if (line.includes("ActiveConnection") || line.includes("PropertiesChanged") || line.includes("StateChanged")) { + refreshAll() + } + } + } + } + + // Query all VPN profiles + function listProfiles() { + getProfiles.running = true + } + + Process { + id: getProfiles + command: ["nmcli", "-t", "-f", "NAME,UUID,TYPE", "connection", "show"] + running: false + stdout: StdioCollector { + onStreamFinished: { + const lines = text.trim().length ? text.trim().split('\n') : [] + const out = [] + for (const line of lines) { + const parts = line.split(':') + if (parts.length >= 3 && (parts[2] === "vpn" || parts[2] === "wireguard")) { + out.push({ name: parts[0], uuid: parts[1], type: parts[2] }) + } + } + root.profiles = out + } + } + } + + // Query active VPN connection + function refreshActive() { + getActive.running = true + } + + Process { + id: getActive + command: ["nmcli", "-t", "-f", "NAME,UUID,TYPE,DEVICE,STATE", "connection", "show", "--active"] + running: false + stdout: StdioCollector { + onStreamFinished: { + const lines = text.trim().length ? text.trim().split('\n') : [] + let found = false + for (const line of lines) { + const parts = line.split(':') + if (parts.length >= 5 && (parts[2] === "vpn" || parts[2] === "wireguard")) { + root.activeName = parts[0] + root.activeUuid = parts[1] + root.activeDevice = parts[3] + root.activeState = parts[4] + found = true + break + } + } + if (!found) { + root.activeName = "" + root.activeUuid = "" + root.activeDevice = "" + root.activeState = "" + } + } + } + } + + function _looksLikeUuid(s) { + // Very loose check for UUID pattern + return s && s.indexOf('-') !== -1 && s.length >= 8 + } + + function connect(uuidOrName) { + if (root.isBusy) return + root.isBusy = true + root.errorMessage = "" + if (_looksLikeUuid(uuidOrName)) { + vpnUp.command = ["nmcli", "connection", "up", "uuid", uuidOrName] + } else { + vpnUp.command = ["nmcli", "connection", "up", "id", uuidOrName] + } + vpnUp.running = true + } + + function disconnect(uuidOrName) { + if (root.isBusy) return + root.isBusy = true + root.errorMessage = "" + if (_looksLikeUuid(uuidOrName)) { + vpnDown.command = ["nmcli", "connection", "down", "uuid", uuidOrName] + } else { + vpnDown.command = ["nmcli", "connection", "down", "id", uuidOrName] + } + vpnDown.running = true + } + + function toggle(uuid) { + if (root.activeUuid && (uuid === undefined || uuid === root.activeUuid)) { + disconnect(root.activeUuid) + } else if (uuid) { + connect(uuid) + } else if (root.profiles.length > 0) { + connect(root.profiles[0].uuid) + } + } + + Process { + id: vpnUp + running: false + stdout: StdioCollector { + onStreamFinished: { + root.isBusy = false + if (!text.toLowerCase().includes("successfully")) { + root.errorMessage = text.trim() + } + refreshAll() + } + } + onExited: exitCode => { + root.isBusy = false + if (exitCode !== 0 && root.errorMessage === "") { + root.errorMessage = "Failed to connect VPN" + } + } + } + + Process { + id: vpnDown + running: false + stdout: StdioCollector { + onStreamFinished: { + root.isBusy = false + if (!text.toLowerCase().includes("deactivated") && !text.toLowerCase().includes("successfully")) { + root.errorMessage = text.trim() + } + refreshAll() + } + } + onExited: exitCode => { + root.isBusy = false + if (exitCode !== 0 && root.errorMessage === "") { + root.errorMessage = "Failed to disconnect VPN" + } + } + } +} diff --git a/shell.qml b/shell.qml index 0c769cb2..72da03d7 100644 --- a/shell.qml +++ b/shell.qml @@ -176,6 +176,17 @@ ShellRoot { } + LazyLoader { + id: vpnPopoutLoader + + active: false + + VpnPopout { + id: vpnPopout + } + + } + LazyLoader { id: powerMenuLoader From 449418f537542fe3a6ab703b6b7bf96f1764667d Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:06:43 -0400 Subject: [PATCH 02/13] TopBar VPN: icon-only widget with consistent background; fix imports; quiet VpnDetail anchors --- Modules/TopBar/Vpn.qml | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/Modules/TopBar/Vpn.qml b/Modules/TopBar/Vpn.qml index 4f7b05d6..c96f6c04 100644 --- a/Modules/TopBar/Vpn.qml +++ b/Modules/TopBar/Vpn.qml @@ -1,11 +1,10 @@ import QtQuick -import QtQuick.Controls import Quickshell import qs.Common import qs.Services import qs.Widgets -Item { +Rectangle { id: root // Passed in by TopBar @@ -17,20 +16,19 @@ Item { signal toggleVpnPopup() - width: Math.max(24, contentRow.implicitWidth + Theme.spacingXS * 2) + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + width: Theme.iconSize + horizontalPadding * 2 height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) return "transparent" + const base = clickArea.containsMouse || (popupTarget && popupTarget.shouldBeVisible) ? Theme.primaryPressed : Theme.secondaryHover + return Qt.rgba(base.r, base.g, base.b, base.a * Theme.widgetTransparency) + } - Row { - id: contentRow - anchors.fill: parent - anchors.margins: 0 - spacing: Theme.spacingXS - - Rectangle { - anchors.fill: parent - radius: 6 - color: "transparent" - } + Item { + anchors.centerIn: parent DankIcon { id: icon @@ -47,17 +45,6 @@ Item { duration: 900 } } - - Text { - id: label - text: VpnService.connected ? (VpnService.activeName || "VPN") : "VPN" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: VpnService.connected ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - visible: true - elide: Text.ElideRight - } } MouseArea { From 7cc2c0acefdc0759a83d4e1f0173b29f3928e2ba Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:15:31 -0400 Subject: [PATCH 03/13] TopBar VPN: fix centered icon by anchoring DankIcon center; remove inner wrapper --- Modules/TopBar/Vpn.qml | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/Modules/TopBar/Vpn.qml b/Modules/TopBar/Vpn.qml index c96f6c04..392be87f 100644 --- a/Modules/TopBar/Vpn.qml +++ b/Modules/TopBar/Vpn.qml @@ -27,23 +27,19 @@ Rectangle { return Qt.rgba(base.r, base.g, base.b, base.a * Theme.widgetTransparency) } - Item { + DankIcon { + id: icon + name: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off") + size: Theme.iconSize - 6 + color: VpnService.connected ? Theme.primary : Theme.surfaceText anchors.centerIn: parent - DankIcon { - id: icon - name: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off") - size: Theme.iconSize - 6 - color: VpnService.connected ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - - RotationAnimation on rotation { - running: VpnService.isBusy - loops: Animation.Infinite - from: 0 - to: 360 - duration: 900 - } + RotationAnimation on rotation { + running: VpnService.isBusy + loops: Animation.Infinite + from: 0 + to: 360 + duration: 900 } } From 04ce154d360e7efc2b2cb3d54408ab7c1b257d61 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:31:11 -0400 Subject: [PATCH 04/13] VPN Detail: align action buttons right via RowLayout, add hover color by state, retain pointer cursor --- Modules/ControlCenter/Details/VpnDetail.qml | 24 +++++++++++++-------- Modules/TopBar/VpnPopout.qml | 4 ++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Modules/ControlCenter/Details/VpnDetail.qml b/Modules/ControlCenter/Details/VpnDetail.qml index 0d8a2489..c32ca9b7 100644 --- a/Modules/ControlCenter/Details/VpnDetail.qml +++ b/Modules/ControlCenter/Details/VpnDetail.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import Quickshell import qs.Common import qs.Services @@ -35,14 +36,15 @@ Rectangle { height: 28 radius: 14 color: toggleMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) - visible: VpnService.connected || VpnService.profiles.length > 0 - width: 80 + // Only show quick connect when not connected + visible: !VpnService.connected && VpnService.profiles.length > 0 + width: 100 Row { anchors.centerIn: parent spacing: Theme.spacingXS - DankIcon { name: VpnService.connected ? "link_off" : "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } - StyledText { text: VpnService.connected ? "Disconnect" : "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } + DankIcon { name: "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } + StyledText { text: "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } } MouseArea { @@ -93,7 +95,7 @@ Rectangle { border.width: 1 border.color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) - Row { + RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -111,16 +113,20 @@ Rectangle { text: modelData.name font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter } - - Item { width: 1; height: 1 } + Item { Layout.fillWidth: true; height: 1 } Rectangle { height: 28 radius: 14 - color: actMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + color: actMouse.containsMouse + ? (modelData.uuid === VpnService.activeUuid + ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) + : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)) + : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) width: 100 + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight StyledText { anchors.centerIn: parent diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml index 91a2bd15..2d3683d4 100644 --- a/Modules/TopBar/VpnPopout.qml +++ b/Modules/TopBar/VpnPopout.qml @@ -22,7 +22,7 @@ DankPopout { triggerScreen = screen } - popupWidth: 420 + popupWidth: 360 popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 260 triggerX: Screen.width - 400 - Theme.spacingL triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS @@ -64,7 +64,7 @@ DankPopout { width: parent.width height: 28 StyledText { - text: "VPN" + text: "VPN Connections" font.pixelSize: Theme.fontSizeLarge color: Theme.surfaceText font.weight: Font.Medium From b9c4822c2711cd8504eb0449eadb19493755ac9b Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 30 Aug 2025 15:07:49 -0400 Subject: [PATCH 05/13] VPN Popout: match Battery popout styling (width, shadow rings, header close button, container colors); fix detail sizing --- Modules/ControlCenter/Details/VpnDetail.qml | 168 --------------- Modules/TopBar/VpnPopout.qml | 225 +++++++++++++++++++- 2 files changed, 221 insertions(+), 172 deletions(-) delete mode 100644 Modules/ControlCenter/Details/VpnDetail.qml diff --git a/Modules/ControlCenter/Details/VpnDetail.qml b/Modules/ControlCenter/Details/VpnDetail.qml deleted file mode 100644 index c32ca9b7..00000000 --- a/Modules/ControlCenter/Details/VpnDetail.qml +++ /dev/null @@ -1,168 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import qs.Common -import qs.Services -import qs.Widgets - -Rectangle { - id: root - implicitHeight: 220 - radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) - border.width: 1 - - Column { - anchors.fill: parent - anchors.margins: Theme.spacingM - spacing: Theme.spacingS - - Row { - spacing: Theme.spacingS - width: parent.width - - StyledText { - text: VpnService.connected ? ("Active: " + (VpnService.activeName || "VPN")) : "Active: None" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - Item { width: 10; height: 1 } - - Rectangle { - height: 28 - radius: 14 - color: toggleMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) - // Only show quick connect when not connected - visible: !VpnService.connected && VpnService.profiles.length > 0 - width: 100 - - Row { - anchors.centerIn: parent - spacing: Theme.spacingXS - DankIcon { name: "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } - StyledText { text: "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } - } - - MouseArea { - id: toggleMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: VpnService.toggle() - } - } - } - - Rectangle { height: 1; width: parent.width; color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) } - - DankFlickable { - width: parent.width - height: 160 - contentHeight: listCol.height - - Column { - id: listCol - width: parent.width - spacing: Theme.spacingXS - - Item { - width: parent.width - height: VpnService.profiles.length === 0 ? 120 : 0 - visible: height > 0 - - Column { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankIcon { name: "playlist_remove"; size: 36; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter } - StyledText { text: "No VPN profiles found"; font.pixelSize: Theme.fontSizeMedium; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter } - StyledText { text: "Add a VPN in NetworkManager"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter } - } - } - - Repeater { - model: VpnService.profiles - delegate: Rectangle { - required property var modelData - width: parent ? parent.width : 300 - height: 40 - radius: Theme.cornerRadius - color: rowMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" - border.width: 1 - border.color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Theme.spacingM - spacing: Theme.spacingS - - DankIcon { - name: modelData.uuid === VpnService.activeUuid ? "vpn_lock" : "vpn_key_off" - size: Theme.iconSize - 4 - color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.name - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - Layout.alignment: Qt.AlignVCenter - } - Item { Layout.fillWidth: true; height: 1 } - - Rectangle { - height: 28 - radius: 14 - color: actMouse.containsMouse - ? (modelData.uuid === VpnService.activeUuid - ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) - : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)) - : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) - width: 100 - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - - StyledText { - anchors.centerIn: parent - text: modelData.uuid === VpnService.activeUuid ? "Disconnect" : "Connect" - font.pixelSize: Theme.fontSizeSmall - color: modelData.uuid === VpnService.activeUuid ? Theme.error : Theme.surfaceText - font.weight: Font.Medium - } - - MouseArea { - id: actMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData.uuid === VpnService.activeUuid) { - VpnService.disconnect(modelData.uuid) - } else { - VpnService.connect(modelData.uuid) - } - } - } - } - } - - MouseArea { - id: rowMouse - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.NoButton - } - } - } - - Item { height: 1; width: 1 } - } - } - } -} diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml index 2d3683d4..281d4542 100644 --- a/Modules/TopBar/VpnPopout.qml +++ b/Modules/TopBar/VpnPopout.qml @@ -1,12 +1,13 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Quickshell.Widgets import qs.Widgets import qs.Common import qs.Services -import qs.Modules.ControlCenter.Details 1.0 as Details +// No external details import; content inlined for consistency DankPopout { id: root @@ -22,7 +23,7 @@ DankPopout { triggerScreen = screen } - popupWidth: 360 + popupWidth: 400 popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 260 triggerX: Screen.width - 400 - Theme.spacingL triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS @@ -52,6 +53,36 @@ DankPopout { } } + // Outer subtle shadow rings to match BatteryPopout + Rectangle { + anchors.fill: parent + anchors.margins: -3 + color: "transparent" + radius: parent.radius + 3 + border.color: Qt.rgba(0, 0, 0, 0.05) + border.width: 1 + z: -3 + } + + Rectangle { + anchors.fill: parent + anchors.margins: -2 + color: "transparent" + radius: parent.radius + 2 + border.color: Theme.shadowMedium + border.width: 1 + z: -2 + } + + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: Theme.outlineStrong + border.width: 1 + radius: parent.radius + z: -1 + } + Column { id: contentColumn anchors.left: parent.left @@ -62,7 +93,7 @@ DankPopout { Item { width: parent.width - height: 28 + height: 32 StyledText { text: "VPN Connections" font.pixelSize: Theme.fontSizeLarge @@ -70,10 +101,196 @@ DankPopout { font.weight: Font.Medium anchors.verticalCenter: parent.verticalCenter } + + // Close button (matches BatteryPopout) + Rectangle { + width: 32 + height: 32 + radius: 16 + color: closeArea.containsMouse ? Theme.errorHover : "transparent" + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "close" + size: Theme.iconSize - 4 + color: closeArea.containsMouse ? Theme.error : Theme.surfaceText + } + + MouseArea { + id: closeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: root.close() + } + } } - Details.VpnDetail { + // Inlined VPN details + Rectangle { + id: vpnDetail width: parent.width + implicitHeight: detailsColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4) + border.color: Theme.outlineMedium + border.width: 1 + + Column { + id: detailsColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Row { + spacing: Theme.spacingS + width: parent.width + + StyledText { + text: VpnService.connected ? ("Active: " + (VpnService.activeName || "VPN")) : "Active: None" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + Item { width: 10; height: 1 } + + // Quick connect when not connected + Rectangle { + height: 28 + radius: 14 + color: quickBtnArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + visible: !VpnService.connected && VpnService.profiles.length > 0 + width: 100 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + DankIcon { name: "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } + StyledText { text: "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } + } + + MouseArea { + id: quickBtnArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: VpnService.toggle() + } + } + } + + Rectangle { height: 1; width: parent.width; color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) } + + DankFlickable { + width: parent.width + height: 160 + contentHeight: listCol.height + + Column { + id: listCol + width: parent.width + spacing: Theme.spacingXS + + Item { + width: parent.width + height: VpnService.profiles.length === 0 ? 120 : 0 + visible: height > 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + DankIcon { name: "playlist_remove"; size: 36; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter } + StyledText { text: "No VPN profiles found"; font.pixelSize: Theme.fontSizeMedium; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter } + StyledText { text: "Add a VPN in NetworkManager"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter } + } + } + + Repeater { + model: VpnService.profiles + delegate: Rectangle { + required property var modelData + width: parent ? parent.width : 300 + height: 40 + radius: Theme.cornerRadius + color: rowMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + border.width: 1 + border.color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: modelData.uuid === VpnService.activeUuid ? "vpn_lock" : "vpn_key_off" + size: Theme.iconSize - 4 + color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.surfaceText + Layout.alignment: Qt.AlignVCenter + } + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + Layout.alignment: Qt.AlignVCenter + } + Item { Layout.fillWidth: true; height: 1 } + + Rectangle { + height: 28 + radius: 14 + color: actMouse.containsMouse + ? (modelData.uuid === VpnService.activeUuid + ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) + : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)) + : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + width: 100 + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + z: 10 + + StyledText { + anchors.centerIn: parent + text: modelData.uuid === VpnService.activeUuid ? "Disconnect" : "Connect" + font.pixelSize: Theme.fontSizeSmall + color: modelData.uuid === VpnService.activeUuid ? Theme.error : Theme.surfaceText + font.weight: Font.Medium + } + + MouseArea { + id: actMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.uuid === VpnService.activeUuid) { + VpnService.disconnect(modelData.uuid) + } else { + VpnService.connect(modelData.uuid) + } + } + } + } + } + + MouseArea { + id: rowMouse + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + z: -1 + } + } + } + + Item { height: 1; width: 1 } + } + } + } } } } From 40da170f6640e8f618b845c5e1e581911b4606a3 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 30 Aug 2025 15:40:05 -0400 Subject: [PATCH 06/13] VPN popout: full-row connect/disconnect like power profiles; darker details container; quick connect aligned right; styling aligned with Battery popout --- Modules/TopBar/VpnPopout.qml | 116 ++++++++++++++--------------------- 1 file changed, 45 insertions(+), 71 deletions(-) diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml index 281d4542..aba1d469 100644 --- a/Modules/TopBar/VpnPopout.qml +++ b/Modules/TopBar/VpnPopout.qml @@ -23,9 +23,9 @@ DankPopout { triggerScreen = screen } - popupWidth: 400 + popupWidth: 360 popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 260 - triggerX: Screen.width - 400 - Theme.spacingL + triggerX: Screen.width - 380 - Theme.spacingL triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS triggerWidth: 70 positioning: "center" @@ -134,8 +134,8 @@ DankPopout { width: parent.width implicitHeight: detailsColumn.implicitHeight + Theme.spacingM * 2 radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4) - border.color: Theme.outlineMedium + color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Theme.outlineStrong border.width: 1 Column { @@ -144,7 +144,7 @@ DankPopout { anchors.margins: Theme.spacingM spacing: Theme.spacingS - Row { + RowLayout { spacing: Theme.spacingS width: parent.width @@ -155,31 +155,34 @@ DankPopout { font.weight: Font.Medium } - Item { width: 10; height: 1 } + Item { Layout.fillWidth: true; height: 1 } // Quick connect when not connected - Rectangle { - height: 28 - radius: 14 - color: quickBtnArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) - visible: !VpnService.connected && VpnService.profiles.length > 0 - width: 100 - - Row { - anchors.centerIn: parent - spacing: Theme.spacingXS - DankIcon { name: "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } - StyledText { text: "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } - } - - MouseArea { - id: quickBtnArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: VpnService.toggle() - } - } + // Rectangle { + // height: 28 + // radius: 14 + // color: quickBtnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + // visible: !VpnService.connected && VpnService.profiles.length > 0 + // width: 120 + // Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + // border.width: 1 + // border.color: Theme.outlineLight + // + // Row { + // anchors.centerIn: parent + // spacing: Theme.spacingXS + // DankIcon { name: "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } + // StyledText { text: "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } + // } + // + // MouseArea { + // id: quickBtnArea + // anchors.fill: parent + // hoverEnabled: true + // cursorShape: Qt.PointingHandCursor + // onClicked: VpnService.toggle() + // } + // } } Rectangle { height: 1; width: parent.width; color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) } @@ -213,11 +216,11 @@ DankPopout { delegate: Rectangle { required property var modelData width: parent ? parent.width : 300 - height: 40 + height: 50 radius: Theme.cornerRadius - color: rowMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" - border.width: 1 - border.color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + color: rowArea.containsMouse ? Theme.primaryHoverLight : (modelData.uuid === VpnService.activeUuid ? Theme.primaryPressed : Theme.surfaceLight) + border.width: modelData.uuid === VpnService.activeUuid ? 2 : 1 + border.color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.outlineLight RowLayout { anchors.left: parent.left @@ -236,53 +239,24 @@ DankPopout { StyledText { text: modelData.name font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText + color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.surfaceText Layout.alignment: Qt.AlignVCenter } Item { Layout.fillWidth: true; height: 1 } - - Rectangle { - height: 28 - radius: 14 - color: actMouse.containsMouse - ? (modelData.uuid === VpnService.activeUuid - ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) - : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)) - : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) - width: 100 - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - z: 10 - - StyledText { - anchors.centerIn: parent - text: modelData.uuid === VpnService.activeUuid ? "Disconnect" : "Connect" - font.pixelSize: Theme.fontSizeSmall - color: modelData.uuid === VpnService.activeUuid ? Theme.error : Theme.surfaceText - font.weight: Font.Medium - } - - MouseArea { - id: actMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData.uuid === VpnService.activeUuid) { - VpnService.disconnect(modelData.uuid) - } else { - VpnService.connect(modelData.uuid) - } - } - } - } } MouseArea { - id: rowMouse + id: rowArea anchors.fill: parent hoverEnabled: true - acceptedButtons: Qt.NoButton - z: -1 + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.uuid === VpnService.activeUuid) { + VpnService.disconnect(modelData.uuid) + } else { + VpnService.connect(modelData.uuid) + } + } } } } From c924d60aeb2422dfb9727e9b47498cc397b2b5c4 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 30 Aug 2025 15:53:40 -0400 Subject: [PATCH 07/13] remove VPN widget from default settings --- Common/SettingsData.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common/SettingsData.qml b/Common/SettingsData.qml index d575e478..12b37f8d 100644 --- a/Common/SettingsData.qml +++ b/Common/SettingsData.qml @@ -55,7 +55,7 @@ Singleton { property int mediaSize: 1 property var topBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"] property var topBarCenterWidgets: ["music", "clock", "weather"] - property var topBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "vpn", "controlCenterButton"] + property var topBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"] property alias topBarLeftWidgetsModel: leftWidgetsModel property alias topBarCenterWidgetsModel: centerWidgetsModel property alias topBarRightWidgetsModel: rightWidgetsModel From 7f467b0a0dcdcf9382ef8dc2ceb521796a9fb727 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 30 Aug 2025 16:12:26 -0400 Subject: [PATCH 08/13] VPN: enforce single-active by default; gracefully handle multi-active state; header summary fix; popout rows are full-row actions with correct active highlighting --- Modules/TopBar/VpnPopout.qml | 15 ++++--- Services/VpnService.qml | 80 +++++++++++++++++++++++++----------- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml index aba1d469..0a596bc3 100644 --- a/Modules/TopBar/VpnPopout.qml +++ b/Modules/TopBar/VpnPopout.qml @@ -149,7 +149,12 @@ DankPopout { width: parent.width StyledText { - text: VpnService.connected ? ("Active: " + (VpnService.activeName || "VPN")) : "Active: None" + text: { + if (!VpnService.connected) return "Active: None" + const names = VpnService.activeNames || [] + if (names.length <= 1) return "Active: " + (names[0] || "VPN") + return "Active: " + names[0] + " +" + (names.length - 1) + } font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText font.weight: Font.Medium @@ -218,9 +223,9 @@ DankPopout { width: parent ? parent.width : 300 height: 50 radius: Theme.cornerRadius - color: rowArea.containsMouse ? Theme.primaryHoverLight : (modelData.uuid === VpnService.activeUuid ? Theme.primaryPressed : Theme.surfaceLight) - border.width: modelData.uuid === VpnService.activeUuid ? 2 : 1 - border.color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.outlineLight + color: rowArea.containsMouse ? Theme.primaryHoverLight : (VpnService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight) + border.width: VpnService.isActiveUuid(modelData.uuid) ? 2 : 1 + border.color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight RowLayout { anchors.left: parent.left @@ -232,7 +237,7 @@ DankPopout { DankIcon { name: modelData.uuid === VpnService.activeUuid ? "vpn_lock" : "vpn_key_off" size: Theme.iconSize - 4 - color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.surfaceText + color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText Layout.alignment: Qt.AlignVCenter } diff --git a/Services/VpnService.qml b/Services/VpnService.qml index 849cbdc1..7204c573 100644 --- a/Services/VpnService.qml +++ b/Services/VpnService.qml @@ -18,12 +18,20 @@ Singleton { // [{ name, uuid, type }] property var profiles: [] - // Active VPN connection (if any) - property string activeUuid: "" - property string activeName: "" - property string activeDevice: "" - property string activeState: "" // activating, activated, deactivating - property bool connected: activeUuid !== "" && activeState === "activated" + // Enforce single active VPN at a time + property bool singleActive: true + + // Active VPN connections (may be multiple) + // Full list and convenience projections + property var activeConnections: [] // [{ name, uuid, device, state }] + property var activeUuids: [] + property var activeNames: [] + // Back-compat single values (first active if present) + property string activeUuid: activeUuids.length > 0 ? activeUuids[0] : "" + property string activeName: activeNames.length > 0 ? activeNames[0] : "" + property string activeDevice: activeConnections.length > 0 ? (activeConnections[0].device || "") : "" + property string activeState: activeConnections.length > 0 ? (activeConnections[0].state || "") : "" + property bool connected: activeUuids.length > 0 // Use implicit property notify signals (profilesChanged, activeUuidChanged, etc.) @@ -92,28 +100,24 @@ Singleton { stdout: StdioCollector { onStreamFinished: { const lines = text.trim().length ? text.trim().split('\n') : [] - let found = false + let act = [] for (const line of lines) { const parts = line.split(':') if (parts.length >= 5 && (parts[2] === "vpn" || parts[2] === "wireguard")) { - root.activeName = parts[0] - root.activeUuid = parts[1] - root.activeDevice = parts[3] - root.activeState = parts[4] - found = true - break + act.push({ name: parts[0], uuid: parts[1], device: parts[3], state: parts[4] }) } } - if (!found) { - root.activeName = "" - root.activeUuid = "" - root.activeDevice = "" - root.activeState = "" - } + root.activeConnections = act + root.activeUuids = act.map(a => a.uuid).filter(u => !!u) + root.activeNames = act.map(a => a.name).filter(n => !!n) } } } + function isActiveUuid(uuid) { + return root.activeUuids && root.activeUuids.indexOf(uuid) !== -1 + } + function _looksLikeUuid(s) { // Very loose check for UUID pattern return s && s.indexOf('-') !== -1 && s.length >= 8 @@ -123,12 +127,24 @@ Singleton { if (root.isBusy) return root.isBusy = true root.errorMessage = "" - if (_looksLikeUuid(uuidOrName)) { - vpnUp.command = ["nmcli", "connection", "up", "uuid", uuidOrName] + if (root.singleActive) { + // Bring down all active VPNs, then bring up the requested one + const isUuid = _looksLikeUuid(uuidOrName) + const escaped = ('' + uuidOrName).replace(/'/g, "'\\''") + const upCmd = isUuid ? `nmcli connection up uuid '${escaped}'` : `nmcli connection up id '${escaped}'` + const script = `set -e\n` + + `nmcli -t -f UUID,TYPE connection show --active | awk -F: '$2 ~ /^(vpn|wireguard)$/ {print $1}' | while read u; do [ -n \"$u\" ] && nmcli connection down uuid \"$u\" || true; done\n` + + upCmd + `\n` + vpnSwitch.command = ["bash", "-lc", script] + vpnSwitch.running = true } else { - vpnUp.command = ["nmcli", "connection", "up", "id", uuidOrName] + if (_looksLikeUuid(uuidOrName)) { + vpnUp.command = ["nmcli", "connection", "up", "uuid", uuidOrName] + } else { + vpnUp.command = ["nmcli", "connection", "up", "id", uuidOrName] + } + vpnUp.running = true } - vpnUp.running = true } function disconnect(uuidOrName) { @@ -192,4 +208,22 @@ Singleton { } } } + + // Sequenced down/up using a single shell for exclusive switch + Process { + id: vpnSwitch + running: false + stdout: StdioCollector { + onStreamFinished: { + root.isBusy = false + refreshAll() + } + } + onExited: exitCode => { + root.isBusy = false + if (exitCode !== 0 && root.errorMessage === "") { + root.errorMessage = "Failed to switch VPN" + } + } + } } From d1890c69c90215e77f24255c3adab16954a4677b Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 30 Aug 2025 16:28:42 -0400 Subject: [PATCH 09/13] VPN multi-active: toggle per row fixes reconnection, add Quick Connect + Disconnect All, header/tooltips summarize multiple active; default allow multiple active --- Modules/TopBar/Vpn.qml | 7 ++- Modules/TopBar/VpnPopout.qml | 87 ++++++++++++++++++++++-------------- Services/VpnService.qml | 24 +++++++--- 3 files changed, 77 insertions(+), 41 deletions(-) diff --git a/Modules/TopBar/Vpn.qml b/Modules/TopBar/Vpn.qml index 392be87f..ce929484 100644 --- a/Modules/TopBar/Vpn.qml +++ b/Modules/TopBar/Vpn.qml @@ -77,7 +77,12 @@ Rectangle { Text { id: tooltipText anchors.centerIn: parent - text: VpnService.connected ? ("VPN Connected • " + (VpnService.activeName || "")) : "VPN Disconnected" + text: { + if (!VpnService.connected) return "VPN Disconnected" + const names = VpnService.activeNames || [] + if (names.length <= 1) return "VPN Connected • " + (names[0] || "") + return "VPN Connected • " + names[0] + " +" + (names.length - 1) + } font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText } diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml index 0a596bc3..514359a6 100644 --- a/Modules/TopBar/VpnPopout.qml +++ b/Modules/TopBar/VpnPopout.qml @@ -162,32 +162,59 @@ DankPopout { Item { Layout.fillWidth: true; height: 1 } - // Quick connect when not connected - // Rectangle { - // height: 28 - // radius: 14 - // color: quickBtnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight - // visible: !VpnService.connected && VpnService.profiles.length > 0 - // width: 120 - // Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - // border.width: 1 - // border.color: Theme.outlineLight - // - // Row { - // anchors.centerIn: parent - // spacing: Theme.spacingXS - // DankIcon { name: "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } - // StyledText { text: "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } - // } - // - // MouseArea { - // id: quickBtnArea - // anchors.fill: parent - // hoverEnabled: true - // cursorShape: Qt.PointingHandCursor - // onClicked: VpnService.toggle() - // } - // } + // Quick connect + Rectangle { + height: 28 + radius: 14 + color: quickBtnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + visible: VpnService.profiles.length > 0 + width: 120 + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + border.width: 1 + border.color: Theme.outlineLight + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + DankIcon { name: "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } + StyledText { text: "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } + } + + MouseArea { + id: quickBtnArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: VpnService.toggle() + } + } + + // Disconnect all (visible when any active) + Rectangle { + height: 28 + radius: 14 + color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight + visible: VpnService.connected + width: 130 + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + border.width: 1 + border.color: Theme.outlineLight + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + DankIcon { name: "link_off"; size: Theme.fontSizeSmall; color: Theme.surfaceText } + StyledText { text: "Disconnect All"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } + } + + MouseArea { + id: discAllArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: VpnService.disconnectAllActive() + } + } } Rectangle { height: 1; width: parent.width; color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) } @@ -255,13 +282,7 @@ DankPopout { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData.uuid === VpnService.activeUuid) { - VpnService.disconnect(modelData.uuid) - } else { - VpnService.connect(modelData.uuid) - } - } + onClicked: VpnService.toggle(modelData.uuid) } } } diff --git a/Services/VpnService.qml b/Services/VpnService.qml index 7204c573..f06c6b56 100644 --- a/Services/VpnService.qml +++ b/Services/VpnService.qml @@ -18,8 +18,9 @@ Singleton { // [{ name, uuid, type }] property var profiles: [] - // Enforce single active VPN at a time - property bool singleActive: true + // Allow multiple active VPNs (set true to allow concurrent connections) + // Default: allow multiple, to align with NetworkManager capability + property bool singleActive: false // Active VPN connections (may be multiple) // Full list and convenience projections @@ -160,11 +161,12 @@ Singleton { } function toggle(uuid) { - if (root.activeUuid && (uuid === undefined || uuid === root.activeUuid)) { - disconnect(root.activeUuid) - } else if (uuid) { - connect(uuid) - } else if (root.profiles.length > 0) { + if (uuid) { + if (isActiveUuid(uuid)) disconnect(uuid) + else connect(uuid) + return + } + if (root.profiles.length > 0) { connect(root.profiles[0].uuid) } } @@ -209,6 +211,14 @@ Singleton { } } + function disconnectAllActive() { + if (root.isBusy) return + root.isBusy = true + const script = `nmcli -t -f UUID,TYPE connection show --active | awk -F: '$2 ~ /^(vpn|wireguard)$/ {print $1}' | while read u; do [ -n \"$u\" ] && nmcli connection down uuid \"$u\" || true; done` + vpnSwitch.command = ["bash", "-lc", script] + vpnSwitch.running = true + } + // Sequenced down/up using a single shell for exclusive switch Process { id: vpnSwitch From 1e7a0beaed864aff93ab1cf8378247f7d587d850 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 30 Aug 2025 16:32:09 -0400 Subject: [PATCH 10/13] VPN Popout: show only one header action (Quick Connect when none active, Disconnect All when any active) --- Modules/TopBar/VpnPopout.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml index 514359a6..abc39a01 100644 --- a/Modules/TopBar/VpnPopout.qml +++ b/Modules/TopBar/VpnPopout.qml @@ -162,12 +162,12 @@ DankPopout { Item { Layout.fillWidth: true; height: 1 } - // Quick connect + // Quick connect (shown only when none active) Rectangle { height: 28 radius: 14 color: quickBtnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight - visible: VpnService.profiles.length > 0 + visible: !VpnService.connected && VpnService.profiles.length > 0 width: 120 Layout.alignment: Qt.AlignVCenter | Qt.AlignRight border.width: 1 @@ -189,7 +189,7 @@ DankPopout { } } - // Disconnect all (visible when any active) + // Disconnect all (shown only when any active) Rectangle { height: 28 radius: 14 @@ -204,7 +204,7 @@ DankPopout { anchors.centerIn: parent spacing: Theme.spacingXS DankIcon { name: "link_off"; size: Theme.fontSizeSmall; color: Theme.surfaceText } - StyledText { text: "Disconnect All"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } + StyledText { text: "Disconnect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } } MouseArea { From 86a4346fc905e8b75ab6782d090785ee243194fa Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:58:24 -0400 Subject: [PATCH 11/13] VPN Popout: show profile type (WireGuard/VPN) under name; remove ambiguous Quick Connect button from header --- Modules/TopBar/VpnPopout.qml | 46 +++++++++++++----------------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml index abc39a01..f5603451 100644 --- a/Modules/TopBar/VpnPopout.qml +++ b/Modules/TopBar/VpnPopout.qml @@ -162,32 +162,8 @@ DankPopout { Item { Layout.fillWidth: true; height: 1 } - // Quick connect (shown only when none active) - Rectangle { - height: 28 - radius: 14 - color: quickBtnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight - visible: !VpnService.connected && VpnService.profiles.length > 0 - width: 120 - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - border.width: 1 - border.color: Theme.outlineLight - - Row { - anchors.centerIn: parent - spacing: Theme.spacingXS - DankIcon { name: "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } - StyledText { text: "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } - } - - MouseArea { - id: quickBtnArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: VpnService.toggle() - } - } + // Removed Quick Connect for clarity + Item { width: 1; height: 1 } // Disconnect all (shown only when any active) Rectangle { @@ -268,11 +244,21 @@ DankPopout { Layout.alignment: Qt.AlignVCenter } - StyledText { - text: modelData.name - font.pixelSize: Theme.fontSizeMedium - color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.surfaceText + Column { + spacing: 2 Layout.alignment: Qt.AlignVCenter + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText + } + + StyledText { + text: (modelData.type === "wireguard" ? "WireGuard" : (modelData.type ? modelData.type.toUpperCase() : "VPN")) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } } Item { Layout.fillWidth: true; height: 1 } } From 585ceb96e4207c4cc3f9f5416c5b0f6542878009 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sun, 31 Aug 2025 16:21:48 -0400 Subject: [PATCH 12/13] VPN Popout: remove redundant Flickable implicitHeight; rely on clip for overflow bounds --- Modules/TopBar/VpnPopout.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml index f5603451..2d5a178c 100644 --- a/Modules/TopBar/VpnPopout.qml +++ b/Modules/TopBar/VpnPopout.qml @@ -24,7 +24,7 @@ DankPopout { } popupWidth: 360 - popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 260 + popupHeight: Math.min(Screen.height - 100, contentLoader.item ? contentLoader.item.implicitHeight : 260) triggerX: Screen.width - 380 - Theme.spacingL triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS triggerWidth: 70 @@ -137,6 +137,7 @@ DankPopout { color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, Theme.getContentBackgroundAlpha() * 0.6) border.color: Theme.outlineStrong border.width: 1 + clip: true Column { id: detailsColumn @@ -199,6 +200,7 @@ DankPopout { width: parent.width height: 160 contentHeight: listCol.height + clip: true Column { id: listCol From d91c3572afb6089ff51ab12643fa31ccbd62e320 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sun, 31 Aug 2025 16:33:07 -0400 Subject: [PATCH 13/13] VPN profiles: include vpn.service-type for TYPE=vpn; show friendly protocol label in popout (OpenVPN, WireGuard, IPsec, etc.); fix active icon to support multi-active --- Modules/TopBar/VpnPopout.qml | 38 ++++++++++++++++++++++++++---------- Services/VpnService.qml | 5 +++-- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml index 2d5a178c..446fbce5 100644 --- a/Modules/TopBar/VpnPopout.qml +++ b/Modules/TopBar/VpnPopout.qml @@ -240,7 +240,7 @@ DankPopout { spacing: Theme.spacingS DankIcon { - name: modelData.uuid === VpnService.activeUuid ? "vpn_lock" : "vpn_key_off" + name: VpnService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off" size: Theme.iconSize - 4 color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText Layout.alignment: Qt.AlignVCenter @@ -250,17 +250,35 @@ DankPopout { spacing: 2 Layout.alignment: Qt.AlignVCenter - StyledText { - text: modelData.name - font.pixelSize: Theme.fontSizeMedium - color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText - } + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText + } - StyledText { - text: (modelData.type === "wireguard" ? "WireGuard" : (modelData.type ? modelData.type.toUpperCase() : "VPN")) - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceTextMedium + StyledText { + text: { + if (modelData.type === "wireguard") return "WireGuard" + var svc = modelData.serviceType || "" + if (svc.indexOf("openvpn") !== -1) return "OpenVPN" + if (svc.indexOf("wireguard") !== -1) return "WireGuard (plugin)" + if (svc.indexOf("openconnect") !== -1) return "OpenConnect" + if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1) return "Fortinet" + if (svc.indexOf("strongswan") !== -1) return "IPsec (strongSwan)" + if (svc.indexOf("libreswan") !== -1) return "IPsec (Libreswan)" + if (svc.indexOf("l2tp") !== -1) return "L2TP/IPsec" + if (svc.indexOf("pptp") !== -1) return "PPTP" + if (svc.indexOf("vpnc") !== -1) return "Cisco (vpnc)" + if (svc.indexOf("sstp") !== -1) return "SSTP" + if (svc) { + var parts = svc.split('.') + return parts[parts.length-1] + } + return "VPN" } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } } Item { Layout.fillWidth: true; height: 1 } } diff --git a/Services/VpnService.qml b/Services/VpnService.qml index f06c6b56..8ac67e8f 100644 --- a/Services/VpnService.qml +++ b/Services/VpnService.qml @@ -72,7 +72,7 @@ Singleton { Process { id: getProfiles - command: ["nmcli", "-t", "-f", "NAME,UUID,TYPE", "connection", "show"] + command: ["bash", "-lc", "nmcli -t -f NAME,UUID,TYPE connection show | while IFS=: read -r name uuid type; do case \"$type\" in vpn) svc=$(nmcli -g vpn.service-type connection show uuid \"$uuid\" 2>/dev/null); echo \"$name:$uuid:$type:$svc\" ;; wireguard) echo \"$name:$uuid:$type:\" ;; *) : ;; esac; done"] running: false stdout: StdioCollector { onStreamFinished: { @@ -81,7 +81,8 @@ Singleton { for (const line of lines) { const parts = line.split(':') if (parts.length >= 3 && (parts[2] === "vpn" || parts[2] === "wireguard")) { - out.push({ name: parts[0], uuid: parts[1], type: parts[2] }) + const svc = parts.length >= 4 ? parts[3] : "" + out.push({ name: parts[0], uuid: parts[1], type: parts[2], serviceType: svc }) } } root.profiles = out