Give the VPN a toggle, an indicator, and a way back

Turning on a WireGuard profile whose server was unreachable used to cost the
whole network stack, and the only way out was nmcli typed into a terminal.
Quickshell's Networking module has no VPN surface, so this arrives as the one
sanctioned nmcli exception: a helper that lists, raises and lowers profiles,
a service that watches NetworkManager for changes made anywhere, a quick
settings tile (left-click toggles the most recently used profile, right-click
picks among them), and a bar glyph while a tunnel is up.

The safety property is in the helper, where it cannot be skipped: activation
waits a bounded 25 seconds, and a failure is rolled back down and reported
instead of leaving a black-hole default route. The contract pins exactly that,
against a stateful stub NetworkManager.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
This commit is contained in:
Gabriel Brown
2026-08-23 10:32:17 -04:00
parent d865cb74a1
commit 36fdd4e076
9 changed files with 528 additions and 2 deletions
@@ -21,8 +21,8 @@ Item {
implicitWidth: Theme.controlCenterWidth
implicitHeight: content.implicitHeight + Theme.popoverPadding * 2
// "" | "wifi" | "bluetooth" | "sink" | "source". Only one detail list is
// open at a time, so the panel never grows past the screen.
// "" | "wifi" | "vpn" | "bluetooth" | "sink" | "source". Only one detail
// list is open at a time, so the panel never grows past the screen.
property string expandedSection: ""
function expand(name: string): void {
@@ -128,6 +128,25 @@ Item {
onToggled: Connectivity.setWired(!Connectivity.wiredOn)
}
// Only when a VPN profile is saved at all: a machine with none has
// nothing to toggle, and the tile would be a control for absent
// configuration -- same reasoning as the Ethernet tile above.
Toggle {
width: root.cellWidth
visible: Vpn.available
icon: "network-vpn-symbolic"
label: "VPN"
active: Vpn.anyActive
enabled: !Vpn.busy
sublabel: {
if (Vpn.busy)
return "Working…";
return Vpn.anyActive ? Vpn.activeSummary : "Off";
}
onToggled: Vpn.toggle()
onExpanded: root.expand("vpn")
}
Toggle {
width: root.cellWidth
icon: root.btAdapter && root.btAdapter.enabled ? "bluetooth-active-symbolic" : "bluetooth-disabled-symbolic"
@@ -209,6 +228,16 @@ Item {
}
}
Section {
width: content.width
expanded: root.expandedSection === "vpn"
VpnList {
anchors.left: parent.left
anchors.right: parent.right
}
}
Section {
width: content.width
expanded: root.expandedSection === "bluetooth"