1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00
Files
DankMaterialShell/Services/VpnService.qml
2025-09-20 10:11:17 -04:00

245 lines
8.3 KiB
QML

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: []
// 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
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.)
Component.onCompleted: initialize()
Component.onDestruction: {
nmMonitor.running = false
}
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: ["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: {
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")) {
const svc = parts.length >= 4 ? parts[3] : ""
out.push({ name: parts[0], uuid: parts[1], type: parts[2], serviceType: svc })
}
}
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 act = []
for (const line of lines) {
const parts = line.split(':')
if (parts.length >= 5 && (parts[2] === "vpn" || parts[2] === "wireguard")) {
act.push({ name: parts[0], uuid: parts[1], device: parts[3], state: parts[4] })
}
}
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
}
function connect(uuidOrName) {
if (root.isBusy) return
root.isBusy = true
root.errorMessage = ""
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 {
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 (uuid) {
if (isActiveUuid(uuid)) disconnect(uuid)
else connect(uuid)
return
}
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"
}
}
}
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
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"
}
}
}
}