import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.modules.common
import qs.modules.common.utils
import qs.modules.common.widgets
import qs.modules.icons
import qs.services
Item {
id: root
property real scale: Config.data.cpu.scale || 1.0
property real history: Config.data.cpu.graph.history || 60 // Seconds
property real updateInterval: Config.data.cpu.updateInterval || 1000 // Milliseconds
property color lineColor: Config.data.cpu.graph.lineColor
property color lowUsageColor: Config.data.cpu.graph.lowUsageColor
property color highUsageColor: Config.data.cpu.graph.highUsageColor
readonly property color mixedUsageColor: ColorUtils.mix(lowUsageColor, highUsageColor)
implicitWidth: (icon.visible ? icon.width : 0)
+ (graph.visible ? graph.width : 0) + 4
implicitHeight: Config.data.bar.size - Config.data.bar.size * 0.2
// Buffer for graph points (rolling window)
property var points: []
// Initial dummy point to avoid empty graph
Component.onCompleted: {
if (!graph.visible) return;
points.push({ x: Date.now(), y: 0.0 });
graph.requestPaint();
}
Timer {
interval: updateInterval
running: Config.data.cpu.graph.enabled
repeat: true
onTriggered: updateGraph()
}
function updateGraph() {
// Add new point
let x = Date.now();
let y = CPU.overallUsage;
points.push({ x: x, y: y });
// Clean old points beyond history
let cutoff = Date.now() - history * 1000;
while (points.length > 0 && points[0].x < cutoff) {
points.splice(0, 1);
}
graph.requestPaint();
}
CPUIcon {
id: icon
visible: Config.data.cpu.icon.enabled
color: Config.data.cpu.icon.color
scale: Config.data.cpu.icon.scale * root.height
anchors.verticalCenter: parent.verticalCenter
}
Canvas {
id: graph
visible: Config.data.cpu.graph.enabled
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
}
width: 100 * root.scale
onPaint: {
let ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
let range = history * 1000;
let currentTime = Date.now();
let minTime = currentTime - range;
let visiblePoints = points.filter(function(p) { return p.x >= minTime; });
if (visiblePoints.length < 2) return;
// Margin to avoid graph cutoff at top/bottom
let margin = 2;
let effectiveHeight = height - 2 * margin;
ctx.strokeStyle = lineColor;
ctx.lineWidth = 2;
let gradient = ctx.createLinearGradient(0, height, 0, 0); // Vertical from bottom to top
gradient.addColorStop(0, lowUsageColor);
gradient.addColorStop(0.5, mixedUsageColor);
gradient.addColorStop(0.9, highUsageColor);
ctx.fillStyle = gradient;
// Draw fill under the line first (closed path)
ctx.beginPath();
ctx.moveTo(0, height+10); // Start bottom-left
for (let i = 0; i < visiblePoints.length; ++i) {
let px = ((visiblePoints[i].x - minTime) / range) * width;
let py = margin + (1 - visiblePoints[i].y) * effectiveHeight;
if (i === 0) ctx.lineTo(px, py);
ctx.lineTo(px, py);
}
ctx.lineTo(width, height); // Bottom-right
ctx.closePath();
ctx.fill();
// Draw stroke on top of the line (separate path)
ctx.beginPath();
for (let i = 0; i < visiblePoints.length; ++i) {
let px = ((visiblePoints[i].x - minTime) / range) * width;
let py = margin + (1 - visiblePoints[i].y) * effectiveHeight;
if (i === 0) ctx.moveTo(px, py);
ctx.lineTo(px, py);
}
ctx.stroke();
}
}
HoverPopup {
anchors.centerIn: root
hoverTarget: root
anchorPosition: Types.stringToPosition(Config.data.bar.position)
contentComponent: Component {
ColumnLayout {
id: contentColumn
spacing: 2
Text {
Layout.alignment: Qt.AlignHCenter
text: "Load Average"
font.family: Config.data.theme.font.family
font.pixelSize: Config.data.theme.font.size
font.bold: true
color: Config.data.theme.colors.foreground
}
Text {
Layout.alignment: Qt.AlignHCenter
font.family: Config.data.theme.fontMono.family
font.pixelSize: Config.data.theme.fontMono.size
color: Config.data.theme.colors.foreground
text: {
let names = ['1m', '5m', '15m'];
let out = [];
for (let i = 0; i < CPU.loadAvg.length; ++i) {
let name = `${names[i]}:`.padStart(5).padEnd(6);
let val = CPU.loadAvg[i].toFixed(2);
out.push(`${name}${val}`);
}
return out.join("\n")
}
}
Text {
Layout.alignment: Qt.AlignHCenter
text: "CPU Core Usage"
font.family: Config.data.theme.font.family
font.pixelSize: Config.data.theme.font.size
font.bold: true
color: Config.data.theme.colors.foreground
Layout.topMargin: 6
}
Text {
Layout.alignment: Qt.AlignHCenter
font.family: Config.data.theme.fontMono.family
font.pixelSize: Config.data.theme.fontMono.size
color: Config.data.theme.colors.foreground
text: {
let out = [];
for (let i = 0; i < CPU.coreUsages.length; ++i) {
let coreNum = `Core ${i+1}:`.padEnd(10);
let coreUsage = CPU.coreUsages[i].toFixed(2);
out.push(`${coreNum}${coreUsage}`);
}
return out.join("\n");
}
}
Text {
Layout.alignment: Qt.AlignHCenter
text: "Top Processes"
font.family: Config.data.theme.font.family
font.pixelSize: Config.data.theme.font.size
font.bold: true
color: Config.data.theme.colors.foreground
Layout.topMargin: 6
}
Text {
Layout.alignment: Qt.AlignHCenter
font.family: Config.data.theme.fontMono.family
font.pixelSize: Config.data.theme.fontMono.size
color: Config.data.theme.colors.foreground
text: {
let out = [];
for (let p of CPU.topProcesses) {
let pidStr = p.pid.toString().padStart(6);
let commStr = p.comm.slice(0, 15).padEnd(15);
let cpuStr = `${p.cpu.toFixed(1)}%`;
out.push(`${pidStr} ${commStr}\t${cpuStr.padStart(6)}`);
}
return out.join("\n");
}
}
}
}
}
}