Deliberate, never automatic. The autonomous gate it replaces recycled the tunnel 652 times in 90 minutes and could not be switched off, because 'nmcli connection down' fired the dispatcher that brought it back up. Reads NM for state rather than tracking a bool. No privilege needed: polkit already lets the seat user activate a system connection. Registered in the ii-phone chooser as well as ii-base, or the phone overlay wins and the delegate never appears.
108 lines
3.8 KiB
QML
108 lines
3.8 KiB
QML
import QtQuick
|
|
import qs.services
|
|
import qs.modules.common
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
|
|
/**
|
|
* A WireGuard tunnel, toggled deliberately.
|
|
*
|
|
* DELIBERATE, NEVER AUTOMATIC — and that is the design, not a limitation.
|
|
* An autonomous gate held this tunnel on 2026-07-30: it recycled 652 times in
|
|
* 90 minutes, through doze tiers whose contract is "network fetchers stopped",
|
|
* and the user could not switch it off because `nmcli connection down` fired
|
|
* the NM dispatcher that immediately brought it back up. A control surface the
|
|
* owner cannot overrule is not a control surface.
|
|
*
|
|
* So there is no daemon, no timer and no dispatcher hook deciding this. The
|
|
* toggle is the whole of it. If tunnel posture later becomes something the
|
|
* device decides for itself, it belongs in sessiond's state machine as an
|
|
* Action (DEVICE-STATE-MACHINE §12) and reachable as a verb (doctrine §13) —
|
|
* not as another actor off to the side.
|
|
*
|
|
* No privilege needed: polkit already lets the seat user activate a system
|
|
* connection, verified on device.
|
|
*/
|
|
QuickToggleModel {
|
|
id: root
|
|
|
|
/// The NetworkManager connection this drives. A property rather than a
|
|
/// constant so a second profile does not need a second component.
|
|
property string connectionName: "wiuf"
|
|
|
|
/// True while an up/down is in flight, so a double tap cannot race itself.
|
|
property bool busy: false
|
|
|
|
name: Translation.tr("Wiuf VPN")
|
|
icon: root.toggled ? "vpn_lock" : "vpn_key"
|
|
statusText: root.busy ? Translation.tr("…") : (root.toggled ? Translation.tr("On") : Translation.tr("Off"))
|
|
tooltipText: root.toggled
|
|
? Translation.tr("Wiuf VPN — 10.10.0.0/16 routed over the tunnel")
|
|
: Translation.tr("Wiuf VPN — off")
|
|
|
|
mainAction: () => {
|
|
if (root.busy)
|
|
return;
|
|
root.busy = true;
|
|
if (root.toggled)
|
|
downProc.running = true;
|
|
else
|
|
upProc.running = true;
|
|
}
|
|
|
|
function notify(body) {
|
|
Quickshell.execDetached(["notify-send", Translation.tr("Wiuf VPN"), body, "-a", "Shell"]);
|
|
}
|
|
|
|
Process {
|
|
id: upProc
|
|
command: ["nmcli", "connection", "up", root.connectionName]
|
|
onExited: (exitCode, exitStatus) => {
|
|
root.busy = false;
|
|
refreshProc.running = true;
|
|
if (exitCode !== 0)
|
|
root.notify(Translation.tr("Could not connect."));
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: downProc
|
|
command: ["nmcli", "connection", "down", root.connectionName]
|
|
onExited: (exitCode, exitStatus) => {
|
|
root.busy = false;
|
|
refreshProc.running = true;
|
|
if (exitCode !== 0)
|
|
root.notify(Translation.tr("Could not disconnect."));
|
|
}
|
|
}
|
|
|
|
// Read NM rather than tracking a local bool: the connection can also be
|
|
// brought up or down from nmcli, from Settings, or by NM itself, and a
|
|
// shadow copy that disagrees with the owning subsystem is the failure
|
|
// doctrine §4 names ("never a hand-tracked bool").
|
|
Process {
|
|
id: refreshProc
|
|
running: true
|
|
command: ["bash", "-c", `nmcli -t -f NAME connection show --active | grep -qx '${root.connectionName}' && echo up || echo down`]
|
|
stdout: StdioCollector {
|
|
id: stateCollector
|
|
onStreamFinished: {
|
|
if (stateCollector.text.length === 0)
|
|
return;
|
|
root.toggled = stateCollector.text.trim() === "up";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cheap resync for changes made outside the panel. One nmcli call every
|
|
// 15 s while the shell runs; deliberately not a decision loop.
|
|
Timer {
|
|
interval: 15000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
if (!root.busy)
|
|
refreshProc.running = true;
|
|
}
|
|
}
|
|
}
|