aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/network
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/network')
-rw-r--r--desktop/modules/network/NetworkPage.qml204
-rw-r--r--desktop/modules/network/README.md12
2 files changed, 215 insertions, 1 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 ---
diff --git a/desktop/modules/network/README.md b/desktop/modules/network/README.md
index a806488..551e23c 100644
--- a/desktop/modules/network/README.md
+++ b/desktop/modules/network/README.md
@@ -10,7 +10,17 @@ slow timer while a link is up. When both wired and wifi are up the tile shows
both: the API exposes no route metric, so it does not guess which link
carries traffic.
-The page lists managed wired devices, then a wifi radio switch and list. A
+The page opens on traffic: `br0`'s download and upload over the last minute,
+sampled always by `perf/Perf.qml`, the five busiest processes below it, then
+the LAN address and the public IP with its pin (see the perf README; Perf
+still owns both, since a mismatch lights the perf tile's dot).
+Those come from `ss -tinp`, its per-socket byte counters diffed every 2s
+while the page is open, summed by process name. That is TCP only and the
+user's own processes only: QUIC and system services are missing. nethogs
+would see both, but unprivileged it cannot open the interfaces. The parsing
+is in `perf/Perf.js`, tested by `perf/test_perf.js`.
+
+Then the page lists managed wired devices, then a wifi radio switch and list. A
row click connects; a secured network that is not yet known reveals an inline
password field. Known networks offer Forget, the connected one Disconnect.