diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-13 11:24:10 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-13 11:24:10 +0200 |
| commit | 782c34578b6ea48ee27bff25e9c7b7d0efab5333 (patch) | |
| tree | 3be8539e4e645f9180588bd9e9e76e7686d07cf7 /desktop/island | |
| parent | 05c204284c135ae5ec6e00bfa997e188e6b6fbfb (diff) | |
| download | quickshell-782c34578b6ea48ee27bff25e9c7b7d0efab5333.tar.gz quickshell-782c34578b6ea48ee27bff25e9c7b7d0efab5333.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'desktop/island')
| -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) + } + } + } + } +} |
