Initial release of KDE Power Monitor plasmoid
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
import QtQuick
|
||||
import org.kde.plasma.plasmoid
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.plasma5support as P5Support
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
PlasmoidItem {
|
||||
id: root
|
||||
|
||||
readonly property int updateMs: 1000
|
||||
readonly property color cpuAccent: "#55c5c0"
|
||||
readonly property color gpuAccent: "#9876d9"
|
||||
readonly property color ramAccent: "#d9a84e"
|
||||
|
||||
property real cpuUsage: NaN
|
||||
property real cpuTemp: NaN
|
||||
property real cpuW: NaN
|
||||
property real gpuUsage: NaN
|
||||
property real gpuTemp: NaN
|
||||
property real gpuW: NaN
|
||||
property real gpuWAvg: NaN
|
||||
property real gpuMhz: NaN
|
||||
property real ramUsed: NaN
|
||||
property real ramTotal: NaN
|
||||
property real ramAvailable: NaN
|
||||
property real swapUsed: NaN
|
||||
property real swapTotal: NaN
|
||||
|
||||
property var cpuHistory: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
property var gpuHistory: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
property var ramHistory: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
|
||||
signal metricsUpdated()
|
||||
|
||||
// Come gli altri dashboard KDE: sfondo semitrasparente nativo Plasma.
|
||||
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
|
||||
preferredRepresentation: fullRepresentation
|
||||
|
||||
P5Support.DataSource {
|
||||
engine: "executable"
|
||||
interval: root.updateMs
|
||||
connectedSources: [root.script]
|
||||
onNewData: function(name, data) {
|
||||
if (data["exit code"] === 0) root.parse(data["stdout"] || "")
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string script: Qt.resolvedUrl("../scripts/power.sh").toString().replace("file://", "")
|
||||
|
||||
function number(values, key, divisor) {
|
||||
const value = parseFloat(values[key])
|
||||
return isNaN(value) ? NaN : value / (divisor || 1)
|
||||
}
|
||||
|
||||
function appendSample(samples, value) {
|
||||
const copy = samples.slice(0)
|
||||
copy.push(isNaN(value) ? 0 : value)
|
||||
if (copy.length > 24) copy.shift()
|
||||
return copy
|
||||
}
|
||||
|
||||
function parse(output) {
|
||||
const values = {}
|
||||
output.split("\n").forEach(function(line) {
|
||||
const separator = line.indexOf("=")
|
||||
if (separator > 0) values[line.slice(0, separator)] = line.slice(separator + 1)
|
||||
})
|
||||
|
||||
cpuUsage = number(values, "cpu_usage_pct")
|
||||
cpuTemp = number(values, "cpu_temp_mdegC", 1000)
|
||||
cpuW = number(values, "cpu_power_cw", 100)
|
||||
gpuUsage = number(values, "gpu_busy_pct")
|
||||
gpuTemp = number(values, "gpu_temp_mdegC", 1000)
|
||||
gpuW = number(values, "gpu_power_uw", 1000000)
|
||||
gpuWAvg = number(values, "gpu_power_avg_uw", 1000000)
|
||||
gpuMhz = number(values, "gpu_freq_hz", 1000000)
|
||||
ramTotal = number(values, "ram_total_kb")
|
||||
ramUsed = number(values, "ram_used_kb")
|
||||
ramAvailable = number(values, "ram_available_kb")
|
||||
swapTotal = number(values, "swap_total_kb")
|
||||
swapUsed = number(values, "swap_used_kb")
|
||||
|
||||
cpuHistory = appendSample(cpuHistory, cpuUsage)
|
||||
gpuHistory = appendSample(gpuHistory, gpuUsage)
|
||||
ramHistory = appendSample(ramHistory, ramPercent())
|
||||
metricsUpdated()
|
||||
}
|
||||
|
||||
function clampPercent(value) {
|
||||
return isNaN(value) ? 0 : Math.max(0, Math.min(100, value))
|
||||
}
|
||||
|
||||
function ramPercent() {
|
||||
return ramTotal > 0 ? ramUsed / ramTotal * 100 : NaN
|
||||
}
|
||||
|
||||
function formatNumber(value, decimals) {
|
||||
return isNaN(value) ? "—" : value.toFixed(decimals === undefined ? 0 : decimals)
|
||||
}
|
||||
|
||||
function watts(value) {
|
||||
return isNaN(value) ? "n/d" : value.toFixed(1) + " W"
|
||||
}
|
||||
|
||||
function gibibytes(kilobytes) {
|
||||
return isNaN(kilobytes) ? "—" : (kilobytes / 1048576).toFixed(1) + " GiB"
|
||||
}
|
||||
|
||||
fullRepresentation: Rectangle {
|
||||
id: dashboard
|
||||
readonly property bool narrow: width < 520
|
||||
|
||||
implicitWidth: 570
|
||||
implicitHeight: narrow ? 650 : 368
|
||||
radius: 14
|
||||
color: "transparent"
|
||||
border.color: "transparent"
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 10
|
||||
|
||||
// Due righe esplicite: nessuna collisione tra titolo e stato LIVE.
|
||||
Column {
|
||||
width: parent.width
|
||||
height: 39
|
||||
spacing: 3
|
||||
|
||||
Row {
|
||||
spacing: 9
|
||||
|
||||
Rectangle {
|
||||
width: 4
|
||||
height: 18
|
||||
radius: 2
|
||||
color: root.cpuAccent
|
||||
}
|
||||
|
||||
PlasmaComponents.Label {
|
||||
text: "POWER MONITOR"
|
||||
color: "#e8edf4"
|
||||
font.bold: true
|
||||
font.pointSize: dashboard.narrow ? 11 : 13
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 14
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
|
||||
Rectangle {
|
||||
width: 6
|
||||
height: 6
|
||||
radius: 3
|
||||
color: "#62c98b"
|
||||
}
|
||||
|
||||
PlasmaComponents.Label {
|
||||
text: "LIVE / 1 s"
|
||||
color: "#8293a8"
|
||||
font.family: "monospace"
|
||||
font.pointSize: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
id: metricsGrid
|
||||
width: parent.width
|
||||
columns: dashboard.narrow ? 1 : 3
|
||||
spacing: 8
|
||||
|
||||
MetricCard {
|
||||
width: dashboard.narrow ? metricsGrid.width : (metricsGrid.width - 16) / 3
|
||||
height: dashboard.narrow ? 105 : 126
|
||||
title: "CPU"
|
||||
value: root.formatNumber(root.cpuUsage) + "%"
|
||||
detail: root.watts(root.cpuW) + " / " + (isNaN(root.cpuTemp) ? "n/d" : root.formatNumber(root.cpuTemp) + " °C")
|
||||
accent: root.cpuAccent
|
||||
progress: root.clampPercent(root.cpuUsage)
|
||||
metricIcon: "cpu"
|
||||
}
|
||||
|
||||
MetricCard {
|
||||
width: dashboard.narrow ? metricsGrid.width : (metricsGrid.width - 16) / 3
|
||||
height: dashboard.narrow ? 105 : 126
|
||||
title: "iGPU 780M"
|
||||
value: root.formatNumber(root.gpuUsage) + "%"
|
||||
detail: root.watts(root.gpuWAvg) + " / " + (isNaN(root.gpuTemp) ? "n/d" : root.formatNumber(root.gpuTemp) + " °C")
|
||||
accent: root.gpuAccent
|
||||
progress: root.clampPercent(root.gpuUsage)
|
||||
metricIcon: "video-display"
|
||||
}
|
||||
|
||||
MetricCard {
|
||||
width: dashboard.narrow ? metricsGrid.width : (metricsGrid.width - 16) / 3
|
||||
height: dashboard.narrow ? 105 : 126
|
||||
title: "RAM"
|
||||
value: root.formatNumber(root.ramPercent()) + "%"
|
||||
detail: root.gibibytes(root.ramUsed) + " / " + root.gibibytes(root.ramTotal)
|
||||
accent: root.ramAccent
|
||||
progress: root.clampPercent(root.ramPercent())
|
||||
metricIcon: "media-flash-memory"
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
spacing: 7
|
||||
|
||||
StatChip { label: "CPU"; metric: root.watts(root.cpuW); accent: root.cpuAccent }
|
||||
StatChip { label: "PPT APU"; metric: root.watts(root.gpuW); accent: root.gpuAccent }
|
||||
StatChip { label: "FREE"; metric: root.gibibytes(root.ramAvailable); accent: root.ramAccent }
|
||||
StatChip { label: "SWAP"; metric: root.gibibytes(root.swapUsed) + " / " + root.gibibytes(root.swapTotal); accent: "#7187a4" }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: dashboard.narrow ? 118 : 104
|
||||
radius: 8
|
||||
color: Qt.rgba(0, 0, 0, 0.25)
|
||||
border.color: Qt.rgba(0.40, 0.48, 0.58, 0.40)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 5
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 18
|
||||
|
||||
PlasmaComponents.Label {
|
||||
anchors.left: parent.left
|
||||
text: dashboard.narrow ? "HISTORY" : "UTILIZATION HISTORY"
|
||||
color: "#8293a8"
|
||||
font.family: "monospace"
|
||||
font.pointSize: 8
|
||||
font.letterSpacing: 0.5
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
spacing: dashboard.narrow ? 5 : 9
|
||||
Legend { label: "CPU"; accent: root.cpuAccent }
|
||||
Legend { label: "GPU"; accent: root.gpuAccent }
|
||||
Legend { label: "RAM"; accent: root.ramAccent }
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: trend
|
||||
width: parent.width
|
||||
height: parent.height - 23
|
||||
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onMetricsUpdated() { trend.requestPaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
const context = getContext("2d")
|
||||
context.clearRect(0, 0, width, height)
|
||||
|
||||
context.lineWidth = 1
|
||||
context.strokeStyle = "#202b39"
|
||||
for (let row = 1; row < 4; row++) {
|
||||
const y = row * height / 4
|
||||
context.beginPath()
|
||||
context.moveTo(0, y)
|
||||
context.lineTo(width, y)
|
||||
context.stroke()
|
||||
}
|
||||
|
||||
function drawSeries(samples, color) {
|
||||
if (!samples || samples.length < 2) return
|
||||
context.lineWidth = 1.5
|
||||
context.strokeStyle = color
|
||||
context.beginPath()
|
||||
for (let index = 0; index < samples.length; index++) {
|
||||
const x = index * width / (samples.length - 1)
|
||||
const y = height - root.clampPercent(samples[index]) / 100 * height
|
||||
if (index === 0) context.moveTo(x, y)
|
||||
else context.lineTo(x, y)
|
||||
}
|
||||
context.stroke()
|
||||
}
|
||||
|
||||
drawSeries(root.ramHistory, root.ramAccent)
|
||||
drawSeries(root.gpuHistory, root.gpuAccent)
|
||||
drawSeries(root.cpuHistory, root.cpuAccent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component MetricCard: Rectangle {
|
||||
id: card
|
||||
property string title
|
||||
property string value
|
||||
property string detail
|
||||
property color accent
|
||||
property real progress
|
||||
property string metricIcon
|
||||
|
||||
implicitWidth: 170
|
||||
implicitHeight: 126
|
||||
radius: 8
|
||||
color: Qt.rgba(0, 0, 0, 0.25)
|
||||
border.color: Qt.rgba(card.accent.r, card.accent.g, card.accent.b, 0.55)
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
anchors { left: parent.left; top: parent.top; bottom: parent.bottom; topMargin: 9; bottomMargin: 9 }
|
||||
width: 2
|
||||
radius: 1
|
||||
color: card.accent
|
||||
opacity: 0.9
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 11
|
||||
spacing: 7
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 27
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 5
|
||||
color: Qt.rgba(card.accent.r, card.accent.g, card.accent.b, 0.11)
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: 14
|
||||
height: 14
|
||||
source: card.metricIcon
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents.Label {
|
||||
text: card.title
|
||||
color: "#b9c4d2"
|
||||
font.bold: true
|
||||
font.pointSize: 9
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents.Label {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: card.value
|
||||
color: card.accent
|
||||
font.family: "monospace"
|
||||
font.bold: true
|
||||
font.pointSize: 18
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 4
|
||||
radius: 2
|
||||
color: "#293649"
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.max(0, Math.min(100, card.progress)) / 100
|
||||
height: parent.height
|
||||
radius: 2
|
||||
color: card.accent
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents.Label {
|
||||
width: parent.width
|
||||
text: card.detail
|
||||
color: "#899aaf"
|
||||
font.family: "monospace"
|
||||
font.pointSize: 8
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component StatChip: Rectangle {
|
||||
id: chip
|
||||
property string label
|
||||
property string metric
|
||||
property color accent
|
||||
|
||||
implicitWidth: chipRow.implicitWidth + 16
|
||||
implicitHeight: 25
|
||||
radius: 5
|
||||
color: Qt.rgba(0, 0, 0, 0.25)
|
||||
border.color: Qt.rgba(0.40, 0.48, 0.58, 0.40)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
id: chipRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
Rectangle { width: 4; height: 4; radius: 2; color: chip.accent }
|
||||
PlasmaComponents.Label { text: chip.label; color: "#72849a"; font.family: "monospace"; font.pointSize: 7 }
|
||||
PlasmaComponents.Label { text: chip.metric; color: "#c1cad6"; font.family: "monospace"; font.bold: true; font.pointSize: 8 }
|
||||
}
|
||||
}
|
||||
|
||||
component Legend: Row {
|
||||
id: legend
|
||||
property string label
|
||||
property color accent
|
||||
spacing: 4
|
||||
Rectangle { width: 7; height: 2; color: legend.accent; anchors.verticalCenter: parent.verticalCenter }
|
||||
PlasmaComponents.Label { text: legend.label; color: "#73859b"; font.family: "monospace"; font.pointSize: 7 }
|
||||
}
|
||||
|
||||
compactRepresentation: PlasmaComponents.Label {
|
||||
text: root.formatNumber(root.cpuUsage) + "% / " + root.formatNumber(root.ramPercent()) + "%"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: "monospace"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user