diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-27 13:56:24 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-27 13:56:24 +0100 |
| commit | 8c15dc1205d879591318cf365e15b014477329c1 (patch) | |
| tree | f315c92394f8db96c36f56c8a93f9d8feedade20 /desktop/modules/network/NetworkPage.qml | |
| parent | 6b68cae8e0c4ec43c4a9a8a812a644b7bea746e7 (diff) | |
| download | quickshell-8c15dc1205d879591318cf365e15b014477329c1.tar.gz quickshell-8c15dc1205d879591318cf365e15b014477329c1.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/network/NetworkPage.qml')
| -rw-r--r-- | desktop/modules/network/NetworkPage.qml | 204 |
1 files changed, 204 insertions, 0 deletions
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 --- |
