resources popout refresh + new components & reskinned old components
This commit is contained in:
@@ -1,19 +1,27 @@
|
||||
qml_module(ZShell-services
|
||||
URI ZShell.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
|
||||
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
|
||||
desktopmodel.hpp desktopmodel.cpp
|
||||
desktopstatemanager.hpp desktopstatemanager.cpp
|
||||
hyprsunsetmanager.hpp hyprsunsetmanager.cpp
|
||||
LIBRARIES
|
||||
tickingservice.hpp tickingservice.cpp
|
||||
sensorslib.hpp sensorslib.cpp
|
||||
usagefmt.hpp usagefmt.cpp
|
||||
cpu.hpp cpu.cpp
|
||||
memory.hpp memory.cpp
|
||||
diskinfo.hpp diskinfo.cpp
|
||||
storage.hpp storage.cpp
|
||||
LIBRARIES
|
||||
Qt6::Core
|
||||
Qt6::Qml
|
||||
PkgConfig::Pipewire
|
||||
PkgConfig::Aubio
|
||||
PkgConfig::Cava
|
||||
PkgConfig::Pipewire
|
||||
PkgConfig::Aubio
|
||||
PkgConfig::Cava
|
||||
Sensors::Sensors
|
||||
)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "cpu.hpp"
|
||||
|
||||
#include "sensorslib.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <qfile.h>
|
||||
#include <qregularexpression.h>
|
||||
|
||||
namespace ZShell::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 ZShell::services
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "tickingservice.hpp"
|
||||
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace ZShell::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 ZShell::services
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "diskinfo.hpp"
|
||||
|
||||
namespace ZShell::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 ZShell::services
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace ZShell::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;
|
||||
[[nodiscard]] qreal total() const;
|
||||
[[nodiscard]] qreal free() const;
|
||||
[[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 ZShell::services
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "memory.hpp"
|
||||
|
||||
#include <qfile.h>
|
||||
#include <qregularexpression.h>
|
||||
|
||||
namespace ZShell::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;
|
||||
}
|
||||
|
||||
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 ZShell::services
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "tickingservice.hpp"
|
||||
|
||||
#include <qqmlintegration.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
namespace ZShell::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;
|
||||
|
||||
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 ZShell::services
|
||||
@@ -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, "ZShell.services.sensorslib", QtInfoMsg)
|
||||
|
||||
namespace ZShell::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 ZShell::services::sensorslib
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace ZShell::services::sensorslib {
|
||||
|
||||
void ensureInit();
|
||||
|
||||
[[nodiscard]] std::optional<double> cpuPackageTemp();
|
||||
[[nodiscard]] std::optional<double> gpuPciAverageTemp();
|
||||
|
||||
} // namespace ZShell::services::sensorslib
|
||||
@@ -0,0 +1,313 @@
|
||||
#include "storage.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, "ZShell.services.storage", QtInfoMsg)
|
||||
|
||||
namespace ZShell::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();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Multiple mounts can share a single backing filesystem (btrfs subvolumes,
|
||||
// bind mounts, etc.) and each one reports identical bytesTotal/bytesAvailable.
|
||||
// Dedupe by source device so the filesystem only contributes once per disk.
|
||||
struct DeviceEntry {
|
||||
quint64 totalBytes = 0;
|
||||
quint64 usedBytes = 0;
|
||||
bool hasRoot = false;
|
||||
QByteArray device;
|
||||
};
|
||||
|
||||
QHash<QByteArray, DeviceEntry> byDevice;
|
||||
|
||||
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 QByteArray device = v.device();
|
||||
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("/");
|
||||
|
||||
DeviceEntry& e = byDevice[device];
|
||||
e.device = device;
|
||||
e.totalBytes = totalBytes;
|
||||
e.usedBytes = usedBytes;
|
||||
e.hasRoot = e.hasRoot || isRoot;
|
||||
}
|
||||
|
||||
for (auto it = byDevice.constBegin(); it != byDevice.constEnd(); ++it) {
|
||||
const DeviceEntry& e = it.value();
|
||||
const QStringList disks = resolveToPhysicalDisks(QString::fromLocal8Bit(e.device));
|
||||
if (disks.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (const QString& d : disks) {
|
||||
if (d.startsWith(QStringLiteral("zram"))) {
|
||||
continue;
|
||||
}
|
||||
Accum& a = byDisk[d];
|
||||
a.usedBytes += e.usedBytes;
|
||||
a.totalBytes += e.totalBytes;
|
||||
a.hasRoot = a.hasRoot || e.hasRoot;
|
||||
}
|
||||
}
|
||||
|
||||
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 ZShell::services
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "diskinfo.hpp"
|
||||
#include "tickingservice.hpp"
|
||||
|
||||
#include <qbytearrayview.h>
|
||||
#include <qpointer.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class Storage : public TickingService {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
|
||||
Q_PROPERTY(QQmlListProperty<ZShell::services::DiskInfo> disks READ disksProp NOTIFY disksChanged)
|
||||
Q_PROPERTY(ZShell::services::DiskInfo* manualPrimaryDisk READ manualPrimaryDisk WRITE setManualPrimaryDisk NOTIFY
|
||||
manualPrimaryDiskChanged)
|
||||
Q_PROPERTY(ZShell::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;
|
||||
|
||||
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 ZShell::services
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "tickingservice.hpp"
|
||||
|
||||
namespace ZShell::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();
|
||||
});
|
||||
}
|
||||
|
||||
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 ZShell::services
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "service.hpp"
|
||||
#include <qtimer.h>
|
||||
|
||||
namespace ZShell::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 ZShell::services
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "usagefmt.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr qreal kKib = 1024.0;
|
||||
constexpr qreal kMib = kKib * 1024.0;
|
||||
constexpr qreal kGib = kMib * 1024.0;
|
||||
|
||||
bool finitePositive(qreal v) {
|
||||
return std::isfinite(v) && v >= 0.0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ZShell::services::usagefmt {
|
||||
|
||||
FormatResult UsageFmt::formatKib(qreal kib, qreal total) const {
|
||||
if (!finitePositive(kib) || !finitePositive(total)) {
|
||||
return { 0.0, 0.0, "KiB" };
|
||||
}
|
||||
if (total >= kGib) {
|
||||
return { kib / kGib, total / kGib, "TiB" };
|
||||
}
|
||||
if (total >= kMib) {
|
||||
return { kib / kMib, total / kMib, "GiB" };
|
||||
}
|
||||
if (total >= kKib) {
|
||||
return { kib / kKib, total / kKib, "MiB" };
|
||||
}
|
||||
return { kib, total, "KiB" };
|
||||
}
|
||||
|
||||
} // namespace ZShell::services::usagefmt
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qobjectdefs.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
namespace ZShell::services::usagefmt {
|
||||
|
||||
struct FormatResult {
|
||||
Q_GADGET
|
||||
QML_ANONYMOUS
|
||||
|
||||
Q_PROPERTY(qreal value MEMBER value CONSTANT)
|
||||
Q_PROPERTY(qreal total MEMBER total CONSTANT)
|
||||
Q_PROPERTY(QString unit MEMBER unit CONSTANT)
|
||||
|
||||
public:
|
||||
qreal value;
|
||||
qreal total;
|
||||
QString unit;
|
||||
};
|
||||
|
||||
class UsageFmt : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib, qreal total) const;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services::usagefmt
|
||||
Reference in New Issue
Block a user