plasmoides KDE Store/GitHub (gnome-pager, kMenu, kvitals, windowtitle, claude, launchpad)

This commit is contained in:
Josevi
2026-07-08 20:47:59 +02:00
parent ca606377ee
commit dd3b0f6ae8
136 changed files with 20239 additions and 0 deletions
@@ -0,0 +1,26 @@
import QtQuick 2.0
import org.kde.plasma.configuration 2.0
ConfigModel {
ConfigCategory {
name: i18n("General")
icon: "configure"
source: "configGeneral.qml"
}
ConfigCategory {
name: i18n("Metrics")
icon: "view-statistics"
source: "configMetrics.qml"
}
ConfigCategory {
name: i18n("Icons")
icon: "preferences-desktop-icons"
source: "configIcons.qml"
}
ConfigCategory {
name: i18n("Colors")
icon: "color-management"
source: "configColors.qml"
}
}
@@ -0,0 +1,209 @@
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0 http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
<kcfgfile name=""/>
<group name="General">
<entry name="showCpu" type="Bool">
<default>true</default>
</entry>
<entry name="showCpuFreq" type="Bool">
<default>false</default>
</entry>
<entry name="showRam" type="Bool">
<default>true</default>
</entry>
<entry name="showTemp" type="Bool">
<default>true</default>
</entry>
<entry name="showGpu" type="Bool">
<default>true</default>
</entry>
<entry name="showBattery" type="Bool">
<default>true</default>
</entry>
<entry name="showPower" type="Bool">
<default>true</default>
</entry>
<entry name="showNetwork" type="Bool">
<default>true</default>
</entry>
<entry name="showDisk" type="Bool">
<default>false</default>
</entry>
<entry name="showFan" type="Bool">
<default>false</default>
</entry>
<entry name="compactShowCpu" type="Bool">
<default>true</default>
</entry>
<entry name="compactShowRam" type="Bool">
<default>true</default>
</entry>
<entry name="compactShowTemp" type="Bool">
<default>true</default>
</entry>
<entry name="compactShowGpu" type="Bool">
<default>true</default>
</entry>
<entry name="compactShowBattery" type="Bool">
<default>true</default>
</entry>
<entry name="compactShowPower" type="Bool">
<default>true</default>
</entry>
<entry name="compactShowNetwork" type="Bool">
<default>true</default>
</entry>
<entry name="compactShowDisk" type="Bool">
<default>false</default>
</entry>
<entry name="compactShowFan" type="Bool">
<default>false</default>
</entry>
<entry name="networkInterface" type="String">
<default>auto</default>
</entry>
<entry name="batteryDevice" type="String">
<default>auto</default>
</entry>
<entry name="gpuSelection" type="String">
<default></default>
</entry>
<entry name="gpuLabels" type="String">
<default></default>
</entry>
<entry name="displayMode" type="String">
<default>text</default>
</entry>
<entry name="layoutType" type="String">
<default>horizontal</default>
</entry>
<entry name="mergeCpuTemp" type="Bool">
<default>false</default>
</entry>
<entry name="mergeCpuFreq" type="Bool">
<default>false</default>
</entry>
<entry name="mergeBatPwr" type="Bool">
<default>false</default>
</entry>
<entry name="splitGpu" type="Bool">
<default>false</default>
</entry>
<entry name="gpuMetrics" type="String">
<default></default>
</entry>
<entry name="iconSize" type="Int">
<default>12</default>
</entry>
<entry name="cpuIcon" type="String">
<default>cpu</default>
</entry>
<entry name="ramIcon" type="String">
<default>memory</default>
</entry>
<entry name="tempIcon" type="String">
<default>temperature-normal</default>
</entry>
<entry name="gpuIcon" type="String">
<default>video-card</default>
</entry>
<entry name="batteryIcon" type="String">
<default>battery-good</default>
</entry>
<entry name="powerIcon" type="String">
<default>battery-charging-60</default>
</entry>
<entry name="networkIcon" type="String">
<default>network-wireless</default>
</entry>
<entry name="diskIcon" type="String">
<default>drive-harddisk</default>
</entry>
<entry name="fanIcon" type="String">
<default>fan</default>
</entry>
<entry name="fontFamily" type="String">
<default>monospace</default>
</entry>
<entry name="fontSize" type="Int">
<default>0</default>
</entry>
<entry name="fontBold" type="Bool">
<default>false</default>
</entry>
<entry name="metricOrder" type="String">
<default>cpu,ram,temp,gpu,bat,pwr,net,disk,fan</default>
</entry>
<entry name="updateInterval" type="Int">
<default>2000</default>
</entry>
<entry name="useCustomColors" type="Bool">
<default>false</default>
</entry>
<entry name="fontColor" type="String">
<default></default>
</entry>
<entry name="enableThresholdColors" type="Bool">
<default>false</default>
</entry>
<entry name="warningColor" type="String">
<default>#e5a50a</default>
</entry>
<entry name="criticalColor" type="String">
<default>#da4453</default>
</entry>
<entry name="cpuWarningThreshold" type="Int">
<default>70</default>
</entry>
<entry name="cpuCriticalThreshold" type="Int">
<default>90</default>
</entry>
<entry name="tempWarningThreshold" type="Int">
<default>60</default>
</entry>
<entry name="tempCriticalThreshold" type="Int">
<default>85</default>
</entry>
<entry name="ramWarningThreshold" type="Int">
<default>70</default>
</entry>
<entry name="ramCriticalThreshold" type="Int">
<default>90</default>
</entry>
<entry name="gpuWarningThreshold" type="Int">
<default>70</default>
</entry>
<entry name="gpuCriticalThreshold" type="Int">
<default>90</default>
</entry>
<entry name="gpuTempWarningThreshold" type="Int">
<default>60</default>
</entry>
<entry name="gpuTempCriticalThreshold" type="Int">
<default>85</default>
</entry>
<entry name="batteryWarningThreshold" type="Int">
<default>30</default>
</entry>
<entry name="batteryCriticalThreshold" type="Int">
<default>15</default>
</entry>
<entry name="diskTempWarningThreshold" type="Int">
<default>45</default>
</entry>
<entry name="diskTempCriticalThreshold" type="Int">
<default>60</default>
</entry>
<entry name="tempUnit" type="String">
<default>C</default>
</entry>
<entry name="networkUnit" type="String">
<default>bytes</default>
</entry>
<entry name="fanUnit" type="String">
<default>rpm</default>
</entry>
</group>
</kcfg>
@@ -0,0 +1,211 @@
import QtQuick
import QtQuick.Layouts
import org.kde.plasma.components as PlasmaComponents
import org.kde.kirigami as Kirigami
RowLayout {
id: compactRow
spacing: Kirigami.Units.smallSpacing
required property var metricsModel
required property bool useIcons
required property bool useText
required property int effectiveFontSize
required property string fontFamily
required property bool fontBold
required property int iconSize
required property color baseTextColor
required property string layoutType
readonly property bool isVertical: layoutType === "vertical"
signal toggleExpanded()
TapHandler {
onTapped: compactRow.toggleExpanded()
}
// Shared segments renderer
component SegmentsRow: Row {
required property var segments
spacing: 2
Repeater {
model: segments
delegate: Row {
required property var modelData
required property int index
spacing: 2
PlasmaComponents.Label {
visible: index > 0
text: "·"
font.pixelSize: compactRow.effectiveFontSize
font.family: compactRow.fontFamily
font.bold: compactRow.fontBold
color: compactRow.baseTextColor
opacity: 0.5
}
PlasmaComponents.Label {
text: modelData.value
font.pixelSize: compactRow.effectiveFontSize
font.family: compactRow.fontFamily
font.bold: compactRow.fontBold
color: modelData.color
}
}
}
}
Repeater {
model: compactRow.metricsModel
delegate: Item {
required property var modelData
required property int index
implicitWidth: loader.implicitWidth
implicitHeight: loader.implicitHeight
Loader {
id: loader
anchors.fill: parent
sourceComponent: compactRow.isVertical ? verticalDelegate : horizontalDelegate
property var itemData: modelData
property int itemIndex: index
}
}
}
// ── Horizontal delegate (unchanged behaviour) ──────────────────────────
Component {
id: horizontalDelegate
RowLayout {
spacing: 2
Layout.fillHeight: true
PlasmaComponents.Label {
visible: itemIndex > 0 && !itemData.hideSeparator
text: "|"
font.pixelSize: compactRow.effectiveFontSize
font.family: compactRow.fontFamily
color: compactRow.baseTextColor
opacity: 0.4
Layout.alignment: Qt.AlignVCenter
}
Kirigami.Icon {
visible: compactRow.useIcons
source: itemData.icon
isMask: true
Layout.preferredWidth: compactRow.iconSize
Layout.preferredHeight: compactRow.iconSize
Layout.alignment: Qt.AlignVCenter
}
PlasmaComponents.Label {
visible: compactRow.useText
text: itemData.label
font.pixelSize: compactRow.effectiveFontSize
font.family: compactRow.fontFamily
color: compactRow.baseTextColor
Layout.alignment: Qt.AlignVCenter
}
PlasmaComponents.Label {
visible: !itemData.segments
text: itemData.value || ""
font.pixelSize: compactRow.effectiveFontSize
font.family: compactRow.fontFamily
font.bold: compactRow.fontBold
color: itemData.color || compactRow.baseTextColor
Layout.alignment: Qt.AlignVCenter
}
SegmentsRow {
visible: !!itemData.segments
segments: itemData.segments || []
Layout.alignment: Qt.AlignVCenter
}
}
}
// ── Vertical delegate (value on top, icon+label below) ─────────────────
Component {
id: verticalDelegate
RowLayout {
spacing: Kirigami.Units.smallSpacing
Layout.fillHeight: true
// Thin line separator between metrics
Rectangle {
visible: itemIndex > 0 && !itemData.hideSeparator
width: 1
Layout.fillHeight: true
color: compactRow.baseTextColor
opacity: 0.2
}
ColumnLayout {
spacing: 1
Layout.alignment: Qt.AlignVCenter
// Top: value(s)
RowLayout {
spacing: 0
Layout.alignment: Qt.AlignHCenter
PlasmaComponents.Label {
visible: !itemData.segments
text: itemData.value || ""
font.pixelSize: compactRow.effectiveFontSize
font.family: compactRow.fontFamily
font.bold: compactRow.fontBold
color: itemData.color || compactRow.baseTextColor
horizontalAlignment: Text.AlignHCenter
}
SegmentsRow {
visible: !!itemData.segments
segments: itemData.segments || []
Layout.alignment: Qt.AlignHCenter
}
}
// Bottom: icon + label
RowLayout {
visible: compactRow.useIcons || compactRow.useText
spacing: 2
Layout.alignment: Qt.AlignHCenter
Kirigami.Icon {
visible: compactRow.useIcons
source: itemData.icon
isMask: true
Layout.preferredWidth: Math.round(compactRow.iconSize * 0.85)
Layout.preferredHeight: Math.round(compactRow.iconSize * 0.85)
Layout.alignment: Qt.AlignVCenter
}
PlasmaComponents.Label {
visible: compactRow.useText
text: {
var lbl = itemData.label || "";
return lbl.endsWith(":") ? lbl.slice(0, -1) : lbl;
}
font.pixelSize: Math.max(8, compactRow.effectiveFontSize - 2)
font.family: compactRow.fontFamily
color: compactRow.baseTextColor
opacity: 0.65
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
}
@@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Layouts
import org.kde.plasma.components as PlasmaComponents
import org.kde.kirigami as Kirigami
ColumnLayout {
id: fullView
spacing: Kirigami.Units.smallSpacing
Layout.preferredWidth: Kirigami.Units.gridUnit * 18
Layout.preferredHeight: Kirigami.Units.gridUnit * 12
required property var metricsModel
required property color baseTextColor
required property bool fontBold
PlasmaComponents.Label {
text: "KVitals"
font.bold: true // intentional: title always bold for visual hierarchy
font.pixelSize: Kirigami.Theme.defaultFont.pixelSize * 1.2
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.smallSpacing
}
Repeater {
model: fullView.metricsModel
delegate: RowLayout {
required property var modelData
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
PlasmaComponents.Label {
text: modelData.label
color: fullView.baseTextColor
opacity: 0.7
Layout.fillWidth: true
}
PlasmaComponents.Label {
text: modelData.value
font.bold: fullView.fontBold
color: modelData.color
horizontalAlignment: Text.AlignRight
}
}
}
}
@@ -0,0 +1,425 @@
import QtQuick 2.0
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
import Qt.labs.platform 1.0 as Platform
import org.kde.kirigami 2.5 as Kirigami
import org.kde.kcmutils as KCM
import "./sensors"
KCM.SimpleKCM {
id: colorsPage
property alias cfg_useCustomColors: useCustomColorsCheck.checked
property string cfg_fontColor
property alias cfg_enableThresholdColors: enableThresholdCheck.checked
property string cfg_warningColor: "#e5a50a"
property string cfg_criticalColor: "#da4453"
property alias cfg_cpuWarningThreshold: cpuWarnSlider.value
property alias cfg_cpuCriticalThreshold: cpuCritSlider.value
property alias cfg_tempWarningThreshold: tempWarnSlider.value
property alias cfg_tempCriticalThreshold: tempCritSlider.value
property alias cfg_ramWarningThreshold: ramWarnSlider.value
property alias cfg_ramCriticalThreshold: ramCritSlider.value
property alias cfg_gpuWarningThreshold: gpuWarnSlider.value
property alias cfg_gpuCriticalThreshold: gpuCritSlider.value
property alias cfg_gpuTempWarningThreshold: gpuTempWarnSlider.value
property alias cfg_gpuTempCriticalThreshold: gpuTempCritSlider.value
property alias cfg_batteryWarningThreshold: batWarnSlider.value
property alias cfg_batteryCriticalThreshold: batCritSlider.value
property alias cfg_diskTempWarningThreshold: diskTempWarnSlider.value
property alias cfg_diskTempCriticalThreshold: diskTempCritSlider.value
readonly property string defaultWarningColor: "#e5a50a"
readonly property string defaultCriticalColor: "#da4453"
property string cfg_tempUnit: "C"
function isRgbHex(value) {
return /^#[0-9A-Fa-f]{6}$/.test(value)
}
function colorToRgbHex(colorValue) {
if (!colorValue || colorValue.r === undefined || colorValue.g === undefined || colorValue.b === undefined) {
return ""
}
function channelToHex(channel) {
const value = Math.max(0, Math.min(255, Math.round(channel * 255)))
return value.toString(16).padStart(2, "0")
}
return "#" + channelToHex(colorValue.r) + channelToHex(colorValue.g) + channelToHex(colorValue.b)
}
function openColorDialog(dialog, value, fallbackColor) {
const initialColor = isRgbHex(value) ? value : fallbackColor
dialog.color = initialColor
dialog.currentColor = initialColor
dialog.open()
}
Kirigami.FormLayout {
// === Section: Custom Font Color ===
Kirigami.Separator {
Kirigami.FormData.isSection: true
Kirigami.FormData.label: i18n("Font Color")
}
CheckBox {
id: useCustomColorsCheck
Kirigami.FormData.label: i18n("Use custom font color:")
}
RowLayout {
Kirigami.FormData.label: i18n("Color:")
enabled: cfg_useCustomColors
spacing: Kirigami.Units.smallSpacing
Button {
id: fontColorButton
text: ""
padding: 0
implicitWidth: Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Units.gridUnit * 1.5
onClicked: colorsPage.openColorDialog(fontColorDialog, cfg_fontColor, Kirigami.Theme.textColor)
background: Rectangle {
radius: 4
border.width: 1
border.color: fontColorButton.visualFocus ? Kirigami.Theme.highlightColor : Qt.rgba(1, 1, 1, 0.15)
color: "transparent"
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: 3
color: colorsPage.isRgbHex(cfg_fontColor) ? cfg_fontColor : Kirigami.Theme.textColor
}
}
}
Platform.ColorDialog {
id: fontColorDialog
title: i18n("Choose Font Color")
parentWindow: colorsPage.Window.window
onAccepted: {
const hex = colorsPage.colorToRgbHex(color)
if (!hex) {
return
}
if (hex !== fontColorField.text) {
fontColorField.text = hex
}
cfg_fontColor = hex
}
}
TextField {
id: fontColorField
placeholderText: "#ffffff"
text: cfg_fontColor
maximumLength: 7
Layout.preferredWidth: 100
onTextChanged: {
if (/^#[0-9A-Fa-f]{6}$/.test(text))
cfg_fontColor = text
}
}
Button {
text: i18n("Reset")
icon.name: "edit-undo"
onClicked: { fontColorField.text = ""; cfg_fontColor = "" }
}
}
// === Section: Threshold Colors ===
Kirigami.Separator {
Kirigami.FormData.isSection: true
Kirigami.FormData.label: i18n("Threshold Colors")
}
CheckBox {
id: enableThresholdCheck
Kirigami.FormData.label: i18n("Enable threshold coloring:")
}
RowLayout {
Kirigami.FormData.label: i18n("Warning color:")
enabled: cfg_enableThresholdColors
spacing: Kirigami.Units.smallSpacing
Button {
id: warningColorButton
text: ""
padding: 0
implicitWidth: Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Units.gridUnit * 1.5
onClicked: colorsPage.openColorDialog(warningColorDialog, cfg_warningColor, colorsPage.defaultWarningColor)
background: Rectangle {
radius: 4
border.width: 1
border.color: warningColorButton.visualFocus ? Kirigami.Theme.highlightColor : Qt.rgba(1, 1, 1, 0.15)
color: "transparent"
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: 3
color: colorsPage.isRgbHex(cfg_warningColor) ? cfg_warningColor : colorsPage.defaultWarningColor
}
}
}
Platform.ColorDialog {
id: warningColorDialog
title: i18n("Choose Warning Color")
parentWindow: colorsPage.Window.window
onAccepted: {
const hex = colorsPage.colorToRgbHex(color)
if (!hex) {
return
}
if (hex !== warningColorField.text) {
warningColorField.text = hex
}
cfg_warningColor = hex
}
}
TextField {
id: warningColorField
text: cfg_warningColor
maximumLength: 7
Layout.preferredWidth: 100
onTextChanged: {
if (/^#[0-9A-Fa-f]{6}$/.test(text))
cfg_warningColor = text
}
}
}
RowLayout {
Kirigami.FormData.label: i18n("Critical color:")
enabled: cfg_enableThresholdColors
spacing: Kirigami.Units.smallSpacing
Button {
id: criticalColorButton
text: ""
padding: 0
implicitWidth: Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Units.gridUnit * 1.5
onClicked: colorsPage.openColorDialog(criticalColorDialog, cfg_criticalColor, colorsPage.defaultCriticalColor)
background: Rectangle {
radius: 4
border.width: 1
border.color: criticalColorButton.visualFocus ? Kirigami.Theme.highlightColor : Qt.rgba(1, 1, 1, 0.15)
color: "transparent"
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: 3
color: colorsPage.isRgbHex(cfg_criticalColor) ? cfg_criticalColor : colorsPage.defaultCriticalColor
}
}
}
Platform.ColorDialog {
id: criticalColorDialog
title: i18n("Choose Critical Color")
parentWindow: colorsPage.Window.window
onAccepted: {
const hex = colorsPage.colorToRgbHex(color)
if (!hex) {
return
}
if (hex !== criticalColorField.text) {
criticalColorField.text = hex
}
cfg_criticalColor = hex
}
}
TextField {
id: criticalColorField
text: cfg_criticalColor
maximumLength: 7
Layout.preferredWidth: 100
onTextChanged: {
if (/^#[0-9A-Fa-f]{6}$/.test(text))
cfg_criticalColor = text
}
}
}
// === Section: Thresholds Grid ===
Kirigami.Separator {
Kirigami.FormData.isSection: true
Kirigami.FormData.label: i18n("Thresholds")
visible: cfg_enableThresholdColors
}
GridLayout {
visible: cfg_enableThresholdColors
columns: 4
columnSpacing: Kirigami.Units.largeSpacing
rowSpacing: Kirigami.Units.smallSpacing
Layout.fillWidth: true
// --- Header ---
Label { text: "" }
Label { text: i18n("Warning"); font.bold: true; horizontalAlignment: Text.AlignHCenter; Layout.fillWidth: true }
Label { text: i18n("Critical"); font.bold: true; horizontalAlignment: Text.AlignHCenter; Layout.fillWidth: true }
Label { text: "" }
// --- CPU Usage ---
Label { text: i18n("CPU Usage") }
RowLayout {
Layout.fillWidth: true
Slider { id: cpuWarnSlider; from: 10; to: 100; stepSize: 5; value: 70; Layout.fillWidth: true }
Label { text: Math.round(cpuWarnSlider.value) + "%"; Layout.preferredWidth: 40 }
}
RowLayout {
Layout.fillWidth: true
Slider { id: cpuCritSlider; from: 10; to: 100; stepSize: 5; value: 90; Layout.fillWidth: true }
Label { text: Math.round(cpuCritSlider.value) + "%"; Layout.preferredWidth: 40 }
}
Rectangle { Layout.preferredWidth: 10; Layout.preferredHeight: 10; radius: 5; color: cpuWarnSlider.value >= cpuCritSlider.value ? cfg_criticalColor : "transparent" }
// --- CPU Temperature ---
Label { text: i18n("CPU Temp") }
RowLayout {
Layout.fillWidth: true
Slider { id: tempWarnSlider; from: 30; to: 110; stepSize: 5; value: 60; Layout.fillWidth: true }
Label { text: Utils.formatTemp(tempWarnSlider.value, cfg_tempUnit); Layout.preferredWidth: 40 }
}
RowLayout {
Layout.fillWidth: true
Slider { id: tempCritSlider; from: 30; to: 110; stepSize: 5; value: 85; Layout.fillWidth: true }
Label { text: Utils.formatTemp(tempCritSlider.value, cfg_tempUnit); Layout.preferredWidth: 40 }
}
Rectangle { Layout.preferredWidth: 10; Layout.preferredHeight: 10; radius: 5; color: tempWarnSlider.value >= tempCritSlider.value ? cfg_criticalColor : "transparent" }
// --- RAM Usage ---
Label { text: i18n("RAM Usage") }
RowLayout {
Layout.fillWidth: true
Slider { id: ramWarnSlider; from: 10; to: 100; stepSize: 5; value: 70; Layout.fillWidth: true }
Label { text: Math.round(ramWarnSlider.value) + "%"; Layout.preferredWidth: 40 }
}
RowLayout {
Layout.fillWidth: true
Slider { id: ramCritSlider; from: 10; to: 100; stepSize: 5; value: 90; Layout.fillWidth: true }
Label { text: Math.round(ramCritSlider.value) + "%"; Layout.preferredWidth: 40 }
}
Rectangle { Layout.preferredWidth: 10; Layout.preferredHeight: 10; radius: 5; color: ramWarnSlider.value >= ramCritSlider.value ? cfg_criticalColor : "transparent" }
// --- GPU Usage ---
Label { text: i18n("GPU Usage") }
RowLayout {
Layout.fillWidth: true
Slider { id: gpuWarnSlider; from: 10; to: 100; stepSize: 5; value: 70; Layout.fillWidth: true }
Label { text: Math.round(gpuWarnSlider.value) + "%"; Layout.preferredWidth: 40 }
}
RowLayout {
Layout.fillWidth: true
Slider { id: gpuCritSlider; from: 10; to: 100; stepSize: 5; value: 90; Layout.fillWidth: true }
Label { text: Math.round(gpuCritSlider.value) + "%"; Layout.preferredWidth: 40 }
}
Rectangle { Layout.preferredWidth: 10; Layout.preferredHeight: 10; radius: 5; color: gpuWarnSlider.value >= gpuCritSlider.value ? cfg_criticalColor : "transparent" }
// --- GPU Temperature ---
Label { text: i18n("GPU Temp") }
RowLayout {
Layout.fillWidth: true
Slider { id: gpuTempWarnSlider; from: 30; to: 110; stepSize: 5; value: 60; Layout.fillWidth: true }
Label { text: Utils.formatTemp(gpuTempWarnSlider.value, cfg_tempUnit); Layout.preferredWidth: 40 }
}
RowLayout {
Layout.fillWidth: true
Slider { id: gpuTempCritSlider; from: 30; to: 110; stepSize: 5; value: 85; Layout.fillWidth: true }
Label { text: Utils.formatTemp(gpuTempCritSlider.value, cfg_tempUnit); Layout.preferredWidth: 40 }
}
Rectangle { Layout.preferredWidth: 10; Layout.preferredHeight: 10; radius: 5; color: gpuTempWarnSlider.value >= gpuTempCritSlider.value ? cfg_criticalColor : "transparent" }
// --- Battery (inverted) ---
Label { text: i18n("Battery ↓") }
RowLayout {
Layout.fillWidth: true
Slider { id: batWarnSlider; from: 5; to: 60; stepSize: 5; value: 30; Layout.fillWidth: true }
Label { text: Math.round(batWarnSlider.value) + "%"; Layout.preferredWidth: 40 }
}
RowLayout {
Layout.fillWidth: true
Slider { id: batCritSlider; from: 5; to: 60; stepSize: 5; value: 15; Layout.fillWidth: true }
Label { text: Math.round(batCritSlider.value) + "%"; Layout.preferredWidth: 40 }
}
Rectangle { Layout.preferredWidth: 10; Layout.preferredHeight: 10; radius: 5; color: batWarnSlider.value <= batCritSlider.value ? cfg_criticalColor : "transparent" }
// --- Disk Temperature ---
Label { text: i18n("Disk Temp") }
RowLayout {
Layout.fillWidth: true
Slider { id: diskTempWarnSlider; from: 30; to: 80; stepSize: 5; value: 45; Layout.fillWidth: true }
Label { text: Utils.formatTemp(diskTempWarnSlider.value, cfg_tempUnit); Layout.preferredWidth: 40 }
}
RowLayout {
Layout.fillWidth: true
Slider { id: diskTempCritSlider; from: 30; to: 80; stepSize: 5; value: 60; Layout.fillWidth: true }
Label { text: Utils.formatTemp(diskTempCritSlider.value, cfg_tempUnit); Layout.preferredWidth: 40 }
}
Rectangle { Layout.preferredWidth: 10; Layout.preferredHeight: 10; radius: 5; color: diskTempWarnSlider.value >= diskTempCritSlider.value ? cfg_criticalColor : "transparent" }
}
Label {
visible: cfg_enableThresholdColors
text: i18n("Battery uses inverted thresholds — warning/critical trigger below the set values.\nA red dot means warning ≥ critical (invalid config).")
opacity: 0.6
font.italic: true
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// --- Reset ---
Kirigami.Separator {
Kirigami.FormData.isSection: true
}
Button {
icon.name: "edit-undo"
text: i18n("Reset all to defaults")
onClicked: {
useCustomColorsCheck.checked = false
fontColorField.text = ""
cfg_fontColor = ""
enableThresholdCheck.checked = false
warningColorField.text = colorsPage.defaultWarningColor
cfg_warningColor = colorsPage.defaultWarningColor
criticalColorField.text = colorsPage.defaultCriticalColor
cfg_criticalColor = colorsPage.defaultCriticalColor
cpuWarnSlider.value = 70; cpuCritSlider.value = 90
tempWarnSlider.value = 60; tempCritSlider.value = 85
ramWarnSlider.value = 70; ramCritSlider.value = 90
gpuWarnSlider.value = 70; gpuCritSlider.value = 90
gpuTempWarnSlider.value = 60; gpuTempCritSlider.value = 85
batWarnSlider.value = 30; batCritSlider.value = 15
diskTempWarnSlider.value = 45; diskTempCritSlider.value = 60
}
}
}
}
@@ -0,0 +1,154 @@
import QtQuick 2.0
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
import org.kde.kirigami 2.5 as Kirigami
import org.kde.kcmutils as KCM
KCM.SimpleKCM {
id: configPage
property alias cfg_updateInterval: intervalSlider.value
property alias cfg_iconSize: iconSizeSlider.value
property alias cfg_fontSize: fontSizeSlider.value
property alias cfg_fontBold: fontBoldCheck.checked
property string cfg_displayMode: "text"
property string cfg_fontFamily: "monospace"
property string cfg_layoutType: "horizontal"
property string cfg_tempUnit: "C"
property string cfg_networkUnit: "bytes"
property string cfg_fanUnit: "rpm"
readonly property var displayModes: ["text", "icons", "icons+text", "none"]
readonly property var displayModeLabels: [i18n("Text"), i18n("Icons"), i18n("Icons + Text"), i18n("None")]
readonly property bool iconsEnabled: cfg_displayMode === "icons" || cfg_displayMode === "icons+text"
readonly property var layoutTypes: ["horizontal", "vertical"]
readonly property var layoutTypeLabels: [i18n("Horizontal"), i18n("Vertical")]
Kirigami.FormLayout {
ComboBox {
id: displayModeCombo
Kirigami.FormData.label: i18n("Display mode:")
model: configPage.displayModeLabels
currentIndex: {
var idx = configPage.displayModes.indexOf(cfg_displayMode);
return idx >= 0 ? idx : 0;
}
onActivated: {
cfg_displayMode = configPage.displayModes[currentIndex];
}
}
ComboBox {
id: layoutTypeCombo
Kirigami.FormData.label: i18n("Layout:")
model: configPage.layoutTypeLabels
currentIndex: {
var idx = configPage.layoutTypes.indexOf(cfg_layoutType);
return idx >= 0 ? idx : 0;
}
onActivated: {
cfg_layoutType = configPage.layoutTypes[currentIndex];
}
}
Slider {
id: iconSizeSlider
Kirigami.FormData.label: i18n("Icon size:")
from: 8
to: 24
stepSize: 2
value: 12
visible: configPage.iconsEnabled
}
Label {
text: iconSizeSlider.value + " px"
opacity: 0.7
visible: configPage.iconsEnabled
}
ComboBox {
id: fontFamilyCombo
Kirigami.FormData.label: i18n("Font:")
model: Qt.fontFamilies()
editable: true
currentIndex: {
var families = Qt.fontFamilies();
var idx = families.indexOf(cfg_fontFamily);
return idx >= 0 ? idx : 0;
}
onActivated: {
cfg_fontFamily = currentText;
}
onAccepted: {
cfg_fontFamily = editText;
}
popup.height: 300
}
Slider {
id: fontSizeSlider
Kirigami.FormData.label: i18n("Font size:")
from: 0
to: 24
stepSize: 1
value: 0
}
Label {
text: fontSizeSlider.value === 0 ? i18n("System default") : fontSizeSlider.value + " px"
opacity: 0.7
}
CheckBox {
id: fontBoldCheck
Kirigami.FormData.label: i18n("Bold font:")
text: i18n("Bold")
}
Slider {
id: intervalSlider
Kirigami.FormData.label: i18n("Update interval:")
from: 1000
to: 10000
stepSize: 500
value: 2000
}
Label {
text: (intervalSlider.value / 1000).toFixed(1) + " " + i18n("seconds")
opacity: 0.7
}
Kirigami.Separator {
Kirigami.FormData.isSection: true
Kirigami.FormData.label: i18n("Unit Preferences")
}
ComboBox {
id: tempUnitCombo
Kirigami.FormData.label: i18n("Temperature unit:")
model: [i18n("Celsius (°C)"), i18n("Fahrenheit (°F)")]
currentIndex: cfg_tempUnit === "F" ? 1 : 0
onActivated: cfg_tempUnit = (currentIndex === 1 ? "F" : "C")
}
ComboBox {
id: networkUnitCombo
Kirigami.FormData.label: i18n("Network/Disk I/O unit:")
model: [i18n("Bytes (KB, MB)"), i18n("Bits (Kb, Mb)")]
currentIndex: cfg_networkUnit === "bits" ? 1 : 0
onActivated: cfg_networkUnit = (currentIndex === 1 ? "bits" : "bytes")
}
ComboBox {
id: fanUnitCombo
Kirigami.FormData.label: i18n("Fan Speed unit:")
model: [i18n("RPM"), i18n("Percentage (%)")]
currentIndex: cfg_fanUnit === "percent" ? 1 : 0
onActivated: cfg_fanUnit = (currentIndex === 1 ? "percent" : "rpm")
}
}
}
@@ -0,0 +1,131 @@
import QtQuick 2.0
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
import org.kde.kirigami 2.5 as Kirigami
import org.kde.kcmutils as KCM
import org.kde.iconthemes as KIconThemes
KCM.SimpleKCM {
id: iconsPage
property string cfg_cpuIcon: "cpu"
property string cfg_ramIcon: "memory"
property string cfg_tempIcon: "temperature-normal"
property string cfg_gpuIcon: "video-card"
property string cfg_batteryIcon: "battery-good"
property string cfg_powerIcon: "battery-charging-60"
property string cfg_networkIcon: "network-wireless"
property string cfg_diskIcon: "drive-harddisk"
property string cfg_fanIcon: "fan"
KIconThemes.IconDialog {
id: cpuIconDialog
onIconNameChanged: if (iconName) cfg_cpuIcon = iconName
}
KIconThemes.IconDialog {
id: ramIconDialog
onIconNameChanged: if (iconName) cfg_ramIcon = iconName
}
KIconThemes.IconDialog {
id: tempIconDialog
onIconNameChanged: if (iconName) cfg_tempIcon = iconName
}
KIconThemes.IconDialog {
id: gpuIconDialog
onIconNameChanged: if (iconName) cfg_gpuIcon = iconName
}
KIconThemes.IconDialog {
id: batteryIconDialog
onIconNameChanged: if (iconName) cfg_batteryIcon = iconName
}
KIconThemes.IconDialog {
id: powerIconDialog
onIconNameChanged: if (iconName) cfg_powerIcon = iconName
}
KIconThemes.IconDialog {
id: networkIconDialog
onIconNameChanged: if (iconName) cfg_networkIcon = iconName
}
KIconThemes.IconDialog {
id: diskIconDialog
onIconNameChanged: if (iconName) cfg_diskIcon = iconName
}
KIconThemes.IconDialog {
id: fanIconDialog
onIconNameChanged: if (iconName) cfg_fanIcon = iconName
}
Kirigami.FormLayout {
RowLayout {
Kirigami.FormData.label: i18n("CPU:")
Kirigami.Icon { source: cfg_cpuIcon; isMask: true; Layout.preferredWidth: 22; Layout.preferredHeight: 22 }
Button { text: i18n("Change..."); onClicked: cpuIconDialog.open(); icon.name: "document-edit" }
}
RowLayout {
Kirigami.FormData.label: i18n("RAM:")
Kirigami.Icon { source: cfg_ramIcon; isMask: true; Layout.preferredWidth: 22; Layout.preferredHeight: 22 }
Button { text: i18n("Change..."); onClicked: ramIconDialog.open(); icon.name: "document-edit" }
}
RowLayout {
Kirigami.FormData.label: i18n("Temperature:")
Kirigami.Icon { source: cfg_tempIcon; isMask: true; Layout.preferredWidth: 22; Layout.preferredHeight: 22 }
Button { text: i18n("Change..."); onClicked: tempIconDialog.open(); icon.name: "document-edit" }
}
RowLayout {
Kirigami.FormData.label: i18n("GPU:")
Kirigami.Icon { source: cfg_gpuIcon; isMask: true; Layout.preferredWidth: 22; Layout.preferredHeight: 22 }
Button { text: i18n("Change..."); onClicked: gpuIconDialog.open(); icon.name: "document-edit" }
}
RowLayout {
Kirigami.FormData.label: i18n("Battery:")
Kirigami.Icon { source: cfg_batteryIcon; isMask: true; Layout.preferredWidth: 22; Layout.preferredHeight: 22 }
Button { text: i18n("Change..."); onClicked: batteryIconDialog.open(); icon.name: "document-edit" }
}
RowLayout {
Kirigami.FormData.label: i18n("Power:")
Kirigami.Icon { source: cfg_powerIcon; isMask: true; Layout.preferredWidth: 22; Layout.preferredHeight: 22 }
Button { text: i18n("Change..."); onClicked: powerIconDialog.open(); icon.name: "document-edit" }
}
RowLayout {
Kirigami.FormData.label: i18n("Network:")
Kirigami.Icon { source: cfg_networkIcon; isMask: true; Layout.preferredWidth: 22; Layout.preferredHeight: 22 }
Button { text: i18n("Change..."); onClicked: networkIconDialog.open(); icon.name: "document-edit" }
}
RowLayout {
Kirigami.FormData.label: i18n("Disk:")
Kirigami.Icon { source: cfg_diskIcon; isMask: true; Layout.preferredWidth: 22; Layout.preferredHeight: 22 }
Button { text: i18n("Change..."); onClicked: diskIconDialog.open(); icon.name: "document-edit" }
}
RowLayout {
Kirigami.FormData.label: i18n("Fan:")
Kirigami.Icon { source: cfg_fanIcon; isMask: true; Layout.preferredWidth: 22; Layout.preferredHeight: 22 }
Button { text: i18n("Change..."); onClicked: fanIconDialog.open(); icon.name: "document-edit" }
}
Button {
icon.name: "edit-undo"
text: i18n("Reset to defaults")
Kirigami.FormData.label: " "
onClicked: {
cfg_cpuIcon = "cpu";
cfg_ramIcon = "memory";
cfg_tempIcon = "temperature-normal";
cfg_gpuIcon = "video-card";
cfg_batteryIcon = "battery-good";
cfg_powerIcon = "battery-charging-60";
cfg_networkIcon = "network-wireless";
cfg_diskIcon = "drive-harddisk";
cfg_fanIcon = "fan";
}
}
}
}
@@ -0,0 +1,669 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kcmutils as KCM
import org.kde.plasma.plasma5support as Plasma5Support
import org.kde.ksysguard.sensors as Sensors
import org.kde.kitemmodels as KItemModels
KCM.SimpleKCM {
id: metricsPage
// --- cfg_ bindings (must match main.xml keys exactly) ---
property bool cfg_showCpu
property bool cfg_showRam
property bool cfg_showTemp
property bool cfg_showGpu
property bool cfg_showBattery
property bool cfg_showPower
property bool cfg_showNetwork
property bool cfg_showDisk
property bool cfg_showFan
property bool cfg_compactShowCpu
property bool cfg_compactShowRam
property bool cfg_compactShowTemp
property bool cfg_compactShowGpu
property bool cfg_compactShowBattery
property bool cfg_compactShowPower
property bool cfg_compactShowNetwork
property bool cfg_compactShowDisk
property bool cfg_compactShowFan
property string cfg_networkInterface: "auto"
property string cfg_batteryDevice
property string cfg_gpuSelection: ""
property string cfg_gpuLabels: ""
property string cfg_metricOrder: "cpu,ram,temp,gpu,bat,pwr,net,disk"
property bool cfg_mergeCpuTemp: false
property bool cfg_mergeCpuFreq: false
property bool cfg_mergeBatPwr: false
property bool cfg_splitGpu: false
property string cfg_gpuMetrics: ""
property bool cfg_showCpuFreq: false
// --- GPU discovery via SensorTreeModel (pure metadata, no polling) ---
Sensors.SensorTreeModel { id: cfgSensorTree }
KItemModels.KDescendantsProxyModel { id: cfgFlatSensors; model: cfgSensorTree }
property var _liveDiscoveredGpus: []
Timer {
id: gpuRefreshDebounce
interval: 100
repeat: false
onTriggered: {
var found = [];
for (var row = 0; row < cfgFlatSensors.rowCount(); row++) {
var idx = cfgFlatSensors.index(row, 0);
var sensorId = cfgFlatSensors.data(idx, Sensors.SensorTreeModel.SensorId);
if (!sensorId || sensorId.length === 0) continue;
var match = sensorId.match(/^gpu\/(gpu\d+)\/usage$/);
if (!match) continue;
found.push({ id: match[1], name: "GPU " + (found.length + 1) });
}
if (JSON.stringify(found) !== JSON.stringify(_liveDiscoveredGpus))
_liveDiscoveredGpus = found;
}
}
function refreshConfigGpus() {
gpuRefreshDebounce.restart();
}
property bool _discoveryDirty: false
Timer {
id: discoveryTimer
interval: 500
repeat: false
running: _discoveryDirty
onTriggered: {
_discoveryDirty = false;
metricsPage.refreshConfigGpus();
}
}
Connections {
target: cfgFlatSensors
function onRowsInserted() { metricsPage._discoveryDirty = true; }
function onRowsRemoved() { metricsPage._discoveryDirty = true; }
function onModelReset() { metricsPage._discoveryDirty = true; }
function onDataChanged() { metricsPage._discoveryDirty = true; }
}
readonly property var discoveredGpus: {
if (_liveDiscoveredGpus.length > 0) return _liveDiscoveredGpus;
if (!cfg_gpuDiscovered) return [];
return cfg_gpuDiscovered.split(",").filter(function(s){ return s.indexOf(":") >= 0; }).map(function(s){
var parts = s.split(":");
return { id: parts[0], name: parts.slice(1).join(":") };
});
}
// --- Network interface discovery ---
property var ifaceList: ["auto"]
Plasma5Support.DataSource {
id: ifaceSource
engine: "executable"
connectedSources: ["ls /sys/class/net/"]
onNewData: function (source, data) {
if (data["exit code"] !== 0) return;
var raw = data["stdout"].trim();
if (raw.length === 0) return;
var ifaces = raw.split("\n").filter(function (name) {
return name !== "lo" && name.length > 0;
});
ifaces.unshift("auto");
metricsPage.ifaceList = ifaces;
}
}
// --- GPU label helpers ---
function parseGpuLabels(str) {
var result = {};
if (!str) return result;
str.split("|").forEach(function(pair) {
var sep = pair.indexOf(":");
if (sep > 0) result[pair.substring(0, sep)] = pair.substring(sep + 1);
});
return result;
}
function saveGpuLabel(gpuId, label) {
var labels = parseGpuLabels(cfg_gpuLabels);
var trimmed = (label || "").trim();
if (trimmed.length > 0) labels[gpuId] = trimmed;
else delete labels[gpuId];
var parts = [];
for (var id in labels) parts.push(id + ":" + labels[id]);
cfg_gpuLabels = parts.join("|");
}
// --- GPU sub-metric helpers ---
function parseGpuMetrics(str) {
var result = {};
if (!str) return result;
str.split("|").forEach(function(pair) {
var sep = pair.indexOf(":");
if (sep <= 0) return;
var id = pair.substring(0, sep);
var metrics = pair.substring(sep + 1).split(",").filter(function(m){
return m === "usage" || m === "vram" || m === "temp";
});
if (metrics.length > 0) result[id] = metrics;
});
return result;
}
function gpuMetricsFor(gpuId) {
var mm = parseGpuMetrics(cfg_gpuMetrics);
return (mm[gpuId] && mm[gpuId].length > 0) ? mm[gpuId] : ["usage", "vram", "temp"];
}
function saveGpuMetric(gpuId, metric, enable) {
var mm = parseGpuMetrics(cfg_gpuMetrics);
var current = mm[gpuId] ? mm[gpuId].slice() : ["usage", "vram", "temp"];
if (enable) {
if (current.indexOf(metric) < 0) current.push(metric);
} else {
if (current.length <= 1) return;
current = current.filter(function(m){ return m !== metric; });
}
var order = ["usage", "vram", "temp"];
current.sort(function(a, b){ return order.indexOf(a) - order.indexOf(b); });
if (current.length === 3) delete mm[gpuId];
else mm[gpuId] = current;
var parts = [];
for (var id in mm) parts.push(id + ":" + mm[id].join(","));
cfg_gpuMetrics = parts.join("|");
}
// --- Metric order helpers ---
readonly property var allKeys: ["cpu", "ram", "temp", "gpu", "bat", "pwr", "net", "disk", "fan"]
readonly property var metricMeta: ({
"cpu": { label: i18n("CPU usage"), icon: "cpu" },
"ram": { label: i18n("RAM usage"), icon: "memory" },
"temp": { label: i18n("CPU temperature"), icon: "temperature-normal" },
"gpu": { label: i18n("GPU metrics"), icon: "video-card" },
"bat": { label: i18n("Battery status"), icon: "battery-good" },
"pwr": { label: i18n("Power consumption"), icon: "battery-charging-60" },
"net": { label: i18n("Network speed"), icon: "network-wireless" },
"disk": { label: i18n("Disk I/O & temp"), icon: "drive-harddisk" },
"fan": { label: i18n("Fan speed"), icon: "fan" }
})
property var currentOrder: {
var keys = cfg_metricOrder.split(",").map(function(k){ return k.trim(); })
.filter(function(k){ return k.length > 0 && metricMeta[k] !== undefined; });
allKeys.forEach(function(k){ if (keys.indexOf(k) < 0) keys.push(k); });
return keys;
}
function isChecked(key) {
switch (key) {
case "cpu": return cfg_showCpu;
case "ram": return cfg_showRam;
case "temp": return cfg_showTemp;
case "gpu": return cfg_showGpu;
case "bat": return cfg_showBattery;
case "pwr": return cfg_showPower;
case "net": return cfg_showNetwork;
case "disk": return cfg_showDisk;
case "fan": return cfg_showFan;
}
return false;
}
function isCompactChecked(key) {
switch (key) {
case "cpu": return cfg_compactShowCpu;
case "ram": return cfg_compactShowRam;
case "temp": return cfg_compactShowTemp;
case "gpu": return cfg_compactShowGpu;
case "bat": return cfg_compactShowBattery;
case "pwr": return cfg_compactShowPower;
case "net": return cfg_compactShowNetwork;
case "disk": return cfg_compactShowDisk;
case "fan": return cfg_compactShowFan;
}
return false;
}
function setChecked(key, val) {
switch (key) {
case "cpu": cfg_showCpu = val; break;
case "ram": cfg_showRam = val; break;
case "temp": cfg_showTemp = val; break;
case "gpu": cfg_showGpu = val; break;
case "bat": cfg_showBattery = val; break;
case "pwr": cfg_showPower = val; break;
case "net": cfg_showNetwork = val; break;
case "disk": cfg_showDisk = val; break;
case "fan": cfg_showFan = val; break;
}
if (!val) {
if ((key === "cpu" || key === "temp") && cfg_mergeCpuTemp) cfg_mergeCpuTemp = false;
if ((key === "bat" || key === "pwr") && cfg_mergeBatPwr) cfg_mergeBatPwr = false;
if (key === "gpu" && cfg_splitGpu) cfg_splitGpu = false;
}
}
function setCompactChecked(key, val) {
switch (key) {
case "cpu": cfg_compactShowCpu = val; break;
case "ram": cfg_compactShowRam = val; break;
case "temp": cfg_compactShowTemp = val; break;
case "gpu": cfg_compactShowGpu = val; break;
case "bat": cfg_compactShowBattery = val; break;
case "pwr": cfg_compactShowPower = val; break;
case "net": cfg_compactShowNetwork = val; break;
case "disk": cfg_compactShowDisk = val; break;
case "fan": cfg_compactShowFan = val; break;
}
}
function moveMetric(fromIndex, toIndex) {
var keys = currentOrder.slice();
var item = keys.splice(fromIndex, 1)[0];
keys.splice(toIndex, 0, item);
cfg_metricOrder = keys.join(",");
}
// =========================================================================
// UI
// =========================================================================
// Prevent "ugly to correct" UI flash during initial layout calculations
property bool _isReady: false
Timer {
id: readyTimer
interval: 100
running: true
repeat: false
onTriggered: metricsPage._isReady = true
}
BusyIndicator {
anchors.centerIn: metricsPage
running: !metricsPage._isReady
visible: running
z: 999
}
Kirigami.FormLayout {
opacity: metricsPage._isReady ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 150 } }
// ----- Section header -----
Kirigami.Separator {
Kirigami.FormData.isSection: true
Kirigami.FormData.label: i18n("Visibility & Order")
}
// Column headers row
RowLayout {
spacing: Kirigami.Units.smallSpacing
Label {
text: i18n("Metric (Full view)")
font.bold: true
opacity: 0.7
Layout.preferredWidth: Kirigami.Units.gridUnit * 14
}
Label {
text: i18n("Compact")
font.bold: true
opacity: 0.7
Layout.preferredWidth: Kirigami.Units.gridUnit * 4
horizontalAlignment: Text.AlignHCenter
}
Item { Layout.fillWidth: true }
}
// Metric rows
ColumnLayout {
spacing: 0
Layout.fillWidth: true
Repeater {
model: metricsPage.currentOrder
delegate: ColumnLayout {
id: metricDelegate
required property var modelData
required property int index
spacing: 0
Layout.fillWidth: true
// Hide metrics absorbed by a merge group
visible: !(modelData === "temp" && cfg_mergeCpuTemp && cfg_showCpu) &&
!(modelData === "pwr" && cfg_mergeBatPwr && cfg_showBattery)
// Resolved shorthands for readability inside this delegate
readonly property bool metricEnabled: metricsPage.isChecked(modelData)
readonly property string metricLabel: (metricsPage.metricMeta[modelData] || {}).label || modelData
readonly property string metricIcon: (metricsPage.metricMeta[modelData] || {}).icon || "help-about"
// ── Main row ──────────────────────────────────────────────
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
// Metric toggle
CheckBox {
id: enabledCheck
text: metricDelegate.metricLabel
checked: metricDelegate.metricEnabled
onToggled: metricsPage.setChecked(modelData, checked)
Layout.preferredWidth: Kirigami.Units.gridUnit * 14
}
// Compact-view toggle
Item {
Layout.preferredWidth: Kirigami.Units.gridUnit * 4
Layout.minimumHeight: compactCheck.implicitHeight
CheckBox {
id: compactCheck
anchors.centerIn: parent
checked: metricsPage.isCompactChecked(modelData)
enabled: metricDelegate.metricEnabled
onToggled: metricsPage.setCompactChecked(modelData, checked)
// Accessible name for keyboard/screen-reader users
Accessible.name: i18n("Show %1 in compact panel", metricDelegate.metricLabel)
}
}
// Spacer
Item { Layout.fillWidth: true }
// Reorder buttons
Button {
icon.name: "arrow-up"
flat: true
enabled: index > 0
implicitWidth: Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Units.gridUnit * 2
onClicked: metricsPage.moveMetric(index, index - 1)
ToolTip.text: i18n("Move up")
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
}
Button {
icon.name: "arrow-down"
flat: true
enabled: index < metricsPage.currentOrder.length - 1
implicitWidth: Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Units.gridUnit * 2
onClicked: metricsPage.moveMetric(index, index + 1)
ToolTip.text: i18n("Move down")
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
// ── Inline per-metric settings (visible only when enabled) ──
// Each block is keyed to its metric so context is never lost.
// CPU settings
Loader {
active: modelData === "cpu" && metricDelegate.metricEnabled
visible: active
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
sourceComponent: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
RowLayout {
spacing: Kirigami.Units.largeSpacing
CheckBox {
text: i18n("Show CPU frequency")
checked: cfg_showCpuFreq
onToggled: {
cfg_showCpuFreq = checked;
if (!checked) cfg_mergeCpuFreq = false;
}
}
CheckBox {
text: i18n("Merge frequency into compact view")
checked: cfg_mergeCpuFreq
enabled: cfg_showCpuFreq
onToggled: cfg_mergeCpuFreq = checked
}
}
CheckBox {
visible: cfg_showTemp
text: i18n("Show temperature next to usage (merge CPU & temp)")
checked: cfg_mergeCpuTemp
enabled: cfg_showTemp
onToggled: cfg_mergeCpuTemp = checked
}
}
}
// Network settings
Loader {
active: modelData === "net" && metricDelegate.metricEnabled
visible: active
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
sourceComponent: RowLayout {
spacing: Kirigami.Units.smallSpacing
Label { text: i18n("Interface:") }
ComboBox {
id: ifaceCombo
model: metricsPage.ifaceList
currentIndex: {
var idx = metricsPage.ifaceList.indexOf(cfg_networkInterface);
return idx >= 0 ? idx : 0;
}
onActivated: cfg_networkInterface = metricsPage.ifaceList[currentIndex]
implicitWidth: Kirigami.Units.gridUnit * 10
}
}
}
// Battery settings
Loader {
active: modelData === "bat" && metricDelegate.metricEnabled
visible: active
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
sourceComponent: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
RowLayout {
spacing: Kirigami.Units.smallSpacing
Label { text: i18n("Device:") }
TextField {
id: batteryInput
text: cfg_batteryDevice === "auto" ? "" : cfg_batteryDevice
placeholderText: i18n("Leave empty for auto-detect (e.g. BAT0)")
implicitWidth: Kirigami.Units.gridUnit * 14
onTextEdited: {
var v = text.trim();
cfg_batteryDevice = v.length > 0 ? v : "auto";
}
}
}
CheckBox {
visible: cfg_showPower
text: i18n("Show power consumption next to battery level (merge battery & power)")
checked: cfg_mergeBatPwr
enabled: cfg_showPower
onToggled: cfg_mergeBatPwr = checked
}
}
}
// GPU settings
Loader {
active: modelData === "gpu" && metricDelegate.metricEnabled
visible: active
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
sourceComponent: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
// Split GPU option
CheckBox {
text: i18n("Show usage, VRAM and temperature as separate entries (split GPU metrics)")
checked: cfg_splitGpu
onToggled: cfg_splitGpu = checked
}
// No GPU found yet (Loading state)
RowLayout {
visible: metricsPage.discoveredGpus.length === 0
spacing: Kirigami.Units.smallSpacing
BusyIndicator {
running: parent.visible
Layout.preferredWidth: Kirigami.Units.gridUnit
Layout.preferredHeight: Kirigami.Units.gridUnit
}
Label {
text: i18n("Discovering GPUs...")
opacity: 0.7
font.italic: true
}
}
// Hybrid GPU tip
Label {
visible: metricsPage.discoveredGpus.length > 1
text: i18n("Tip: on hybrid-GPU laptops, uncheck the discrete GPU to let it suspend and save power.")
opacity: 0.7
font.italic: true
wrapMode: Text.WordWrap
Layout.maximumWidth: Kirigami.Units.gridUnit * 24
}
// Per-GPU rows
Repeater {
id: gpuSelectorRepeater
model: metricsPage.discoveredGpus
delegate: ColumnLayout {
id: gpuDelegate
required property var modelData
spacing: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.smallSpacing
property var _activeMetrics: metricsPage.gpuMetricsFor(modelData.id)
property bool _gpuEnabled: {
if (!cfg_gpuSelection || cfg_gpuSelection === "") return true;
if (cfg_gpuSelection === "none") return false;
return cfg_gpuSelection.split(",").indexOf(modelData.id) >= 0;
}
// GPU enable checkbox
CheckBox {
text: gpuDelegate.modelData.name
checked: gpuDelegate._gpuEnabled
onToggled: {
var ids;
if (!cfg_gpuSelection || cfg_gpuSelection === "") {
ids = gpuSelectorRepeater.model.map(function(g){ return g.id; });
} else if (cfg_gpuSelection === "none") {
ids = [];
} else {
ids = cfg_gpuSelection.split(",").filter(function(s){ return s.length > 0; });
}
if (checked) {
if (ids.indexOf(modelData.id) < 0) ids.push(modelData.id);
} else {
ids = ids.filter(function(id){ return id !== modelData.id; });
}
var allIds = gpuSelectorRepeater.model.map(function(g){ return g.id; });
var allSelected = allIds.every(function(id){ return ids.indexOf(id) >= 0; });
if (allSelected) cfg_gpuSelection = "";
else if (ids.length === 0) cfg_gpuSelection = "none";
else cfg_gpuSelection = ids.join(",");
}
}
// Label + sub-metrics (indented under GPU checkbox)
ColumnLayout {
enabled: gpuDelegate._gpuEnabled
opacity: gpuDelegate._gpuEnabled ? 1.0 : 0.4
spacing: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing
// Custom label
RowLayout {
spacing: Kirigami.Units.smallSpacing
Label { text: i18n("Label:"); opacity: 0.8 }
TextField {
implicitWidth: Kirigami.Units.gridUnit * 12
text: metricsPage.parseGpuLabels(cfg_gpuLabels)[gpuDelegate.modelData.id] || ""
placeholderText: gpuDelegate.modelData.name
onTextEdited: metricsPage.saveGpuLabel(gpuDelegate.modelData.id, text)
}
}
// Sub-metric toggles
RowLayout {
spacing: Kirigami.Units.largeSpacing
Label { text: i18n("Show:"); opacity: 0.8 }
CheckBox {
text: i18n("Usage")
checked: gpuDelegate._activeMetrics.indexOf("usage") >= 0
enabled: !(checked && gpuDelegate._activeMetrics.length <= 1)
onToggled: metricsPage.saveGpuMetric(gpuDelegate.modelData.id, "usage", checked)
}
CheckBox {
text: i18n("VRAM")
checked: gpuDelegate._activeMetrics.indexOf("vram") >= 0
enabled: !(checked && gpuDelegate._activeMetrics.length <= 1)
onToggled: metricsPage.saveGpuMetric(gpuDelegate.modelData.id, "vram", checked)
}
CheckBox {
text: i18n("Temperature")
checked: gpuDelegate._activeMetrics.indexOf("temp") >= 0
enabled: !(checked && gpuDelegate._activeMetrics.length <= 1)
onToggled: metricsPage.saveGpuMetric(gpuDelegate.modelData.id, "temp", checked)
}
}
}
}
}
}
}
// Thin divider between rows (not after the last visible one)
Rectangle {
visible: {
for (var i = index + 1; i < metricsPage.currentOrder.length; i++) {
var nextData = metricsPage.currentOrder[i];
var isHidden = (nextData === "temp" && cfg_mergeCpuTemp && cfg_showCpu) ||
(nextData === "pwr" && cfg_mergeBatPwr && cfg_showBattery);
if (!isHidden) return true;
}
return false;
}
Layout.fillWidth: true
height: 1
color: Kirigami.Theme.textColor
opacity: 0.08
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
}
}
}
}
}
}
@@ -0,0 +1,558 @@
import QtQuick
import QtQuick.Layouts
import org.kde.plasma.plasmoid
import org.kde.plasma.components as PlasmaComponents
import org.kde.kirigami as Kirigami
import "./sensors"
PlasmoidItem {
id: root
property bool _dbg: { console.warn("[KVitals] main.qml: constructing..."); return true; }
preferredRepresentation: compactRepresentation
// --- Configuration properties ---
property bool showCpu: Plasmoid.configuration.showCpu
property bool showRam: Plasmoid.configuration.showRam
property bool showTemp: Plasmoid.configuration.showTemp
property bool showGpu: Plasmoid.configuration.showGpu
property bool showBattery: Plasmoid.configuration.showBattery
property bool showPower: Plasmoid.configuration.showPower
property bool showNetwork: Plasmoid.configuration.showNetwork
property bool showDisk: Plasmoid.configuration.showDisk
property bool showFan: Plasmoid.configuration.showFan
property bool compactShowCpu: Plasmoid.configuration.compactShowCpu
property bool compactShowRam: Plasmoid.configuration.compactShowRam
property bool compactShowTemp: Plasmoid.configuration.compactShowTemp
property bool compactShowGpu: Plasmoid.configuration.compactShowGpu
property bool compactShowBattery: Plasmoid.configuration.compactShowBattery
property bool compactShowPower: Plasmoid.configuration.compactShowPower
property bool compactShowNetwork: Plasmoid.configuration.compactShowNetwork
property bool compactShowDisk: Plasmoid.configuration.compactShowDisk
property bool compactShowFan: Plasmoid.configuration.compactShowFan
property string networkInterface: Plasmoid.configuration.networkInterface
property string batteryDevice: Plasmoid.configuration.batteryDevice
property string gpuSelection: Plasmoid.configuration.gpuSelection
property string gpuLabels: Plasmoid.configuration.gpuLabels
property string displayMode: Plasmoid.configuration.displayMode
property string layoutType: Plasmoid.configuration.layoutType
property bool mergeCpuTemp: Plasmoid.configuration.mergeCpuTemp
property bool mergeCpuFreq: Plasmoid.configuration.mergeCpuFreq
property bool showCpuFreq: Plasmoid.configuration.showCpuFreq
property bool mergeBatPwr: Plasmoid.configuration.mergeBatPwr
property bool splitGpu: Plasmoid.configuration.splitGpu
property string gpuMetrics: Plasmoid.configuration.gpuMetrics
property int iconSize: Plasmoid.configuration.iconSize
property string cpuIcon: Plasmoid.configuration.cpuIcon
property string ramIcon: Plasmoid.configuration.ramIcon
property string tempIcon: Plasmoid.configuration.tempIcon
property string gpuIcon: Plasmoid.configuration.gpuIcon
property string batteryIcon: Plasmoid.configuration.batteryIcon
property string powerIcon: Plasmoid.configuration.powerIcon
property string networkIcon: Plasmoid.configuration.networkIcon
property string diskIcon: Plasmoid.configuration.diskIcon
property string fanIcon: Plasmoid.configuration.fanIcon
property string fontFamily: Plasmoid.configuration.fontFamily
property int fontSize: Plasmoid.configuration.fontSize
property bool fontBold: Plasmoid.configuration.fontBold
property int effectiveFontSize: fontSize > 0 ? fontSize : Kirigami.Theme.smallFont.pixelSize
property bool useIcons: displayMode === "icons" || displayMode === "icons+text"
property bool useText: displayMode === "text" || displayMode === "icons+text"
property string metricOrder: Plasmoid.configuration.metricOrder || "cpu,ram,temp,gpu,bat,pwr,net,disk"
property var orderedKeys: metricOrder.split(",").map(function (k) {
return k.trim();
})
property int updateInterval: Plasmoid.configuration.updateInterval || 2000
property string tempUnit: Plasmoid.configuration.tempUnit || "C"
property string networkUnit: Plasmoid.configuration.networkUnit || "bytes"
property string fanUnit: Plasmoid.configuration.fanUnit || "rpm"
// --- Color configuration properties ---
property bool useCustomColors: Plasmoid.configuration.useCustomColors
property string fontColor: Plasmoid.configuration.fontColor
property bool enableThresholdColors: Plasmoid.configuration.enableThresholdColors
property string warningColor: Plasmoid.configuration.warningColor || "#e5a50a"
property string criticalColor: Plasmoid.configuration.criticalColor || "#da4453"
property int cpuWarningThreshold: Plasmoid.configuration.cpuWarningThreshold
property int cpuCriticalThreshold: Plasmoid.configuration.cpuCriticalThreshold
property int tempWarningThreshold: Plasmoid.configuration.tempWarningThreshold
property int tempCriticalThreshold: Plasmoid.configuration.tempCriticalThreshold
property int ramWarningThreshold: Plasmoid.configuration.ramWarningThreshold
property int ramCriticalThreshold: Plasmoid.configuration.ramCriticalThreshold
property int gpuWarningThreshold: Plasmoid.configuration.gpuWarningThreshold
property int gpuCriticalThreshold: Plasmoid.configuration.gpuCriticalThreshold
property int gpuTempWarningThreshold: Plasmoid.configuration.gpuTempWarningThreshold
property int gpuTempCriticalThreshold: Plasmoid.configuration.gpuTempCriticalThreshold
property int batteryWarningThreshold: Plasmoid.configuration.batteryWarningThreshold
property int batteryCriticalThreshold: Plasmoid.configuration.batteryCriticalThreshold
property int diskTempWarningThreshold: Plasmoid.configuration.diskTempWarningThreshold
property int diskTempCriticalThreshold: Plasmoid.configuration.diskTempCriticalThreshold
// --- Computed base text color ---
property color baseTextColor: (useCustomColors && fontColor !== "") ? fontColor : Kirigami.Theme.textColor
// --- Pre-resolved per-metric colors (reactive properties) ---
property color cpuColor: enableThresholdColors
? Utils.resolveColor(cpu.cpuNumericValue, cpuWarningThreshold, cpuCriticalThreshold,
warningColor, criticalColor, baseTextColor, false)
: baseTextColor
property color tempColor: enableThresholdColors
? Utils.resolveColor(temp.tempNumericValue, tempWarningThreshold, tempCriticalThreshold,
warningColor, criticalColor, baseTextColor, false)
: baseTextColor
property color ramColor: enableThresholdColors
? Utils.resolveColor(memory.ramPercentage, ramWarningThreshold, ramCriticalThreshold,
warningColor, criticalColor, baseTextColor, false)
: baseTextColor
property color gpuColor: enableThresholdColors
? Utils.resolveColor(gpu.gpuUsageNumber, gpuWarningThreshold, gpuCriticalThreshold,
warningColor, criticalColor, baseTextColor, false)
: baseTextColor
property color gpuTempColor: enableThresholdColors
? Utils.resolveColor(gpu.gpuTempNumber, gpuTempWarningThreshold, gpuTempCriticalThreshold,
warningColor, criticalColor, baseTextColor, false)
: baseTextColor
property color batteryColor: enableThresholdColors
? Utils.resolveColor(battery.batNumericValue, batteryWarningThreshold, batteryCriticalThreshold,
warningColor, criticalColor, baseTextColor, true)
: baseTextColor
property color diskTempColor: enableThresholdColors
? Utils.resolveColor(disk.diskTempNumber, diskTempWarningThreshold, diskTempCriticalThreshold,
warningColor, criticalColor, baseTextColor, false)
: baseTextColor
// --- Deferred sensor loading ---
// Sensors are loaded AFTER the window attachment is complete to avoid
// triggering a SIGSEGV in KirigamiPlasmaStyle's PlasmaTheme::syncColors()
// during the recursive QQuickItemPrivate::refWindow() walk at boot.
// See: https://github.com/nicehash/KVitals/issues/42
property bool _sensorsReady: sensorLoader.status === Loader.Ready
property var cpu: _sensorsReady ? sensorLoader.item.cpu : _nullCpu
property var memory: _sensorsReady ? sensorLoader.item.memory : _nullMemory
property var temp: _sensorsReady ? sensorLoader.item.temp : _nullTemp
property var gpu: _sensorsReady ? sensorLoader.item.gpu : _nullGpu
property var battery: _sensorsReady ? sensorLoader.item.battery : _nullBattery
property var network: _sensorsReady ? sensorLoader.item.network : _nullNetwork
property var disk: _sensorsReady ? sensorLoader.item.disk : _nullDisk
property var fans: _sensorsReady ? sensorLoader.item.fans : _nullFans
// Safe defaults so bindings don't error before sensors load
QtObject {
id: _nullCpu
property string cpuValue: ""
property string cpuFreqValue: ""
property real cpuNumericValue: NaN
}
QtObject {
id: _nullMemory
property string ramValue: ""
property real ramPercentage: NaN
}
QtObject {
id: _nullTemp
property string tempValue: "--"
property real tempNumericValue: NaN
}
QtObject {
id: _nullGpu
property real gpuUsageNumber: NaN
property real gpuTempNumber: NaN
property string gpuValue: ""
property string gpuRamValue: ""
property string gpuTempValue: ""
property string gpuDisplayValue: ""
property bool hasGpuData: false
property bool hasGpuUsageData: false
property bool hasGpuVramData: false
property bool hasGpuTempData: false
property var gpuDataList: []
property var discoveredGpus: []
}
QtObject {
id: _nullBattery
property string batValue: ""
property string powerValue: ""
property real batNumericValue: NaN
}
QtObject {
id: _nullNetwork
property string netDownValue: "0"
property string netUpValue: "0"
}
QtObject {
id: _nullDisk
property string diskReadValue: "0"
property string diskWriteValue: "0"
property string diskTempValue: ""
property real diskTempNumber: NaN
}
QtObject {
id: _nullFans
property string fanValue: ""
property bool hasFanData: false
}
Loader {
id: sensorLoader
active: false
sourceComponent: Item {
property alias cpu: _cpu
property alias memory: _memory
property alias temp: _temp
property alias gpu: _gpu
property alias battery: _battery
property alias network: _network
property alias disk: _disk
property alias fans: _fans
CpuSensors {
id: _cpu
updateInterval: root.updateInterval
}
MemorySensors {
id: _memory
updateInterval: root.updateInterval
}
TempSensors {
id: _temp
updateInterval: root.updateInterval
tempUnit: root.tempUnit
}
GpuSensors {
id: _gpu
updateInterval: root.updateInterval
gpuSelection: root.gpuSelection
gpuLabels: root.gpuLabels
tempUnit: root.tempUnit
gpuMetrics: root.gpuMetrics
}
BatterySensors {
id: _battery
updateInterval: root.updateInterval
batteryDevice: root.batteryDevice || "auto"
}
NetworkSensors {
id: _network
updateInterval: root.updateInterval
networkInterface: root.networkInterface
networkUnit: root.networkUnit
}
DiskSensors {
id: _disk
updateInterval: root.updateInterval
enabled: root.showDisk
tempUnit: root.tempUnit
networkUnit: root.networkUnit
}
FanSensors {
id: _fans
updateInterval: root.updateInterval
fanUnit: root.fanUnit
}
}
}
// Activate the sensor loader after the initial refWindow() walk is complete.
// The SIGSEGV in PlasmaTheme::syncColors() happens synchronously during
// ShellCorona::load() → refWindow(), so deferring to the next event loop
// iteration (Timer interval: 0) guarantees we're past the dangerous window.
Timer {
id: sensorActivationTimer
interval: 0
repeat: false
onTriggered: {
console.warn("[KVitals] main.qml: deferred load — activating sensors...");
sensorLoader.active = true;
}
}
Component.onCompleted: {
console.warn("[KVitals] main.qml: ready. config: showCpu=" + showCpu + " showGpu=" + showGpu + " showBattery=" + showBattery + " showNetwork=" + showNetwork + " showDisk=" + showDisk);
sensorActivationTimer.start();
}
// --- Representations ---
compactRepresentation: CompactView {
metricsModel: {
var items = [];
for (var i = 0; i < root.orderedKeys.length; i++) {
var key = root.orderedKeys[i];
if (key === "cpu" && root.showCpu && root.compactShowCpu && cpu.cpuValue) {
if (root.mergeCpuTemp && root.showTemp && root.compactShowTemp && temp.tempValue && temp.tempValue !== "--") {
var segs = [{value: cpu.cpuValue, color: root.cpuColor},
{value: temp.tempValue, color: root.tempColor}];
if (root.mergeCpuFreq && root.showCpuFreq)
segs.push({value: cpu.cpuFreqValue, color: root.baseTextColor});
items.push({icon: root.cpuIcon, label: "CPU:", color: root.cpuColor, segments: segs});
} else if (root.mergeCpuFreq && root.showCpuFreq) {
items.push({
icon: root.cpuIcon, label: "CPU:", color: root.cpuColor,
segments: [
{value: cpu.cpuValue, color: root.cpuColor},
{value: cpu.cpuFreqValue, color: root.baseTextColor}
]
});
} else {
items.push({icon: root.cpuIcon, label: "CPU:", value: cpu.cpuValue, color: root.cpuColor});
}
} else if (key === "ram" && root.showRam && root.compactShowRam && memory.ramValue)
items.push({
icon: root.ramIcon, label: "RAM:", value: memory.ramValue,
color: root.ramColor
});
else if (key === "temp" && root.showTemp && root.compactShowTemp && temp.tempValue && temp.tempValue !== "--"
&& !(root.mergeCpuTemp && root.showCpu && root.compactShowCpu))
items.push({
icon: root.tempIcon, label: "TEMP:", value: temp.tempValue,
color: root.tempColor
});
else if (key === "gpu" && root.showGpu && root.compactShowGpu && gpu.hasGpuData) {
var multiGpu = gpu.gpuDataList.length > 1;
if (multiGpu) {
// Show each GPU as a separate entry
for (var g = 0; g < gpu.gpuDataList.length; g++) {
var gd = gpu.gpuDataList[g];
var label = (gd.name.length > 0 ? gd.name : gd.id) + ":";
if (root.splitGpu) {
if (gd.usage) items.push({icon: root.gpuIcon, label: label, value: gd.usage, color: root.gpuColor});
if (gd.vram) items.push({icon: root.gpuIcon, label: "VRAM:", value: gd.vram, color: root.baseTextColor});
if (gd.temp) items.push({icon: root.gpuIcon, label: "GTEMP:", value: gd.temp, color: root.gpuTempColor});
} else {
var segs2 = [];
if (gd.usage) segs2.push({value: gd.usage, color: root.gpuColor});
if (gd.vram) segs2.push({value: gd.vram, color: root.baseTextColor});
if (gd.temp) segs2.push({value: gd.temp, color: root.gpuTempColor});
if (segs2.length > 0)
items.push({icon: root.gpuIcon, label: label, segments: segs2, color: root.gpuColor});
}
}
} else if (root.splitGpu) {
if (gpu.hasGpuUsageData)
items.push({icon: root.gpuIcon, label: "GPU:", value: gpu.gpuValue, color: root.gpuColor});
if (gpu.hasGpuVramData)
items.push({
icon: root.gpuIcon,
label: "VRAM:",
value: gpu.gpuRamValue,
color: root.baseTextColor
});
if (gpu.hasGpuTempData)
items.push({
icon: root.gpuIcon,
label: "GTEMP:",
value: gpu.gpuTempValue,
color: root.gpuTempColor
});
} else {
var gpuSegs = [];
if (gpu.hasGpuUsageData)
gpuSegs.push({value: gpu.gpuValue, color: root.gpuColor});
if (gpu.hasGpuVramData)
gpuSegs.push({value: gpu.gpuRamValue, color: root.baseTextColor});
if (gpu.hasGpuTempData)
gpuSegs.push({value: gpu.gpuTempValue, color: root.gpuTempColor});
items.push({
icon: root.gpuIcon, label: "GPU:", segments: gpuSegs,
color: root.gpuColor
});
}
} else if (key === "bat" && root.showBattery && root.compactShowBattery && battery.batValue) {
if (root.mergeBatPwr && root.showPower && root.compactShowPower && battery.powerValue) {
items.push({
icon: root.batteryIcon, label: "BAT:",
color: root.batteryColor,
segments: [
{value: battery.batValue, color: root.batteryColor},
{value: battery.powerValue, color: root.baseTextColor}
]
});
} else {
items.push({
icon: root.batteryIcon, label: "BAT:", value: battery.batValue,
color: root.batteryColor
});
}
} else if (key === "pwr" && root.showPower && root.compactShowPower && battery.powerValue
&& !(root.mergeBatPwr && root.showBattery && root.compactShowBattery))
items.push({
icon: root.powerIcon, label: "PWR:", value: battery.powerValue,
color: root.baseTextColor
});
else if (key === "net" && root.showNetwork && root.compactShowNetwork)
items.push({
icon: root.networkIcon, label: "NET:",
value: "↓" + network.netDownValue + " ↑" + network.netUpValue,
color: root.baseTextColor
});
else if (key === "disk" && root.showDisk && root.compactShowDisk) {
var diskSegs = [{value: "↓" + disk.diskReadValue, color: root.baseTextColor},
{value: "↑" + disk.diskWriteValue, color: root.baseTextColor}];
if (disk.diskTempValue)
diskSegs.push({value: disk.diskTempValue, color: root.diskTempColor});
items.push({icon: root.diskIcon, label: "DSK:", segments: diskSegs, color: root.baseTextColor});
}
else if (key === "fan" && root.showFan && root.compactShowFan && fans.hasFanData) {
items.push({
icon: root.fanIcon, label: "FAN:", value: fans.fanValue,
color: root.baseTextColor
});
}
}
return items;
}
layoutType: root.layoutType
useIcons: root.useIcons
useText: root.useText
effectiveFontSize: root.effectiveFontSize
fontFamily: root.fontFamily
fontBold: root.fontBold
iconSize: root.iconSize
baseTextColor: root.baseTextColor
onToggleExpanded: root.expanded = !root.expanded
}
fullRepresentation: FullView {
baseTextColor: root.baseTextColor
fontBold: root.fontBold
metricsModel: {
var items = [];
for (var i = 0; i < root.orderedKeys.length; i++) {
var key = root.orderedKeys[i];
if (key === "cpu" && root.showCpu)
items.push({
label: "CPU Usage", value: cpu.cpuValue,
color: root.cpuColor
});
if (key === "cpu" && root.showCpu && root.showCpuFreq)
items.push({
label: "CPU Frequency", value: cpu.cpuFreqValue,
color: root.baseTextColor
});
else if (key === "ram" && root.showRam)
items.push({
label: "Memory", value: memory.ramValue,
color: root.ramColor
});
else if (key === "temp" && root.showTemp && temp.tempValue !== "--")
items.push({
label: "CPU Temp", value: temp.tempValue,
color: root.tempColor
});
else if (key === "gpu" && root.showGpu) {
if (gpu.gpuDataList.length > 1) {
for (var g = 0; g < gpu.gpuDataList.length; g++) {
var gd = gpu.gpuDataList[g];
var label = gd.name || gd.id;
if (gd.usage) items.push({label: label + " Usage", value: gd.usage, color: root.gpuColor});
if (gd.vram) items.push({label: label + " VRAM", value: gd.vram, color: root.baseTextColor});
if (gd.temp) items.push({label: label + " Temp", value: gd.temp, color: root.gpuTempColor});
}
} else {
if (gpu.hasGpuUsageData) items.push({
label: "GPU Usage", value: gpu.gpuValue,
color: root.gpuColor
});
if (gpu.hasGpuVramData) items.push({
label: "GPU VRAM", value: gpu.gpuRamValue,
color: root.baseTextColor
});
if (gpu.hasGpuTempData) items.push({
label: "GPU Temp", value: gpu.gpuTempValue,
color: root.gpuTempColor
});
}
} else if (key === "bat" && root.showBattery && battery.batValue)
items.push({
label: "Battery", value: battery.batValue,
color: root.batteryColor
});
else if (key === "pwr" && root.showPower && battery.powerValue)
items.push({
label: "Power", value: battery.powerValue,
color: root.baseTextColor
});
else if (key === "net" && root.showNetwork) {
items.push({label: "Network ↓", value: network.netDownValue, color: root.baseTextColor});
items.push({label: "Network ↑", value: network.netUpValue, color: root.baseTextColor});
}
else if (key === "disk" && root.showDisk) {
items.push({label: "Disk Read", value: disk.diskReadValue, color: root.baseTextColor});
items.push({label: "Disk Write", value: disk.diskWriteValue, color: root.baseTextColor});
if (disk.diskTempValue)
items.push({label: "Disk Temp", value: disk.diskTempValue, color: root.diskTempColor});
}
else if (key === "fan" && root.showFan && fans.hasFanData) {
items.push({label: "Fans", value: fans.fanValue, color: root.baseTextColor});
}
}
return items;
}
}
// --- Tooltip ---
toolTipMainText: "KVitals"
toolTipSubText: {
var parts = [];
for (var i = 0; i < root.orderedKeys.length; i++) {
var key = root.orderedKeys[i];
if (key === "cpu" && root.showCpu && cpu.cpuValue)
parts.push("CPU: " + cpu.cpuValue);
else if (key === "ram" && root.showRam && memory.ramValue)
parts.push("RAM: " + memory.ramValue);
else if (key === "temp" && root.showTemp && temp.tempValue && temp.tempValue !== "--")
parts.push("TEMP: " + temp.tempValue);
else if (key === "gpu" && root.showGpu && gpu.hasGpuData) {
if (gpu.gpuDataList.length > 1) {
for (var g = 0; g < gpu.gpuDataList.length; g++) {
var gd = gpu.gpuDataList[g];
var label = gd.name || gd.id;
var vals = [gd.usage, gd.vram, gd.temp].filter(function(v) { return v; });
if (vals.length > 0) parts.push(label + ": " + vals.join(" "));
}
} else {
if (gpu.hasGpuUsageData) parts.push("GPU: " + gpu.gpuValue);
if (gpu.hasGpuVramData) parts.push("VRAM: " + gpu.gpuRamValue);
if (gpu.hasGpuTempData) parts.push("GPU TEMP: " + gpu.gpuTempValue);
}
} else if (key === "bat" && root.showBattery && battery.batValue)
parts.push("BAT: " + battery.batValue);
else if (key === "pwr" && root.showPower && battery.powerValue)
parts.push("PWR: " + battery.powerValue);
else if (key === "net" && root.showNetwork)
parts.push("NET: ↓" + network.netDownValue + " ↑" + network.netUpValue);
else if (key === "disk" && root.showDisk) {
var dParts = ["↓" + disk.diskReadValue, "↑" + disk.diskWriteValue];
if (disk.diskTempValue) dParts.push(disk.diskTempValue);
parts.push("DSK: " + dParts.join(" "));
}
else if (key === "fan" && root.showFan && fans.hasFanData) {
parts.push("FAN: " + fans.fanValue);
}
}
return parts.join("\n");
}
}
@@ -0,0 +1,193 @@
import QtQuick
import org.kde.ksysguard.sensors as Sensors
import org.kde.plasma.plasma5support as Plasma5Support
Item {
id: root
property bool _dbg: { console.warn("[KVitals] BatterySensors: constructing..."); return true; }
property int updateInterval: 2000
property string batteryDevice: "auto"
readonly property real batNumericValue: {
if (batChargeSensor.status !== Sensors.Sensor.Ready)
return NaN;
return batChargeSensor.value;
}
readonly property string batValue: {
if (isNaN(batNumericValue)) return "";
return Math.round(batNumericValue) + "%";
}
readonly property string powerValue: {
if (batRateSensor.status !== Sensors.Sensor.Ready) return "";
var watts = Math.abs(batRateSensor.value);
if (watts < 0.01) return "0.0W";
var sign = batRateSensor.value > 0 ? "+" : "-";
return sign + watts.toFixed(1) + "W";
}
// --- Dynamic battery sensor discovery ---
property string discoveredBatId: ""
property string batChargeSensorId: {
var base = (batteryDevice && batteryDevice !== "auto") ? batteryDevice : discoveredBatId;
return base ? ("power/" + base + "/chargePercentage") : "";
}
property string batRateSensorId: {
var base = (batteryDevice && batteryDevice !== "auto") ? batteryDevice : discoveredBatId;
return base ? ("power/" + base + "/chargeRate") : "";
}
property var batteryCandidates: [
"battery_BAT0",
"battery_BAT1",
"battery_BAT2",
"battery_BATT",
"battery_BATT0"
]
property var stage1Probes: []
Component.onCompleted: {
console.warn("[KVitals] BatterySensors: ready. batteryDevice = " + batteryDevice);
if (batteryDevice && batteryDevice !== "auto")
return;
for (var i = 0; i < batteryCandidates.length; i++) {
var pre = "power/" + batteryCandidates[i] + "/chargePercentage";
var code = 'import org.kde.ksysguard.sensors as Sensors; Sensors.Sensor { sensorId: "' + pre + '"; updateRateLimit: 2000 }';
try {
var probe = Qt.createQmlObject(code, root, "probe_" + i);
stage1Probes.push({ candidate: batteryCandidates[i], probe: probe });
} catch(e) {
}
}
}
Timer {
id: stage1Timer
interval: 500
repeat: true
running: (!batteryDevice || batteryDevice === "auto") && !discoveredBatId
property int attempts: 0
onTriggered: {
attempts++;
console.warn("[KVitals] BatterySensors: probe attempt = " + attempts);
for (var i = 0; i < stage1Probes.length; i++) {
if (stage1Probes[i].probe && stage1Probes[i].probe.status === Sensors.Sensor.Ready) {
console.warn("[KVitals] BatterySensors: stage 1 found = " + stage1Probes[i].candidate);
persistDetectedBattery(stage1Probes[i].candidate);
running = false;
cleanupProbes();
return;
}
}
if (attempts >= 6) { // 3 seconds timeout
console.warn("[KVitals] BatterySensors: stage 1 timeout, starting qdbus fallback...");
running = false;
cleanupProbes();
// Stage 2: qdbus fallback
tryNextQdbus();
}
}
}
function cleanupProbes() {
for (var i = 0; i < stage1Probes.length; i++) {
if (stage1Probes[i].probe)
stage1Probes[i].probe.destroy();
}
stage1Probes = [];
}
function persistDetectedBattery(deviceId) {
discoveredBatId = deviceId;
}
function extractBatteryIds(stdout) {
if (!stdout)
return [];
var matches = stdout.match(/power\/[^\/"\s]+\/chargePercentage/g);
if (!matches)
return [];
var ids = [];
for (var i = 0; i < matches.length; i++) {
var parts = matches[i].split("/");
if (parts.length < 3)
continue;
if (ids.indexOf(parts[1]) === -1)
ids.push(parts[1]);
}
return ids;
}
// Stage 2: qdbus fallback
property var qdbusVariants: ["qdbus", "qdbus6", "qdbus-qt6", "qdbus-qt5"]
property int qdbusIndex: 0
Plasma5Support.DataSource {
id: qdbusDetector
engine: "executable"
property bool active: false
onNewData: function(sourceName, data) {
if (!active) return;
active = false;
disconnectSource(sourceName);
console.debug("[KVitals] BatterySensors: qdbus exit code = " + data["exit code"]);
if (data["exit code"] === 0) {
var stdoutStr = data["stdout"] ? data["stdout"].toString() : "";
var ids = extractBatteryIds(stdoutStr);
console.debug("[KVitals] BatterySensors: qdbus ids = " + JSON.stringify(ids));
if (ids.length === 1) {
persistDetectedBattery(ids[0]);
return;
}
if (ids.length > 1) {
console.warn("BatterySensors: multiple batteries found:", JSON.stringify(ids), "— using first:", ids[0]);
persistDetectedBattery(ids[0]);
return;
}
} else {
console.warn("[KVitals] BatterySensors: qdbus command failed.");
}
tryNextQdbus();
}
function run(variant) {
var cmd = variant + " --literal org.kde.ksystemstats1" +
" /org/kde/ksystemstats1" +
" org.kde.ksystemstats1.allSensors";
active = true;
connectSource(cmd);
}
}
function tryNextQdbus() {
if (qdbusIndex >= qdbusVariants.length) {
console.warn("[KVitals] BatterySensors: all detection methods exhausted; set batteryDevice manually.");
return;
}
var variant = qdbusVariants[qdbusIndex];
qdbusIndex++;
console.debug("[KVitals] BatterySensors: trying qdbus variant = " + variant);
qdbusDetector.run(variant);
}
Sensors.Sensor {
id: batChargeSensor
sensorId: root.batChargeSensorId
updateRateLimit: root.updateInterval > 5000 ? root.updateInterval : 5000
}
Sensors.Sensor {
id: batRateSensor
sensorId: root.batRateSensorId
updateRateLimit: root.updateInterval > 5000 ? root.updateInterval : 5000
}
}
@@ -0,0 +1,47 @@
import QtQuick
import org.kde.ksysguard.sensors as Sensors
Item {
id: root
property bool _dbg: { console.warn("[KVitals] CpuSensors: constructing..."); return true; }
property int updateInterval: 2000
readonly property real cpuNumericValue: {
if (cpuSensor.status !== Sensors.Sensor.Ready)
return NaN;
return cpuSensor.value;
}
readonly property string cpuValue: {
if (isNaN(cpuNumericValue))
return "...";
return Math.round(cpuNumericValue) + "%";
}
// Frequency in MHz from KSysGuard (unit type 302 = MHz); displays as GHz above 1000 MHz
readonly property string cpuFreqValue: {
if (freqSensor.status !== Sensors.Sensor.Ready || freqSensor.value == null)
return "...";
var mhz = freqSensor.value;
if (mhz >= 1000)
return (mhz / 1000).toFixed(2) + " GHz";
return Math.round(mhz) + " MHz";
}
Sensors.Sensor {
id: cpuSensor
sensorId: "cpu/all/usage"
updateRateLimit: root.updateInterval
}
Sensors.Sensor {
id: freqSensor
sensorId: "cpu/all/averageFrequency"
updateRateLimit: root.updateInterval
}
Component.onCompleted: {
console.warn("[KVitals] CpuSensors: ready.");
}
}
@@ -0,0 +1,113 @@
import QtQuick
import org.kde.ksysguard.sensors as Sensors
import org.kde.kitemmodels as KItemModels
Item {
id: root
property bool _dbg: { console.warn("[KVitals] DiskSensors: constructing..."); return true; }
property int updateInterval: 2000
property bool enabled: true
property string tempUnit: "C"
property string networkUnit: "bytes"
readonly property string diskReadValue: Utils.formatRate(diskReadSensor.status === Sensors.Sensor.Ready ? diskReadSensor.value : NaN, networkUnit)
readonly property string diskWriteValue: Utils.formatRate(diskWriteSensor.status === Sensors.Sensor.Ready ? diskWriteSensor.value : NaN, networkUnit)
// Highest temperature found across all discovered drive temp sensors
readonly property real diskTempNumber: _diskTempNum
readonly property string diskTempValue: isNaN(_diskTempNum) ? "" : Utils.formatTemp(_diskTempNum, tempUnit)
property real _diskTempNum: NaN
// I/O sensors
Sensors.Sensor {
id: diskReadSensor
sensorId: "disk/all/read"
updateRateLimit: root.updateInterval
enabled: root.enabled
}
Sensors.Sensor {
id: diskWriteSensor
sensorId: "disk/all/write"
updateRateLimit: root.updateInterval
enabled: root.enabled
}
// Matches lmsensors chips that are NVMe (nvme-pci-*) or SATA drivetemp
// (drivetemp-scsi-*) and picks temp1 (Composite) or temp2 (secondary sensor)
Sensors.SensorTreeModel { id: sensorTree }
KItemModels.KDescendantsProxyModel {
id: flatSensors
model: sensorTree
}
property var _tempSensorIds: []
function _refreshTempSensors() {
console.debug("[KVitals] DiskSensors: scan started. rows = " + flatSensors.rowCount());
var found = [];
for (var row = 0; row < flatSensors.rowCount(); row++) {
var idx = flatSensors.index(row, 0);
var sid = flatSensors.data(idx, Sensors.SensorTreeModel.SensorId);
if (!sid) continue;
if (/^lmsensors\/(nvme-pci-[^/]+|drivetemp-scsi-[^/]+)\/temp[12]$/.test(sid))
found.push(sid);
}
console.debug("[KVitals] DiskSensors: scan finished. found = " + JSON.stringify(found));
if (JSON.stringify(found) !== JSON.stringify(_tempSensorIds)) {
console.debug("[KVitals] DiskSensors: temp sensors updated. ids = " + JSON.stringify(found));
_tempSensorIds = found;
}
}
property bool _discoveryDirty: false
Timer {
id: discoveryTimer
interval: 500
repeat: false
running: _discoveryDirty
onTriggered: {
_discoveryDirty = false;
root._refreshTempSensors();
}
}
Connections {
target: flatSensors
function onRowsInserted() { root._discoveryDirty = true; }
function onRowsRemoved() { root._discoveryDirty = true; }
function onModelReset() { root._discoveryDirty = true; }
}
Component.onCompleted: {
console.warn("[KVitals] DiskSensors: ready.");
_refreshTempSensors();
}
// Poll discovered temp sensors
Sensors.SensorDataModel {
id: tempData
sensors: root._tempSensorIds
updateRateLimit: root.updateInterval
enabled: root._tempSensorIds.length > 0
onDataChanged: root._aggregateTemp()
onReadyChanged: { if (ready) root._aggregateTemp(); }
}
function _aggregateTemp() {
var max = NaN;
for (var i = 0; i < _tempSensorIds.length; i++) {
var col = tempData.column(_tempSensorIds[i]);
if (col < 0) continue;
var val = tempData.data(tempData.index(0, col), Sensors.SensorDataModel.Value);
if (typeof val !== "number" || isNaN(val) || val <= 0) continue;
if (isNaN(max) || val > max) max = val;
}
_diskTempNum = max;
}
}
@@ -0,0 +1,117 @@
import QtQuick
import org.kde.ksysguard.sensors as Sensors
import org.kde.kitemmodels as KItemModels
Item {
id: root
property int updateInterval: 2000
property string fanUnit: "rpm" // "rpm" or "percent"
readonly property var discoveredFans: _discovered
property var _discovered: []
readonly property string fanValue: _fanStr
readonly property bool hasFanData: _fanStr.length > 0
property string _fanStr: ""
// -------------------------------------------------------------------------
// Step 1: Discover available Fans via SensorTreeModel
// -------------------------------------------------------------------------
Sensors.SensorTreeModel {
id: sensorTree
}
KItemModels.KDescendantsProxyModel {
id: flatSensors
model: sensorTree
}
function refreshDiscovered() {
var found = [];
for (var row = 0; row < flatSensors.rowCount(); row++) {
var idx = flatSensors.index(row, 0);
var sensorId = flatSensors.data(idx, Sensors.SensorTreeModel.SensorId);
if (!sensorId || sensorId.length === 0) continue;
// Match any sensor that contains /fan and doesn't end with a non-digit (typically fan1, fan2, etc)
var match = sensorId.match(/^(lmsensors|cpu|gpu)\/.*\/fan\d+$/i);
if (!match) continue;
// Some names are like "cpu_fan", we can try to extract the last part or use the sensor Name
var name = flatSensors.data(idx, Qt.DisplayRole) || "Fan " + (found.length + 1);
found.push({ id: sensorId, name: name });
}
if (JSON.stringify(found) !== JSON.stringify(_discovered)) {
_discovered = found;
}
}
Connections {
target: flatSensors
function onRowsInserted() { root.refreshDiscovered(); }
function onRowsRemoved() { root.refreshDiscovered(); }
function onModelReset() { root.refreshDiscovered(); }
}
Component.onCompleted: refreshDiscovered()
// -------------------------------------------------------------------------
// Step 2: Poll discovered fans
// -------------------------------------------------------------------------
readonly property var _activeSensorIds: _discovered.map(function(f){ return f.id; })
Sensors.SensorDataModel {
id: fanData
sensors: root._activeSensorIds
updateRateLimit: root.updateInterval
enabled: root._activeSensorIds.length > 0
onDataChanged: root.aggregate()
onReadyChanged: { if (ready) root.aggregate(); }
}
function _modelValue(sensorId) {
var col = fanData.column(sensorId);
if (col < 0) return NaN;
var idx = fanData.index(0, col);
if (!idx.valid) return NaN;
var val = fanData.data(idx, Sensors.SensorDataModel.Value);
return (val === undefined || val === null) ? NaN : val;
}
function _modelMax(sensorId) {
var col = fanData.column(sensorId);
if (col < 0) return NaN;
var idx = fanData.index(0, col);
if (!idx.valid) return NaN;
var val = fanData.data(idx, Sensors.SensorDataModel.Maximum);
return (val === undefined || val === null) ? NaN : val;
}
function aggregate() {
var parts = [];
for (var i = 0; i < _discovered.length; i++) {
var f = _discovered[i];
var v = _modelValue(f.id);
if (isNaN(v) || v <= 0) continue; // Ignore 0 RPM or disconnected fans
var str = "";
if (fanUnit === "percent") {
var max = _modelMax(f.id);
if (isNaN(max) || max <= 0) {
continue;
}
var pct = Math.min(100, Math.round((v / max) * 100));
str = pct + "%";
} else {
str = Math.round(v) + " RPM";
}
parts.push(str);
}
_fanStr = parts.join(" ");
}
onFanUnitChanged: aggregate()
}
@@ -0,0 +1,270 @@
import QtQuick
import org.kde.ksysguard.sensors as Sensors
import org.kde.kitemmodels as KItemModels
Item {
id: root
property bool _dbg: { console.warn("[KVitals] GpuSensors: constructing..."); return true; }
property int updateInterval: 2000
// Comma-separated selected GPU IDs e.g. "gpu0,gpu1". Empty = all discovered.
property string gpuSelection: ""
// User-defined labels: "gpu0:My iGPU|gpu1:dGPU". Empty string = use default "GPU N" name.
property string gpuLabels: ""
// Temperature unit: "C" (default) or "F"
property string tempUnit: "C"
// Per-GPU sub-metric visibility: "gpu0:usage,vram|gpu1:usage,temp"
// Empty string (default) = all three sub-metrics enabled for every GPU.
property string gpuMetrics: ""
// List of { id: "gpu0", name: "GPU 1" } derived from SensorTreeModel (no polling)
readonly property var discoveredGpus: _discovered
property var _discovered: []
// Aggregated display (single-GPU compat)
readonly property real gpuUsageNumber: _usageNum
readonly property real gpuTempNumber: _tempNum
readonly property string gpuValue: _usageStr
readonly property string gpuRamValue: _vramStr
readonly property string gpuTempValue: _tempStr
readonly property string gpuDisplayValue:
[_usageStr, _vramStr, _tempStr].filter(function(v){return v;}).join(" ")
readonly property bool hasGpuData: gpuDisplayValue.length > 0
readonly property bool hasGpuUsageData: _usageStr.length > 0
readonly property bool hasGpuVramData: _vramStr.length > 0
readonly property bool hasGpuTempData: _tempStr.length > 0
// Per-GPU list for multi display: [{ id, name, usage, vram, temp, usageNumber, tempNumber }]
readonly property var gpuDataList: _dataList
property var _dataList: []
property real _usageNum: NaN
property real _tempNum: NaN
property string _usageStr: ""
property string _vramStr: ""
property string _tempStr: ""
// -------------------------------------------------------------------------
// Step 1: Discover available GPUs via SensorTreeModel (metadata only, no polling)
// -------------------------------------------------------------------------
Sensors.SensorTreeModel {
id: sensorTree
}
KItemModels.KDescendantsProxyModel {
id: flatSensors
model: sensorTree
}
// Parse "gpu0:Label A|gpu1:Label B" → { gpu0: "Label A", gpu1: "Label B" }
function parseGpuLabels(str) {
var result = {};
if (!str) return result;
var pairs = str.split("|");
for (var i = 0; i < pairs.length; i++) {
var sep = pairs[i].indexOf(":");
if (sep > 0)
result[pairs[i].substring(0, sep)] = pairs[i].substring(sep + 1);
}
return result;
}
// Parse "gpu0:usage,vram|gpu1:usage,temp" → { gpu0: ["usage","vram"], gpu1: ["usage","temp"] }
// Missing GPU or empty string → default all three enabled.
function parseGpuMetrics(str) {
var result = {};
if (!str) return result;
var pairs = str.split("|");
for (var i = 0; i < pairs.length; i++) {
var sep = pairs[i].indexOf(":");
if (sep <= 0) continue;
var id = pairs[i].substring(0, sep);
var metrics = pairs[i].substring(sep + 1).split(",")
.filter(function(m){ return m === "usage" || m === "vram" || m === "temp"; });
if (metrics.length > 0) result[id] = metrics;
}
return result;
}
// Return enabled sub-metric list for a specific GPU (["usage","vram","temp"] if unconfigured)
function gpuMetricsFor(gpuId, metricsMap) {
return (metricsMap[gpuId] && metricsMap[gpuId].length > 0)
? metricsMap[gpuId]
: ["usage", "vram", "temp"];
}
function refreshDiscovered() {
console.debug("[KVitals] GpuSensors: scan started. rows = " + flatSensors.rowCount());
var found = [];
for (var row = 0; row < flatSensors.rowCount(); row++) {
var idx = flatSensors.index(row, 0);
var sensorId = flatSensors.data(idx, Sensors.SensorTreeModel.SensorId);
if (!sensorId || sensorId.length === 0) continue;
var match = sensorId.match(/^gpu\/(gpu\d+)\/usage$/);
if (!match) continue;
found.push({ id: match[1], name: "GPU " + (found.length + 1) });
}
console.debug("[KVitals] GpuSensors: scan finished. found = " + JSON.stringify(found));
if (JSON.stringify(found) !== JSON.stringify(_discovered)) {
console.warn("[KVitals] GpuSensors: discovered GPUs updated = " + JSON.stringify(found));
_discovered = found;
if (typeof Plasmoid !== "undefined" && Plasmoid.configuration)
Plasmoid.configuration.gpuDiscovered =
found.map(function(g){ return g.id + ":" + g.name; }).join(",");
}
}
property bool _discoveryDirty: false
Timer {
id: discoveryTimer
interval: 500
repeat: false
running: _discoveryDirty
onTriggered: {
_discoveryDirty = false;
root.refreshDiscovered();
}
}
Connections {
target: flatSensors
function onRowsInserted() { root._discoveryDirty = true; }
function onRowsRemoved() { root._discoveryDirty = true; }
function onModelReset() { root._discoveryDirty = true; }
function onDataChanged() { root._discoveryDirty = true; }
}
Component.onCompleted: {
console.warn("[KVitals] GpuSensors: ready. selection=" + gpuSelection);
refreshDiscovered();
}
// -------------------------------------------------------------------------
// Step 2: Compute active sensor IDs — per GPU, only poll enabled sub-metrics
// -------------------------------------------------------------------------
readonly property var _activeIds: {
if (!gpuSelection || gpuSelection === "")
return _discovered.map(function(g){ return g.id; });
if (gpuSelection === "none")
return [];
return gpuSelection.split(",")
.map(function(s){ return s.trim(); })
.filter(function(s){ return s.length > 0; });
}
readonly property var _activeSensorIds: {
var ids = [];
var mm = parseGpuMetrics(gpuMetrics);
for (var i = 0; i < _activeIds.length; i++) {
var g = _activeIds[i];
var m = gpuMetricsFor(g, mm);
if (m.indexOf("usage") >= 0) ids.push("gpu/" + g + "/usage");
if (m.indexOf("vram") >= 0) {
ids.push("gpu/" + g + "/usedVram");
ids.push("gpu/" + g + "/totalVram");
}
if (m.indexOf("temp") >= 0) ids.push("gpu/" + g + "/temperature");
}
return ids;
}
// -------------------------------------------------------------------------
// Step 3: Single SensorDataModel — polls ONLY the selected sensors
// -------------------------------------------------------------------------
Sensors.SensorDataModel {
id: gpuData
sensors: root._activeSensorIds
updateRateLimit: root.updateInterval
enabled: root._activeSensorIds.length > 0
onDataChanged: root.aggregate()
onReadyChanged: { if (ready) root.aggregate(); }
}
// -------------------------------------------------------------------------
// Step 4: Aggregate values — label resolution: custom label > default name > "GPU N"
// -------------------------------------------------------------------------
function _modelValue(sensorId) {
var col = gpuData.column(sensorId);
if (col < 0) return NaN;
var idx = gpuData.index(0, col);
if (!idx.valid) return NaN;
var val = gpuData.data(idx, Sensors.SensorDataModel.Value);
return (val === undefined || val === null) ? NaN : val;
}
function aggregate() {
var ids = _activeIds;
var customLabels = parseGpuLabels(gpuLabels);
var mm = parseGpuMetrics(gpuMetrics);
var newList = [];
var totalUsage = 0, usageCount = 0;
var totalVramUsed = 0, totalVramTotal = 0, hasVram = false;
var maxTemp = NaN;
for (var i = 0; i < ids.length; i++) {
var g = ids[i];
var m = gpuMetricsFor(g, mm);
var showU = m.indexOf("usage") >= 0;
var showV = m.indexOf("vram") >= 0;
var showT = m.indexOf("temp") >= 0;
// Resolve display name: custom label > default name > fallback
var hwName = "GPU " + (i + 1);
for (var j = 0; j < _discovered.length; j++) {
if (_discovered[j].id === g) { hwName = _discovered[j].name; break; }
}
var name = customLabels[g] || hwName;
var uVal = showU ? _modelValue("gpu/" + g + "/usage") : NaN;
var vuVal = showV ? _modelValue("gpu/" + g + "/usedVram") : NaN;
var vtVal = showV ? _modelValue("gpu/" + g + "/totalVram") : NaN;
var tVal = showT ? _modelValue("gpu/" + g + "/temperature") : NaN;
var uStr = !isNaN(uVal) ? Math.round(uVal) + "%" : "";
var vStr = "";
if (!isNaN(vuVal) && !isNaN(vtVal) && vtVal > 0 && vuVal >= 0)
vStr = Utils.formatBytes(vuVal) + "/" + Utils.formatBytes(vtVal) + "G";
// tVal === 0 is ksystemstats' null sentinel for iGPU (no hwmon node)
var tStr = (!isNaN(tVal) && tVal > 0) ? Utils.formatTemp(tVal, tempUnit) : "";
newList.push({ id: g, name: name,
usage: uStr, vram: vStr, temp: tStr,
usageNumber: !isNaN(uVal) ? uVal : NaN,
tempNumber: (!isNaN(tVal) && tVal > 0) ? tVal : NaN });
if (!isNaN(uVal)) { totalUsage += uVal; usageCount++; }
if (!isNaN(vuVal) && !isNaN(vtVal) && vtVal > 0 && vuVal >= 0) {
totalVramUsed += vuVal;
totalVramTotal += vtVal;
hasVram = true;
}
if (!isNaN(tVal) && tVal > 0 && (isNaN(maxTemp) || tVal > maxTemp)) maxTemp = tVal;
}
_dataList = newList;
_usageNum = usageCount > 0 ? totalUsage / usageCount : NaN;
_usageStr = usageCount > 0 ? Math.round(_usageNum) + "%" : "";
_vramStr = (hasVram && totalVramTotal > 0)
? Utils.formatBytes(totalVramUsed) + "/" + Utils.formatBytes(totalVramTotal) + "G" : "";
_tempNum = !isNaN(maxTemp) ? maxTemp : NaN;
_tempStr = !isNaN(maxTemp) ? Utils.formatTemp(maxTemp, tempUnit) : "";
}
// Re-aggregate when gpuMetrics, labels, selection, or unit change
onGpuMetricsChanged: aggregate()
onGpuLabelsChanged: aggregate()
onGpuSelectionChanged: aggregate()
onTempUnitChanged: aggregate()
}
@@ -0,0 +1,38 @@
import QtQuick
import org.kde.ksysguard.sensors as Sensors
Item {
id: root
property bool _dbg: { console.warn("[KVitals] MemorySensors: constructing..."); return true; }
property int updateInterval: 2000
readonly property real ramPercentage: {
if (ramUsedSensor.status !== Sensors.Sensor.Ready || ramTotalSensor.status !== Sensors.Sensor.Ready)
return NaN;
if (ramTotalSensor.value <= 0) return NaN;
return (ramUsedSensor.value / ramTotalSensor.value) * 100;
}
readonly property string ramValue: {
if (isNaN(ramPercentage))
return "...";
return Utils.formatBytes(ramUsedSensor.value) + "/" + Utils.formatBytes(ramTotalSensor.value) + "G";
}
Sensors.Sensor {
id: ramUsedSensor
sensorId: "memory/physical/used"
updateRateLimit: root.updateInterval
}
Sensors.Sensor {
id: ramTotalSensor
sensorId: "memory/physical/total"
updateRateLimit: root.updateInterval
}
Component.onCompleted: {
console.warn("[KVitals] MemorySensors: ready.");
}
}
@@ -0,0 +1,43 @@
import QtQuick
import org.kde.ksysguard.sensors as Sensors
Item {
id: root
property bool _dbg: { console.warn("[KVitals] NetworkSensors: constructing..."); return true; }
property int updateInterval: 2000
property string networkInterface: "auto"
property string networkUnit: "bytes"
readonly property string netIfacePath: {
if (networkInterface === "" || networkInterface === "auto")
return "all";
return networkInterface;
}
readonly property string netDownValue: {
if (netDownSensor.status !== Sensors.Sensor.Ready) return "...";
return Utils.formatRate(netDownSensor.value, networkUnit);
}
readonly property string netUpValue: {
if (netUpSensor.status !== Sensors.Sensor.Ready) return "...";
return Utils.formatRate(netUpSensor.value, networkUnit);
}
Sensors.Sensor {
id: netDownSensor
sensorId: "network/" + root.netIfacePath + "/download"
updateRateLimit: root.updateInterval
}
Sensors.Sensor {
id: netUpSensor
sensorId: "network/" + root.netIfacePath + "/upload"
updateRateLimit: root.updateInterval
}
Component.onCompleted: {
console.warn("[KVitals] NetworkSensors: ready.");
}
}
@@ -0,0 +1,31 @@
import QtQuick
import org.kde.ksysguard.sensors as Sensors
Item {
id: root
property bool _dbg: { console.warn("[KVitals] TempSensors: constructing..."); return true; }
property int updateInterval: 2000
property string tempUnit: "C"
readonly property real tempNumericValue: {
if (tempSensor.status !== Sensors.Sensor.Ready)
return NaN;
return tempSensor.value;
}
readonly property string tempValue: {
if (isNaN(tempNumericValue)) return "--";
return Utils.formatTemp(tempNumericValue, tempUnit);
}
Sensors.Sensor {
id: tempSensor
sensorId: "cpu/all/averageTemperature"
updateRateLimit: root.updateInterval
}
Component.onCompleted: {
console.warn("[KVitals] TempSensors: ready.");
}
}
@@ -0,0 +1,99 @@
pragma Singleton
import QtQuick
import org.kde.ksysguard.sensors as Sensors
QtObject {
function formatBytes(bytes) {
if (typeof bytes !== "number" || isNaN(bytes))
return "...";
var gb = bytes / (1024 * 1024 * 1024);
return gb.toFixed(1);
}
// unit: "bytes" (default, KB/MB) or "bits" (Kb/Mb)
// Suffix convention: uppercase B = bytes, lowercase b = bits
function formatRate(bytesPerSec, unit) {
if (typeof bytesPerSec !== "number" || isNaN(bytesPerSec))
return "...";
if (unit === "bits") {
var bps = bytesPerSec * 8;
if (bps >= 1000000)
return (bps / 1000000).toFixed(1) + "Mb";
return Math.max(0, bps / 1000).toFixed(1) + "Kb";
}
var kbps = bytesPerSec / 1024;
if (kbps >= 1024)
return (kbps / 1024).toFixed(1) + "MB";
return Math.max(0, kbps).toFixed(1) + "KB";
}
// celsiusValue: raw °C number from sensor; unit: "C" or "F"
function formatTemp(celsiusValue, unit) {
if (typeof celsiusValue !== "number" || isNaN(celsiusValue))
return "";
if (unit === "F")
return Math.round(celsiusValue * 9 / 5 + 32) + "°F";
return Math.round(celsiusValue) + "°C";
}
function sensorValueOrNaN(sensor) {
if (!sensor || sensor.status !== Sensors.Sensor.Ready)
return NaN;
if (typeof sensor.value !== "number" || isNaN(sensor.value))
return NaN;
return sensor.value;
}
function firstReadyNumber(sensors, requirePositive) {
for (var i = 0; i < sensors.length; i++) {
var value = sensorValueOrNaN(sensors[i]);
if (isNaN(value))
continue;
if (requirePositive && value <= 0)
continue;
return value;
}
return NaN;
}
function maxReadyNumber(sensors, requirePositive) {
var maxValue = NaN;
for (var i = 0; i < sensors.length; i++) {
var value = sensorValueOrNaN(sensors[i]);
if (isNaN(value))
continue;
if (requirePositive && value <= 0)
continue;
if (isNaN(maxValue) || value > maxValue)
maxValue = value;
}
return maxValue;
}
function firstReadyVramPair(pairs) {
for (var i = 0; i < pairs.length; i++) {
var used = sensorValueOrNaN(pairs[i].used);
var total = sensorValueOrNaN(pairs[i].total);
if (isNaN(used) || isNaN(total))
continue;
if (total <= 0 || used < 0)
continue;
return { used: used, total: total };
}
return null;
}
function resolveColor(numericValue, warningThreshold, criticalThreshold,
warningColor, criticalColor, baseColor, inverted) {
if (isNaN(numericValue))
return baseColor;
if (inverted) {
if (numericValue <= criticalThreshold) return criticalColor;
if (numericValue <= warningThreshold) return warningColor;
} else {
if (numericValue >= criticalThreshold) return criticalColor;
if (numericValue >= warningThreshold) return warningColor;
}
return baseColor;
}
}
@@ -0,0 +1,10 @@
module sensors
singleton Utils 1.0 Utils.qml
CpuSensors 1.0 CpuSensors.qml
MemorySensors 1.0 MemorySensors.qml
TempSensors 1.0 TempSensors.qml
GpuSensors 1.0 GpuSensors.qml
BatterySensors 1.0 BatterySensors.qml
NetworkSensors 1.0 NetworkSensors.qml
DiskSensors 1.0 DiskSensors.qml
FanSensors 1.0 FanSensors.qml