Watch
1
0
Fork
You've already forked souveraine
0

shell: VPN quick toggle in the right panel

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.
This commit is contained in:
Fimeg 2026-07-30 19:13:19 -04:00
commit 0b57daac06
5 changed files with 150 additions and 1 deletions

View file

@ -0,0 +1,108 @@
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;
}
}
}

View file

@ -265,6 +265,20 @@ DelegateChooser {
cellSize: modelData.size cellSize: modelData.size
} } } }
DelegateChoice { roleValue: "vpn"; AndroidVpnToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "screenSnip"; AndroidScreenSnipToggle { DelegateChoice { roleValue: "screenSnip"; AndroidScreenSnipToggle {
required property int index required property int index
required property var modelData required property var modelData

View file

@ -0,0 +1,13 @@
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Io
AndroidQuickToggleButton {
id: root
toggleModel: VpnToggle {}
}

View file

@ -295,6 +295,20 @@ DelegateChooser {
cellSize: modelData.size cellSize: modelData.size
} } } }
DelegateChoice { roleValue: "vpn"; AndroidVpnToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "screenSnip"; AndroidScreenSnipToggle { DelegateChoice { roleValue: "screenSnip"; AndroidScreenSnipToggle {
required property int index required property int index
required property var modelData required property var modelData

View file

@ -815,7 +815,7 @@ Singleton {
property bool leftAlignApps: false property bool leftAlignApps: false
} }
property JsonObject actionCenter: JsonObject { property JsonObject actionCenter: JsonObject {
property list<string> toggles: [ "network", "bluetooth", "easyEffects", "powerProfile", "idleInhibitor", "nightLight", "darkMode", "antiFlashbang", "cloudflareWarp", "mic", "musicRecognition", "notifications", "onScreenKeyboard", "gameMode", "screenSnip", "colorPicker" ] property list<string> toggles: [ "network", "vpn", "bluetooth", "easyEffects", "powerProfile", "idleInhibitor", "nightLight", "darkMode", "antiFlashbang", "cloudflareWarp", "mic", "musicRecognition", "notifications", "onScreenKeyboard", "gameMode", "screenSnip", "colorPicker" ]
} }
property JsonObject calendar: JsonObject { property JsonObject calendar: JsonObject {
property bool force2CharDayOfWeek: true property bool force2CharDayOfWeek: true