Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/Network.qml

Raw
import 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;
            }
        }
    }
}