import QtQuick import Quickshell.Io import "." as M M.BarSection { id: root spacing: Math.max(1, M.Theme.moduleSpacing - 2) tooltip: "" property int usage: 0 Behavior on usage { NumberAnimation { duration: 400 easing.type: Easing.OutCubic } } property real freqGhz: 0 Behavior on freqGhz { NumberAnimation { duration: 400 easing.type: Easing.OutCubic } } property var _prev: null property var _corePrev: [] property var _coreUsage: [] property var _coreFreq: [] property var _coreHistory: [] // array of arrays, last 16 samples per core property var _coreMaxFreq: [] // max freq in GHz per core, from cpufreq property var _coreTypes: [] // "Performance" or "Efficiency" per core FileView { id: stat path: "/proc/stat" onLoaded: { const lines = text().split("\n"); // Aggregate const agg = lines[0].trim().split(/\s+/).slice(1).map(Number); const idle = agg[3] + agg[4]; const total = agg.reduce((a, b) => a + b, 0); if (root._prev) { const dIdle = idle - root._prev.idle; const dTotal = total - root._prev.total; if (dTotal > 0) root.usage = Math.round((1 - dIdle / dTotal) * 100); } root._prev = { idle, total }; // Per-core const coreLines = lines.filter(l => /^cpu\d+\s/.test(l)); const newUsage = []; const newPrev = root._corePrev.length === coreLines.length ? root._corePrev : Array(coreLines.length).fill(null); for (let i = 0; i < coreLines.length; i++) { const f = coreLines[i].trim().split(/\s+/).slice(1).map(Number); const ci = f[3] + f[4]; const ct = f.reduce((a, b) => a + b, 0); if (newPrev[i]) { const di = ci - newPrev[i].idle; const dt = ct - newPrev[i].total; newUsage.push(dt > 0 ? Math.round((1 - di / dt) * 100) : 0); } else { newUsage.push(0); } newPrev[i] = { idle: ci, total: ct }; } root._coreUsage = newUsage; root._corePrev = newPrev; // Update sparkline history const histLen = 16; const oldH = root._coreHistory; const newH = []; for (let i = 0; i < newUsage.length; i++) { const prev = i < oldH.length ? oldH[i] : []; const next = prev.concat([newUsage[i]]); newH.push(next.length > histLen ? next.slice(next.length - histLen) : next); } root._coreHistory = newH; } } FileView { id: cpuinfo path: "/proc/cpuinfo" onLoaded: { const lines = text().split("\n").filter(l => l.startsWith("cpu MHz")); if (lines.length === 0) return; const freqs = lines.map(l => parseFloat(l.split(":")[1]) / 1000); root.freqGhz = freqs.reduce((a, b) => a + b, 0) / freqs.length; root._coreFreq = freqs; } } // Read per-core max freq once at init Process { running: true command: ["sh", "-c", "for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do [ -f \"$f\" ] && cat \"$f\" || echo 0; done 2>/dev/null"] stdout: StdioCollector { onStreamFinished: { root._coreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6); } } } // Read P/E-core topology once at init Process { running: true command: ["sh", "-c", "for d in /sys/devices/system/cpu/cpu[0-9]*/topology/core_type; do [ -f \"$d\" ] && cat \"$d\" || echo Performance; done 2>/dev/null"] stdout: StdioCollector { onStreamFinished: { root._coreTypes = text.trim().split("\n").filter(l => l).map(l => l.trim()); } } } Timer { interval: M.Modules.cpu.interval || 1000 running: true repeat: true onTriggered: { stat.reload(); cpuinfo.reload(); } } property bool _pinned: false readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered readonly property bool _showPanel: _anyHover || _pinned on_AnyHoverChanged: { if (_anyHover) _unpinTimer.stop(); else if (_pinned) _unpinTimer.start(); } Timer { id: _unpinTimer interval: 500 onTriggered: root._pinned = false } function _loadColor(pct) { const t = Math.max(0, Math.min(100, pct)) / 100; const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; const b = t < 0.5 ? M.Theme.base0A : M.Theme.base08; const u = t < 0.5 ? t * 2 : (t - 0.5) * 2; return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1); } M.BarIcon { icon: "\uF2DB" anchors.verticalCenter: parent.verticalCenter TapHandler { cursorShape: Qt.PointingHandCursor onTapped: root._pinned = !root._pinned } } M.BarLabel { label: root.usage.toString().padStart(2) + "%@" + root.freqGhz.toFixed(2) minText: "99%@9.99" anchors.verticalCenter: parent.verticalCenter TapHandler { cursorShape: Qt.PointingHandCursor onTapped: root._pinned = !root._pinned } } M.HoverPanel { id: hoverPanel showPanel: root._showPanel screen: QsWindow.window?.screen ?? null anchorItem: root accentColor: root.accentColor panelNamespace: "nova-cpu" contentWidth: 260 // Header Item { width: parent.width height: 28 Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: "CPU" color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.fontFamily } Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: root.usage + "% " + root.freqGhz.toFixed(2) + " GHz" color: root.accentColor font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily font.bold: true } } // Per-core rows Repeater { model: root._coreUsage.length delegate: Item { required property int index width: hoverPanel.contentWidth readonly property int _u: root._coreUsage[index] ?? 0 readonly property real _f: root._coreFreq[index] ?? 0 readonly property color _barColor: root._loadColor(_u) readonly property bool _throttled: { const maxF = root._coreMaxFreq[index] ?? 0; return maxF > 0 && _f < maxF * 0.85 && _u >= 60; } readonly property bool _isFirstECore: { const types = root._coreTypes; if (!types.length || index >= types.length) return false; if (types[index] !== "Efficiency") return false; return index === 0 || types[index - 1] !== "Efficiency"; } height: _isFirstECore ? 28 : 20 // P/E-core divider Rectangle { visible: parent._isFirstECore anchors.top: parent.top anchors.topMargin: 3 anchors.horizontalCenter: parent.horizontalCenter width: parent.width - 16 height: 1 color: M.Theme.base03 } // Row content pinned to bottom of delegate Item { anchors.bottom: parent.bottom width: parent.width height: 20 Text { id: coreLabel anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: index color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily width: 16 } Item { id: coreBar anchors.left: coreLabel.right anchors.leftMargin: 6 anchors.right: sparkline.left anchors.rightMargin: 6 anchors.verticalCenter: parent.verticalCenter height: 4 Rectangle { anchors.fill: parent color: M.Theme.base02 radius: 2 } Rectangle { width: parent.width * (parent.parent.parent._u / 100) height: parent.height color: parent.parent.parent._barColor radius: 2 Behavior on width { NumberAnimation { duration: 150 } } } } // Sparkline Canvas { id: sparkline anchors.right: freqLabel.left anchors.rightMargin: 6 anchors.verticalCenter: parent.verticalCenter width: 32 height: 10 property var _hist: root._coreHistory[parent.parent.index] || [] property color _col: parent.parent._barColor on_HistChanged: requestPaint() on_ColChanged: requestPaint() onPaint: { const ctx = getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, width, height); const d = _hist; if (!d.length) return; const bw = width / d.length; ctx.fillStyle = _col.toString(); for (let i = 0; i < d.length; i++) { const h = Math.max(1, height * d[i] / 100); ctx.fillRect(i * bw, height - h, Math.max(1, bw - 0.5), h); } } } Text { id: freqLabel anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: parent.parent._f.toFixed(2) color: parent.parent._throttled ? M.Theme.base08 : M.Theme.base04 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily width: 34 horizontalAlignment: Text.AlignRight } } } } // Process list separator Rectangle { width: parent.width - 16 height: 1 anchors.horizontalCenter: parent.horizontalCenter color: M.Theme.base03 } // Top processes by CPU Repeater { model: M.ProcessList.byCpu delegate: Item { required property var modelData width: hoverPanel.contentWidth height: 20 Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: modelData.cmd color: M.Theme.base05 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily elide: Text.ElideRight width: parent.width - 80 } Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: modelData.cpu.toFixed(1) + "%" color: root._loadColor(modelData.cpu) font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily width: 36 horizontalAlignment: Text.AlignRight } } } Item { width: 1 height: 4 } } }