// 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.Io import Quickshell.Networking import QtQuick import "../.." import "../perf" import "../perf/Perf.js" as P Column { id: page required property var net signal back spacing: 14 function stateLabel(s) { return s === ConnectionState.Connected ? "connected" : s === ConnectionState.Connecting ? "connecting" : s === ConnectionState.Disconnecting ? "disconnecting" : "disconnected"; } function last(list) { return list.length ? list[list.length - 1] : null; } // --- Traffic --- // The rates are Perf's, sampled always, so the graph opens on the last // minute rather than empty. Row { spacing: 6 Text { anchors.verticalCenter: parent.verticalCenter text: String.fromCodePoint(0xF012A) font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 2 } color: Theme.subtext } Text { anchors.verticalCenter: parent.verticalCenter text: "Traffic · " + Perf.iface font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } color: Theme.subtext } } Canvas { id: graph width: page.width height: 80 onPaint: { const ctx = getContext("2d"); ctx.reset(); const max = Math.max(1, ...Perf.rx, ...Perf.tx); const line = (data, color) => { ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.beginPath(); data.forEach((v, i) => { const x = width * i / 59; const y = height - 1 - (height - 2) * v / max; if (i) ctx.lineTo(x, y); else ctx.moveTo(x, y); }); ctx.stroke(); }; line(Perf.rx, Theme.teal); line(Perf.tx, Theme.peach); } Connections { target: Perf function onRxChanged() { graph.requestPaint(); } function onTxChanged() { graph.requestPaint(); } } } Item { width: page.width implicitHeight: rxText.implicitHeight Text { id: rxText text: "↓ " + P.speed(page.last(Perf.rx)) font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.teal } Text { anchors.right: parent.right text: "↑ " + P.speed(page.last(Perf.tx)) font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.peach } } // The five busiest processes, from `ss -tinp` byte counters diffed every // 2s while the page is open; it stops with the page. TCP only, and only // the user's own processes: UDP (QUIC, so much of a browser's HTTP/3) // and system services are not counted. nethogs would see both but needs // capabilities it does not have here. property var socks: null // {list, t} from the last sample property var netProcs: [] Timer { interval: 2000 running: true repeat: true triggeredOnStart: true onTriggered: { ssProc.running = false; ssProc.running = true; } } Process { id: ssProc command: ["/sbin/ss", "-tinpH"] stdout: StdioCollector { onStreamFinished: { const cur = { list: P.sockets(text), t: Date.now() / 1000 }; if (page.socks) page.netProcs = P.netProcs(page.socks.list, cur.list, cur.t - page.socks.t).slice(0, 5); page.socks = cur; } } } // Five rows tall whatever the count, so what sits below never moves. Column { width: page.width height: 5 * rowMetrics.height + 4 * spacing spacing: 8 FontMetrics { id: rowMetrics font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } } Repeater { model: page.netProcs delegate: Item { required property var modelData width: page.width implicitHeight: procName.implicitHeight Text { id: procName anchors { left: parent.left; right: procRx.left; rightMargin: 8 } text: modelData.name elide: Text.ElideRight font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.text } Text { id: procRx anchors.right: procTx.left width: Theme.fontSize * 6 horizontalAlignment: Text.AlignRight text: "↓ " + P.speed(modelData.rx) font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } font.features: { "tnum": 1 } color: Theme.teal } Text { id: procTx anchors.right: parent.right width: Theme.fontSize * 6 horizontalAlignment: Text.AlignRight text: "↑ " + P.speed(modelData.tx) font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } font.features: { "tnum": 1 } color: Theme.peach } } } } // The LAN address and the public IP, red when it differs from the // pinned one; Perf keeps both, since the mismatch also lights the perf // tile's dot. The pin stores the address shown and is lit while they // are the same. Item { width: page.width implicitHeight: lanText.implicitHeight Text { text: "LAN" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.subtext } Text { id: lanText anchors.right: parent.right text: Perf.lan || "--" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.text } } Item { width: page.width implicitHeight: pubText.implicitHeight Text { text: "Public" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.subtext } Text { id: pubText anchors { right: pin.left; rightMargin: 6 } text: Perf.ip.ip || "--" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Perf.ip.mismatch ? Theme.red : Theme.text } Text { id: pin anchors { right: parent.right; verticalCenter: pubText.verticalCenter } text: String.fromCodePoint(0xF0403) font { family: Theme.iconFamily; pixelSize: Theme.fontSize } color: Perf.ip.ip !== "" && Perf.ip.ip === Prefs.pinnedIp ? Theme.text : Theme.overlay MouseArea { anchors.fill: parent anchors.margins: -4 enabled: Perf.ip.ip !== "" cursorShape: Qt.PointingHandCursor onClicked: Prefs.setPinnedIp(Perf.ip.ip) } } } Rectangle { width: page.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } // --- Wired --- Row { visible: page.net.wiredDevices.length > 0 spacing: 6 Text { anchors.verticalCenter: parent.verticalCenter text: "\uef44" font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 2 } color: Theme.subtext } Text { anchors.verticalCenter: parent.verticalCenter text: "Wired" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } color: Theme.subtext } } Repeater { model: page.net.wiredDevices Rectangle { required property var modelData width: page.width implicitHeight: wiredText.implicitHeight + 16 radius: 8 color: Qt.alpha(Theme.surface, 0.35) Text { id: wiredText anchors { left: parent.left; leftMargin: 10; verticalCenter: parent.verticalCenter } text: modelData.name + " " + page.stateLabel(modelData.state) font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: modelData.connected ? Theme.text : Theme.subtext } Button { anchors { right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter } visible: modelData.connected text: "Disconnect" onClicked: page.net.disconnectNetwork(modelData) } } } Rectangle { width: page.width; height: 1; visible: page.net.wiredDevices.length > 0; color: Qt.alpha(Theme.text, 0.12) } // --- Wifi --- Item { width: page.width implicitHeight: Math.max(wifiLabel.implicitHeight, wifiSwitch.implicitHeight) Text { id: wifiLabel anchors { left: wifiGlyph.right; leftMargin: 6; verticalCenter: parent.verticalCenter } text: "Wi-Fi" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1; bold: true } color: Theme.text } Text { id: wifiGlyph anchors.verticalCenter: parent.verticalCenter text: "\uf1eb" font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 2 } color: Theme.subtext } Switch { id: wifiSwitch anchors { right: parent.right; verticalCenter: parent.verticalCenter } enabled: Networking.wifiHardwareEnabled checked: page.net.wifiOn onToggled: on => Networking.wifiEnabled = on } } Text { width: page.width visible: !page.net.wifiOn text: "Wi-Fi off" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.subtext } Row { width: page.width visible: page.net.wifiOn spacing: 8 Button { text: page.net.wifiDevice && page.net.wifiDevice.scannerEnabled ? "Scanning…" : "Scan" onClicked: if (page.net.wifiDevice) page.net.wifiDevice.scannerEnabled = !page.net.wifiDevice.scannerEnabled } // The scanner repeats until disabled, and closing the drawer does not // disable it. A continuous scan also costs throughput on the connected // link. See the matching timer in BluetoothPage. Timer { interval: 60000 running: page.net.wifiDevice ? page.net.wifiDevice.scannerEnabled : false onTriggered: if (page.net.wifiDevice) page.net.wifiDevice.scannerEnabled = false } } Repeater { model: page.net.wifiOn ? page.net.wifiNetworksSorted : [] NetworkRow { required property var modelData net: page.net entry: modelData width: page.width } } Text { width: page.width visible: page.net.error !== "" wrapMode: Text.Wrap text: page.net.error font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } color: Theme.red } }