450 lines
16 KiB
QML
450 lines
16 KiB
QML
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"
|
|
}
|
|
}
|