From 8c15dc1205d879591318cf365e15b014477329c1 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 27 Oct 2026 13:56:24 +0100 Subject: network: traffic, its busiest processes and the IPs on the page The rx/tx graph, the LAN address and the public IP with its pin move from the perf page's NET card to the network page, where they belong; the NET card is gone. Perf still samples and owns all of them: the graph opens on the last minute of history, and a public IP that differs from the pin still lights the perf tile's dot. Below the graph, the five busiest processes. nethogs is installed but cannot open any interface unprivileged, so the rates come from `ss -tinpH`: per-socket byte counters diffed every 2s while the page is open, summed by process name. That counts TCP only, and only the user's own processes, so QUIC and system services are missing. The list is held at five rows tall so the IP lines under it never move. Perf.js gains sockets() and netProcs(), with tests in test_perf.js. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/network/NetworkPage.qml | 204 ++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) (limited to 'desktop/modules/network/NetworkPage.qml') diff --git a/desktop/modules/network/NetworkPage.qml b/desktop/modules/network/NetworkPage.qml index b789d17..e0f32e8 100644 --- a/desktop/modules/network/NetworkPage.qml +++ b/desktop/modules/network/NetworkPage.qml @@ -9,9 +9,12 @@ // 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 @@ -27,6 +30,207 @@ Column { : 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 --- -- cgit v1.2.3