aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/island/perf
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/island/perf')
-rw-r--r--desktop/island/perf/PerfPanel.qml330
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)
+ }
+ }
+ }
+ }
+}