From 782c34578b6ea48ee27bff25e9c7b7d0efab5333 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 13 Oct 2026 11:24:10 +0200 Subject: feat(perf): the performance panel's card grid CPU with per-core bars, RAM and temperatures down the left; GPU and network beside it; disks under those two. Colours follow conky's bands so the numbers read as they did there. The pin beside the public IP stores the address shown, and the address turns red when a fresh one differs from the pin. Co-Authored-By: Claude Haiku 5.5 --- desktop/island/perf/PerfPanel.qml | 330 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 desktop/island/perf/PerfPanel.qml (limited to 'desktop') 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. +// +// 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) + } + } + } + } +} -- cgit v1.2.3