Files
quickshell_bar/widgets/Network.qml
2026-06-06 23:37:44 +02:00

240 lines
9.5 KiB
QML

import QtQuick
import QtQuick.Layouts
import Quickshell
import "../config"
import "../services"
// Down/up throughput on the active interface, with a wifi/ethernet icon.
// 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 === "" ? Icons.netOff
: SysStats.iface.startsWith("wl") ? Icons.wifi : Icons.ethernet
font.family: Theme.monoFont
font.pixelSize: Theme.fontSize + 1
color: SysStats.iface === "" ? Theme.overlay : 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
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: drawer.visible = !drawer.visible
}
// ---- top network-processes drawer -------------------------------------
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 === "" ? Icons.netOff
: SysStats.iface.startsWith("wl") ? Icons.wifi : Icons.ethernet
font.family: Theme.monoFont
font.pixelSize: Theme.fontSize + 1
color: SysStats.iface === "" ? Theme.overlay : 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
}
}
}
}
}
}
}
}
}
}