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
81 lines
2.4 KiB
QML
81 lines
2.4 KiB
QML
// VPN profile picker: every saved VPN or WireGuard connection, active first.
|
|
// Clicking a row flips that one profile, so a machine with several tunnels
|
|
// can switch without a trip through nmcli — which is the whole reason this
|
|
// list exists (see services/Vpn.qml).
|
|
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
Item {
|
|
id: root
|
|
|
|
implicitHeight: list.implicitHeight
|
|
|
|
readonly property var profiles: {
|
|
const list = Vpn.connections.slice();
|
|
list.sort((a, b) => {
|
|
if (a.active !== b.active)
|
|
return a.active ? -1 : 1;
|
|
return (a.name || "").localeCompare(b.name || "");
|
|
});
|
|
return list;
|
|
}
|
|
|
|
function stateText(profile): string {
|
|
const kind = profile.kind === "wireguard" ? "WireGuard" : "VPN";
|
|
return profile.active ? kind + " · Connected" : kind;
|
|
}
|
|
|
|
ScrollColumn {
|
|
id: list
|
|
anchors.fill: parent
|
|
maxHeight: 300
|
|
|
|
Text {
|
|
width: parent.width
|
|
visible: root.profiles.length === 0
|
|
topPadding: 12
|
|
bottomPadding: 12
|
|
horizontalAlignment: Text.AlignHCenter
|
|
text: "No VPN profiles"
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
}
|
|
|
|
Repeater {
|
|
model: root.profiles
|
|
|
|
RowButton {
|
|
required property var modelData
|
|
|
|
width: parent.width
|
|
icon: "network-vpn-symbolic"
|
|
iconFallback: "network-workgroup-symbolic"
|
|
label: modelData.name
|
|
sublabel: root.stateText(modelData)
|
|
selected: modelData.active
|
|
dimmed: Vpn.busy
|
|
onClicked: Vpn.setActive(modelData.uuid, !modelData.active)
|
|
}
|
|
}
|
|
|
|
// Activation failures land here rather than vanishing: "the toggle
|
|
// did nothing" was exactly the complaint that motivated this panel.
|
|
Text {
|
|
width: parent.width
|
|
visible: Vpn.lastError !== ""
|
|
topPadding: 4
|
|
bottomPadding: 8
|
|
leftPadding: 12
|
|
rightPadding: 12
|
|
wrapMode: Text.Wrap
|
|
text: Vpn.lastError
|
|
color: Theme.warn
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
}
|
|
}
|