diff options
| -rw-r--r-- | desktop/island/perf/PerfPanel.qml | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/desktop/island/perf/PerfPanel.qml b/desktop/island/perf/PerfPanel.qml new file mode 100644 index 0000000..5d3f6ff --- /dev/null +++ b/desktop/island/perf/PerfPanel.qml @@ -0,0 +1,330 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import Quickshell +import QtQuick +import QtQuick.Layouts +import "../.." +import "Perf.js" as P + +// CPU, GPU, network and disks as cards in three columns: CPU down the left, +// GPU and network beside it, disks under those two. Numbers come from Perf; +// colours follow conky's bands, so they read as they did there. +GridLayout { + id: panel + + columns: 3 + columnSpacing: 8 + rowSpacing: 8 + + readonly property real col: (width - 2 * columnSpacing) / 3 + readonly property var gpuTemp: sensor("GPU") + readonly property var vramTemp: sensor("VRAM") + + function tone(v, warn, crit) { + const b = P.band(v, warn, crit); + return b === "crit" ? Theme.red : b === "warn" ? Theme.yellow : b === "ok" ? Theme.green : Theme.subtext; + } + function deg(v) { return v === null ? "--" : Math.round(v) + "°"; } + function sensor(label) { + return Perf.temps.find(t => t.label === label) || { value: null, warn: 0, crit: 0 }; + } + function pct(part, whole) { return part === null || !whole ? null : 100 * part / whole; } + function last(list) { return list.length ? list[list.length - 1] : null; } + // Conky's glyphs, re-checked in Inconsolata Nerd Font. + function glyph(d) { + if (d.host) return String.fromCodePoint(0xF06F3); // network share + return String.fromCodePoint({ "/": 0xF318, "/home": 0xF015 }[d.mount] ?? 0xF02CA); + } + + component Label: Text { + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.text + } + + // A label left, a value right, an optional glyph before the label. + component Pair: Item { + property string glyph: "" + property string label: "" + property string value: "" + property color labelColor: Theme.subtext + property color valueColor: Theme.text + width: parent ? parent.width : 0 + implicitHeight: valueText.implicitHeight + + Row { + spacing: 6 + Text { + visible: text !== "" + text: parent.parent.glyph + font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 2 } + color: parent.parent.labelColor + } + Label { text: parent.parent.label; color: parent.parent.labelColor } + } + Label { + id: valueText + anchors.right: parent.right + text: parent.value + color: parent.valueColor + } + } + + component Bar: Rectangle { + property real frac: 0 + property color fill: Theme.green + width: parent ? parent.width : 0 + height: 5 + radius: 3 + color: Theme.surfaceAlt + Rectangle { + width: parent.width * Math.max(0, Math.min(1, parent.frac)) + height: parent.height + radius: 3 + color: parent.fill + } + } + + component Card: Rectangle { + default property alias content: body.data + property string title: "" + property string headline: "" + property color headlineColor: Theme.text + radius: 10 + color: Qt.alpha(Theme.surface, 0.6) + implicitHeight: body.implicitHeight + 20 + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + + Column { + id: body + x: 10 + y: 10 + width: parent.width - 20 + spacing: 5 + + Item { + width: parent.width + height: Math.max(titleText.implicitHeight, headText.implicitHeight) + Text { + id: titleText + anchors.verticalCenter: parent.verticalCenter + text: parent.parent.parent.title + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4; bold: true } + color: Theme.subtext + } + Text { + id: headText + anchors.right: parent.right + text: parent.parent.parent.headline + font { family: Theme.fontFamily; pixelSize: Theme.fontSize + 4; bold: true } + color: parent.parent.parent.headlineColor + } + } + } + } + + // --- CPU ---------------------------------------------------------------- + + Card { + Layout.row: 0 + Layout.column: 0 + Layout.rowSpan: 2 + Layout.preferredWidth: panel.col + title: "CPU" + headline: Perf.load === null ? "--" : Math.round(Perf.load) + "%" + headlineColor: panel.tone(Perf.load, 25, 75) + + Label { + width: parent.width + text: Perf.cpuModel + color: Theme.subtext + elide: Text.ElideRight + } + + // One bar per core, rising from a common baseline. + Row { + id: eq + width: parent.width + height: 40 + spacing: 2 + Repeater { + model: Perf.cores + delegate: Item { + required property var modelData + width: (eq.width - eq.spacing * (Perf.cores.length - 1)) / Perf.cores.length + height: eq.height + Rectangle { + anchors.bottom: parent.bottom + width: parent.width + height: Math.max(1, parent.height * modelData / 100) + radius: 1 + color: panel.tone(modelData, 25, 75) + } + } + } + } + + Pair { + label: "RAM" + value: Perf.mem ? P.bytes(Perf.mem.used) + " / " + P.bytes(Perf.mem.total) : "--" + } + Bar { + frac: Perf.mem ? Perf.mem.used / Perf.mem.total : 0 + fill: panel.tone(panel.pct(Perf.mem ? Perf.mem.used : null, Perf.mem ? Perf.mem.total : 0), 25, 75) + } + + Repeater { + model: Perf.temps.filter(t => t.label !== "GPU" && t.label !== "VRAM") + delegate: Pair { + required property var modelData + label: modelData.label + value: panel.deg(modelData.value) + valueColor: panel.tone(modelData.value, modelData.warn, modelData.crit) + } + } + } + + // --- GPU ---------------------------------------------------------------- + + // No utilisation: xe exposes no busy percentage. + Card { + Layout.row: 0 + Layout.column: 1 + Layout.preferredWidth: panel.col + title: "GPU · ARC B580" + headline: panel.deg(panel.gpuTemp.value) + headlineColor: panel.tone(panel.gpuTemp.value, panel.gpuTemp.warn, panel.gpuTemp.crit) + + Pair { + label: "VRAM" + value: Perf.vram === null ? "--" : P.bytes(Perf.vram) + " / " + P.bytes(Perf.vramTotal) + } + Bar { + frac: (Perf.vram ?? 0) / Perf.vramTotal + fill: panel.tone(panel.pct(Perf.vram, Perf.vramTotal), 25, 75) + } + Pair { + label: "VRAM temp" + value: panel.deg(panel.vramTemp.value) + valueColor: panel.tone(panel.vramTemp.value, panel.vramTemp.warn, panel.vramTemp.crit) + } + Pair { + label: "Fan" + value: Perf.fan === null ? "--" : Perf.fan + " rpm" + } + } + + // --- network ------------------------------------------------------------ + + Card { + Layout.row: 0 + Layout.column: 2 + Layout.preferredWidth: panel.col + title: "NET · " + Perf.iface + + Canvas { + id: graph + width: parent.width + height: 44 + 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(); } + } + } + + Pair { + label: "↓ " + P.speed(panel.last(Perf.rx)) + labelColor: Theme.teal + value: "↑ " + P.speed(panel.last(Perf.tx)) + valueColor: Theme.peach + } + Pair { label: "LAN"; value: Perf.lan || "--" } + + // The public IP, red when it differs from the pinned one. The pin + // stores the address shown; it is lit while they are the same. + Item { + width: parent.width + height: pubText.implicitHeight + + Label { text: "Public"; color: Theme.subtext } + Label { + id: pubText + anchors { right: pin.left; rightMargin: 6 } + text: Perf.ip.ip || "--" + 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 - 2 } + 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) + } + } + } + } + + // --- disks -------------------------------------------------------------- + + Card { + Layout.row: 1 + Layout.column: 1 + Layout.columnSpan: 2 + Layout.preferredWidth: 2 * panel.col + panel.columnSpacing + title: "DISKS" + + Repeater { + model: Perf.disks + delegate: Column { + required property var modelData + width: parent.width + spacing: 2 + Pair { + glyph: panel.glyph(modelData) + label: modelData.host || modelData.mount + value: modelData.pct + "% · " + P.bytes(modelData.used) + " / " + P.bytes(modelData.size) + valueColor: panel.tone(modelData.pct, 25, 75) + } + Bar { + frac: modelData.pct / 100 + fill: panel.tone(modelData.pct, 25, 75) + } + } + } + } +} |
