feat: split SystemUsage into separate C++ services
This commit is contained in:
parent
9fefb53440
commit
81fc9dda02
25 changed files with 1510 additions and 378 deletions
|
|
@ -4,6 +4,7 @@ import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell.Services.UPower
|
import Quickshell.Services.UPower
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
|
import Caelestia.Services
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.components.controls
|
import qs.components.controls
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
@ -12,8 +13,8 @@ SectionContainer {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var rootItem
|
required property var rootItem
|
||||||
// GPU toggle is hidden when gpuType is "NONE" (no GPU data available)
|
// GPU toggle is hidden when type is Gpu.None (no GPU data available)
|
||||||
readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE"
|
readonly property bool gpuAvailable: Gpu.type !== Gpu.None
|
||||||
// Battery toggle is hidden when no laptop battery is present
|
// Battery toggle is hidden when no laptop battery is present
|
||||||
readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery
|
readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import QtQuick.Layouts
|
||||||
import Quickshell.Services.UPower
|
import Quickshell.Services.UPower
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import Caelestia.Internal
|
import Caelestia.Internal
|
||||||
|
import Caelestia.Services
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.components.misc
|
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -29,7 +29,7 @@ Item {
|
||||||
height: 350
|
height: 350
|
||||||
radius: Tokens.rounding.extraLarge
|
radius: Tokens.rounding.extraLarge
|
||||||
color: Colours.tPalette.m3surfaceContainer
|
color: Colours.tPalette.m3surfaceContainer
|
||||||
visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery)
|
visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None) && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery)
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
@ -66,8 +66,12 @@ Item {
|
||||||
spacing: Tokens.spacing.medium
|
spacing: Tokens.spacing.medium
|
||||||
visible: !placeholder.visible
|
visible: !placeholder.visible
|
||||||
|
|
||||||
Ref {
|
ServiceRef {
|
||||||
service: SystemUsage
|
service: root.Config.dashboard.performance.showCpu ? Cpu : null
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: root.Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None ? Gpu : null
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|
@ -79,27 +83,27 @@ Item {
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Tokens.spacing.medium
|
spacing: Tokens.spacing.medium
|
||||||
visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE")
|
visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None)
|
||||||
|
|
||||||
HeroCard {
|
HeroCard {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: Config.dashboard.performance.showCpu
|
visible: Config.dashboard.performance.showCpu
|
||||||
icon: "memory"
|
icon: "memory"
|
||||||
label: qsTr("CPU")
|
label: qsTr("CPU")
|
||||||
subLabel: SystemUsage.cpuName
|
subLabel: Cpu.name
|
||||||
usage: SystemUsage.cpuPerc
|
usage: Cpu.percentage
|
||||||
temperature: SystemUsage.cpuTemp
|
temperature: Cpu.temperature
|
||||||
accent: Colours.palette.m3primary
|
accent: Colours.palette.m3primary
|
||||||
}
|
}
|
||||||
|
|
||||||
HeroCard {
|
HeroCard {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE"
|
visible: Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None
|
||||||
icon: "desktop_windows"
|
icon: "desktop_windows"
|
||||||
label: qsTr("GPU")
|
label: qsTr("GPU")
|
||||||
subLabel: SystemUsage.gpuName
|
subLabel: Gpu.name
|
||||||
usage: SystemUsage.gpuPerc
|
usage: Gpu.percentage
|
||||||
temperature: SystemUsage.gpuTemp
|
temperature: Gpu.temperature
|
||||||
accent: Colours.palette.m3secondary
|
accent: Colours.palette.m3secondary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
|
import Caelestia.Services
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.components.controls
|
import qs.components.controls
|
||||||
import qs.components.misc
|
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -14,8 +14,16 @@ Item {
|
||||||
|
|
||||||
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
|
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
|
||||||
|
|
||||||
Ref {
|
ServiceRef {
|
||||||
service: SystemUsage
|
service: Cpu
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Memory
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|
@ -29,18 +37,18 @@ Item {
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
icon: "memory"
|
icon: "memory"
|
||||||
value: SystemUsage.cpuPerc
|
value: Cpu.percentage
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
icon: "memory_alt"
|
icon: "memory_alt"
|
||||||
value: SystemUsage.memPerc
|
value: Memory.percentage
|
||||||
fgColour: Colours.palette.m3tertiary
|
fgColour: Colours.palette.m3tertiary
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
icon: "hard_disk"
|
icon: "hard_disk"
|
||||||
value: SystemUsage.storagePerc
|
value: Storage.percentage
|
||||||
fgColour: Colours.palette.m3secondary
|
fgColour: Colours.palette.m3secondary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
|
import Caelestia.Services
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.components.controls
|
import qs.components.controls
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
@ -16,6 +17,10 @@ StyledRect {
|
||||||
implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2
|
implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2
|
||||||
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
|
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Memory
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: layout
|
id: layout
|
||||||
|
|
||||||
|
|
@ -47,7 +52,7 @@ StyledRect {
|
||||||
sweepAngle: 270
|
sweepAngle: 270
|
||||||
|
|
||||||
fgColour: root.accent
|
fgColour: root.accent
|
||||||
value: SystemUsage.memPerc
|
value: Memory.percentage
|
||||||
|
|
||||||
Behavior on clampedVal {
|
Behavior on clampedVal {
|
||||||
Anim {}
|
Anim {}
|
||||||
|
|
@ -62,7 +67,7 @@ StyledRect {
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
text: Math.round(SystemUsage.memPerc * 100) + "%"
|
text: Math.round(Memory.percentage * 100) + "%"
|
||||||
font: Tokens.font.title.builders.large.width(90).build()
|
font: Tokens.font.title.builders.large.width(90).build()
|
||||||
color: root.accent
|
color: root.accent
|
||||||
}
|
}
|
||||||
|
|
@ -79,8 +84,8 @@ StyledRect {
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
text: {
|
text: {
|
||||||
const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed);
|
const usedFmt = Memory.formatKib(Memory.used);
|
||||||
const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal);
|
const totalFmt = Memory.formatKib(Memory.total);
|
||||||
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
|
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
|
||||||
}
|
}
|
||||||
font: Tokens.font.body.medium
|
font: Tokens.font.body.medium
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
|
import Caelestia.Services
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.components.controls
|
import qs.components.controls
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
@ -10,7 +11,7 @@ StyledRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property color accent: Colours.palette.m3secondary
|
readonly property color accent: Colours.palette.m3secondary
|
||||||
readonly property real percentage: SystemUsage.primaryDisk?.perc ?? 0
|
readonly property real percentage: Storage.primaryDisk?.perc ?? 0
|
||||||
|
|
||||||
color: Colours.tPalette.m3surfaceContainer
|
color: Colours.tPalette.m3surfaceContainer
|
||||||
radius: Tokens.rounding.extraExtraLarge
|
radius: Tokens.rounding.extraExtraLarge
|
||||||
|
|
@ -18,6 +19,10 @@ StyledRect {
|
||||||
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
|
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
|
||||||
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
|
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Storage
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: layout
|
id: layout
|
||||||
|
|
||||||
|
|
@ -83,11 +88,11 @@ StyledRect {
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
if (!SystemUsage.primaryDisk)
|
if (!Storage.primaryDisk)
|
||||||
return qsTr("No disks detected");
|
return qsTr("No disks detected");
|
||||||
|
|
||||||
const usedFmt = SystemUsage.formatKib(SystemUsage.primaryDisk.used);
|
const usedFmt = Storage.formatKib(Storage.primaryDisk.used);
|
||||||
const totalFmt = SystemUsage.formatKib(SystemUsage.primaryDisk.total);
|
const totalFmt = Storage.formatKib(Storage.primaryDisk.total);
|
||||||
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
|
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
|
||||||
}
|
}
|
||||||
font: Tokens.font.body.large
|
font: Tokens.font.body.large
|
||||||
|
|
@ -100,20 +105,20 @@ StyledRect {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
type: SplitButton.Tonal
|
type: SplitButton.Tonal
|
||||||
disabled: !SystemUsage.disks.length
|
disabled: !Storage.disks.length
|
||||||
fallbackIcon: "storage"
|
fallbackIcon: "storage"
|
||||||
fallbackText: qsTr("No disks")
|
fallbackText: qsTr("No disks")
|
||||||
menuOnTop: true
|
menuOnTop: true
|
||||||
minLeftWidth: row.implicitWidth * 0.6
|
minLeftWidth: row.implicitWidth * 0.6
|
||||||
|
|
||||||
menuItems: disks.instances
|
menuItems: disks.instances
|
||||||
active: menuItems.find(m => m.modelData === SystemUsage.primaryDisk) ?? menuItems[0] ?? null
|
active: menuItems.find(m => m.modelData === Storage.primaryDisk) ?? menuItems[0] ?? null
|
||||||
menu.onItemSelected: item => SystemUsage.manualPrimaryDisk = (item as DiskItem).modelData
|
menu.onItemSelected: item => Storage.manualPrimaryDisk = (item as DiskItem).modelData
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
id: disks
|
id: disks
|
||||||
|
|
||||||
model: SystemUsage.disks
|
model: Storage.disks
|
||||||
|
|
||||||
DiskItem {}
|
DiskItem {}
|
||||||
}
|
}
|
||||||
|
|
@ -123,7 +128,7 @@ StyledRect {
|
||||||
component DiskItem: MenuItem {
|
component DiskItem: MenuItem {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
icon: modelData === SystemUsage.primaryDisk ? "check" : ""
|
icon: modelData === Storage.primaryDisk ? "check" : ""
|
||||||
text: modelData.mount
|
text: modelData.mount
|
||||||
activeIcon: "storage"
|
activeIcon: "storage"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
|
import Caelestia.Services
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.components.controls
|
import qs.components.controls
|
||||||
import qs.components.misc
|
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
|
|
@ -18,35 +18,43 @@ GridLayout {
|
||||||
rows: 2
|
rows: 2
|
||||||
columns: 2
|
columns: 2
|
||||||
|
|
||||||
Ref {
|
ServiceRef {
|
||||||
service: SystemUsage
|
service: Cpu
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Memory
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
Layout.topMargin: Tokens.padding.large
|
Layout.topMargin: Tokens.padding.large
|
||||||
icon: "memory"
|
icon: "memory"
|
||||||
value: SystemUsage.cpuPerc
|
value: Cpu.percentage
|
||||||
colour: Colours.palette.m3primary
|
colour: Colours.palette.m3primary
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
Layout.topMargin: Tokens.padding.large
|
Layout.topMargin: Tokens.padding.large
|
||||||
icon: "thermostat"
|
icon: "thermostat"
|
||||||
value: Math.min(1, SystemUsage.cpuTemp / 90)
|
value: Math.min(1, Cpu.temperature / 90)
|
||||||
colour: Colours.palette.m3secondary
|
colour: Colours.palette.m3secondary
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
Layout.bottomMargin: Tokens.padding.large
|
Layout.bottomMargin: Tokens.padding.large
|
||||||
icon: "memory_alt"
|
icon: "memory_alt"
|
||||||
value: SystemUsage.memPerc
|
value: Memory.percentage
|
||||||
colour: Colours.palette.m3secondary
|
colour: Colours.palette.m3secondary
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
Layout.bottomMargin: Tokens.padding.large
|
Layout.bottomMargin: Tokens.padding.large
|
||||||
icon: "hard_disk"
|
icon: "hard_disk"
|
||||||
value: SystemUsage.storagePerc
|
value: Storage.percentage
|
||||||
colour: Colours.palette.m3tertiary
|
colour: Colours.palette.m3tertiary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,16 @@ if(NOT Cava_FOUND)
|
||||||
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
find_library(SENSORS_LIBRARY NAMES sensors REQUIRED)
|
||||||
|
find_path(SENSORS_INCLUDE_DIR NAMES sensors/sensors.h REQUIRED)
|
||||||
|
if(NOT TARGET Sensors::Sensors)
|
||||||
|
add_library(Sensors::Sensors UNKNOWN IMPORTED)
|
||||||
|
set_target_properties(Sensors::Sensors PROPERTIES
|
||||||
|
IMPORTED_LOCATION "${SENSORS_LIBRARY}"
|
||||||
|
INTERFACE_INCLUDE_DIRECTORIES "${SENSORS_INCLUDE_DIR}"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
|
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
|
||||||
qt_standard_project_setup(REQUIRES 6.9)
|
qt_standard_project_setup(REQUIRES 6.9)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,25 @@
|
||||||
qml_module(caelestia-services
|
qml_module(caelestia-services
|
||||||
URI Caelestia.Services
|
URI Caelestia.Services
|
||||||
SOURCES
|
SOURCES
|
||||||
service.hpp service.cpp
|
service.cpp
|
||||||
serviceref.hpp serviceref.cpp
|
serviceref.cpp
|
||||||
beattracker.hpp beattracker.cpp
|
beattracker.cpp
|
||||||
audiocollector.hpp audiocollector.cpp
|
audiocollector.cpp
|
||||||
audioprovider.hpp audioprovider.cpp
|
audioprovider.cpp
|
||||||
cavaprovider.hpp cavaprovider.cpp
|
cavaprovider.cpp
|
||||||
|
tickingservice.cpp
|
||||||
|
sensorslib.cpp
|
||||||
|
usagefmt.cpp
|
||||||
|
cpu.cpp
|
||||||
|
gpu.cpp
|
||||||
|
memory.cpp
|
||||||
|
diskinfo.cpp
|
||||||
|
storage.cpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
PkgConfig::Pipewire
|
PkgConfig::Pipewire
|
||||||
PkgConfig::Aubio
|
PkgConfig::Aubio
|
||||||
PkgConfig::Cava
|
PkgConfig::Cava
|
||||||
|
Sensors::Sensors
|
||||||
|
caelestia-config
|
||||||
|
caelestia-internal
|
||||||
)
|
)
|
||||||
|
|
|
||||||
117
plugin/src/Caelestia/Services/cpu.cpp
Normal file
117
plugin/src/Caelestia/Services/cpu.cpp
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
#include "cpu.hpp"
|
||||||
|
|
||||||
|
#include "sensorslib.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <qfile.h>
|
||||||
|
#include <qregularexpression.h>
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
Cpu::Cpu(QObject* parent)
|
||||||
|
: TickingService(parent) {
|
||||||
|
readNameOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Cpu::name() const {
|
||||||
|
return m_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal Cpu::percentage() const {
|
||||||
|
return m_percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal Cpu::temperature() const {
|
||||||
|
return m_temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cpu::tick() {
|
||||||
|
if (!m_nameLoaded) {
|
||||||
|
readNameOnce();
|
||||||
|
}
|
||||||
|
refreshPercentage();
|
||||||
|
refreshTemperature();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cpu::readNameOnce() {
|
||||||
|
QFile f(QStringLiteral("/proc/cpuinfo"));
|
||||||
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QByteArray data = f.readAll();
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
static const QRegularExpression re(QStringLiteral("model name\\s*:\\s*(.+)"));
|
||||||
|
const auto match = re.match(QString::fromLatin1(data));
|
||||||
|
if (!match.hasMatch()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString cleaned = cleanName(match.captured(1));
|
||||||
|
m_nameLoaded = true;
|
||||||
|
if (cleaned == m_name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_name = cleaned;
|
||||||
|
Q_EMIT nameChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cpu::refreshPercentage() {
|
||||||
|
QFile f(QStringLiteral("/proc/stat"));
|
||||||
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QByteArray data = f.readAll();
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
static const QRegularExpression re(
|
||||||
|
QStringLiteral("^cpu\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)"));
|
||||||
|
const auto match = re.match(QString::fromLatin1(data));
|
||||||
|
if (!match.hasMatch()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
quint64 total = 0;
|
||||||
|
quint64 idle = 0;
|
||||||
|
for (int i = 1; i <= 7; ++i) {
|
||||||
|
const quint64 v = match.captured(i).toULongLong();
|
||||||
|
total += v;
|
||||||
|
if (i == 4 || i == 5) {
|
||||||
|
idle += v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const quint64 totalDiff = total > m_lastTotal ? total - m_lastTotal : 0;
|
||||||
|
const quint64 idleDiff = idle > m_lastIdle ? idle - m_lastIdle : 0;
|
||||||
|
const qreal newPerc = totalDiff > 0 ? 1.0 - static_cast<qreal>(idleDiff) / static_cast<qreal>(totalDiff) : 0.0;
|
||||||
|
|
||||||
|
m_lastTotal = total;
|
||||||
|
m_lastIdle = idle;
|
||||||
|
|
||||||
|
if (std::abs(newPerc - m_percentage) > 0.0001) {
|
||||||
|
m_percentage = newPerc;
|
||||||
|
Q_EMIT percentageChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cpu::refreshTemperature() {
|
||||||
|
const auto t = sensorslib::cpuPackageTemp();
|
||||||
|
const qreal newTemp = t.value_or(0.0);
|
||||||
|
if (std::abs(newTemp - m_temperature) > 0.05) {
|
||||||
|
m_temperature = newTemp;
|
||||||
|
Q_EMIT temperatureChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Cpu::cleanName(QString s) {
|
||||||
|
static const QRegularExpression noise(
|
||||||
|
QStringLiteral("\\(R\\)|\\(TM\\)|CPU|\\d+(?:th|nd|rd|st) Gen |Core |Processor"),
|
||||||
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
|
static const QRegularExpression spaces(QStringLiteral("\\s+"));
|
||||||
|
|
||||||
|
s.replace(noise, QString());
|
||||||
|
s.replace(spaces, QStringLiteral(" "));
|
||||||
|
return s.trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
48
plugin/src/Caelestia/Services/cpu.hpp
Normal file
48
plugin/src/Caelestia/Services/cpu.hpp
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "tickingservice.hpp"
|
||||||
|
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
class Cpu : public TickingService {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
|
||||||
|
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
|
||||||
|
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
|
||||||
|
Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Cpu(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] QString name() const;
|
||||||
|
[[nodiscard]] qreal percentage() const;
|
||||||
|
[[nodiscard]] qreal temperature() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void nameChanged();
|
||||||
|
void percentageChanged();
|
||||||
|
void temperatureChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void tick() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void readNameOnce();
|
||||||
|
void refreshPercentage();
|
||||||
|
void refreshTemperature();
|
||||||
|
|
||||||
|
[[nodiscard]] static QString cleanName(QString s);
|
||||||
|
|
||||||
|
QString m_name;
|
||||||
|
qreal m_percentage = 0.0;
|
||||||
|
qreal m_temperature = 0.0;
|
||||||
|
quint64 m_lastIdle = 0;
|
||||||
|
quint64 m_lastTotal = 0;
|
||||||
|
bool m_nameLoaded = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
67
plugin/src/Caelestia/Services/diskinfo.cpp
Normal file
67
plugin/src/Caelestia/Services/diskinfo.cpp
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#include "diskinfo.hpp"
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr qreal kKib = 1024.0;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
DiskInfo::DiskInfo(QString mount, quint64 usedBytes, quint64 totalBytes, bool hasRoot, QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_mount(std::move(mount))
|
||||||
|
, m_usedBytes(usedBytes)
|
||||||
|
, m_totalBytes(totalBytes)
|
||||||
|
, m_hasRoot(hasRoot) {}
|
||||||
|
|
||||||
|
QString DiskInfo::mount() const {
|
||||||
|
return m_mount;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal DiskInfo::used() const {
|
||||||
|
return static_cast<qreal>(m_usedBytes) / kKib;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal DiskInfo::total() const {
|
||||||
|
return static_cast<qreal>(m_totalBytes) / kKib;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal DiskInfo::free() const {
|
||||||
|
const quint64 freeBytes = m_totalBytes > m_usedBytes ? m_totalBytes - m_usedBytes : 0;
|
||||||
|
return static_cast<qreal>(freeBytes) / kKib;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal DiskInfo::perc() const {
|
||||||
|
return m_totalBytes > 0 ? static_cast<qreal>(m_usedBytes) / static_cast<qreal>(m_totalBytes) : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DiskInfo::hasRoot() const {
|
||||||
|
return m_hasRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DiskInfo::update(quint64 usedBytes, quint64 totalBytes, bool hasRoot) {
|
||||||
|
const bool usedDiff = usedBytes != m_usedBytes;
|
||||||
|
const bool totalDiff = totalBytes != m_totalBytes;
|
||||||
|
const bool rootDiff = hasRoot != m_hasRoot;
|
||||||
|
|
||||||
|
m_usedBytes = usedBytes;
|
||||||
|
m_totalBytes = totalBytes;
|
||||||
|
m_hasRoot = hasRoot;
|
||||||
|
|
||||||
|
if (usedDiff) {
|
||||||
|
Q_EMIT usedChanged();
|
||||||
|
}
|
||||||
|
if (totalDiff) {
|
||||||
|
Q_EMIT totalChanged();
|
||||||
|
}
|
||||||
|
if (usedDiff || totalDiff) {
|
||||||
|
Q_EMIT freeChanged();
|
||||||
|
Q_EMIT percChanged();
|
||||||
|
}
|
||||||
|
if (rootDiff) {
|
||||||
|
Q_EMIT hasRootChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
46
plugin/src/Caelestia/Services/diskinfo.hpp
Normal file
46
plugin/src/Caelestia/Services/diskinfo.hpp
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
class DiskInfo : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_UNCREATABLE("DiskInfo is created by DiskUsage")
|
||||||
|
|
||||||
|
Q_PROPERTY(QString mount READ mount CONSTANT)
|
||||||
|
Q_PROPERTY(qreal used READ used NOTIFY usedChanged)
|
||||||
|
Q_PROPERTY(qreal total READ total NOTIFY totalChanged)
|
||||||
|
Q_PROPERTY(qreal free READ free NOTIFY freeChanged)
|
||||||
|
Q_PROPERTY(qreal perc READ perc NOTIFY percChanged)
|
||||||
|
Q_PROPERTY(bool hasRoot READ hasRoot NOTIFY hasRootChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
DiskInfo(QString mount, quint64 usedBytes, quint64 totalBytes, bool hasRoot, QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] QString mount() const;
|
||||||
|
[[nodiscard]] qreal used() const; // KiB
|
||||||
|
[[nodiscard]] qreal total() const; // KiB
|
||||||
|
[[nodiscard]] qreal free() const; // KiB
|
||||||
|
[[nodiscard]] qreal perc() const;
|
||||||
|
[[nodiscard]] bool hasRoot() const;
|
||||||
|
|
||||||
|
void update(quint64 usedBytes, quint64 totalBytes, bool hasRoot);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void usedChanged();
|
||||||
|
void totalChanged();
|
||||||
|
void freeChanged();
|
||||||
|
void percChanged();
|
||||||
|
void hasRootChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_mount;
|
||||||
|
quint64 m_usedBytes;
|
||||||
|
quint64 m_totalBytes;
|
||||||
|
bool m_hasRoot;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
255
plugin/src/Caelestia/Services/gpu.cpp
Normal file
255
plugin/src/Caelestia/Services/gpu.cpp
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
#include "gpu.hpp"
|
||||||
|
|
||||||
|
#include "../Config/config.hpp"
|
||||||
|
#include "../Config/serviceconfig.hpp"
|
||||||
|
#include "sensorslib.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <qdir.h>
|
||||||
|
#include <qfile.h>
|
||||||
|
#include <qregularexpression.h>
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char* kTypeDetectScript =
|
||||||
|
"if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then echo NVIDIA;"
|
||||||
|
" elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC;"
|
||||||
|
" else echo NONE; fi";
|
||||||
|
|
||||||
|
constexpr const char* kNameDetectScript = "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null"
|
||||||
|
" || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1"
|
||||||
|
" || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Gpu::Gpu(QObject* parent)
|
||||||
|
: TickingService(parent) {
|
||||||
|
auto* svc = caelestia::config::GlobalConfig::instance()->services();
|
||||||
|
m_userType = parseType(svc->gpuType());
|
||||||
|
QObject::connect(svc, &caelestia::config::ServiceConfig::gpuTypeChanged, this,
|
||||||
|
[this, svc] { setUserType(parseType(svc->gpuType())); });
|
||||||
|
|
||||||
|
// Detection must run before any ServiceRef appears: callers may gate the ref on
|
||||||
|
// `type !== Gpu.None`, which would otherwise deadlock the detection.
|
||||||
|
if (m_userType == Auto) {
|
||||||
|
detectTypeOnce();
|
||||||
|
}
|
||||||
|
detectNameOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
Gpu::Type Gpu::type() const {
|
||||||
|
return m_userType == Auto ? m_autoType : m_userType;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gpu::Type Gpu::userType() const {
|
||||||
|
return m_userType;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gpu::Type Gpu::autoType() const {
|
||||||
|
return m_autoType;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Gpu::name() const {
|
||||||
|
return m_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal Gpu::percentage() const {
|
||||||
|
return m_percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal Gpu::temperature() const {
|
||||||
|
return m_temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gpu::setUserType(Type value) {
|
||||||
|
if (value == m_userType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Type prevDerived = type();
|
||||||
|
m_userType = value;
|
||||||
|
Q_EMIT userTypeChanged();
|
||||||
|
if (type() != prevDerived) {
|
||||||
|
Q_EMIT typeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gpu::setAutoType(Type value) {
|
||||||
|
if (value == m_autoType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Type prevDerived = type();
|
||||||
|
m_autoType = value;
|
||||||
|
Q_EMIT autoTypeChanged();
|
||||||
|
if (type() != prevDerived) {
|
||||||
|
Q_EMIT typeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gpu::setName(QString value) {
|
||||||
|
if (value == m_name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_name = std::move(value);
|
||||||
|
Q_EMIT nameChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gpu::tick() {
|
||||||
|
const Type t = type();
|
||||||
|
if (t == Generic) {
|
||||||
|
readGenericUsage();
|
||||||
|
readGpuTemperature();
|
||||||
|
} else if (t == Nvidia) {
|
||||||
|
startNvidiaUsage();
|
||||||
|
} else {
|
||||||
|
if (std::abs(m_percentage) > 0.0001) {
|
||||||
|
m_percentage = 0.0;
|
||||||
|
Q_EMIT percentageChanged();
|
||||||
|
}
|
||||||
|
if (std::abs(m_temperature) > 0.05) {
|
||||||
|
m_temperature = 0.0;
|
||||||
|
Q_EMIT temperatureChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gpu::detectTypeOnce() {
|
||||||
|
if (m_typeProc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_typeProc = new QProcess(this);
|
||||||
|
QObject::connect(m_typeProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
|
||||||
|
const QByteArray out = m_typeProc->readAllStandardOutput().trimmed();
|
||||||
|
if (!out.isEmpty()) {
|
||||||
|
setAutoType(parseType(QString::fromLatin1(out)));
|
||||||
|
}
|
||||||
|
m_typeProc->deleteLater();
|
||||||
|
m_typeProc = nullptr;
|
||||||
|
});
|
||||||
|
m_typeProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kTypeDetectScript) });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gpu::detectNameOnce() {
|
||||||
|
if (m_nameProc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_nameProc = new QProcess(this);
|
||||||
|
QObject::connect(m_nameProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
|
||||||
|
const QString output = QString::fromUtf8(m_nameProc->readAllStandardOutput()).trimmed();
|
||||||
|
if (!output.isEmpty()) {
|
||||||
|
const QString lower = output.toLower();
|
||||||
|
if (lower.contains(QStringLiteral("nvidia")) || lower.contains(QStringLiteral("geforce")) ||
|
||||||
|
lower.contains(QStringLiteral("rtx")) || lower.contains(QStringLiteral("gtx")) ||
|
||||||
|
lower.contains(QStringLiteral("rx"))) {
|
||||||
|
setName(cleanName(output));
|
||||||
|
} else {
|
||||||
|
static const QRegularExpression bracketRe(QStringLiteral("\\[([^\\]]+)\\][^\\[]*$"));
|
||||||
|
const auto bracket = bracketRe.match(output);
|
||||||
|
if (bracket.hasMatch()) {
|
||||||
|
setName(cleanName(bracket.captured(1)));
|
||||||
|
} else {
|
||||||
|
static const QRegularExpression colonRe(QStringLiteral(":\\s*(.+)"));
|
||||||
|
const auto colon = colonRe.match(output);
|
||||||
|
if (colon.hasMatch()) {
|
||||||
|
setName(cleanName(colon.captured(1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_nameProc->deleteLater();
|
||||||
|
m_nameProc = nullptr;
|
||||||
|
});
|
||||||
|
m_nameProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript) });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gpu::readGenericUsage() {
|
||||||
|
const QStringList paths =
|
||||||
|
QDir(QStringLiteral("/sys/class/drm"))
|
||||||
|
.entryList(QStringList() << QStringLiteral("card*"), QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
qreal sum = 0.0;
|
||||||
|
int count = 0;
|
||||||
|
for (const QString& card : paths) {
|
||||||
|
QFile f(QStringLiteral("/sys/class/drm/%1/device/gpu_busy_percent").arg(card));
|
||||||
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool ok = false;
|
||||||
|
const qreal v = f.readAll().trimmed().toDouble(&ok);
|
||||||
|
f.close();
|
||||||
|
if (ok) {
|
||||||
|
sum += v;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const qreal newPerc = count > 0 ? sum / count / 100.0 : 0.0;
|
||||||
|
if (std::abs(newPerc - m_percentage) > 0.0001) {
|
||||||
|
m_percentage = newPerc;
|
||||||
|
Q_EMIT percentageChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gpu::startNvidiaUsage() {
|
||||||
|
if (m_nvidiaProc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_nvidiaProc = new QProcess(this);
|
||||||
|
QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
|
||||||
|
const QByteArray out = m_nvidiaProc->readAllStandardOutput().trimmed();
|
||||||
|
m_nvidiaProc->deleteLater();
|
||||||
|
m_nvidiaProc = nullptr;
|
||||||
|
|
||||||
|
const QList<QByteArray> parts = out.split(',');
|
||||||
|
if (parts.size() < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool ok1 = false;
|
||||||
|
bool ok2 = false;
|
||||||
|
const qreal usage = parts.at(0).trimmed().toDouble(&ok1) / 100.0;
|
||||||
|
const qreal temp = parts.at(1).trimmed().toDouble(&ok2);
|
||||||
|
if (ok1 && std::abs(usage - m_percentage) > 0.0001) {
|
||||||
|
m_percentage = usage;
|
||||||
|
Q_EMIT percentageChanged();
|
||||||
|
}
|
||||||
|
if (ok2 && std::abs(temp - m_temperature) > 0.05) {
|
||||||
|
m_temperature = temp;
|
||||||
|
Q_EMIT temperatureChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
m_nvidiaProc->start(QStringLiteral("nvidia-smi"), { QStringLiteral("--query-gpu=utilization.gpu,temperature.gpu"),
|
||||||
|
QStringLiteral("--format=csv,noheader,nounits") });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gpu::readGpuTemperature() {
|
||||||
|
const auto t = sensorslib::gpuPciAverageTemp();
|
||||||
|
const qreal newTemp = t.value_or(0.0);
|
||||||
|
if (std::abs(newTemp - m_temperature) > 0.05) {
|
||||||
|
m_temperature = newTemp;
|
||||||
|
Q_EMIT temperatureChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gpu::Type Gpu::parseType(const QString& s) {
|
||||||
|
const QString u = s.trimmed().toUpper();
|
||||||
|
if (u.isEmpty()) {
|
||||||
|
return Auto;
|
||||||
|
}
|
||||||
|
if (u == QStringLiteral("NVIDIA")) {
|
||||||
|
return Nvidia;
|
||||||
|
}
|
||||||
|
if (u == QStringLiteral("GENERIC")) {
|
||||||
|
return Generic;
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Gpu::cleanName(QString s) {
|
||||||
|
static const QRegularExpression noise(
|
||||||
|
QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"), QRegularExpression::CaseInsensitiveOption);
|
||||||
|
static const QRegularExpression spaces(QStringLiteral("\\s+"));
|
||||||
|
s.replace(noise, QString());
|
||||||
|
s.replace(spaces, QStringLiteral(" "));
|
||||||
|
return s.trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
78
plugin/src/Caelestia/Services/gpu.hpp
Normal file
78
plugin/src/Caelestia/Services/gpu.hpp
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "tickingservice.hpp"
|
||||||
|
|
||||||
|
#include <qprocess.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
class Gpu : public TickingService {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Type {
|
||||||
|
Auto, // user override is empty (config "") — defer to detected autoType
|
||||||
|
None, // no usable GPU
|
||||||
|
Nvidia, // queried via nvidia-smi
|
||||||
|
Generic, // queried via /sys/class/drm/card*/device/gpu_busy_percent
|
||||||
|
};
|
||||||
|
Q_ENUM(Type)
|
||||||
|
|
||||||
|
private:
|
||||||
|
Q_PROPERTY(Type type READ type NOTIFY typeChanged)
|
||||||
|
Q_PROPERTY(Type userType READ userType NOTIFY userTypeChanged)
|
||||||
|
Q_PROPERTY(Type autoType READ autoType NOTIFY autoTypeChanged)
|
||||||
|
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
|
||||||
|
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
|
||||||
|
Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Gpu(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] Type type() const;
|
||||||
|
[[nodiscard]] Type userType() const;
|
||||||
|
[[nodiscard]] Type autoType() const;
|
||||||
|
[[nodiscard]] QString name() const;
|
||||||
|
[[nodiscard]] qreal percentage() const;
|
||||||
|
[[nodiscard]] qreal temperature() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void typeChanged();
|
||||||
|
void userTypeChanged();
|
||||||
|
void autoTypeChanged();
|
||||||
|
void nameChanged();
|
||||||
|
void percentageChanged();
|
||||||
|
void temperatureChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void tick() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void detectTypeOnce();
|
||||||
|
void detectNameOnce();
|
||||||
|
void readGenericUsage();
|
||||||
|
void startNvidiaUsage();
|
||||||
|
void readGpuTemperature();
|
||||||
|
|
||||||
|
void setUserType(Type value);
|
||||||
|
void setAutoType(Type value);
|
||||||
|
void setName(QString value);
|
||||||
|
|
||||||
|
[[nodiscard]] static Type parseType(const QString& s);
|
||||||
|
[[nodiscard]] static QString cleanName(QString s);
|
||||||
|
|
||||||
|
Type m_userType = Auto;
|
||||||
|
Type m_autoType = None;
|
||||||
|
QString m_name;
|
||||||
|
qreal m_percentage = 0.0;
|
||||||
|
qreal m_temperature = 0.0;
|
||||||
|
|
||||||
|
QProcess* m_typeProc = nullptr;
|
||||||
|
QProcess* m_nameProc = nullptr;
|
||||||
|
QProcess* m_nvidiaProc = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
64
plugin/src/Caelestia/Services/memory.cpp
Normal file
64
plugin/src/Caelestia/Services/memory.cpp
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#include "memory.hpp"
|
||||||
|
|
||||||
|
#include "usagefmt.hpp"
|
||||||
|
|
||||||
|
#include <qfile.h>
|
||||||
|
#include <qregularexpression.h>
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
Memory::Memory(QObject* parent)
|
||||||
|
: TickingService(parent) {}
|
||||||
|
|
||||||
|
qreal Memory::used() const {
|
||||||
|
return m_used;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal Memory::total() const {
|
||||||
|
return m_total;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal Memory::percentage() const {
|
||||||
|
return m_total > 0.0 ? m_used / m_total : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap Memory::formatKib(qreal kib) const {
|
||||||
|
return usagefmt::formatKib(kib);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Memory::tick() {
|
||||||
|
QFile f(QStringLiteral("/proc/meminfo"));
|
||||||
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QByteArray data = f.readAll();
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
static const QRegularExpression reTotal(QStringLiteral("MemTotal: *(\\d+)"));
|
||||||
|
static const QRegularExpression reAvail(QStringLiteral("MemAvailable: *(\\d+)"));
|
||||||
|
const QString text = QString::fromLatin1(data);
|
||||||
|
|
||||||
|
const auto totalMatch = reTotal.match(text);
|
||||||
|
const auto availMatch = reAvail.match(text);
|
||||||
|
if (!totalMatch.hasMatch() || !availMatch.hasMatch()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quint64 totalKib = totalMatch.captured(1).toULongLong();
|
||||||
|
const quint64 availKib = availMatch.captured(1).toULongLong();
|
||||||
|
if (totalKib == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const quint64 usedKib = totalKib > availKib ? totalKib - availKib : 0;
|
||||||
|
|
||||||
|
if (totalKib == m_lastTotal && usedKib == m_lastUsed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_lastTotal = totalKib;
|
||||||
|
m_lastUsed = usedKib;
|
||||||
|
m_total = static_cast<qreal>(totalKib);
|
||||||
|
m_used = static_cast<qreal>(usedKib);
|
||||||
|
Q_EMIT changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
41
plugin/src/Caelestia/Services/memory.hpp
Normal file
41
plugin/src/Caelestia/Services/memory.hpp
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "tickingservice.hpp"
|
||||||
|
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
class Memory : public TickingService {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
|
||||||
|
Q_PROPERTY(qreal used READ used NOTIFY changed)
|
||||||
|
Q_PROPERTY(qreal total READ total NOTIFY changed)
|
||||||
|
Q_PROPERTY(qreal percentage READ percentage NOTIFY changed)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Memory(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal used() const;
|
||||||
|
[[nodiscard]] qreal total() const;
|
||||||
|
[[nodiscard]] qreal percentage() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE [[nodiscard]] QVariantMap formatKib(qreal kib) const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void changed();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void tick() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
qreal m_used = 0.0;
|
||||||
|
qreal m_total = 1.0;
|
||||||
|
quint64 m_lastUsed = 0;
|
||||||
|
quint64 m_lastTotal = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
166
plugin/src/Caelestia/Services/sensorslib.cpp
Normal file
166
plugin/src/Caelestia/Services/sensorslib.cpp
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
#include "sensorslib.hpp"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
#include <qloggingcategory.h>
|
||||||
|
#include <sensors/sensors.h>
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(lcSensorsLib, "caelestia.services.sensorslib", QtInfoMsg)
|
||||||
|
|
||||||
|
namespace caelestia::services::sensorslib {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::atomic<bool> g_initOk{ false };
|
||||||
|
std::once_flag g_initFlag;
|
||||||
|
|
||||||
|
void doInit() {
|
||||||
|
if (sensors_init(nullptr) != 0) {
|
||||||
|
qCWarning(lcSensorsLib, "sensors_init failed");
|
||||||
|
g_initOk.store(false, std::memory_order_release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_initOk.store(true, std::memory_order_release);
|
||||||
|
std::atexit([] {
|
||||||
|
if (g_initOk.load(std::memory_order_acquire)) {
|
||||||
|
sensors_cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<double> readTempInput(const sensors_chip_name* chip, const sensors_feature* feat) {
|
||||||
|
const sensors_subfeature* sf = sensors_get_subfeature(chip, feat, SENSORS_SUBFEATURE_TEMP_INPUT);
|
||||||
|
if (!sf) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
double value = 0.0;
|
||||||
|
if (sensors_get_value(chip, sf->number, &value) != 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QByteArray featureLabel(const sensors_chip_name* chip, const sensors_feature* feat) {
|
||||||
|
char* raw = sensors_get_label(chip, feat);
|
||||||
|
if (!raw) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
QByteArray out(raw);
|
||||||
|
std::free(raw);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool labelEquals(const QByteArray& label, const char* literal) {
|
||||||
|
return label == QByteArrayView(literal);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool labelStartsWith(const QByteArray& label, const char* prefix) {
|
||||||
|
const auto n = std::strlen(prefix);
|
||||||
|
return static_cast<size_t>(label.size()) >= n && std::memcmp(label.constData(), prefix, n) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ensureInit() {
|
||||||
|
std::call_once(g_initFlag, doInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<double> cpuPackageTemp() {
|
||||||
|
ensureInit();
|
||||||
|
if (!g_initOk.load(std::memory_order_acquire)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<double> primary; // Package id N / Tdie
|
||||||
|
std::optional<double> fallback; // Tctl
|
||||||
|
|
||||||
|
int chipNr = 0;
|
||||||
|
while (const sensors_chip_name* chip = sensors_get_detected_chips(nullptr, &chipNr)) {
|
||||||
|
int featNr = 0;
|
||||||
|
while (const sensors_feature* feat = sensors_get_features(chip, &featNr)) {
|
||||||
|
if (feat->type != SENSORS_FEATURE_TEMP) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QByteArray label = featureLabel(chip, feat);
|
||||||
|
if (label.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelStartsWith(label, "Package id ") || labelEquals(label, "Tdie")) {
|
||||||
|
if (auto v = readTempInput(chip, feat)) {
|
||||||
|
primary = v;
|
||||||
|
}
|
||||||
|
} else if (labelEquals(label, "Tctl")) {
|
||||||
|
if (auto v = readTempInput(chip, feat)) {
|
||||||
|
fallback = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return primary.has_value() ? primary : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<double> gpuPciAverageTemp() {
|
||||||
|
ensureInit();
|
||||||
|
if (!g_initOk.load(std::memory_order_acquire)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
double sumPrimary = 0.0;
|
||||||
|
int countPrimary = 0;
|
||||||
|
double sumFallback = 0.0;
|
||||||
|
int countFallback = 0;
|
||||||
|
|
||||||
|
int chipNr = 0;
|
||||||
|
while (const sensors_chip_name* chip = sensors_get_detected_chips(nullptr, &chipNr)) {
|
||||||
|
if (chip->bus.type != SENSORS_BUS_TYPE_PCI) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int featNr = 0;
|
||||||
|
while (const sensors_feature* feat = sensors_get_features(chip, &featNr)) {
|
||||||
|
if (feat->type != SENSORS_FEATURE_TEMP) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QByteArray label = featureLabel(chip, feat);
|
||||||
|
if (label.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool tempIndexed = labelStartsWith(label, "temp") && label.size() > 4 &&
|
||||||
|
std::isdigit(static_cast<unsigned char>(label[4]));
|
||||||
|
const bool isPrimary = tempIndexed || labelEquals(label, "GPU core") || labelEquals(label, "edge");
|
||||||
|
const bool isFallback = labelEquals(label, "junction") || labelEquals(label, "mem");
|
||||||
|
|
||||||
|
if (!isPrimary && !isFallback) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto v = readTempInput(chip, feat);
|
||||||
|
if (!v) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isPrimary) {
|
||||||
|
sumPrimary += *v;
|
||||||
|
++countPrimary;
|
||||||
|
} else {
|
||||||
|
sumFallback += *v;
|
||||||
|
++countFallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countPrimary > 0) {
|
||||||
|
return sumPrimary / countPrimary;
|
||||||
|
}
|
||||||
|
if (countFallback > 0) {
|
||||||
|
return sumFallback / countFallback;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace caelestia::services::sensorslib
|
||||||
12
plugin/src/Caelestia/Services/sensorslib.hpp
Normal file
12
plugin/src/Caelestia/Services/sensorslib.hpp
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace caelestia::services::sensorslib {
|
||||||
|
|
||||||
|
void ensureInit();
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<double> cpuPackageTemp();
|
||||||
|
[[nodiscard]] std::optional<double> gpuPciAverageTemp();
|
||||||
|
|
||||||
|
} // namespace caelestia::services::sensorslib
|
||||||
297
plugin/src/Caelestia/Services/storage.cpp
Normal file
297
plugin/src/Caelestia/Services/storage.cpp
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
#include "storage.hpp"
|
||||||
|
|
||||||
|
#include "usagefmt.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <qdir.h>
|
||||||
|
#include <qfile.h>
|
||||||
|
#include <qfileinfo.h>
|
||||||
|
#include <qhash.h>
|
||||||
|
#include <qloggingcategory.h>
|
||||||
|
#include <qstorageinfo.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/sysmacros.h>
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(lcStorage, "caelestia.services.storage", QtInfoMsg)
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Accum {
|
||||||
|
quint64 usedBytes = 0;
|
||||||
|
quint64 totalBytes = 0;
|
||||||
|
bool hasRoot = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] QString sysfsRealPath(uint major, uint minor) {
|
||||||
|
const QString link = QStringLiteral("/sys/dev/block/%1:%2").arg(major).arg(minor);
|
||||||
|
const QString resolved = QFileInfo(link).canonicalFilePath();
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool readDevtFromSysfs(const QString& sysfsBlockDir, uint& major, uint& minor) {
|
||||||
|
QFile f(sysfsBlockDir + QStringLiteral("/dev"));
|
||||||
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QByteArray line = f.readLine().trimmed();
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
const qsizetype colon = line.indexOf(':');
|
||||||
|
if (colon <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool okM = false;
|
||||||
|
bool okN = false;
|
||||||
|
major = line.left(colon).toUInt(&okM);
|
||||||
|
minor = line.mid(colon + 1).toUInt(&okN);
|
||||||
|
return okM && okN;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList resolveByDevt(uint major, uint minor, int depth = 0);
|
||||||
|
|
||||||
|
QStringList resolveAtNode(const QString& node, int depth) {
|
||||||
|
if (node.isEmpty() || depth > 8) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfo nodeInfo(node);
|
||||||
|
if (!nodeInfo.exists() || !nodeInfo.isDir()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QFileInfo::exists(node + QStringLiteral("/partition"))) {
|
||||||
|
const QString diskNode = nodeInfo.path();
|
||||||
|
return { QFileInfo(diskNode).fileName() };
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDir slavesDir(node + QStringLiteral("/slaves"));
|
||||||
|
if (slavesDir.exists()) {
|
||||||
|
const QStringList slaves = slavesDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
if (!slaves.isEmpty()) {
|
||||||
|
QStringList out;
|
||||||
|
for (const QString& slave : slaves) {
|
||||||
|
uint sm = 0;
|
||||||
|
uint sn = 0;
|
||||||
|
const QString slaveDir = QStringLiteral("/sys/class/block/") + slave;
|
||||||
|
if (!readDevtFromSysfs(slaveDir, sm, sn)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto devs = resolveByDevt(sm, sn, depth + 1);
|
||||||
|
for (const QString& d : devs) {
|
||||||
|
if (!out.contains(d)) {
|
||||||
|
out.append(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nodeInfo.fileName() };
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList resolveByDevt(uint major, uint minor, int depth) {
|
||||||
|
return resolveAtNode(sysfsRealPath(major, minor), depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Storage::Storage(QObject* parent)
|
||||||
|
: TickingService(parent) {}
|
||||||
|
|
||||||
|
qreal Storage::percentage() const {
|
||||||
|
qreal totalUsed = 0.0;
|
||||||
|
qreal totalSize = 0.0;
|
||||||
|
for (const DiskInfo* d : m_disks) {
|
||||||
|
totalUsed += d->used();
|
||||||
|
totalSize += d->total();
|
||||||
|
}
|
||||||
|
return totalSize > 0.0 ? totalUsed / totalSize : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Storage::sameOrder(const QList<DiskInfo*>& a, const QList<DiskInfo*>& b) {
|
||||||
|
if (a.size() != b.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (qsizetype i = 0; i < a.size(); ++i) {
|
||||||
|
if (a.at(i) != b.at(i)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QQmlListProperty<DiskInfo> Storage::disksProp() {
|
||||||
|
return QQmlListProperty<DiskInfo>(this, nullptr, &Storage::disksCount, &Storage::disksAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
qsizetype Storage::disksCount(QQmlListProperty<DiskInfo>* prop) {
|
||||||
|
return static_cast<Storage*>(prop->object)->m_disks.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
DiskInfo* Storage::disksAt(QQmlListProperty<DiskInfo>* prop, qsizetype i) {
|
||||||
|
return static_cast<Storage*>(prop->object)->m_disks.at(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
DiskInfo* Storage::manualPrimaryDisk() const {
|
||||||
|
return m_manualPrimaryDisk.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Storage::setManualPrimaryDisk(DiskInfo* disk) {
|
||||||
|
if (m_manualPrimaryDisk.data() == disk) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_manualPrimaryDisk = disk;
|
||||||
|
Q_EMIT manualPrimaryDiskChanged();
|
||||||
|
Q_EMIT primaryDiskChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
DiskInfo* Storage::primaryDisk() const {
|
||||||
|
if (auto* m = m_manualPrimaryDisk.data()) {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
return m_disks.isEmpty() ? nullptr : m_disks.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap Storage::formatKib(qreal kib) const {
|
||||||
|
return usagefmt::formatKib(kib);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Storage::isPseudoFs(QByteArrayView fsType) {
|
||||||
|
static constexpr const char* kPseudo[] = {
|
||||||
|
"tmpfs",
|
||||||
|
"devtmpfs",
|
||||||
|
"proc",
|
||||||
|
"sysfs",
|
||||||
|
"cgroup",
|
||||||
|
"cgroup2",
|
||||||
|
"overlay",
|
||||||
|
"squashfs",
|
||||||
|
"devpts",
|
||||||
|
"mqueue",
|
||||||
|
"ramfs",
|
||||||
|
"rpc_pipefs",
|
||||||
|
"autofs",
|
||||||
|
"configfs",
|
||||||
|
"debugfs",
|
||||||
|
"tracefs",
|
||||||
|
"securityfs",
|
||||||
|
"pstore",
|
||||||
|
"bpf",
|
||||||
|
"binfmt_misc",
|
||||||
|
"hugetlbfs",
|
||||||
|
"fusectl",
|
||||||
|
"efivarfs",
|
||||||
|
"selinuxfs",
|
||||||
|
};
|
||||||
|
for (const char* p : kPseudo) {
|
||||||
|
if (fsType == QByteArrayView(p)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fsType.startsWith(QByteArrayView("fuse."));
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList Storage::resolveToPhysicalDisks(const QString& devicePath) {
|
||||||
|
if (devicePath.isEmpty() || !devicePath.startsWith(QLatin1Char('/'))) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
struct stat st{};
|
||||||
|
if (::stat(devicePath.toLocal8Bit().constData(), &st) != 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!S_ISBLK(st.st_mode)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return resolveByDevt(major(st.st_rdev), minor(st.st_rdev));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Storage::tick() {
|
||||||
|
const qreal prevPercentage = percentage();
|
||||||
|
QHash<QString, Accum> byDisk;
|
||||||
|
|
||||||
|
const auto mountedVols = QStorageInfo::mountedVolumes();
|
||||||
|
for (const QStorageInfo& v : mountedVols) {
|
||||||
|
if (!v.isReady() || !v.isValid() || v.bytesTotal() <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isPseudoFs(QByteArrayView(v.fileSystemType()))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList disks = resolveToPhysicalDisks(QString::fromLocal8Bit(v.device()));
|
||||||
|
if (disks.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto totalBytes = static_cast<quint64>(v.bytesTotal());
|
||||||
|
const auto availBytes = static_cast<quint64>(v.bytesAvailable());
|
||||||
|
const quint64 usedBytes = totalBytes > availBytes ? totalBytes - availBytes : 0;
|
||||||
|
const bool isRoot = v.rootPath() == QStringLiteral("/");
|
||||||
|
|
||||||
|
for (const QString& d : disks) {
|
||||||
|
if (d.startsWith(QStringLiteral("zram"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Accum& a = byDisk[d];
|
||||||
|
a.usedBytes += usedBytes;
|
||||||
|
a.totalBytes += totalBytes;
|
||||||
|
a.hasRoot = a.hasRoot || isRoot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<QString, DiskInfo*> existing;
|
||||||
|
existing.reserve(m_disks.size());
|
||||||
|
for (DiskInfo* d : std::as_const(m_disks)) {
|
||||||
|
existing.insert(d->mount(), d);
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<DiskInfo*> next;
|
||||||
|
next.reserve(byDisk.size());
|
||||||
|
for (auto it = byDisk.constBegin(); it != byDisk.constEnd(); ++it) {
|
||||||
|
if (DiskInfo* survivor = existing.take(it.key())) {
|
||||||
|
survivor->update(it.value().usedBytes, it.value().totalBytes, it.value().hasRoot);
|
||||||
|
next.append(survivor);
|
||||||
|
} else {
|
||||||
|
next.append(new DiskInfo(it.key(), it.value().usedBytes, it.value().totalBytes, it.value().hasRoot, this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(next.begin(), next.end(), [](const DiskInfo* a, const DiskInfo* b) {
|
||||||
|
if (a->hasRoot() != b->hasRoot()) {
|
||||||
|
return a->hasRoot();
|
||||||
|
}
|
||||||
|
return a->mount() < b->mount();
|
||||||
|
});
|
||||||
|
|
||||||
|
bool manualCleared = false;
|
||||||
|
if (DiskInfo* m = m_manualPrimaryDisk.data(); m && existing.contains(m->mount())) {
|
||||||
|
m_manualPrimaryDisk.clear();
|
||||||
|
manualCleared = true;
|
||||||
|
}
|
||||||
|
for (DiskInfo* stale : std::as_const(existing)) {
|
||||||
|
stale->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool listChanged = !sameOrder(m_disks, next);
|
||||||
|
DiskInfo* prevPrimary = primaryDisk();
|
||||||
|
m_disks = next;
|
||||||
|
|
||||||
|
if (listChanged) {
|
||||||
|
Q_EMIT disksChanged();
|
||||||
|
}
|
||||||
|
if (std::abs(percentage() - prevPercentage) > 0.0001) {
|
||||||
|
Q_EMIT percentageChanged();
|
||||||
|
}
|
||||||
|
if (manualCleared) {
|
||||||
|
Q_EMIT manualPrimaryDiskChanged();
|
||||||
|
}
|
||||||
|
if (primaryDisk() != prevPrimary) {
|
||||||
|
Q_EMIT primaryDiskChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
56
plugin/src/Caelestia/Services/storage.hpp
Normal file
56
plugin/src/Caelestia/Services/storage.hpp
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "diskinfo.hpp"
|
||||||
|
#include "tickingservice.hpp"
|
||||||
|
|
||||||
|
#include <qbytearrayview.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qqmllist.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
class Storage : public TickingService {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
|
||||||
|
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
|
||||||
|
Q_PROPERTY(QQmlListProperty<caelestia::services::DiskInfo> disks READ disksProp NOTIFY disksChanged)
|
||||||
|
Q_PROPERTY(caelestia::services::DiskInfo* manualPrimaryDisk READ manualPrimaryDisk WRITE setManualPrimaryDisk NOTIFY
|
||||||
|
manualPrimaryDiskChanged)
|
||||||
|
Q_PROPERTY(caelestia::services::DiskInfo* primaryDisk READ primaryDisk NOTIFY primaryDiskChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Storage(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal percentage() const;
|
||||||
|
[[nodiscard]] QQmlListProperty<DiskInfo> disksProp();
|
||||||
|
[[nodiscard]] DiskInfo* manualPrimaryDisk() const;
|
||||||
|
void setManualPrimaryDisk(DiskInfo* disk);
|
||||||
|
[[nodiscard]] DiskInfo* primaryDisk() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE [[nodiscard]] QVariantMap formatKib(qreal kib) const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void disksChanged();
|
||||||
|
void percentageChanged();
|
||||||
|
void manualPrimaryDiskChanged();
|
||||||
|
void primaryDiskChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void tick() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] static QStringList resolveToPhysicalDisks(const QString& devicePath);
|
||||||
|
[[nodiscard]] static bool isPseudoFs(QByteArrayView fsType);
|
||||||
|
[[nodiscard]] static bool sameOrder(const QList<DiskInfo*>& a, const QList<DiskInfo*>& b);
|
||||||
|
|
||||||
|
static qsizetype disksCount(QQmlListProperty<DiskInfo>* prop);
|
||||||
|
static DiskInfo* disksAt(QQmlListProperty<DiskInfo>* prop, qsizetype i);
|
||||||
|
|
||||||
|
QList<DiskInfo*> m_disks;
|
||||||
|
QPointer<DiskInfo> m_manualPrimaryDisk;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
51
plugin/src/Caelestia/Services/tickingservice.cpp
Normal file
51
plugin/src/Caelestia/Services/tickingservice.cpp
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#include "tickingservice.hpp"
|
||||||
|
|
||||||
|
#include "../Config/config.hpp"
|
||||||
|
#include "../Config/dashboardconfig.hpp"
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
TickingService::TickingService(QObject* parent)
|
||||||
|
: Service(parent)
|
||||||
|
, m_timer(new QTimer(this)) {
|
||||||
|
m_timer->setSingleShot(false);
|
||||||
|
QObject::connect(m_timer, &QTimer::timeout, this, [this] {
|
||||||
|
tick();
|
||||||
|
});
|
||||||
|
|
||||||
|
auto* dash = caelestia::config::GlobalConfig::instance()->dashboard();
|
||||||
|
applyInterval(dash->resourceUpdateInterval());
|
||||||
|
QObject::connect(dash, &caelestia::config::DashboardConfig::resourceUpdateIntervalChanged, this, [this, dash] {
|
||||||
|
applyInterval(dash->resourceUpdateInterval());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int TickingService::updateInterval() const {
|
||||||
|
return m_interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TickingService::start() {
|
||||||
|
m_running = true;
|
||||||
|
if (m_interval > 0) {
|
||||||
|
m_timer->start(m_interval);
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TickingService::stop() {
|
||||||
|
m_running = false;
|
||||||
|
m_timer->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TickingService::applyInterval(int ms) {
|
||||||
|
if (ms <= 0 || ms == m_interval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_interval = ms;
|
||||||
|
if (m_running) {
|
||||||
|
m_timer->start(m_interval);
|
||||||
|
}
|
||||||
|
Q_EMIT updateIntervalChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
34
plugin/src/Caelestia/Services/tickingservice.hpp
Normal file
34
plugin/src/Caelestia/Services/tickingservice.hpp
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "service.hpp"
|
||||||
|
|
||||||
|
namespace caelestia::services {
|
||||||
|
|
||||||
|
class TickingService : public Service {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(int updateInterval READ updateInterval NOTIFY updateIntervalChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TickingService(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] int updateInterval() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void updateIntervalChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void start() final;
|
||||||
|
void stop() final;
|
||||||
|
|
||||||
|
virtual void tick() = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void applyInterval(int ms);
|
||||||
|
|
||||||
|
QTimer* m_timer;
|
||||||
|
int m_interval = 1000;
|
||||||
|
bool m_running = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace caelestia::services
|
||||||
69
plugin/src/Caelestia/Services/usagefmt.cpp
Normal file
69
plugin/src/Caelestia/Services/usagefmt.cpp
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#include "usagefmt.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace caelestia::services::usagefmt {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr qreal kKib = 1024.0;
|
||||||
|
constexpr qreal kMib = kKib * 1024.0;
|
||||||
|
constexpr qreal kGib = kMib * 1024.0;
|
||||||
|
constexpr qreal kTib = kGib * 1024.0;
|
||||||
|
|
||||||
|
QVariantMap make(qreal value, const char* unit) {
|
||||||
|
return QVariantMap{ { QStringLiteral("value"), value }, { QStringLiteral("unit"), QString::fromLatin1(unit) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool finitePositive(qreal v) {
|
||||||
|
return std::isfinite(v) && v >= 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QVariantMap formatKib(qreal kib) {
|
||||||
|
if (kib >= kTib) {
|
||||||
|
return make(kib / kTib, "TiB");
|
||||||
|
}
|
||||||
|
if (kib >= kGib) {
|
||||||
|
return make(kib / kGib, "GiB");
|
||||||
|
}
|
||||||
|
if (kib >= kMib) {
|
||||||
|
return make(kib / kMib, "MiB");
|
||||||
|
}
|
||||||
|
return make(kib, "KiB");
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap formatBytes(qreal bytes) {
|
||||||
|
if (!finitePositive(bytes)) {
|
||||||
|
return make(0.0, "B/s");
|
||||||
|
}
|
||||||
|
if (bytes < kKib) {
|
||||||
|
return make(bytes, "B/s");
|
||||||
|
}
|
||||||
|
if (bytes < kMib) {
|
||||||
|
return make(bytes / kKib, "KB/s");
|
||||||
|
}
|
||||||
|
if (bytes < kGib) {
|
||||||
|
return make(bytes / kMib, "MB/s");
|
||||||
|
}
|
||||||
|
return make(bytes / kGib, "GB/s");
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap formatBytesTotal(qreal bytes) {
|
||||||
|
if (!finitePositive(bytes)) {
|
||||||
|
return make(0.0, "B");
|
||||||
|
}
|
||||||
|
if (bytes < kKib) {
|
||||||
|
return make(bytes, "B");
|
||||||
|
}
|
||||||
|
if (bytes < kMib) {
|
||||||
|
return make(bytes / kKib, "KB");
|
||||||
|
}
|
||||||
|
if (bytes < kGib) {
|
||||||
|
return make(bytes / kMib, "MB");
|
||||||
|
}
|
||||||
|
return make(bytes / kGib, "GB");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace caelestia::services::usagefmt
|
||||||
11
plugin/src/Caelestia/Services/usagefmt.hpp
Normal file
11
plugin/src/Caelestia/Services/usagefmt.hpp
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
|
namespace caelestia::services::usagefmt {
|
||||||
|
|
||||||
|
[[nodiscard]] QVariantMap formatKib(qreal kib);
|
||||||
|
[[nodiscard]] QVariantMap formatBytes(qreal bytes);
|
||||||
|
[[nodiscard]] QVariantMap formatBytesTotal(qreal bytes);
|
||||||
|
|
||||||
|
} // namespace caelestia::services::usagefmt
|
||||||
|
|
@ -1,332 +0,0 @@
|
||||||
pragma Singleton
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Caelestia.Config
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// CPU properties
|
|
||||||
property string cpuName: ""
|
|
||||||
property real cpuPerc
|
|
||||||
property real cpuTemp
|
|
||||||
|
|
||||||
// GPU properties
|
|
||||||
readonly property string gpuType: GlobalConfig.services.gpuType.toUpperCase() || autoGpuType
|
|
||||||
property string autoGpuType: "NONE"
|
|
||||||
property string gpuName: ""
|
|
||||||
property real gpuPerc
|
|
||||||
property real gpuTemp
|
|
||||||
|
|
||||||
// Memory properties
|
|
||||||
property real memUsed
|
|
||||||
property real memTotal
|
|
||||||
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
|
|
||||||
|
|
||||||
// Storage properties (aggregated)
|
|
||||||
readonly property real storagePerc: {
|
|
||||||
let totalUsed = 0;
|
|
||||||
let totalSize = 0;
|
|
||||||
for (const disk of disks) {
|
|
||||||
totalUsed += disk.used;
|
|
||||||
totalSize += disk.total;
|
|
||||||
}
|
|
||||||
return totalSize > 0 ? totalUsed / totalSize : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Individual disks: Array of { mount, used, total, free, perc }
|
|
||||||
property var disks: []
|
|
||||||
property var manualPrimaryDisk
|
|
||||||
readonly property var primaryDisk: manualPrimaryDisk ?? disks[0] ?? null
|
|
||||||
|
|
||||||
property real lastCpuIdle
|
|
||||||
property real lastCpuTotal
|
|
||||||
|
|
||||||
property int refCount
|
|
||||||
|
|
||||||
function cleanCpuName(name: string): string {
|
|
||||||
return name.replace(/\(R\)|\(TM\)|CPU|\d+(?:th|nd|rd|st) Gen |Core |Processor/gi, "").replace(/\s+/g, " ").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanGpuName(name: string): string {
|
|
||||||
return name.replace(/\(R\)|\(TM\)|Graphics/gi, "").replace(/\s+/g, " ").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatKib(kib: real): var {
|
|
||||||
const mib = 1024;
|
|
||||||
const gib = 1024 ** 2;
|
|
||||||
const tib = 1024 ** 3;
|
|
||||||
|
|
||||||
if (kib >= tib)
|
|
||||||
return {
|
|
||||||
value: kib / tib,
|
|
||||||
unit: "TiB"
|
|
||||||
};
|
|
||||||
if (kib >= gib)
|
|
||||||
return {
|
|
||||||
value: kib / gib,
|
|
||||||
unit: "GiB"
|
|
||||||
};
|
|
||||||
if (kib >= mib)
|
|
||||||
return {
|
|
||||||
value: kib / mib,
|
|
||||||
unit: "MiB"
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
value: kib,
|
|
||||||
unit: "KiB"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
running: root.refCount > 0
|
|
||||||
interval: GlobalConfig.dashboard.resourceUpdateInterval
|
|
||||||
repeat: true
|
|
||||||
triggeredOnStart: true
|
|
||||||
onTriggered: {
|
|
||||||
stat.reload();
|
|
||||||
meminfo.reload();
|
|
||||||
storage.running = true;
|
|
||||||
gpuUsage.running = true;
|
|
||||||
sensors.running = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// One-time CPU info detection (name)
|
|
||||||
FileView {
|
|
||||||
id: cpuinfoInit
|
|
||||||
|
|
||||||
path: "/proc/cpuinfo"
|
|
||||||
onLoaded: {
|
|
||||||
const nameMatch = text().match(/model name\s*:\s*(.+)/);
|
|
||||||
if (nameMatch)
|
|
||||||
root.cpuName = root.cleanCpuName(nameMatch[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: stat
|
|
||||||
|
|
||||||
path: "/proc/stat"
|
|
||||||
onLoaded: {
|
|
||||||
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
|
|
||||||
if (data) {
|
|
||||||
const stats = data.slice(1).map(n => parseInt(n, 10));
|
|
||||||
const total = stats.reduce((a, b) => a + b, 0);
|
|
||||||
const idle = stats[3] + (stats[4] ?? 0);
|
|
||||||
|
|
||||||
const totalDiff = total - root.lastCpuTotal;
|
|
||||||
const idleDiff = idle - root.lastCpuIdle;
|
|
||||||
root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
|
|
||||||
|
|
||||||
root.lastCpuTotal = total;
|
|
||||||
root.lastCpuIdle = idle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: meminfo
|
|
||||||
|
|
||||||
path: "/proc/meminfo"
|
|
||||||
onLoaded: {
|
|
||||||
const data = text();
|
|
||||||
root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
|
|
||||||
root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: storage
|
|
||||||
|
|
||||||
// Get physical disks with aggregated usage from their partitions
|
|
||||||
// -J triggers JSON output. -b triggers bytes.
|
|
||||||
command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"]
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const data = JSON.parse(text);
|
|
||||||
const diskList = [];
|
|
||||||
const seenDevices = new Set();
|
|
||||||
|
|
||||||
// Helper to recursively sum usage from children (partitions, crypt, lvm)
|
|
||||||
const aggregateUsage = dev => {
|
|
||||||
let used = 0;
|
|
||||||
let size = 0;
|
|
||||||
let isRoot = dev.mountpoint === "/" || (dev.mountpoints && dev.mountpoints.includes("/"));
|
|
||||||
|
|
||||||
if (!seenDevices.has(dev.name)) {
|
|
||||||
// lsblk returns null for empty/unformatted partitions, which parses to 0 here
|
|
||||||
used = parseInt(dev.fsused) || 0;
|
|
||||||
size = parseInt(dev.fssize) || 0;
|
|
||||||
seenDevices.add(dev.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dev.children) {
|
|
||||||
for (const child of dev.children) {
|
|
||||||
const stats = aggregateUsage(child);
|
|
||||||
used += stats.used;
|
|
||||||
size += stats.size;
|
|
||||||
if (stats.isRoot)
|
|
||||||
isRoot = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
used,
|
|
||||||
size,
|
|
||||||
isRoot
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const dev of data.blockdevices) {
|
|
||||||
// Only process physical disks at the top level
|
|
||||||
if (dev.type === "disk" && !dev.name.startsWith("zram")) {
|
|
||||||
const stats = aggregateUsage(dev);
|
|
||||||
|
|
||||||
if (stats.size === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const total = stats.size;
|
|
||||||
const used = stats.used;
|
|
||||||
|
|
||||||
diskList.push({
|
|
||||||
mount: dev.name,
|
|
||||||
used: used / 1024 // KiB
|
|
||||||
,
|
|
||||||
total: total / 1024 // KiB
|
|
||||||
,
|
|
||||||
free: (total - used) / 1024,
|
|
||||||
perc: total > 0 ? used / total : 0,
|
|
||||||
hasRoot: stats.isRoot
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by putting the disk with root first, then sort the rest alphabetically
|
|
||||||
root.disks = diskList.sort((a, b) => {
|
|
||||||
if (a.hasRoot && !b.hasRoot)
|
|
||||||
return -1;
|
|
||||||
if (!a.hasRoot && b.hasRoot)
|
|
||||||
return 1;
|
|
||||||
return a.mount.localeCompare(b.mount);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GPU name detection (one-time)
|
|
||||||
Process {
|
|
||||||
id: gpuNameDetect
|
|
||||||
|
|
||||||
running: true
|
|
||||||
command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1 || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const output = text.trim();
|
|
||||||
if (!output)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Check if it's from nvidia-smi (clean GPU name)
|
|
||||||
if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
|
|
||||||
root.gpuName = root.cleanGpuName(output);
|
|
||||||
} else if (output.toLowerCase().includes("rx")) {
|
|
||||||
root.gpuName = root.cleanGpuName(output);
|
|
||||||
} else {
|
|
||||||
// Parse lspci output: extract name from brackets or after colon
|
|
||||||
// Handles cases like [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] (rev c0)
|
|
||||||
const bracketMatch = output.match(/\[([^\]]+)\][^\[]*$/);
|
|
||||||
if (bracketMatch) {
|
|
||||||
root.gpuName = root.cleanGpuName(bracketMatch[1]);
|
|
||||||
} else {
|
|
||||||
const colonMatch = output.match(/:\s*(.+)/);
|
|
||||||
if (colonMatch)
|
|
||||||
root.gpuName = root.cleanGpuName(colonMatch[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: gpuTypeCheck
|
|
||||||
|
|
||||||
running: !GlobalConfig.services.gpuType
|
|
||||||
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: root.autoGpuType = text.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: gpuUsage
|
|
||||||
|
|
||||||
command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits"] : ["echo"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (root.gpuType === "GENERIC") {
|
|
||||||
const percs = text.trim().split("\n");
|
|
||||||
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
|
|
||||||
root.gpuPerc = sum / percs.length / 100;
|
|
||||||
} else if (root.gpuType === "NVIDIA") {
|
|
||||||
const [usage, temp] = text.trim().split(",");
|
|
||||||
root.gpuPerc = parseInt(usage, 10) / 100;
|
|
||||||
root.gpuTemp = parseInt(temp, 10);
|
|
||||||
} else {
|
|
||||||
root.gpuPerc = 0;
|
|
||||||
root.gpuTemp = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: sensors
|
|
||||||
|
|
||||||
command: ["sensors"]
|
|
||||||
environment: ({
|
|
||||||
LANG: "C.UTF-8",
|
|
||||||
LC_ALL: "C.UTF-8"
|
|
||||||
})
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
|
|
||||||
if (!cpuTemp)
|
|
||||||
// If AMD Tdie pattern failed, try fallback on Tctl
|
|
||||||
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
|
|
||||||
|
|
||||||
if (cpuTemp)
|
|
||||||
root.cpuTemp = parseFloat(cpuTemp[1]);
|
|
||||||
|
|
||||||
if (root.gpuType !== "GENERIC")
|
|
||||||
return;
|
|
||||||
|
|
||||||
let eligible = false;
|
|
||||||
let sum = 0;
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
for (const line of text.trim().split("\n")) {
|
|
||||||
if (line === "Adapter: PCI adapter")
|
|
||||||
eligible = true;
|
|
||||||
else if (line === "")
|
|
||||||
eligible = false;
|
|
||||||
else if (eligible) {
|
|
||||||
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
|
||||||
if (!match)
|
|
||||||
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
|
|
||||||
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
sum += parseFloat(match[2]);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
root.gpuTemp = count > 0 ? sum / count : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue