Shortcuts you invent, rules you write, gestures you own - all still just data

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-25 01:51:59 -04:00
parent f9e5d3f470
commit 06c53d6c21
48 changed files with 4749 additions and 140 deletions
+100 -11
View File
@@ -35,6 +35,12 @@ Singleton {
// connection name -> the helper's connection shape. See detailsFor().
property var details: ({})
// Every profile NetworkManager holds, in range or not. See the `saved`
// verb: this is the list that is otherwise invisible until you are standing
// next to the network you wanted to tidy up.
property var savedConnections: []
property bool savedScanned: false
property var hotspot: ({})
property string proxyMode: "none"
property string proxyHost: ""
@@ -54,7 +60,7 @@ Singleton {
// Guards read the Process objects directly; a derived binding is stale
// inside the handler that changes it. See DefaultApps.qml.
readonly property bool busy: mutation.running || enterprise.running || detailsQuery.running
readonly property bool busy: mutation.running || passwordJoin.running || detailsQuery.running
// Connections whose details have been asked for, in order, so a burst of
// requests becomes one query at a time rather than one Process each.
@@ -131,6 +137,18 @@ Singleton {
}
}
function absorbSaved(text: string): void {
try {
const parsed = JSON.parse(text);
root.savedConnections = Array.isArray(parsed.connections) ? parsed.connections : [];
root.savedScanned = true;
if (String(parsed.error ?? "") !== "")
root.lastError = String(parsed.error);
} catch (error) {
root.lastError = "Could not read the saved networks.";
}
}
function absorbHotspot(text: string): void {
try {
const parsed = JSON.parse(text);
@@ -185,6 +203,7 @@ Singleton {
function absorb(shape: string, subject: string, text: string): void {
switch (shape) {
case "connection": root.absorbDetails(subject, text); break;
case "saved": root.absorbSaved(text); break;
case "hotspot": root.absorbHotspot(text); break;
case "proxy": root.absorbProxy(text); break;
case "airplane": root.absorbAirplane(text); break;
@@ -206,6 +225,10 @@ Singleton {
function forget(connection: string): void {
root.run("connection", connection, ["forget", connection]);
// The saved list is the one surface that shows profiles nobody is
// standing next to, so a forget nobody re-reads leaves a row for a
// profile that no longer exists. The timer waits for the mutation.
root.refreshSavedSoon();
}
function setAutoconnect(connection: string, enabled: bool): void {
@@ -218,6 +241,36 @@ Singleton {
["set-mac-random", connection, enabled ? "true" : "false"]);
}
// "yes", "no" or "auto". Three states rather than two, because "automatic"
// is NetworkManager guessing from what the network said and "no" is a claim
// — see the helper's set_metered.
function setMetered(connection: string, mode: string): void {
root.run("connection", connection, ["set-metered", connection, String(mode)]);
}
// ---- static addressing
//
// family is "4" or "6" — the helper's own spelling, so nothing has to
// translate between two vocabularies on the way down.
function setIpAuto(connection: string, family: string): void {
root.run("connection", connection, ["set-ip", connection, String(family), "auto"]);
}
// Every field on every call, including the empty ones: the helper writes
// the whole stack at once so that switching modes cannot leave half the old
// configuration behind. `dns` is the comma-separated list as typed.
function setIpManual(connection: string, family: string, address: string,
gateway: string, dns: string): void {
root.run("connection", connection,
["set-ip", connection, String(family), "manual",
String(address), String(gateway), String(dns)]);
}
// ---- saved profiles
function refreshSaved(): void { root.run("saved", "", ["saved"]); }
// ---- VPN
function importVpn(path: string): void {
@@ -273,16 +326,34 @@ Singleton {
// above: only this one ever opens stdin.
function joinEnterprise(ssid: string, profile: string, identity: string,
password: string, caCert: string): void {
if (enterprise.running)
if (passwordJoin.running)
return;
root.lastError = "";
root.pendingPassword = password;
enterprise.subject = ssid;
enterprise.command = String(caCert ?? "") !== ""
passwordJoin.subject = ssid;
passwordJoin.command = String(caCert ?? "") !== ""
? [root.helperPath, "join-enterprise", ssid, profile, identity, caCert]
: [root.helperPath, "join-enterprise", ssid, profile, identity];
enterprise.stdinEnabled = true;
enterprise.running = true;
passwordJoin.stdinEnabled = true;
passwordJoin.running = true;
}
// ---- hidden Wi-Fi
//
// Same passphrase path as the enterprise join, for the same reason: a
// passphrase in argv is published to every process on this machine through
// /proc. An open hidden network still goes down this path and writes an
// empty line, so the helper's read returns rather than waiting forever.
function joinHidden(ssid: string, profile: string, security: string,
password: string): void {
if (passwordJoin.running)
return;
root.lastError = "";
root.pendingPassword = password;
passwordJoin.subject = profile;
passwordJoin.command = [root.helperPath, "join-hidden", ssid, profile, security];
passwordJoin.stdinEnabled = true;
passwordJoin.running = true;
}
// Everything that is not per-connection, in one call: what a page asks for
@@ -291,6 +362,7 @@ Singleton {
root.refreshProxy();
root.refreshAirplaneSoon();
root.refreshHotspotSoon();
root.refreshSavedSoon();
for (const connection in root.details)
root.requestDetails(connection);
}
@@ -299,6 +371,7 @@ Singleton {
// over it rather than dropped by its running guard.
function refreshAirplaneSoon(): void { airplaneSoon.restart(); }
function refreshHotspotSoon(): void { hotspotSoon.restart(); }
function refreshSavedSoon(): void { savedSoon.restart(); }
onActiveChanged: if (root.active) root.refresh()
@@ -324,6 +397,17 @@ Singleton {
}
}
Timer {
id: savedSoon
interval: 400
onTriggered: {
if (mutation.running)
savedSoon.restart();
else
root.refreshSaved();
}
}
// The next queued details read, one tick after the last one exits. Draining
// from inside onExited would look at a `running` that has not gone false
// yet, and the queue would stall on its own guard.
@@ -369,24 +453,29 @@ Singleton {
}
Process {
id: enterprise
id: passwordJoin
property string subject: ""
onStarted: {
enterprise.write(root.pendingPassword + "\n");
passwordJoin.write(root.pendingPassword + "\n");
// Held for as long as it takes to hand over, and no longer.
root.pendingPassword = "";
// Closing stdin is what lets the helper's read return; without it
// the join waits forever for a line that is already sent.
enterprise.stdinEnabled = false;
passwordJoin.stdinEnabled = false;
}
stdout: StdioCollector {
onStreamFinished: root.absorbDetails(enterprise.subject, this.text)
onStreamFinished: root.absorbDetails(passwordJoin.subject, this.text)
}
stderr: StdioCollector {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
onExited: root.pendingPassword = ""
onExited: {
root.pendingPassword = "";
// A join makes a profile, so the saved list is out of date the
// moment this returns.
root.refreshSavedSoon();
}
}
}