diff options
| -rw-r--r-- | README.md | 13 | ||||
| -rw-r--r-- | config/UtilitiesConfig.qml | 7 | ||||
| -rw-r--r-- | modules/utilities/cards/Toggles.qml | 8 | ||||
| -rw-r--r-- | services/VPN.qml | 176 |
4 files changed, 203 insertions, 1 deletions
@@ -589,7 +589,18 @@ default, you must create it manually. "dndChanged": true, "gameModeChanged": true, "kbLayoutChanged": true, - "numLockChanged": true + "numLockChanged": true, + "vpnChanged": true, + }, + "vpn": { + "enabled": false, + "provider": [ + { + "name": "wireguard", + "interface": "your-connection-name", + "displayName": "Wireguard (Your VPN)" + } + ] } } } diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml index 3094efa..0c48034 100644 --- a/config/UtilitiesConfig.qml +++ b/config/UtilitiesConfig.qml @@ -6,6 +6,7 @@ JsonObject { property Sizes sizes: Sizes {} property Toasts toasts: Toasts {} + property Vpn vpn: Vpn {} component Sizes: JsonObject { property int width: 430 @@ -22,5 +23,11 @@ JsonObject { property bool capsLockChanged: true property bool numLockChanged: true property bool kbLayoutChanged: true + property bool vpnChanged: true + } + + component Vpn: JsonObject { + property bool enabled: false + property list<var> provider: ["netbird"] } } diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 82aac95..3d18e72 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -84,6 +84,14 @@ StyledRect { checked: Notifs.dnd onClicked: Notifs.dnd = !Notifs.dnd } + + Toggle { + icon: "vpn_key" + checked: VPN.connected + enabled: !VPN.connecting + visible: VPN.enabled + onClicked: VPN.toggle() + } } } diff --git a/services/VPN.qml b/services/VPN.qml new file mode 100644 index 0000000..10e5e7e --- /dev/null +++ b/services/VPN.qml @@ -0,0 +1,176 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick +import qs.config +import Caelestia + +Singleton { + id: root + + property bool connected: false + + readonly property bool connecting: connectProc.running || disconnectProc.running + readonly property bool enabled: Config.utilities.vpn.enabled + readonly property var providerInput: (Config.utilities.vpn.provider && Config.utilities.vpn.provider.length > 0) ? Config.utilities.vpn.provider[0] : "wireguard" + readonly property bool isCustomProvider: typeof providerInput === "object" + readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput) + readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : "" + readonly property var currentConfig: { + const name = providerName; + const iface = interfaceName; + const defaults = getBuiltinDefaults(name, iface); + + if (isCustomProvider) { + const custom = providerInput; + return { + connectCmd: custom.connectCmd || defaults.connectCmd, + disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd, + interface: custom.interface || defaults.interface, + displayName: custom.displayName || defaults.displayName + }; + } + + return defaults; + } + + function getBuiltinDefaults(name, iface) { + const builtins = { + "wireguard": { + connectCmd: ["pkexec", "wg-quick", "up", iface], + disconnectCmd: ["pkexec", "wg-quick", "down", iface], + interface: iface, + displayName: iface + }, + "warp": { + connectCmd: ["warp-cli", "connect"], + disconnectCmd: ["warp-cli", "disconnect"], + interface: "CloudflareWARP", + displayName: "Warp" + }, + "netbird": { + connectCmd: ["netbird", "up"], + disconnectCmd: ["netbird", "down"], + interface: "wt0", + displayName: "NetBird" + }, + "tailscale": { + connectCmd: ["tailscale", "up"], + disconnectCmd: ["tailscale", "down"], + interface: "tailscale0", + displayName: "Tailscale" + } + }; + + return builtins[name] || { + connectCmd: [name, "up"], + disconnectCmd: [name, "down"], + interface: iface || name, + displayName: name + }; + } + + function connect(): void { + if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) { + connectProc.exec(root.currentConfig.connectCmd); + } + } + + function disconnect(): void { + if (connected && !connecting && root.currentConfig && root.currentConfig.disconnectCmd) { + disconnectProc.exec(root.currentConfig.disconnectCmd); + } + } + + function toggle(): void { + if (connected) { + disconnect(); + } else { + connect(); + } + } + + function checkStatus(): void { + if (root.enabled) { + statusProc.running = true; + } + } + + onConnectedChanged: { + if (!Config.utilities.toasts.vpnChanged) + return; + + const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN"; + if (connected) { + Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key"); + } else { + Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); + } + } + + Component.onCompleted: root.enabled && statusCheckTimer.start() + + Process { + id: nmMonitor + + running: root.enabled + command: ["nmcli", "monitor"] + stdout: SplitParser { + onRead: statusCheckTimer.restart() + } + } + + Process { + id: statusProc + + command: ["ip", "link", "show"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: StdioCollector { + onStreamFinished: { + const iface = root.currentConfig ? root.currentConfig.interface : ""; + root.connected = iface && text.includes(iface + ":"); + } + } + } + + Process { + id: connectProc + + onExited: statusCheckTimer.start() + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && !error.includes("[#]") && !error.includes("already exists")) { + console.warn("VPN connection error:", error); + } else if (error.includes("already exists")) { + root.connected = true; + } + } + } + } + + Process { + id: disconnectProc + + onExited: statusCheckTimer.start() + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && !error.includes("[#]")) { + console.warn("VPN disconnection error:", error); + } + } + } + } + + Timer { + id: statusCheckTimer + + interval: 500 + onTriggered: root.checkStatus() + } +} |