repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/Network.qml
Rawimport QtQuick
import Quickshell.Io
import "network.js" as Network
// Service / native boundary for iwd WiFi management.
//
// iwd exposes a full D-Bus API (net.connman.iwd). We talk to it via
// `busctl --json=short`, which emits structured JSON — so there is NO text/ANSI
// parsing. Native objects (Process) stay here; all JSON shaping lives in
// network.js. Two reads drive the model:
// ObjectManager.GetManagedObjects -> devices / stations / networks / known
// Station.GetOrderedNetworks -> [path, signal] strongest-first
//
// Writes are D-Bus method calls (Scan / Connect / Forget / Disconnect) and a
// set-property (Powered) — except connecting to a NEW secured network, which
// needs iwd to call back to an agent for the passphrase; for that one case we
// use `iwctl --passphrase` (a registered agent), as a DISCRETE ARGV array.
//
// SECURITY: every external call uses a discrete argv array — never sh -c with
// interpolation, because SSIDs may contain spaces / quotes / shell metachars.
// The passphrase is a transient argument to the connect call only; it NEVER
// enters stateJson / lastAction / error / any log line.
Item {
id: root
// --- reactive state, fed by the read Processes ---
property bool autoRefresh: true
property string deviceName: ""
property string devicePath: ""
property bool wifiEnabled: true
property bool scanning: false
property bool actionPending: false
property string lastError: ""
property string actionError: ""
// Parsed GetManagedObjects payload (device/station/networks/knownBySsid).
property var managed: ({})
property var mergedModel: []
property var lastAction: ({})
property int scanRetriesLeft: 0
// StationDiagnostic.GetDiagnostics detail for the CONNECTED BSS only
// (bssid/band/channel/rssi/rates/mode), fetched lazily on expand. {} = none.
property var connectionDetail: ({})
// Emitted whenever the model is recomputed after an async read. The view
// connects to this to re-pull stateJson().
signal refreshed
// D-Bus constants.
readonly property string svc: "net.connman.iwd"
readonly property string ifaceStation: "net.connman.iwd.Station"
readonly property string ifaceDevice: "net.connman.iwd.Device"
readonly property string ifaceNetwork: "net.connman.iwd.Network"
readonly property string ifaceKnown: "net.connman.iwd.KnownNetwork"
readonly property string ifaceDiag: "net.connman.iwd.StationDiagnostic"
function busctlCall(objectPath, iface, method) {
return ["busctl", "--json=short", "call", svc, objectPath, iface, method];
}
function activeRow() {
for (var i = 0; i < mergedModel.length; i++)
if (mergedModel[i].connected) {
return mergedModel[i];
}
return null;
}
function rowForId(id) {
const name = String(id);
for (var i = 0; i < mergedModel.length; i++)
if (mergedModel[i].id === name || mergedModel[i].ssid === name) {
return mergedModel[i];
}
return null;
}
function role() {
if (actionError.length > 0 || lastError.length > 0)
return "error";
if (!wifiEnabled)
return "ui.minor";
const active = activeRow();
if (active && active.state === "connecting")
return "warning";
return active ? "ui.accent" : "ui.minor";
}
// Build the view model from parsed managed objects + ordered list. Decorates
// the pure network.js rows with label/percent/glyph/role/state view fields.
function buildModel(ordered) {
const rows = Network.networkRows(managed, ordered);
const station = managed.station || {};
const connecting = station.state === "connecting";
const out = [];
for (var i = 0; i < rows.length; i++) {
const n = rows[i];
const isConnecting = connecting && station.connectedNetwork === n.path;
out.push({
"id": n.id,
"ssid": n.ssid,
"security": n.security,
"securityLabel": Network.securityLabel(n.security),
"signal": n.signal,
"signalDbm": n.signalDbm,
"signalPercent": n.signal * 25,
"signalRole": Network.signalRole(n.signal),
"signalGlyph": Network.signalGlyph(n.signal),
"known": n.known,
"connected": n.connected,
"needsPassphrase": n.needsPassphrase,
"path": n.path,
"state": n.connected ? "connected" : (isConnecting ? "connecting" : "available")
});
}
mergedModel = out;
// Drop stale per-BSS detail if nothing is connected anymore.
var stillConnected = false;
for (var j = 0; j < out.length; j++)
if (out[j].connected)
stillConnected = true;
if (!stillConnected)
connectionDetail = ({});
refreshed();
}
function stateObject() {
const active = activeRow();
const compactSignal = wifiEnabled && active ? active.signal * 25 : 0;
return {
"scanning": scanning,
"pending": actionPending,
"error": actionError || lastError,
"device": deviceName,
"wifi": {
"enabled": wifiEnabled
},
"active": active,
"connectionDetail": connectionDetail,
"networks": mergedModel,
"knownNetworks": mergedModel,
"compact": {
"label": "network",
"role": role(),
"signal": compactSignal
},
// MUST stay a real object: shell.qml:networkWidgetState() assigns
// state.shader.values / state.shader.history (throws on undefined).
"shader": {
"values": [],
"history": {}
},
"lastAction": lastAction
};
}
function stateJson() {
return JSON.stringify(stateObject());
}
// selfTest() — runs the pure network.js functions over small INLINE JSON
// literals (the fixtures are not shipped to runtime). Returns {pass,fail,
// cases}, same shape as Audio/Brightness selfTest. The literals mirror the
// busctl --json=short reply envelope ({type,data:[value]}).
function selfTest() {
var cases = [];
var pass = 0;
var fail = 0;
var check = function (label, got, want) {
const ok = JSON.stringify(got) === JSON.stringify(want);
if (ok)
pass += 1;
else
fail += 1;
cases.push({
"case": label,
"ok": ok,
"got": got,
"want": want
});
};
var devPath = "/net/connman/iwd/0/4";
var homePath = devPath + "/home_psk";
var fivegPath = devPath + "/fiveg_psk";
var cafePath = devPath + "/cafe_open";
// Build with bracket assignment (avoid ES6 computed keys for V4 safety).
var objs = {};
objs["/net/connman/iwd/0"] = {
"net.connman.iwd.Adapter": {
"Powered": {
"type": "b",
"data": true
}
}
};
objs[devPath] = {
"net.connman.iwd.Device": {
"Name": {
"type": "s",
"data": "wlan0"
},
"Powered": {
"type": "b",
"data": true
},
"Mode": {
"type": "s",
"data": "station"
}
},
"net.connman.iwd.Station": {
"State": {
"type": "s",
"data": "connected"
},
"Scanning": {
"type": "b",
"data": false
},
"ConnectedNetwork": {
"type": "o",
"data": homePath
}
}
};
objs[homePath] = {
"net.connman.iwd.Network": {
"Name": {
"type": "s",
"data": "home-wifi"
},
"Type": {
"type": "s",
"data": "psk"
},
"Connected": {
"type": "b",
"data": true
},
"KnownNetwork": {
"type": "o",
"data": "/known/home"
}
}
};
objs[fivegPath] = {
"net.connman.iwd.Network": {
"Name": {
"type": "s",
"data": "home-wifi-5g"
},
"Type": {
"type": "s",
"data": "psk"
},
"Connected": {
"type": "b",
"data": false
}
}
};
objs[cafePath] = {
"net.connman.iwd.Network": {
"Name": {
"type": "s",
"data": "Cafe Guest WiFi"
},
"Type": {
"type": "s",
"data": "open"
},
"Connected": {
"type": "b",
"data": false
}
}
};
var gmo = JSON.stringify({
"type": "a{oa{sa{sv}}}",
"data": [objs]
});
var gon = JSON.stringify({
"type": "a(on)",
"data": [[[homePath, -6800], [fivegPath, -4900], [cafePath, -6500]]]
});
var m = Network.parseManaged(gmo);
check("parseManaged device", m.device.name, "wlan0");
check("parseManaged mode", m.device.mode, "station");
check("parseManaged powered", m.device.powered, true);
check("parseManaged devicePath", m.devicePath, devPath);
check("parseManaged state", m.station.state, "connected");
check("parseManaged home known", m.networks[homePath].known, true);
check("parseManaged 5g known", m.networks[fivegPath].known, false);
var ordered = Network.parseOrdered(gon);
check("parseOrdered length", ordered.length, 3);
check("parseOrdered first signal", ordered[0].signal, -6800);
var rows = Network.networkRows(m, ordered);
check("networkRows length", rows.length, 3);
check("rows home ssid", rows[0].ssid, "home-wifi");
check("rows home signal", rows[0].signal, 2);
check("rows home connected", rows[0].connected, true);
check("rows home needsPass", rows[0].needsPassphrase, false);
check("rows home path", rows[0].path, homePath);
check("rows 5g signal", rows[1].signal, 4);
check("rows 5g needsPass", rows[1].needsPassphrase, true);
check("rows cafe ssid (spaces)", rows[2].ssid, "Cafe Guest WiFi");
check("rows cafe needsPass", rows[2].needsPassphrase, false);
check("signalBars(-4900)", Network.signalBars(-4900), 4);
check("signalBars(-6800)", Network.signalBars(-6800), 2);
check("signalBars(NaN)", Network.signalBars("x"), 0);
check("signalRole(0)", Network.signalRole(0), "ui.minor");
check("signalRole(2)", Network.signalRole(2), "ui.accent");
check("signalRole(4)", Network.signalRole(4), "content.accent");
check("securityLabel psk", Network.securityLabel("psk"), "wpa");
check("securityLabel open", Network.securityLabel("open"), "open");
check("needsPassphrase open", Network.needsPassphrase("open", false), false);
check("needsPassphrase psk unknown", Network.needsPassphrase("psk", false), true);
check("networkId empty non-empty", Network.networkId("").length > 0, true);
check("signalGlyph(0) non-empty", Network.signalGlyph(0).length > 0, true);
check("parseManaged(null) safe", Network.parseManaged(null).devicePath, "");
// connectionDetail (StationDiagnostic) — synthetic, sanitized BSSID.
var diag = JSON.stringify({
"type": "a{sv}",
"data": [
{
"ConnectedBss": {
"type": "s",
"data": "00:aa:bb:cc:dd:01"
},
"Frequency": {
"type": "u",
"data": 5220
},
"Channel": {
"type": "q",
"data": 44
},
"RSSI": {
"type": "n",
"data": -65
},
"RxMode": {
"type": "s",
"data": "802.11n"
},
"TxBitrate": {
"type": "u",
"data": 1080
},
"RxBitrate": {
"type": "u",
"data": 1350
}
}
]
});
var det = Network.connectionDetail(diag);
check("connectionDetail band", det.band, "5 GHz");
check("connectionDetail channel", det.channel, 44);
check("connectionDetail rxRate", det.rxRate, "135 Mbps");
check("connectionDetail rssi", det.rssi, -65);
check("connectionDetail disconnected null", Network.connectionDetail('{"type":"a{sv}","data":[{}]}'), null);
check("dbmFromCenti", Network.dbmFromCenti(-6500), -65);
check("successful action has no error", actionFailure(0), "");
check("failed action is sanitized", actionFailure(1), "action-failed");
return JSON.stringify({
"pass": pass,
"fail": fail,
"cases": cases
});
}
// --- read cadence ------------------------------------------------------
// refresh(): one call returns every iwd object + properties; its handler
// then reads the ordered list (which needs the discovered device path).
function refresh() {
gmoProc.exec(["busctl", "--json=short", "call", svc, "/", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"]);
}
// --- actions -----------------------------------------------------------
function actionFailure(exitCode) {
return Number(exitCode) === 0 ? "" : "action-failed";
}
function executeAction(command) {
actionError = "";
actionPending = true;
actionProc.exec(command);
}
function unavailableAction() {
actionPending = false;
actionError = "action-unavailable";
}
function scan() {
if (!devicePath) {
refresh();
return stateJson();
}
scanning = true;
lastError = "";
actionError = "";
scanRetriesLeft = 2;
scanProc.exec(busctlCall(devicePath, ifaceStation, "Scan"));
scanTimer.start();
return stateJson();
}
// connect(ssid, passphrase) — in-process secured/open/known connect. NOT an
// IPC handler. open/known go over D-Bus Network.Connect (no agent needed).
// A NEW secured network needs the passphrase handed to iwd via an agent, so
// it uses `iwctl --passphrase`. The passphrase is a transient argv only.
function connect(ssid, passphrase) {
const name = String(ssid);
const pp = (passphrase === undefined || passphrase === null) ? "" : String(passphrase);
const row = rowForId(name);
lastError = "";
actionError = "";
lastAction = {
"kind": "connect",
"ssid": name
};
if (pp.length > 0) {
// New secured network: iwctl registers an agent and supplies the key.
if (deviceName)
executeAction(["iwctl", "--passphrase", pp, "station", deviceName, "connect", name]);
else
unavailableAction();
} else if (row && row.path) {
executeAction(busctlCall(row.path, ifaceNetwork, "Connect"));
} else {
unavailableAction();
}
return stateJson();
}
// select(id) — IPC entry: connect a known/open network by id (= ssid).
function select(id) {
const name = String(id);
lastAction = {
"kind": "select",
"id": name
};
return connect(name, "");
}
function forget(id) {
const name = String(id);
lastAction = {
"kind": "forget",
"id": name
};
const knownPath = (managed.knownBySsid || {})[name];
if (knownPath)
executeAction(busctlCall(knownPath, ifaceKnown, "Forget"));
else
unavailableAction();
return stateJson();
}
function toggleWifi(enabled) {
const on = !!enabled;
lastAction = {
"kind": "toggle-wifi",
"enabled": on
};
if (devicePath) {
wifiEnabled = on;
executeAction(["busctl", "set-property", svc, devicePath, ifaceDevice, "Powered", "b", on ? "true" : "false"]);
} else {
unavailableAction();
}
return stateJson();
}
function disconnect() {
lastAction = {
"kind": "disconnect"
};
if (devicePath)
executeAction(busctlCall(devicePath, ifaceStation, "Disconnect"));
else
unavailableAction();
return stateJson();
}
// diagnostics() — lazy fetch of the connected BSS detail. The view calls
// this when the connected row is expanded; diagProc.onStreamFinished folds
// the result into connectionDetail and emits refreshed() so the drawer fills.
function diagnostics() {
if (devicePath && activeRow())
diagProc.exec(busctlCall(devicePath, ifaceDiag, "GetDiagnostics"));
return stateJson();
}
function checkConnectivity() {
lastAction = {
"kind": "check-connectivity"
};
refresh();
return stateJson();
}
Component.onCompleted: if (autoRefresh)
refresh()
// --- read Processes ----------------------------------------------------
Process {
id: gmoProc
running: false
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
root.managed = Network.parseManaged(this.text);
const m = root.managed;
if (!m.device || !m.devicePath) {
root.lastError = "no-wifi-device";
root.deviceName = "";
root.devicePath = "";
root.buildModel([]);
return;
}
root.deviceName = m.device.name;
root.devicePath = m.devicePath;
root.wifiEnabled = m.device.powered;
root.lastError = "";
if (m.device.powered) {
gonProc.exec(root.busctlCall(m.devicePath, root.ifaceStation, "GetOrderedNetworks"));
} else {
root.buildModel([]);
}
}
}
}
Process {
id: gonProc
running: false
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
root.buildModel(Network.parseOrdered(this.text));
}
}
}
// Lazy StationDiagnostic read (connected BSS only). On reply, shape via
// network.js#connectionDetail and emit refreshed() so the open drawer fills.
Process {
id: diagProc
running: false
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
root.connectionDetail = Network.connectionDetail(this.text) || ({});
root.refreshed();
}
}
}
// --- action Processes --------------------------------------------------
Process {
id: scanProc
running: false
stderr: StdioCollector {
id: scanErr
waitForEnd: true
}
// "Operation already in progress" = scan already running, NOT a hard
// error: keep scanning, let the retry timer re-read the list.
function onExited(exitCode) {
const err = String(scanErr.text || "");
if (exitCode !== 0 && err.indexOf("already in progress") < 0)
root.lastError = "scan-failed";
}
}
// Shared action runner. Re-reads state when done so the model reflects the
// effect. stderr is intentionally NOT surfaced verbatim (belt-and-suspenders:
// a connect arg must never leak via an error string).
Process {
id: actionProc
running: false
function onExited(exitCode) {
command = [];
root.actionPending = false;
root.actionError = root.actionFailure(exitCode);
root.refresh();
root.refreshed();
}
}
// Delayed/polled re-read after an async scan (results lag ~1-4 s).
Timer {
id: scanTimer
interval: 2000
repeat: true
running: false
onTriggered: {
root.refresh();
root.scanRetriesLeft -= 1;
if (root.scanRetriesLeft <= 0) {
root.scanning = false;
running = false;
}
}
}
}