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:
@@ -1,43 +1,237 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../config"
|
||||
import "../services"
|
||||
|
||||
// Down/up throughput on the active interface, with a wifi/ethernet icon.
|
||||
Pill {
|
||||
Text {
|
||||
text: SysStats.iface.startsWith("wl") ? Icons.wifi : Icons.ethernet
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: Theme.sky
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
// Clicking the pill toggles a drawer listing the top network-consuming
|
||||
// processes, refreshed live (SysStats only scans sockets while the drawer is
|
||||
// open). CpuGraph.qml is the reference for the drawer pattern.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: pill.implicitWidth
|
||||
implicitHeight: pill.implicitHeight
|
||||
|
||||
Pill {
|
||||
id: pill
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacing
|
||||
|
||||
Text {
|
||||
text: SysStats.iface.startsWith("wl") ? Icons.wifi : Icons.ethernet
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: Theme.sky
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// reserve the width of the widest rate string so values never shift
|
||||
TextMetrics {
|
||||
id: rateMetrics
|
||||
font: rxText.font
|
||||
text: Icons.down + " 99.9M"
|
||||
}
|
||||
|
||||
Text {
|
||||
id: rxText
|
||||
text: Icons.down + " " + SysStats.fmtRate(SysStats.rxRate)
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize - 1
|
||||
color: Theme.green
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: rateMetrics.advanceWidth
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Icons.up + " " + SysStats.fmtRate(SysStats.txRate)
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize - 1
|
||||
color: Theme.peach
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: rateMetrics.advanceWidth
|
||||
}
|
||||
}
|
||||
|
||||
// reserve the width of the widest rate string so values never shift
|
||||
TextMetrics {
|
||||
id: rateMetrics
|
||||
font: rxText.font
|
||||
text: Icons.down + " 99.9M"
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: drawer.visible = !drawer.visible
|
||||
}
|
||||
|
||||
Text {
|
||||
id: rxText
|
||||
text: Icons.down + " " + SysStats.fmtRate(SysStats.rxRate)
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize - 1
|
||||
color: Theme.green
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: rateMetrics.advanceWidth
|
||||
}
|
||||
// ---- top network-processes drawer -------------------------------------
|
||||
|
||||
Text {
|
||||
text: Icons.up + " " + SysStats.fmtRate(SysStats.txRate)
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize - 1
|
||||
color: Theme.peach
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: rateMetrics.advanceWidth
|
||||
PopupWindow {
|
||||
id: drawer
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
implicitWidth: panel.implicitWidth
|
||||
implicitHeight: panel.implicitHeight
|
||||
|
||||
// Hang below the pill, top-right corner aligned to the pill's
|
||||
// bottom-right (same anchoring rationale as CpuGraph's drawer): anchor
|
||||
// to the item so the rect is in the pill's own coordinates and tracks
|
||||
// it wherever the RowLayout places it.
|
||||
anchor {
|
||||
item: root
|
||||
rect.x: 0
|
||||
rect.y: root.height + Theme.gap
|
||||
rect.width: root.width
|
||||
rect.height: 1
|
||||
edges: Edges.Bottom | Edges.Right
|
||||
gravity: Edges.Bottom | Edges.Left
|
||||
}
|
||||
|
||||
// Only pay for the socket scan while the drawer is actually showing.
|
||||
onVisibleChanged: SysStats.netProcPollEnabled = visible
|
||||
|
||||
Rectangle {
|
||||
id: panel
|
||||
readonly property int rowH: 22
|
||||
implicitWidth: 340
|
||||
// Fixed height (header + netProcCount rows): the panel must NOT
|
||||
// resize when rows replace "sampling…", or the popup's layer surface
|
||||
// shows a stale/blank buffer under Sway on first open (same class of
|
||||
// bug as Bar.qml's geomKey remap). A constant size sidesteps it.
|
||||
implicitHeight: col.implicitHeight + Theme.padding * 2
|
||||
radius: Theme.radius
|
||||
color: Theme.barColor
|
||||
border.color: Theme.surface1
|
||||
border.width: 1
|
||||
|
||||
// Largest combined rate in the current frame, used to scale the
|
||||
// per-row background bar (network has no fixed ceiling like CPU%).
|
||||
readonly property real maxRate: {
|
||||
let m = 1;
|
||||
const ps = SysStats.topNetProcs;
|
||||
for (let i = 0; i < ps.length; i++)
|
||||
m = Math.max(m, ps[i].rx + ps[i].tx);
|
||||
return m;
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.padding
|
||||
spacing: 3
|
||||
|
||||
// header: icon + title + total throughput
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing
|
||||
|
||||
Text {
|
||||
text: SysStats.iface.startsWith("wl") ? Icons.wifi : Icons.ethernet
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: Theme.sky
|
||||
}
|
||||
Text {
|
||||
text: "Network by process"
|
||||
font.family: Theme.font
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: Icons.down + SysStats.fmtRate(SysStats.rxRate)
|
||||
+ " " + Icons.up + SysStats.fmtRate(SysStats.txRate)
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize - 3
|
||||
color: Theme.subtext
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // divider
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 2
|
||||
Layout.bottomMargin: 2
|
||||
implicitHeight: 1
|
||||
color: Theme.surface1
|
||||
}
|
||||
|
||||
// Fixed-height body so the panel stays one constant size whether
|
||||
// it shows the placeholder or the rows (see panel.implicitHeight).
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 2
|
||||
implicitHeight: SysStats.netProcCount * panel.rowH
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: SysStats.topNetProcs.length === 0
|
||||
text: "sampling…"
|
||||
font.family: Theme.font
|
||||
font.pixelSize: Theme.fontSize - 2
|
||||
color: Theme.subtext
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
|
||||
Repeater {
|
||||
model: SysStats.topNetProcs
|
||||
|
||||
delegate: Item {
|
||||
id: rowItem
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
height: panel.rowH
|
||||
|
||||
// throughput bar behind the text, scaled to the
|
||||
// busiest process in the frame.
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
height: parent.height - 4
|
||||
radius: 3
|
||||
width: parent.width *
|
||||
Math.min(1, (rowItem.modelData.rx + rowItem.modelData.tx) / panel.maxRate)
|
||||
color: Qt.rgba(Theme.sky.r, Theme.sky.g, Theme.sky.b, 0.16)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 6
|
||||
anchors.rightMargin: 6
|
||||
spacing: Theme.spacing
|
||||
|
||||
Text {
|
||||
text: rowItem.modelData.name
|
||||
font.family: Theme.font
|
||||
font.pixelSize: Theme.fontSize - 1
|
||||
color: Theme.text
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: Icons.down + " " + SysStats.fmtRate(rowItem.modelData.rx)
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize - 2
|
||||
color: Theme.green
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.preferredWidth: 62
|
||||
}
|
||||
Text {
|
||||
text: Icons.up + " " + SysStats.fmtRate(rowItem.modelData.tx)
|
||||
font.family: Theme.monoFont
|
||||
font.pixelSize: Theme.fontSize - 2
|
||||
color: Theme.peach
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.preferredWidth: 62
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user