diff --git a/dots/.config/quickshell/ii/modules/common/widgets/AbstractCombinedProgressBar.qml b/dots/.config/quickshell/ii/modules/common/widgets/AbstractCombinedProgressBar.qml new file mode 100644 index 000000000..9d6e0b59b --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/AbstractCombinedProgressBar.qml @@ -0,0 +1,62 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls + +Control { + id: root + + property list valueWeights: [1] + property list values: [0.5] + property list valueHighlights: ["white"] + property list valueTroughs: [] + + readonly property list normalizedValueWeights: { + const totalWeight = valueWeights.reduce((sum, weight) => sum + weight, 0) + return valueWeights.map(weight => weight / totalWeight) + } + + readonly property list visualEnds: { + let cumsum = 0; + let positions = []; + for (let i = 0; i < normalizedValueWeights.length; i++) { + cumsum += normalizedValueWeights[i]; + positions.push(cumsum); + } + return positions; + } + + readonly property list visualPositions: { + let positions = []; + let lastEnd = 0; + for(let i = 0; i < visualEnds.length; i++) { + const thisEnd = visualEnds[i]; + const width = thisEnd - lastEnd; + const thisPos = lastEnd + width * values[i]; + positions.push(thisPos); + lastEnd = visualEnds[i]; + } + return positions; + } + + readonly property list visualSegments: { + let segs = []; + let lastEnd = 0; + for(let i = 0; i < visualEnds.length; i++) { + const thisEnd = visualEnds[i]; + const thisPos = visualPositions[i]; + segs.push([lastEnd, thisPos]); + segs.push([thisPos, thisEnd]); + lastEnd = visualEnds[i]; + } + return segs; + } + + readonly property list segmentColors: { + var cols = []; + for(let i = 0; i < valueHighlights.length; i++) { + cols.push(valueHighlights[i]); + cols.push(valueTroughs[i]); + } + return cols; + } +} diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledCombinedProgressBar.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledCombinedProgressBar.qml new file mode 100644 index 000000000..15478a145 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledCombinedProgressBar.qml @@ -0,0 +1,72 @@ +pragma ComponentBehavior: Bound +import QtQuick +import qs.modules.common + +AbstractCombinedProgressBar { + id: root + + property real valueBarWidth: 120 + property real valueBarHeight: 4 + property real valueBarGap: 4 + property real valueBarInnerRadius: Appearance.rounding.unsharpen + valueHighlights: [Appearance.colors.colPrimary, Appearance.colors.colTertiary] + valueTroughs: [Appearance.colors.colSecondaryContainer, Appearance.colors.colTertiaryContainer] + + background: Item { + implicitWidth: root.valueBarWidth + implicitHeight: root.valueBarHeight + } + + // "negligible" = too small that it'd look weird when shown + function isNegligibleSegment(seg: var): bool { + const wdth = seg[1] - seg[0]; + const visualWidth = availableWidth * wdth; + return (visualWidth <= valueBarGap + valueBarHeight) + } + + contentItem: Item { + Repeater { + model: root.visualSegments + + delegate: Rectangle { + required property int index + required property var modelData + + visible: !root.isNegligibleSegment(modelData) + anchors { + top: parent.top + bottom: parent.bottom + } + property bool atStart: index == 0 + property bool atEnd: index == root.visualSegments.length - 1 + property real displaySegStart: { // swallow previous segments if they're "negligible" + var i = index; + while ((i > 0 && root.isNegligibleSegment(root.visualSegments[i-1]))) + i--; + return root.visualSegments[i][0] + } + + x: { + var result = root.availableWidth * displaySegStart; + if (!atStart) result += root.valueBarGap / 2; + return result; + } + width: { + var result = root.availableWidth * (modelData[1] - displaySegStart) + if (atStart || atEnd) result -= root.valueBarGap / 2; + else result -= root.valueBarGap; + return result; + } + color: root.segmentColors[index % root.segmentColors.length] + + property real startRadius: atStart ? height / 2 : root.valueBarInnerRadius + property real endRadius: atEnd ? height / 2 : root.valueBarInnerRadius + + topLeftRadius: startRadius + bottomLeftRadius: startRadius + topRightRadius: endRadius + bottomRightRadius: endRadius + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/hefty/topLayer/bar/widgets/HResources.qml b/dots/.config/quickshell/ii/modules/hefty/topLayer/bar/widgets/HResources.qml index 6d4f0e8a3..676aa6421 100644 --- a/dots/.config/quickshell/ii/modules/hefty/topLayer/bar/widgets/HResources.qml +++ b/dots/.config/quickshell/ii/modules/hefty/topLayer/bar/widgets/HResources.qml @@ -162,6 +162,102 @@ HBarWidgetWithPopout { W.FlyFadeEnterChoreographable { Layout.fillWidth: true + Column { + anchors { + left: parent.left + right: parent.right + } + + spacing: 2 + + Item { + anchors { + left: parent.left + right: parent.right + } + implicitHeight: memUsed.implicitHeight + + BigSmallTextPair { + id: memUsed + materialSymbol: "memory" + bigText: S.ResourceUsage.kbToGbString(S.ResourceUsage.memoryUsed, false) + smallText: { + const total = S.ResourceUsage.kbToGbString(S.ResourceUsage.memoryTotal, false); + return S.Translation.tr("%1").arg(`/ ${total}`) + } + W.StyledText { + Layout.alignment: Qt.AlignBaseline + text: S.Translation.tr("Memory") + color: C.Appearance.colors.colOutline + } + } + BigSmallTextPair { + id: swapUsed + TextMetrics { + id: plusTextMetric + font: swapUsed.bigFont + text: "+" + } + property real halfWidthOfAPlus: plusTextMetric.width / 2 + x: Math.min(memProg.availableWidth * memProg.visualEnds[0] - halfWidthOfAPlus, parent.width - width) + bigText: "+ " + S.ResourceUsage.kbToGbString(S.ResourceUsage.swapUsed, false) + smallText: { + const total = S.ResourceUsage.kbToGbString(S.ResourceUsage.swapTotal, false); + return `/ ${total} GB` + } + } + + } + W.StyledCombinedProgressBar { + id: memProg + anchors { + left: parent.left + right: parent.right + } + valueWeights: [S.ResourceUsage.memoryTotal, S.ResourceUsage.swapTotal] + values: [S.ResourceUsage.memoryUsedPercentage, S.ResourceUsage.swapUsedPercentage] + } + } + } + + W.FlyFadeEnterChoreographable { + Layout.fillWidth: true + + Column { + anchors { + left: parent.left + right: parent.right + } + + spacing: 2 + + BigSmallTextPair { + spacing: 0 + materialSymbol: "developer_board" + bigText: Math.round(S.ResourceUsage.cpuUsage * 100) + smallText: "%" + W.StyledText { + Layout.alignment: Qt.AlignBaseline + text: " " + S.Translation.tr("CPU") + color: C.Appearance.colors.colOutline + } + } + W.StyledCombinedProgressBar { + anchors { + left: parent.left + right: parent.right + } + property bool useSingleAggregate: S.ResourceUsage.cpuCoreUsages.length > 8 + valueWeights: useSingleAggregate ? [1] : S.ResourceUsage.cpuCoreFreqCaps + values: useSingleAggregate ? [S.ResourceUsage.cpuUsage] : S.ResourceUsage.cpuCoreUsages + } + } + } + + W.FlyFadeEnterChoreographable { + Layout.topMargin: 8 + Layout.fillWidth: true + RowLayout { spacing: 10 width: parent.width @@ -258,6 +354,36 @@ HBarWidgetWithPopout { } } + component BigSmallTextPair: RowLayout { + id: txtPair + property string materialSymbol: "" + property string bigText: "" + property string smallText: "" + property alias bigFont: bigTxt.font + property alias smallFont: smallTxt.font + spacing: 6 + + W.MaterialSymbol { + Layout.rightMargin: 6 - spacing + visible: text.length > 0 + Layout.alignment: Qt.AlignVCenter + text: txtPair.materialSymbol + fill: 1 + iconSize: 24 + } + W.StyledText { + id: bigTxt + Layout.alignment: Qt.AlignBaseline + font.pixelSize: C.Appearance.font.pixelSize.title + text: txtPair.bigText + } + W.StyledText { + id: smallTxt + Layout.alignment: Qt.AlignBaseline + text: txtPair.smallText + } + } + component StatWithIcon: Item { id: statItem required property string icon diff --git a/dots/.config/quickshell/ii/services/ResourceUsage.qml b/dots/.config/quickshell/ii/services/ResourceUsage.qml index c513b90ca..8c5570450 100644 --- a/dots/.config/quickshell/ii/services/ResourceUsage.qml +++ b/dots/.config/quickshell/ii/services/ResourceUsage.qml @@ -20,6 +20,8 @@ Singleton { property real swapUsed: swapTotal - swapFree property real swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0 property real cpuUsage: 0 + property list cpuCoreUsages: [] + property list cpuCoreFreqCaps: [] property var previousCpuStats property string maxAvailableMemoryString: kbToGbString(ResourceUsage.memoryTotal) @@ -31,10 +33,13 @@ Singleton { property list memoryUsageHistory: [] property list swapUsageHistory: [] - function kbToGbString(kb) { - return (kb / (1024 * 1024)).toFixed(1) + " GB"; + function kbToGbString(kb, attachUnit = true) { + return (kb / (1024 * 1024)).toFixed(1) + (attachUnit ? " GB" : ""); } + // onCpuCoreUsagesChanged: print(cpuCoreUsages) + // onCpuCoreFreqCapsChanged: print(cpuCoreFreqCaps) + function updateMemoryUsageHistory() { memoryUsageHistory = [...memoryUsageHistory, memoryUsedPercentage] if (memoryUsageHistory.length > historyLength) { @@ -77,20 +82,36 @@ Singleton { // Parse CPU usage const textStat = fileStat.text() - const cpuLine = textStat.match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/) - if (cpuLine) { - const stats = cpuLine.slice(1).map(Number) - const total = stats.reduce((a, b) => a + b, 0) - const idle = stats[3] + const lines = textStat.split("\n") + const currentStats = {} + const coreUsages = [] - if (previousCpuStats) { - const totalDiff = total - previousCpuStats.total - const idleDiff = idle - previousCpuStats.idle - cpuUsage = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0 + for (const line of lines) { + const match = line.match(/^(cpu\d*)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/) + if (match) { + const name = match[1] + const stats = match.slice(2).map(Number) + const total = stats.reduce((a, b) => a + b, 0) + const idle = stats[3] + + let usage = 0 + if (previousCpuStats && previousCpuStats[name]) { + const totalDiff = total - previousCpuStats[name].total + const idleDiff = idle - previousCpuStats[name].idle + usage = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0 + } + + currentStats[name] = { total, idle } + + if (name === "cpu") { + cpuUsage = usage + } else { + coreUsages.push(usage) + } } - - previousCpuStats = { total, idle } } + previousCpuStats = currentStats + cpuCoreUsages = coreUsages root.updateHistories() interval = Config.options?.resources?.updateInterval ?? 3000 @@ -106,12 +127,19 @@ Singleton { LANG: "C", LC_ALL: "C" }) - command: ["bash", "-c", "lscpu | grep 'CPU max MHz' | awk '{print $4}'"] + command: ["bash", "-c", "cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq 2>/dev/null || lscpu | grep 'CPU max MHz' | awk '{print $4 * 1000}'"] running: true stdout: StdioCollector { id: outputCollector onStreamFinished: { - root.maxAvailableCpuString = (parseFloat(outputCollector.text) / 1000).toFixed(0) + " GHz" + const lines = outputCollector.text.trim().split("\n") + const caps = lines.map(line => parseFloat(line)).filter(val => !isNaN(val)) + + if (caps.length > 0) { + root.cpuCoreFreqCaps = caps + const maxFreq = Math.max(...caps) + root.maxAvailableCpuString = (maxFreq / 1000000).toFixed(1) + " GHz" + } } } }