import QtQuick import Quickshell.Io import qs.Commons import qs.Services.UI QtObject { id: root readonly property var pluginSettings: pluginApi?.pluginSettings ?? ({}) readonly property var toast: root.pluginSettings?.disableToastNotifications ? null : ToastService property var pluginApi: null property var vpnList: [] property real connectedCount: 0 readonly property bool isLoading: Object.keys(root._pending).length > 0 property var _pending: ({}) // Needed only to detect disconnection not initiated by the user property var _pollTimer: Timer { interval: 5000 running: true repeat: true onTriggered: root.refresh() } property var _lines: [] property var _listProc: Process { command: ["nmcli", "-t", "-f", "NAME,TYPE,STATE,UUID", "connection", "show"] running: true stdout: SplitParser { onRead: (line) => { if (line.trim() !== "") root._lines.push(line) } } onExited: (exitCode) => { if (exitCode === 0) { const parsed = [] const newPending = Object.assign({}, root._pending) for (const line of root._lines) { const parts = line.split(":") if (parts.length >= 4) { const name = parts[0] const type = parts[1] const state = parts[2] const uuid = parts[3] if (type === "vpn" || type === "wireguard") { if (newPending[uuid]) { const wasConnecting = newPending[uuid] === "connect" if (wasConnecting && state === "activated") delete newPending[uuid] else if (!wasConnecting && state !== "activated") delete newPending[uuid] } parsed.push({ name, type, connected: state === "activated", isLoading: !!newPending[uuid], uuid }) } } } root._pending = newPending root.vpnList = parsed root.connectedCount = parsed.filter(v => v.connected).length } root._lines = [] } } property var _connectProc: Process { property string targetName: "" property string targetUuid: "" command: ["nmcli", "connection", "up", "uuid", targetUuid] onExited: (exitCode) => { if (exitCode === 0) toast?.showNotice(t("toast.connectedTo", { name: targetName })) else { root.stopLoading(targetUuid) toast?.showError(t("toast.connectionError", { name: targetName })) } root.refresh() } } property var _disconnectProc: Process { property string targetName: "" property string targetUuid: "" command: ["nmcli", "connection", "down", "uuid", targetUuid] onExited: (exitCode) => { if (exitCode === 0) toast?.showNotice(t("toast.disconnectedFrom", { name: targetName })) else { root.stopLoading(targetUuid) toast?.showError(t("toast.disconnectionError", { name: targetName })) } root.refresh() } } property var _addProc: Process { property string targetType: "" command: ["nm-connection-editor", "--create", "--type", targetType] onExited: (exitCode) => { root.refresh(); } } property var _editProc: Process { property string targetName: "" property string targetUuid: "" command: ["nm-connection-editor", "--edit", targetUuid] onExited: (exitCode) => { root.refresh(); } } property var _removeProc: Process { property string targetName: "" property string targetUuid: "" command: ["nmcli", "connection", "delete", "uuid", targetUuid] onExited: (exitCode) => { if (exitCode === 0) toast?.showNotice(t("toast.vpnRemoved", { "name": targetName })); else toast?.showError(t("toast.vpnRemoveError", { "name": targetName })); root.refresh(); } } function t(key: string, data) { if (!pluginApi) return null; return pluginApi.tr(key, data); } function refresh() { _listProc.running = true } function stopLoading(uuid) { if (uuid && _pending[uuid]) { const p = Object.assign({}, _pending) delete p[uuid] _pending = p } vpnList = vpnList.map(v => { if (v.uuid !== uuid) { return v } return Object.assign({}, v, { isLoading: false }) }) } function connectTo(uuid) { const p = Object.assign({}, _pending) p[uuid] = "connect" _pending = p let name = "" vpnList = vpnList.map(v => { if (v.uuid !== uuid) { return v } name = v.name return Object.assign({}, v, { isLoading: true }) }) _connectProc.targetName = name _connectProc.targetUuid = uuid _connectProc.running = true } function disconnectFrom(uuid) { const p = Object.assign({}, _pending) p[uuid] = "disconnect" _pending = p let name = "" vpnList = vpnList.map(v => { if (v.uuid !== uuid) { return v } name = v.name return Object.assign({}, v, { isLoading: true }) }) _disconnectProc.targetName = name _disconnectProc.targetUuid = uuid _disconnectProc.running = true } function addConnection(type) { _addProc.targetType = type || "vpn"; _addProc.running = true; } function editConnection(uuid) { const vpn = vpnList.find((v) => { return v.uuid === uuid; }); _editProc.targetName = vpn ? vpn.name : uuid; _editProc.targetUuid = uuid; _editProc.running = true; } function removeConnection(uuid) { const vpn = vpnList.find((v) => { return v.uuid === uuid; }); _removeProc.targetName = vpn ? vpn.name : uuid; _removeProc.targetUuid = uuid; _removeProc.running = true; } Component.onCompleted: { Logger.i("NetworkManagerVPN", "Started") } }