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 Quickshell.Services.UPower
|
||||
import Caelestia.Config
|
||||
import Caelestia.Services
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
|
|
@ -12,8 +13,8 @@ SectionContainer {
|
|||
id: root
|
||||
|
||||
required property var rootItem
|
||||
// GPU toggle is hidden when gpuType is "NONE" (no GPU data available)
|
||||
readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE"
|
||||
// GPU toggle is hidden when type is Gpu.None (no GPU data available)
|
||||
readonly property bool gpuAvailable: Gpu.type !== Gpu.None
|
||||
// Battery toggle is hidden when no laptop battery is present
|
||||
readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import QtQuick.Layouts
|
|||
import Quickshell.Services.UPower
|
||||
import Caelestia.Config
|
||||
import Caelestia.Internal
|
||||
import Caelestia.Services
|
||||
import qs.components
|
||||
import qs.components.misc
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
|
|
@ -29,7 +29,7 @@ Item {
|
|||
height: 350
|
||||
radius: Tokens.rounding.extraLarge
|
||||
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 {
|
||||
anchors.centerIn: parent
|
||||
|
|
@ -66,8 +66,12 @@ Item {
|
|||
spacing: Tokens.spacing.medium
|
||||
visible: !placeholder.visible
|
||||
|
||||
Ref {
|
||||
service: SystemUsage
|
||||
ServiceRef {
|
||||
service: root.Config.dashboard.performance.showCpu ? Cpu : null
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: root.Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None ? Gpu : null
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
|
@ -79,27 +83,27 @@ Item {
|
|||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
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 {
|
||||
Layout.fillWidth: true
|
||||
visible: Config.dashboard.performance.showCpu
|
||||
icon: "memory"
|
||||
label: qsTr("CPU")
|
||||
subLabel: SystemUsage.cpuName
|
||||
usage: SystemUsage.cpuPerc
|
||||
temperature: SystemUsage.cpuTemp
|
||||
subLabel: Cpu.name
|
||||
usage: Cpu.percentage
|
||||
temperature: Cpu.temperature
|
||||
accent: Colours.palette.m3primary
|
||||
}
|
||||
|
||||
HeroCard {
|
||||
Layout.fillWidth: true
|
||||
visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE"
|
||||
visible: Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None
|
||||
icon: "desktop_windows"
|
||||
label: qsTr("GPU")
|
||||
subLabel: SystemUsage.gpuName
|
||||
usage: SystemUsage.gpuPerc
|
||||
temperature: SystemUsage.gpuTemp
|
||||
subLabel: Gpu.name
|
||||
usage: Gpu.percentage
|
||||
temperature: Gpu.temperature
|
||||
accent: Colours.palette.m3secondary
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Caelestia.Config
|
||||
import Caelestia.Services
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.misc
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
|
|
@ -14,8 +14,16 @@ Item {
|
|||
|
||||
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
|
||||
|
||||
Ref {
|
||||
service: SystemUsage
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Memory
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Storage
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
|
@ -29,18 +37,18 @@ Item {
|
|||
|
||||
Resource {
|
||||
icon: "memory"
|
||||
value: SystemUsage.cpuPerc
|
||||
value: Cpu.percentage
|
||||
}
|
||||
|
||||
Resource {
|
||||
icon: "memory_alt"
|
||||
value: SystemUsage.memPerc
|
||||
value: Memory.percentage
|
||||
fgColour: Colours.palette.m3tertiary
|
||||
}
|
||||
|
||||
Resource {
|
||||
icon: "hard_disk"
|
||||
value: SystemUsage.storagePerc
|
||||
value: Storage.percentage
|
||||
fgColour: Colours.palette.m3secondary
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Caelestia.Config
|
||||
import Caelestia.Services
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
|
|
@ -16,6 +17,10 @@ StyledRect {
|
|||
implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2
|
||||
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
|
||||
|
||||
ServiceRef {
|
||||
service: Memory
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
|
|
@ -47,7 +52,7 @@ StyledRect {
|
|||
sweepAngle: 270
|
||||
|
||||
fgColour: root.accent
|
||||
value: SystemUsage.memPerc
|
||||
value: Memory.percentage
|
||||
|
||||
Behavior on clampedVal {
|
||||
Anim {}
|
||||
|
|
@ -62,7 +67,7 @@ StyledRect {
|
|||
|
||||
StyledText {
|
||||
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()
|
||||
color: root.accent
|
||||
}
|
||||
|
|
@ -79,8 +84,8 @@ StyledRect {
|
|||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: {
|
||||
const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed);
|
||||
const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal);
|
||||
const usedFmt = Memory.formatKib(Memory.used);
|
||||
const totalFmt = Memory.formatKib(Memory.total);
|
||||
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
|
||||
}
|
||||
font: Tokens.font.body.medium
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import QtQuick
|
|||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Caelestia.Config
|
||||
import Caelestia.Services
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
|
|
@ -10,7 +11,7 @@ StyledRect {
|
|||
id: root
|
||||
|
||||
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
|
||||
radius: Tokens.rounding.extraExtraLarge
|
||||
|
|
@ -18,6 +19,10 @@ StyledRect {
|
|||
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
|
||||
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
|
||||
|
||||
ServiceRef {
|
||||
service: Storage
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
|
|
@ -83,11 +88,11 @@ StyledRect {
|
|||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!SystemUsage.primaryDisk)
|
||||
if (!Storage.primaryDisk)
|
||||
return qsTr("No disks detected");
|
||||
|
||||
const usedFmt = SystemUsage.formatKib(SystemUsage.primaryDisk.used);
|
||||
const totalFmt = SystemUsage.formatKib(SystemUsage.primaryDisk.total);
|
||||
const usedFmt = Storage.formatKib(Storage.primaryDisk.used);
|
||||
const totalFmt = Storage.formatKib(Storage.primaryDisk.total);
|
||||
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
|
||||
}
|
||||
font: Tokens.font.body.large
|
||||
|
|
@ -100,20 +105,20 @@ StyledRect {
|
|||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
type: SplitButton.Tonal
|
||||
disabled: !SystemUsage.disks.length
|
||||
disabled: !Storage.disks.length
|
||||
fallbackIcon: "storage"
|
||||
fallbackText: qsTr("No disks")
|
||||
menuOnTop: true
|
||||
minLeftWidth: row.implicitWidth * 0.6
|
||||
|
||||
menuItems: disks.instances
|
||||
active: menuItems.find(m => m.modelData === SystemUsage.primaryDisk) ?? menuItems[0] ?? null
|
||||
menu.onItemSelected: item => SystemUsage.manualPrimaryDisk = (item as DiskItem).modelData
|
||||
active: menuItems.find(m => m.modelData === Storage.primaryDisk) ?? menuItems[0] ?? null
|
||||
menu.onItemSelected: item => Storage.manualPrimaryDisk = (item as DiskItem).modelData
|
||||
|
||||
Variants {
|
||||
id: disks
|
||||
|
||||
model: SystemUsage.disks
|
||||
model: Storage.disks
|
||||
|
||||
DiskItem {}
|
||||
}
|
||||
|
|
@ -123,7 +128,7 @@ StyledRect {
|
|||
component DiskItem: MenuItem {
|
||||
required property var modelData
|
||||
|
||||
icon: modelData === SystemUsage.primaryDisk ? "check" : ""
|
||||
icon: modelData === Storage.primaryDisk ? "check" : ""
|
||||
text: modelData.mount
|
||||
activeIcon: "storage"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Caelestia.Config
|
||||
import Caelestia.Services
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.misc
|
||||
import qs.services
|
||||
|
||||
GridLayout {
|
||||
|
|
@ -18,35 +18,43 @@ GridLayout {
|
|||
rows: 2
|
||||
columns: 2
|
||||
|
||||
Ref {
|
||||
service: SystemUsage
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Memory
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Storage
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.topMargin: Tokens.padding.large
|
||||
icon: "memory"
|
||||
value: SystemUsage.cpuPerc
|
||||
value: Cpu.percentage
|
||||
colour: Colours.palette.m3primary
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.topMargin: Tokens.padding.large
|
||||
icon: "thermostat"
|
||||
value: Math.min(1, SystemUsage.cpuTemp / 90)
|
||||
value: Math.min(1, Cpu.temperature / 90)
|
||||
colour: Colours.palette.m3secondary
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Tokens.padding.large
|
||||
icon: "memory_alt"
|
||||
value: SystemUsage.memPerc
|
||||
value: Memory.percentage
|
||||
colour: Colours.palette.m3secondary
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Tokens.padding.large
|
||||
icon: "hard_disk"
|
||||
value: SystemUsage.storagePerc
|
||||
value: Storage.percentage
|
||||
colour: Colours.palette.m3tertiary
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,16 @@ if(NOT Cava_FOUND)
|
|||
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
||||
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")
|
||||
qt_standard_project_setup(REQUIRES 6.9)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
qml_module(caelestia-services
|
||||
URI Caelestia.Services
|
||||
SOURCES
|
||||
service.hpp service.cpp
|
||||
serviceref.hpp serviceref.cpp
|
||||
beattracker.hpp beattracker.cpp
|
||||
audiocollector.hpp audiocollector.cpp
|
||||
audioprovider.hpp audioprovider.cpp
|
||||
cavaprovider.hpp cavaprovider.cpp
|
||||
service.cpp
|
||||
serviceref.cpp
|
||||
beattracker.cpp
|
||||
audiocollector.cpp
|
||||
audioprovider.cpp
|
||||
cavaprovider.cpp
|
||||
tickingservice.cpp
|
||||
sensorslib.cpp
|
||||
usagefmt.cpp
|
||||
cpu.cpp
|
||||
gpu.cpp
|
||||
memory.cpp
|
||||
diskinfo.cpp
|
||||
storage.cpp
|
||||
LIBRARIES
|
||||
PkgConfig::Pipewire
|
||||
PkgConfig::Aubio
|
||||
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