pragma Singleton // VPN and WireGuard connections, for the quick-settings toggle and the bar. // // Everything goes through scripts/panama-vpn, the one sanctioned nmcli // exception (see the note there and in Connectivity.qml): Quickshell's // Networking module has no VPN surface yet. Nothing is stored in settings.json // -- NetworkManager owns which profiles exist and which are active, and a // stored copy would just be restored over whatever the daemon knows. // // The design requirement, learned the hard way: activating a profile whose // server is unreachable must never leave the machine stranded. The helper // bounds activation and rolls a failed attempt back down; this service only // reports what actually happened. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-vpn" // [{name, uuid, kind, active, timestamp}] property var connections: [] property bool scanned: false property bool busy: false property string lastError: "" readonly property bool available: root.connections.length > 0 readonly property var activeConnections: root.connections.filter(connection => connection.active) readonly property bool anyActive: root.activeConnections.length > 0 // What the toggle's sublabel shows: the one active name, or how many. readonly property string activeSummary: { if (root.activeConnections.length === 0) return ""; if (root.activeConnections.length === 1) return root.activeConnections[0].name; return root.activeConnections.length + " active"; } function refresh(): void { if (!query.running) query.running = true; } function setActive(uuid: string, on: bool): void { if (root.busy) return; root.busy = true; root.lastError = ""; apply.command = [root.helperPath, on ? "up" : "down", uuid]; apply.running = true; } // The main toggle: anything active goes down; nothing active brings up the // profile most recently used, which is GNOME's behavior and almost always // what was meant on a machine with more than one profile. function toggle(): void { if (root.anyActive) { root.setActive(root.activeConnections[0].uuid, false); return; } const preferred = root.connections.reduce((best, candidate) => best === null || candidate.timestamp > best.timestamp ? candidate : best, null); if (preferred !== null) root.setActive(preferred.uuid, true); } Process { id: query command: [root.helperPath, "list"] stdout: StdioCollector { onStreamFinished: { try { const parsed = JSON.parse(this.text); root.connections = Array.isArray(parsed.connections) ? parsed.connections : []; if (String(parsed.error ?? "") !== "") root.lastError = String(parsed.error); } catch (error) { root.connections = []; root.lastError = "Could not read the VPN helper's output."; console.warn("Vpn: could not parse helper output:", error); } root.scanned = true; } } } Process { id: apply stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } // Re-read rather than assuming: activation can fail, and the helper // may have rolled a failed attempt back down. onExited: { root.busy = false; root.refresh(); } } // NetworkManager state can change under us -- nmcli in a terminal, a // connection dropping, another device editing profiles. `nmcli monitor` // emits a line per event; the refresh is coalesced because one action // often produces several lines in a burst. Process { id: monitor command: ["nmcli", "monitor"] running: true stdout: SplitParser { onRead: refreshDebounce.restart() } // If NetworkManager restarts, the monitor exits; come back gently // rather than spinning against a daemon that is still down. onExited: monitorRestart.restart() } Timer { id: refreshDebounce interval: 400 onTriggered: root.refresh() } Timer { id: monitorRestart interval: 3000 onTriggered: monitor.running = true } Component.onCompleted: root.refresh() }