// 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 Quickshell.Io import QtQuick import "../.." import "Perf.js" as P // CPU, temperatures, GPU, network, disks and ~/.cache as cards, one per row: // the drawer page scrolls, so nothing has to share a line. Numbers come from // Perf; colours follow conky's bands, so they read as they did there. Column { id: panel spacing: 12 readonly property int fs: Theme.fontSize - 2 readonly property var gpuTemp: sensor("GPU") 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: panel.fs } 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: panel.fs + 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: 6 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 width: panel.width radius: 10 color: Qt.alpha(Theme.surface, 0.6) implicitHeight: body.implicitHeight + 28 Column { id: body x: 14 y: 14 width: parent.width - 28 spacing: 8 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: panel.fs; 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 { 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: 64 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) } } // --- temperatures ------------------------------------------------------- Card { title: "TEMPERATURES" Flow { id: temps width: parent.width spacing: 8 Repeater { model: Perf.temps delegate: Rectangle { required property var modelData width: (temps.width - 3 * temps.spacing) / 4 height: tempCol.implicitHeight + 16 radius: 8 color: Qt.alpha(Theme.surfaceAlt, 0.5) Column { id: tempCol anchors.centerIn: parent spacing: 2 Label { anchors.horizontalCenter: parent.horizontalCenter text: modelData.label color: Theme.subtext } Text { anchors.horizontalCenter: parent.horizontalCenter text: panel.deg(modelData.value) font { family: Theme.fontFamily; pixelSize: panel.fs + 6; bold: true } color: panel.tone(modelData.value, modelData.warn, modelData.crit) } } } } } } // --- GPU ---------------------------------------------------------------- // No utilisation: xe exposes no busy percentage. Card { title: "GPU · " + Perf.gpuName 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: "Fan" value: Perf.fan === null ? "--" : Perf.fan + " rpm" } } // --- network ------------------------------------------------------------ Card { title: "NET · " + Perf.iface Canvas { id: graph width: parent.width height: 80 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: panel.fs + 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 { title: "DISKS" Repeater { model: Perf.disks delegate: Column { required property var modelData width: parent.width spacing: 4 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) } } } } // --- cache -------------------------------------------------------------- // ~/.cache and its five largest children, measured once per page open: // du over the tree took 28ms, so conky's 15-minute sampler file is not // needed. The largest is coloured by its share of the total, as conky // did, so the eye lands on the one worth deleting. property var cache: null // {total, items: [{name, bytes}]} Process { running: true command: ["du", "-B1", "-d1", (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache")] stdout: StdioCollector { onStreamFinished: panel.cache = P.du(text) } } Card { title: "CACHE" headline: panel.cache ? P.bytes(panel.cache.total) : "--" Repeater { model: panel.cache ? panel.cache.items.slice(0, 5) : [] delegate: Column { required property var modelData required property int index readonly property real share: modelData.bytes / panel.cache.total width: parent.width spacing: 4 Pair { label: modelData.name value: P.bytes(modelData.bytes) valueColor: index === 0 ? (share > 0.5 ? Theme.red : Theme.yellow) : Theme.text } Bar { frac: share fill: index === 0 ? (share > 0.5 ? Theme.red : Theme.yellow) : Theme.overlay } } } } }