plasmoides KDE Store/GitHub (gnome-pager, kMenu, kvitals, windowtitle, claude, launchpad)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+154
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+669
@@ -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");
|
||||
}
|
||||
}
|
||||
+193
@@ -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
|
||||
}
|
||||
}
|
||||
+47
@@ -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.");
|
||||
}
|
||||
}
|
||||
+113
@@ -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;
|
||||
}
|
||||
}
|
||||
+117
@@ -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()
|
||||
}
|
||||
+270
@@ -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()
|
||||
}
|
||||
+38
@@ -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.");
|
||||
}
|
||||
}
|
||||
+43
@@ -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.");
|
||||
}
|
||||
}
|
||||
+31
@@ -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
|
||||
Reference in New Issue
Block a user