// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import Quickshell.Networking import Quickshell.Io import QtQuick import "../.." // Always active: the backends push and there is no poll to gate, so the // state the tile shows is live from launch. Referencing Networking here is // also what instantiates the singleton at shell start. Module { id: mod name: "network" label: "Network" alwaysActive: true // .values on the device ObjectModel; empty until the backend is ready, // roughly two seconds in. Bindings repaint when it arrives. readonly property var devices: Networking.devices ? Networking.devices.values : [] readonly property var wiredDevices: devices.filter(d => d.type === DeviceType.Wired) readonly property var wifiDevice: devices.find(d => d.type === DeviceType.Wifi) ?? null readonly property bool wiredUp: wiredDevices.some(d => d.connected) readonly property bool wifiUp: wifiDevice ? wifiDevice.connected : false readonly property bool wifiOn: Networking.wifiEnabled readonly property var wifiNetworks: wifiDevice && wifiDevice.networks ? wifiDevice.networks.values : [] readonly property string wifiSsid: { const n = wifiNetworks.find(x => x.connected); return n ? n.name : ""; } readonly property string wiredName: { const d = wiredDevices.find(x => x.connected); return d ? d.name : ""; } // Local IPs, keyed by interface. The Networking API exposes only the MAC // (NetworkDevice.address), so this is the module's one shell-out. It is // refreshed when a link comes up and on a slow timer while one is up, to // catch a DHCP renew. `ip -j` is JSON, so no text parsing. property var ipByIface: ({}) function ipFor(iface) { return ipByIface[iface] ?? ""; } function refreshIps() { ipProc.running = false; ipProc.running = true; } // Connected first, then known, then strongest. Kept here so both the // tile and the page read the same order. readonly property var wifiNetworksSorted: { const arr = wifiNetworks.slice(); arr.sort((a, b) => ((b.connected ? 1 : 0) - (a.connected ? 1 : 0)) || ((b.known ? 1 : 0) - (a.known ? 1 : 0)) || ((b.signalStrength ?? 0) - (a.signalStrength ?? 0))); return arr; } // The tile is a globe: the wired/wifi distinction belongs to the page's // section headers, not the tile. icon: "\uf0ac" // Active while any link is up. active: mod.wiredUp || mod.wifiUp // The network a connect is pending on, so connectionFailed can be caught. property var pendingNetwork: null property string error: "" function connect(net) { mod.pendingNetwork = net; mod.error = ""; net.connect(); } function connectWithPsk(net, psk) { mod.pendingNetwork = net; mod.error = ""; net.connectWithPsk(psk); } function disconnectNetwork(net) { mod.error = ""; if (net) net.disconnect(); } function forget(net) { mod.error = ""; if (net) net.forget(); } // A failure after the drawer closed would otherwise go unseen, so it also // raises a notification. Same cached-Process shape as VmModule. function notify(title, body) { notifyProc.command = ["notify-send", "--app-name=network", "--urgency=critical", "--icon=error", title, body]; notifyProc.running = false; notifyProc.running = true; } property Process notifyProc: Process {} property Process ipProc: Process { command: ["ip", "-j", "-4", "addr", "show"] stdout: StdioCollector { onStreamFinished: { const map = {}; try { for (const iface of JSON.parse(text)) { const a = (iface.addr_info ?? []).find(x => x.family === "inet"); if (a) map[iface.ifname] = a.local; } } catch (e) {} mod.ipByIface = map; } } } // Only poll while something is actually up. property Timer ipTimer: Timer { interval: 15000 repeat: true running: mod.wiredUp || mod.wifiUp onTriggered: mod.refreshIps() } property Connections conn: Connections { target: mod.pendingNetwork function onConnectionFailed(reason) { mod.error = "Could not connect to " + (mod.pendingNetwork?.name ?? "network"); mod.notify("Network", mod.error); mod.pendingNetwork = null; } function onConnectedChanged() { if (mod.pendingNetwork && mod.pendingNetwork.connected) mod.pendingNetwork = null; } } onWiredUpChanged: mod.refreshIps() onWifiUpChanged: mod.refreshIps() Component.onCompleted: mod.refreshIps() tileContent: Component { NetworkTile { net: mod } } page: Component { Page { title: "Network" NetworkPage { width: parent.width; net: mod } } } }