aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/network/NetworkPage.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-27 13:56:24 +0100
committerDanilo M. <danix@danix.xyz>2026-10-27 13:56:24 +0100
commit8c15dc1205d879591318cf365e15b014477329c1 (patch)
treef315c92394f8db96c36f56c8a93f9d8feedade20 /desktop/modules/network/NetworkPage.qml
parent6b68cae8e0c4ec43c4a9a8a812a644b7bea746e7 (diff)
downloadquickshell-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.qml204
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 ---