diff options
Diffstat (limited to 'desktop/modules/network')
| -rw-r--r-- | desktop/modules/network/NetworkPage.qml | 204 | ||||
| -rw-r--r-- | desktop/modules/network/README.md | 12 |
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. |
