Add per-process network drawer to the Network widget
Clicking the Network pill now toggles a drawer listing processes ranked by network throughput, refreshed live only while open — mirroring the CPU widget's top-processes drawer. services/topnet.sh (single-shot, like topproc.sh) takes two `ss` snapshots procInterval apart, diffs each TCP socket's cumulative rx/tx bytes by inode, sums positive deltas per owning PID, and prints ranked "N <rx/s> <tx/s> <name>" frames. SysStats re-runs it while netProcPollEnabled (drawer open) using the same SplitParser-accumulate / parse-on-exit / re-arm plumbing as procScan. Network.qml wraps its Pill in an Item and hangs a fixed-size PopupWindow drawer off it. ss (iproute2) is added to runtimeInputs since per-process byte accounting has no virtual-file equivalent. Caveat: ss only exposes byte counters for TCP sockets, so UDP/QUIC traffic is not attributed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,16 @@ Singleton {
|
||||
property bool procPollEnabled: false
|
||||
readonly property int procInterval: 2 // seconds between frames
|
||||
|
||||
// --- top network processes (same opt-in streaming model as topProcs) ---
|
||||
// Each entry: { name, rx (bytes/sec down), tx (bytes/sec up) }, highest
|
||||
// combined throughput first. Set netProcPollEnabled true (Network drawer
|
||||
// open) to start the streamer; only TCP traffic is attributable (see
|
||||
// topnet.sh), so UDP/QUIC-heavy apps may underreport.
|
||||
property var topNetProcs: []
|
||||
readonly property int netProcCount: 6
|
||||
property bool netProcPollEnabled: false
|
||||
readonly property int netProcInterval: 2 // seconds between frames
|
||||
|
||||
readonly property bool hasBattery: battery >= 0
|
||||
|
||||
// poll interval in seconds
|
||||
@@ -242,6 +252,24 @@ Singleton {
|
||||
root.topProcs = out;
|
||||
}
|
||||
|
||||
// Build topNetProcs from one run of topnet.sh: lines of
|
||||
// "N <rx_bytes/s> <tx_bytes/s> <name>", already ranked by the script.
|
||||
function _parseNetProcFrame(text) {
|
||||
if (!text) return;
|
||||
const lines = text.split("\n");
|
||||
const out = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const p = lines[i].split(" ");
|
||||
if (p[0] !== "N" || p.length < 4) continue;
|
||||
out.push({
|
||||
rx: Number(p[1]),
|
||||
tx: Number(p[2]),
|
||||
name: p.slice(3).join(" ")
|
||||
});
|
||||
}
|
||||
root.topNetProcs = out;
|
||||
}
|
||||
|
||||
function _tickOnce() {
|
||||
statView.reload();
|
||||
cpuInfoView.reload();
|
||||
@@ -343,6 +371,35 @@ Singleton {
|
||||
else root.topProcs = [];
|
||||
}
|
||||
|
||||
// ---- top network processes (opt-in, drawer-driven) --------------------
|
||||
// Mirror of procScan: services/topnet.sh prints one ranked frame and exits;
|
||||
// we re-run it every ~netProcInterval while netProcPollEnabled (the Network
|
||||
// drawer is open). Each run sleeps netProcInterval between its two ss
|
||||
// snapshots, so the bar pays nothing at rest.
|
||||
|
||||
property var _netFrame: [] // stdout lines accumulated for the current run
|
||||
|
||||
Process {
|
||||
id: netScan
|
||||
command: ["bash", Quickshell.shellPath("services/topnet.sh"),
|
||||
String(root.netProcInterval), String(root.netProcCount)]
|
||||
onStarted: root._netFrame = [];
|
||||
onExited: {
|
||||
root._parseNetProcFrame(root._netFrame.join("\n"));
|
||||
if (root.netProcPollEnabled) // re-arm for the next frame
|
||||
Qt.callLater(function () { netScan.running = true; });
|
||||
}
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => { if (line) root._netFrame.push(line); }
|
||||
}
|
||||
}
|
||||
|
||||
onNetProcPollEnabledChanged: {
|
||||
if (netProcPollEnabled) netScan.running = true;
|
||||
else root.topNetProcs = [];
|
||||
}
|
||||
|
||||
// Human-readable byte rate with kB as the smallest unit, e.g. "1.2M", "8K".
|
||||
function fmtRate(bytes) {
|
||||
const u = ["K", "M", "G"];
|
||||
|
||||
Reference in New Issue
Block a user