diff --git a/modules/Bar.qml b/modules/Bar.qml index 0c4d66c..f200738 100644 --- a/modules/Bar.qml +++ b/modules/Bar.qml @@ -200,6 +200,7 @@ PanelWindow { M.BarGroup { M.Cpu { visible: M.Modules.cpu.enable + bar: bar } M.Memory { visible: M.Modules.memory.enable diff --git a/modules/Cpu.qml b/modules/Cpu.qml index fdb4c88..7cad148 100644 --- a/modules/Cpu.qml +++ b/modules/Cpu.qml @@ -10,15 +10,20 @@ M.BarSection { property int usage: 0 property real freqGhz: 0 property var _prev: null + property var _corePrev: [] + property var _coreUsage: [] + property var _coreFreq: [] FileView { id: stat path: "/proc/stat" onLoaded: { - const line = text().split("\n")[0]; - const f = line.trim().split(/\s+/).slice(1).map(Number); - const idle = f[3] + f[4]; - const total = f.reduce((a, b) => a + b, 0); + 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; @@ -29,8 +34,32 @@ M.BarSection { 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; } } + FileView { id: cpuinfo path: "/proc/cpuinfo" @@ -38,10 +67,12 @@ M.BarSection { const lines = text().split("\n").filter(l => l.startsWith("cpu MHz")); if (lines.length === 0) return; - const sum = lines.reduce((a, l) => a + parseFloat(l.split(":")[1]), 0); - root.freqGhz = sum / lines.length / 1000; + const freqs = lines.map(l => parseFloat(l.split(":")[1]) / 1000); + root.freqGhz = freqs.reduce((a, b) => a + b, 0) / freqs.length; + root._coreFreq = freqs; } } + Timer { interval: M.Modules.cpu.interval || 1000 running: true @@ -52,13 +83,149 @@ M.BarSection { } } + required property var bar + + 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 + } + 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: 220 + + // 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 + height: 20 + + readonly property int _u: root._coreUsage[index] ?? 0 + readonly property real _f: root._coreFreq[index] ?? 0 + + 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: 14 + } + + Item { + id: coreBar + anchors.left: coreLabel.right + anchors.leftMargin: 6 + anchors.right: freqLabel.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._u / 100) + height: parent.height + color: root.accentColor + radius: 2 + Behavior on width { + NumberAnimation { + duration: 150 + } + } + } + } + + Text { + id: freqLabel + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent._f.toFixed(2) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 32 + horizontalAlignment: Text.AlignRight + } + } + } + + // Bottom padding + Item { + width: 1 + height: 4 + } } }