Refine Framework monitoring history and configurable tray telemetry
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include "chart.h"
|
||||
#include "graphdata.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QMouseEvent>
|
||||
@@ -90,11 +91,18 @@ void Chart::setSelected(const QString &id, bool selected)
|
||||
} else m_selected.remove(id);
|
||||
update();
|
||||
}
|
||||
QColor Chart::color(const QString &id) const { return m_series.value(id).color; }
|
||||
QVector<QPointF> Chart::history(const QString &id) const
|
||||
QColor Chart::color(const QString &id) const
|
||||
{
|
||||
const auto series = m_series.constFind(id);
|
||||
return series == m_series.cend() ? QColor() : series->color;
|
||||
}
|
||||
QVector<QPointF> Chart::history(const QString &id, double since) const
|
||||
{
|
||||
QVector<QPointF> points;
|
||||
for (const auto &point : m_series.value(id).points) {
|
||||
const auto series = m_series.constFind(id);
|
||||
if (series == m_series.cend()) return points;
|
||||
for (auto it = historyStart(series->points, since); it != series->points.cend(); ++it) {
|
||||
const auto &point = *it;
|
||||
if (!points.isEmpty() && (point.x() - points.last().x() > m_interval * 3 || crossesSleep(points.last().x(), point.x())))
|
||||
points.append({point.x(), NAN});
|
||||
points.append(point);
|
||||
@@ -107,9 +115,8 @@ void Chart::sample(const QMap<QString, double> &values, int intervalMs, qint64 n
|
||||
m_sampleTime = now;
|
||||
double oldest = now;
|
||||
for (auto it = m_series.begin(); it != m_series.end(); ++it) {
|
||||
it->points.append({double(now), values.value(it.key(), std::numeric_limits<double>::quiet_NaN())});
|
||||
if (it->points.size() > 600) it->points.remove(0, it->points.size() - 600);
|
||||
oldest = std::min(oldest, it->points.first().x());
|
||||
appendSample(it->points, {double(now), values.value(it.key(), std::numeric_limits<double>::quiet_NaN())});
|
||||
oldest = std::min(oldest, it->points.front().x());
|
||||
}
|
||||
m_bands.removeIf([oldest](const Band &band) { return band.end && band.end < oldest; });
|
||||
update();
|
||||
@@ -132,21 +139,40 @@ bool Chart::crossesSleep(double from, double to) const
|
||||
if (band.type == BandType::Sleep && band.begin < to && (!band.end || band.end > from)) return true;
|
||||
return false;
|
||||
}
|
||||
void Chart::setHistoryWindow(qint64 spanMs, bool stretch)
|
||||
{
|
||||
m_historyMs = std::clamp(spanMs, qint64(300000), historyRetentionMs);
|
||||
m_stretch = stretch;
|
||||
update();
|
||||
}
|
||||
QPair<double, double> Chart::timeRange() const
|
||||
{
|
||||
const double now = m_sampleTime;
|
||||
double first = now - m_historyMs;
|
||||
if (m_stretch) {
|
||||
double oldest = now;
|
||||
for (const auto &series : m_series)
|
||||
if (!series.points.empty()) oldest = std::min(oldest, series.points.front().x());
|
||||
first = std::max(first, std::min(oldest, now - 10000));
|
||||
}
|
||||
return {first, now};
|
||||
}
|
||||
Chart::Frame Chart::frame() const
|
||||
{
|
||||
// Hover repaints must not move the data or open a gap after the latest sample.
|
||||
const double now = m_sampleTime;
|
||||
double first = now, low = 0, high = m_unit == "%" ? 100 : 1, low2 = 0, high2 = 1;
|
||||
const auto [first, now] = timeRange();
|
||||
double low = 0, high = m_unit == "%" ? 100 : 1, low2 = 0, high2 = 1;
|
||||
bool secondary = false;
|
||||
const auto fm = fontMetrics();
|
||||
int unitWidth = fm.horizontalAdvance(m_unit);
|
||||
for (auto it = m_series.cbegin(); it != m_series.cend(); ++it) {
|
||||
if (!it->points.isEmpty()) first = std::min(first, it->points.first().x());
|
||||
if (!selected(it.key())) continue;
|
||||
const bool right = it->unit != m_unit;
|
||||
secondary |= right;
|
||||
unitWidth = std::max(unitWidth, fm.horizontalAdvance(it->unit));
|
||||
for (const auto &point : it->points) if (std::isfinite(point.y())) {
|
||||
for (auto sample = historyStart(it->points, first); sample != it->points.cend(); ++sample) {
|
||||
const auto &point = *sample;
|
||||
if (!std::isfinite(point.y()) || point.x() < first) continue;
|
||||
(right ? low2 : low) = std::min(right ? low2 : low, point.y());
|
||||
(right ? high2 : high) = std::max(right ? high2 : high, point.y());
|
||||
}
|
||||
@@ -155,7 +181,7 @@ Chart::Frame Chart::frame() const
|
||||
const int margin = std::max(fm.horizontalAdvance("−99999") + 10, unitWidth + timeTextWidth / 2 + 8);
|
||||
const QRectF area(margin, fm.height() / 2 + 4, width() - margin - (secondary ? margin : 25), height() - 2 * fm.height() - 20);
|
||||
const int intervals = std::max(2, int(area.height()) / (fm.height() * 2));
|
||||
return {area, std::min(first, now - 10000), now, AxisTicks::covering(low, high, intervals),
|
||||
return {area, first, now, AxisTicks::covering(low, high, intervals),
|
||||
AxisTicks::covering(low2, high2, intervals), secondary};
|
||||
}
|
||||
QString Chart::readingAt(qint64 time) const
|
||||
@@ -241,14 +267,21 @@ void Chart::paintEvent(QPaintEvent *)
|
||||
bool hasValues = false;
|
||||
for (auto it = m_series.cbegin(); it != m_series.cend(); ++it) {
|
||||
if (!selected(it.key())) continue;
|
||||
QPainterPath line; bool connected = false; double previous = 0;
|
||||
for (const auto &point : it->points) {
|
||||
const auto columns = timeAverages(historyStart(it->points, f.first), it->points.cend(),
|
||||
f.first, f.last, std::ceil(area.width() * devicePixelRatioF()), [this](const QPointF &a, const QPointF &b) {
|
||||
return b.x() - a.x() <= m_interval * 3 && !crossesSleep(a.x(), b.x());
|
||||
});
|
||||
const auto &scale = it->unit == m_unit ? f.left : f.right;
|
||||
QColor shade = it->color; shade.setAlphaF(.20);
|
||||
for (const auto &column : columns)
|
||||
p.fillRect(QRectF(QPointF(x(column.begin), y(column.maximum, scale)),
|
||||
QPointF(x(column.end), y(column.mean, scale))), shade);
|
||||
QPainterPath line; bool connected = false;
|
||||
for (const auto &point : averageLine(columns)) {
|
||||
if (!std::isfinite(point.y())) { connected = false; continue; }
|
||||
hasValues = true;
|
||||
const QPointF position(x(point.x()), y(point.y(), it->unit == m_unit ? f.left : f.right));
|
||||
if (connected && point.x() - previous <= m_interval * 3 && !crossesSleep(previous, point.x())) line.lineTo(position);
|
||||
else line.moveTo(position);
|
||||
connected = true; previous = point.x();
|
||||
const QPointF position(x(point.x()), y(point.y(), scale));
|
||||
if (connected) line.lineTo(position); else line.moveTo(position);
|
||||
connected = true; hasValues = true;
|
||||
}
|
||||
p.setPen(QPen(it->color, 2)); p.drawPath(line);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <optional>
|
||||
#include "history.h"
|
||||
|
||||
struct AxisTicks {
|
||||
double minimum, maximum, step;
|
||||
@@ -26,7 +27,9 @@ public:
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch());
|
||||
QString readingAt(qint64 time) const;
|
||||
QColor color(const QString &id) const;
|
||||
QVector<QPointF> history(const QString &id) const;
|
||||
QVector<QPointF> history(const QString &id, double since = 0) const;
|
||||
void setHistoryWindow(qint64 spanMs, bool stretch);
|
||||
QPair<double, double> timeRange() const;
|
||||
void setHoverTime(qint64 time) { m_hoverTime = time < 0 ? std::nullopt : std::optional<qint64>(time); update(); }
|
||||
std::optional<qint64> hoverTime() const { return m_hoverTime; }
|
||||
Q_SIGNALS:
|
||||
@@ -36,7 +39,7 @@ protected:
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
private:
|
||||
struct Series { QColor color; QString name, unit; QVector<QPointF> points; };
|
||||
struct Series { QColor color; QString name, unit; Samples points; };
|
||||
enum class BandType { Sleep, Plugged };
|
||||
struct Band { qint64 begin, end; BandType type; };
|
||||
struct Frame { QRectF area; double first, last; AxisTicks left, right; bool secondary; };
|
||||
@@ -47,6 +50,8 @@ private:
|
||||
QVector<Band> m_bands;
|
||||
QString m_unit;
|
||||
int m_interval = 1000;
|
||||
qint64 m_historyMs = historyRetentionMs;
|
||||
bool m_stretch = true;
|
||||
qint64 m_sampleTime = QDateTime::currentMSecsSinceEpoch();
|
||||
std::optional<qint64> m_hoverTime;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
#include <QPointF>
|
||||
#include <QVector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
struct AverageColumn {
|
||||
double begin, end, mean, maximum;
|
||||
bool startsRun;
|
||||
};
|
||||
|
||||
// Integrate the piecewise-linear signal over each pixel's time interval.
|
||||
// Gaps split runs, even within one pixel; absent time is never counted as zero.
|
||||
template<class Iterator, class Connected>
|
||||
QVector<AverageColumn> timeAverages(Iterator begin, Iterator end, double first,
|
||||
double last, int width, Connected connected)
|
||||
{
|
||||
QVector<AverageColumn> result;
|
||||
if (begin == end || width <= 0 || last <= first) return result;
|
||||
const double step = (last - first) / width;
|
||||
bool startsRun = true;
|
||||
int previousColumn = -1;
|
||||
auto previous = begin++;
|
||||
for (; begin != end; previous = begin++) {
|
||||
const auto a = *previous, b = *begin;
|
||||
if (!std::isfinite(a.y()) || !std::isfinite(b.y()) || b.x() <= a.x() || !connected(a, b)) {
|
||||
startsRun = true; continue;
|
||||
}
|
||||
double left = std::max(first, a.x());
|
||||
const double right = std::min(last, b.x());
|
||||
int column = std::max(0, int(std::floor((left - first) / step)));
|
||||
for (; left < right && column < width; ++column) {
|
||||
const double stop = std::min(right, first + (column + 1) * step);
|
||||
if (stop <= left) continue; // A boundary can round back to the preceding column.
|
||||
const auto valueAt = [&](double t) { return a.y() + (b.y() - a.y()) * ((t - a.x()) / (b.x() - a.x())); };
|
||||
const double low = valueAt(left), high = valueAt(stop);
|
||||
const double mean = (low + high) / 2, maximum = std::max(low, high);
|
||||
if (!startsRun && previousColumn == column) {
|
||||
auto &bucket = result.last();
|
||||
bucket.mean += (mean - bucket.mean) * ((stop - left) / (stop - bucket.begin));
|
||||
bucket.maximum = std::max(bucket.maximum, maximum);
|
||||
bucket.end = stop;
|
||||
} else result.append({left, stop, mean, maximum, startsRun});
|
||||
startsRun = false; previousColumn = column; left = stop;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline QVector<QPointF> averageLine(const QVector<AverageColumn> &columns)
|
||||
{
|
||||
QVector<QPointF> points;
|
||||
for (int i = 0; i < columns.size(); ++i) {
|
||||
const auto &column = columns[i];
|
||||
if (column.startsRun) {
|
||||
if (!points.isEmpty()) points.append({column.begin, NAN});
|
||||
points.append({column.begin, column.mean});
|
||||
}
|
||||
points.append({(column.begin + column.end) / 2, column.mean});
|
||||
if (i + 1 == columns.size() || columns[i + 1].startsRun)
|
||||
points.append({column.end, column.mean});
|
||||
}
|
||||
return points;
|
||||
}
|
||||
@@ -131,6 +131,9 @@ QVariantMap batteryStatus(const QString &sys)
|
||||
result["fullMWh"] = result["charge_full"].toDouble() * result["voltage_min_design"].toDouble() / 1e9;
|
||||
result["capacityEstimated"] = true;
|
||||
}
|
||||
if (result.contains("energy_now")) result["remainingMWh"] = result["energy_now"].toDouble() / 1000;
|
||||
else if (result.contains("charge_now") && result["voltage_min_design"].toDouble() > 0)
|
||||
result["remainingMWh"] = result["charge_now"].toDouble() * result["voltage_min_design"].toDouble() / 1e9;
|
||||
return result;
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
#include <QPointF>
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
|
||||
inline constexpr qint64 historyRetentionMs = 24 * 60 * 60 * 1000;
|
||||
using Samples = std::deque<QPointF>;
|
||||
|
||||
inline void appendSample(Samples &points, QPointF point)
|
||||
{
|
||||
points.push_back(point);
|
||||
// Keep one boundary sample for clipping a segment at the left edge.
|
||||
while (points.size() > 1 && points[1].x() <= point.x() - historyRetentionMs)
|
||||
points.pop_front();
|
||||
}
|
||||
|
||||
inline Samples::const_iterator historyStart(const Samples &points, double since)
|
||||
{
|
||||
auto first = std::lower_bound(points.cbegin(), points.cend(), since,
|
||||
[](const QPointF &point, double time) { return point.x() < time; });
|
||||
return first == points.cbegin() ? first : first - 1;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include "processusage.h"
|
||||
#include "hardware.h"
|
||||
#include <KApplicationTrader>
|
||||
#include <KService>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#include <QSettings>
|
||||
#include <QSet>
|
||||
#include <algorithm>
|
||||
|
||||
ProcessUsage::App ProcessUsage::application(const QString &directory, const QString &command)
|
||||
{
|
||||
if (!m_loadedApps) {
|
||||
QSet<QString> ambiguous{"env", "flatpak", "sh", "bash", "python", "python3", "java", "node"};
|
||||
for (const auto &service : KApplicationTrader::query([](const KService::Ptr &s) { return !s->noDisplay(); })) {
|
||||
const auto args = QProcess::splitCommand(service->exec());
|
||||
if (args.isEmpty()) continue;
|
||||
const auto executable = QFileInfo(args.first()).fileName();
|
||||
if (ambiguous.contains(executable)) continue;
|
||||
if (m_apps.contains(executable) && m_apps[executable].id != service->storageId()) {
|
||||
m_apps.remove(executable); ambiguous.insert(executable);
|
||||
} else m_apps.insert(executable, {service->storageId(), service->name()});
|
||||
}
|
||||
m_loadedApps = true;
|
||||
}
|
||||
// Flatpak processes can use generic executables; their sandbox supplies the app ID.
|
||||
const QString flatpak = directory + "/root/.flatpak-info";
|
||||
if (QFileInfo::exists(flatpak)) {
|
||||
QSettings info(flatpak, QSettings::IniFormat);
|
||||
const QString id = info.value("Application/name").toString();
|
||||
if (!id.isEmpty()) {
|
||||
const auto service = KService::serviceByStorageId(id + ".desktop");
|
||||
return {id, service ? service->name() : id};
|
||||
}
|
||||
}
|
||||
const QString executable = QFileInfo(directory + "/exe").symLinkTarget();
|
||||
const QString name = executable.isEmpty() ? command : QFileInfo(executable).fileName();
|
||||
return m_apps.value(name, {executable.isEmpty() ? command : executable, name});
|
||||
}
|
||||
|
||||
QVector<BusyApp> ProcessUsage::sample(std::optional<quint64> totalDelta, const QString &proc)
|
||||
{
|
||||
QMap<int, Process> current;
|
||||
QMap<QString, quint64> usage;
|
||||
QMap<QString, QString> names;
|
||||
// Processes may exit or deny access while being read. Skip those snapshots.
|
||||
for (const auto &entry : QDir(proc).entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
bool ok;
|
||||
const int pid = entry.toInt(&ok); if (!ok) continue;
|
||||
const QString directory = proc + "/" + entry;
|
||||
const QString stat = readText(directory + "/stat");
|
||||
const int open = stat.indexOf('('), close = stat.lastIndexOf(')');
|
||||
if (open < 0 || close < open) continue;
|
||||
const auto fields = stat.mid(close + 1).simplified().split(' ');
|
||||
if (fields.size() < 20) continue;
|
||||
// proc_pid_stat(5): fields 14, 15 and 22, after removing PID and comm.
|
||||
// https://man7.org/linux/man-pages/man5/proc_pid_stat.5.html
|
||||
const auto user = fields[11].toULongLong(&ok); if (!ok) continue;
|
||||
const auto system = fields[12].toULongLong(&ok); if (!ok) continue;
|
||||
const auto start = fields[19].toULongLong(&ok); if (!ok) continue;
|
||||
const auto old = m_previous.constFind(pid);
|
||||
const bool same = old != m_previous.cend() && old->start == start;
|
||||
const QString command = stat.mid(open + 1, close - open - 1);
|
||||
const auto app = same && old->command == command ? old->app : application(directory, command);
|
||||
const quint64 ticks = user + system;
|
||||
current.insert(pid, {start, ticks, command, app});
|
||||
if (same && ticks >= old->ticks) {
|
||||
usage[app.id] += ticks - old->ticks;
|
||||
names[app.id] = app.name.simplified();
|
||||
}
|
||||
}
|
||||
m_previous = std::move(current);
|
||||
if (!totalDelta || !*totalDelta || usage.isEmpty()) return {};
|
||||
QVector<BusyApp> apps;
|
||||
for (auto it = usage.cbegin(); it != usage.cend(); ++it)
|
||||
apps.append({names[it.key()], std::min(100., 100. * *it / *totalDelta)});
|
||||
std::stable_sort(apps.begin(), apps.end(), [](const BusyApp &a, const BusyApp &b) {
|
||||
return a.percent == b.percent ? a.name < b.name : a.percent > b.percent;
|
||||
});
|
||||
if (apps.size() > 3) apps.resize(3);
|
||||
return apps;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <optional>
|
||||
|
||||
struct BusyApp { QString name; double percent; };
|
||||
class ProcessUsage {
|
||||
public:
|
||||
QVector<BusyApp> sample(std::optional<quint64> totalDelta, const QString &proc = "/proc");
|
||||
void reset() { m_previous.clear(); }
|
||||
private:
|
||||
struct App { QString id, name; };
|
||||
struct Process { quint64 start, ticks; QString command; App app; };
|
||||
App application(const QString &directory, const QString &command);
|
||||
QMap<int, Process> m_previous;
|
||||
QMap<QString, App> m_apps;
|
||||
bool m_loadedApps = false;
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include "tooltip.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
QVector<TooltipSensor> tooltipSensors(const QVector<Sensor> &sensors)
|
||||
{
|
||||
QVector<TooltipSensor> result{{"cpu", "CPU", {}}, {"memory", "RAM", {}}, {"nvme", "NVMe", {}},
|
||||
{"battery", "Battery", {}}, {"board", "Board", {}}};
|
||||
const QStringList prefixes{"cros_ec/peci-temp", "spd5118/", "nvme/Composite", "cros_ec/battery_temp@b", "cros_ec/local_f75397@4c"};
|
||||
for (int i = 0; i < result.size(); ++i) {
|
||||
for (const auto &sensor : sensors) if (sensor.id.startsWith(prefixes[i])) { result[i].id = sensor.id; break; }
|
||||
if (i == 0 && result[i].id.isEmpty())
|
||||
for (const auto &sensor : sensors) if (sensor.id.startsWith("coretemp/Package id")) { result[i].id = sensor.id; break; }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool tooltipEnabled(const QVariantMap &settings, const QString &key)
|
||||
{
|
||||
return settings.value("tray/hover/" + key, true).toBool();
|
||||
}
|
||||
int tooltipAppCount(const QVariantMap &settings)
|
||||
{
|
||||
return std::clamp(settings.value("tray/hover/topApps", settings.value("tray/hover/topApp", false).toBool() ? 1 : 0).toInt(), 0, 3);
|
||||
}
|
||||
QString batteryTooltip(const QVariantMap &battery, int limit, const QVariantMap &upower)
|
||||
{
|
||||
if (battery.isEmpty()) return "Battery unavailable";
|
||||
QString text = "Battery " + (battery.contains("capacity") ? QString::number(battery["capacity"].toInt()) + "%" : "—");
|
||||
const auto energy = [&](const QString &key) {
|
||||
const double value = battery.value(key, NAN).toDouble();
|
||||
if (!std::isfinite(value) || value < 0) return QString("—");
|
||||
QString number = QString::number(qRound64(value));
|
||||
for (int i = number.size() - 3; i > 0; i -= 3) number.insert(i, ' ');
|
||||
return number;
|
||||
};
|
||||
text += " · " + energy("remainingMWh") + " mWh / " + energy("fullMWh") + " mWh";
|
||||
const auto watts = batteryRate(battery);
|
||||
text += "\n\u2003" + (watts ? (*watts > 0 ? "+" : "") + QString::number(*watts, 'f', 1) + " W" : "— W");
|
||||
const bool charging = battery["state"] == "Charging", discharging = battery["state"] == "Discharging";
|
||||
double seconds = 0;
|
||||
bool approximate = false;
|
||||
if (discharging) seconds = upower.value("TimeToEmpty").toDouble();
|
||||
else if (charging && limit == 100) seconds = upower.value("TimeToFull").toDouble();
|
||||
else if (charging && limit < 100) {
|
||||
approximate = true;
|
||||
if (battery["current_now"].toDouble() > 0 && battery.contains("charge_now") && battery["charge_full"].toDouble() > 0)
|
||||
seconds = (battery["charge_full"].toDouble() * limit / 100 - battery["charge_now"].toDouble()) / battery["current_now"].toDouble() * 3600;
|
||||
else if (watts && *watts > 0 && battery.contains("energy_now") && battery["energy_full"].toDouble() > 0)
|
||||
seconds = (battery["energy_full"].toDouble() * limit / 100 - battery["energy_now"].toDouble()) / (*watts * 1e6) * 3600;
|
||||
}
|
||||
if (std::isfinite(seconds) && seconds > 0) {
|
||||
const auto minutes = qint64(std::ceil(seconds / 60));
|
||||
text += QString(" · %1%2 h %3 min to %4%").arg(approximate ? "≈" : "")
|
||||
.arg(minutes / 60).arg(minutes % 60).arg(discharging ? 0 : limit);
|
||||
} else if (charging && battery.contains("capacity") && battery["capacity"].toInt() >= limit)
|
||||
text += QString(" · %1% limit reached").arg(limit);
|
||||
else if (charging || discharging) text += " · Time remaining unavailable";
|
||||
else text += " · " + battery["state"].toString();
|
||||
return text;
|
||||
}
|
||||
QString trayTooltip(const QVariantMap &settings, const QMap<QString, double> &values,
|
||||
const QVector<TooltipSensor> &temperatures, const QString &battery, const QStringList &topApps)
|
||||
{
|
||||
const auto reading = [&](const QString &id, int decimals) {
|
||||
const auto it = values.constFind(id);
|
||||
return it != values.cend() && std::isfinite(*it) ? QString::number(*it, 'f', decimals) : QString("—");
|
||||
};
|
||||
QStringList lines;
|
||||
if (tooltipEnabled(settings, "cpu")) {
|
||||
lines << "CPU usage: " + reading("cpu-usage", 1) + " %";
|
||||
const int count = tooltipAppCount(settings);
|
||||
if (count && topApps.isEmpty()) lines << "\u2003CPU app readings unavailable";
|
||||
for (int i = 0; i < std::min(count, int(topApps.size())); ++i) lines << "\u2003" + topApps[i];
|
||||
}
|
||||
if (tooltipEnabled(settings, "battery")) lines << battery;
|
||||
if (tooltipEnabled(settings, "fan")) lines << "Fan speed: " + reading("fan", 0) + " RPM (" + reading("fan-duty", 0) + " %)";
|
||||
QStringList temps;
|
||||
for (const auto &sensor : temperatures)
|
||||
if (!sensor.id.isEmpty() && tooltipEnabled(settings, "temperature/" + sensor.key))
|
||||
temps << sensor.name + " " + reading(sensor.id, 0) + "°C";
|
||||
if (temps.size() > 1) lines << "Temperatures:";
|
||||
for (const auto &temp : temps) lines << (temps.size() > 1 ? "\u2003" : "") + temp;
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
#include "hardware.h"
|
||||
|
||||
struct TooltipSensor { QString key, name, id; };
|
||||
QVector<TooltipSensor> tooltipSensors(const QVector<Sensor> &sensors);
|
||||
bool tooltipEnabled(const QVariantMap &settings, const QString &key);
|
||||
int tooltipAppCount(const QVariantMap &settings);
|
||||
QString batteryTooltip(const QVariantMap &battery, int limit, const QVariantMap &upower);
|
||||
QString trayTooltip(const QVariantMap &settings, const QMap<QString, double> &values,
|
||||
const QVector<TooltipSensor> &temperatures, const QString &battery, const QStringList &topApps);
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include "tray.h"
|
||||
#include "graphdata.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <cmath>
|
||||
@@ -7,6 +8,7 @@
|
||||
|
||||
std::optional<double> CpuUsage::sample(const QString &procStat)
|
||||
{
|
||||
m_delta.reset();
|
||||
const auto fields = procStat.section('\n', 0, 0).simplified().split(' ');
|
||||
if (fields.size() < 5 || fields.first() != "cpu") { reset(); return {}; }
|
||||
Counters current{0, 0};
|
||||
@@ -21,6 +23,7 @@ std::optional<double> CpuUsage::sample(const QString &procStat)
|
||||
if (!previous || current.total <= previous->total || current.idle < previous->idle) return {};
|
||||
const auto total = current.total - previous->total, idle = current.idle - previous->idle;
|
||||
if (idle > total) return {};
|
||||
m_delta = total;
|
||||
return 100. * (total - idle) / total;
|
||||
}
|
||||
QRectF trayPlotRect(bool border)
|
||||
@@ -28,7 +31,7 @@ QRectF trayPlotRect(bool border)
|
||||
// Two-pixel border; leave room for the graph's two-pixel stroke inside it.
|
||||
return border ? QRectF(3, 3, 58, 58) : QRectF(1, 1, 62, 62);
|
||||
}
|
||||
QIcon telemetryIcon(bool graph, const QVector<QPointF> &values, const QString &unit, const TrayStyle &style)
|
||||
QIcon telemetryIcon(bool graph, const QVector<QPointF> &values, const QString &unit, const TrayStyle &style, qint64 now)
|
||||
{
|
||||
QPixmap pixmap(64, 64); pixmap.fill(style.transparent ? Qt::transparent : style.backgroundColor);
|
||||
QPainter p(&pixmap); p.setRenderHint(QPainter::Antialiasing);
|
||||
@@ -37,15 +40,27 @@ QIcon telemetryIcon(bool graph, const QVector<QPointF> &values, const QString &u
|
||||
const double latest = values.isEmpty() ? NAN : values.last().y();
|
||||
p.setPen(style.lineColor);
|
||||
if (graph && !values.isEmpty() && style.maximum > style.minimum) {
|
||||
const double first = values.first().x(), span = std::max(1., values.last().x() - first);
|
||||
const double last = now;
|
||||
const double first = last - style.historyMs;
|
||||
const double span = std::max(1., last - first);
|
||||
p.save(); p.setClipRect(area.adjusted(-1, -1, 1, 1));
|
||||
const auto position = [&](const QPointF &point) {
|
||||
const double fraction = std::clamp((point.y() - style.minimum) / (style.maximum - style.minimum), 0., 1.);
|
||||
return QPointF(area.left() + (point.x() - first) / span * area.width(), area.bottom() - fraction * area.height());
|
||||
};
|
||||
const auto outside = [&](double y) { return y < style.minimum || y > style.maximum; };
|
||||
const auto color = [&](bool out) { return out && style.overflowColor ? style.outsideColor : style.lineColor; };
|
||||
for (int i = 1; i < values.size(); ++i) {
|
||||
const auto a = values[i - 1], b = values[i];
|
||||
const auto columns = timeAverages(values.cbegin(), values.cend(), first, last, std::ceil(area.width()),
|
||||
[](const QPointF &, const QPointF &) { return true; });
|
||||
for (const auto &column : columns) {
|
||||
QColor shade = style.lineColor; shade.setAlphaF(shade.alphaF() * .20);
|
||||
p.fillRect(QRectF(position({column.begin, column.maximum}), position({column.end, column.mean})), shade);
|
||||
}
|
||||
const auto averages = averageLine(columns);
|
||||
QPainterPath line, overflow, fill;
|
||||
for (int i = 1; i < averages.size(); ++i) {
|
||||
const auto a = averages[i - 1], b = averages[i];
|
||||
if (b.x() < first || a.x() > last) continue;
|
||||
if (!std::isfinite(a.y()) || !std::isfinite(b.y())) continue;
|
||||
QVector<double> cuts{0, 1};
|
||||
if (a.y() != b.y()) for (double boundary : {style.minimum, style.maximum}) {
|
||||
@@ -58,16 +73,17 @@ QIcon telemetryIcon(bool graph, const QVector<QPointF> &values, const QString &u
|
||||
if (out && !style.clamp) continue;
|
||||
const auto left = position(a + (b - a) * cuts[j - 1]), right = position(a + (b - a) * cuts[j]);
|
||||
if (style.fill) {
|
||||
QPainterPath fill; fill.moveTo(left); fill.lineTo(right);
|
||||
fill.moveTo(left); fill.lineTo(right);
|
||||
fill.lineTo(right.x(), area.bottom()); fill.lineTo(left.x(), area.bottom()); fill.closeSubpath();
|
||||
p.fillPath(fill, style.fillColor);
|
||||
}
|
||||
p.setPen(QPen(color(out), 2, Qt::SolidLine, Qt::FlatCap)); p.drawLine(left, right);
|
||||
auto &path = out ? overflow : line;
|
||||
path.moveTo(left); path.lineTo(right);
|
||||
}
|
||||
}
|
||||
for (const auto &point : values) if (std::isfinite(point.y()) && (style.clamp || !outside(point.y()))) {
|
||||
p.setPen(QPen(color(outside(point.y())), 2, Qt::SolidLine, Qt::RoundCap)); p.drawPoint(position(point));
|
||||
}
|
||||
if (style.fill) p.fillPath(fill, style.fillColor);
|
||||
p.setPen(QPen(color(false), 2, Qt::SolidLine, Qt::RoundCap)); p.drawPath(line);
|
||||
p.setPen(QPen(color(true), 2, Qt::SolidLine, Qt::RoundCap)); p.drawPath(overflow);
|
||||
p.restore();
|
||||
} else {
|
||||
const QString value = !std::isfinite(latest) ? "—" : unit == "MHz"
|
||||
? QString::number(latest / 1000, 'f', 1) : QString::number(std::round(latest), 'f', 0);
|
||||
|
||||
@@ -3,20 +3,25 @@
|
||||
#include <QIcon>
|
||||
#include <QPalette>
|
||||
#include <QVector>
|
||||
#include <QDateTime>
|
||||
#include <optional>
|
||||
|
||||
class CpuUsage {
|
||||
public:
|
||||
std::optional<double> sample(const QString &procStat);
|
||||
void reset() { m_previous.reset(); }
|
||||
void reset() { m_previous.reset(); m_delta.reset(); }
|
||||
std::optional<quint64> totalDelta() const { return m_delta; }
|
||||
private:
|
||||
struct Counters { quint64 total, idle; };
|
||||
std::optional<Counters> m_previous;
|
||||
std::optional<quint64> m_delta;
|
||||
};
|
||||
struct TrayStyle {
|
||||
bool border = true, transparent = false, fill = true, clamp = true, overflowColor = true;
|
||||
QColor borderColor, backgroundColor, fillColor, lineColor, outsideColor;
|
||||
double minimum = 0, maximum = 100;
|
||||
qint64 historyMs = 60000;
|
||||
};
|
||||
QRectF trayPlotRect(bool border);
|
||||
QIcon telemetryIcon(bool graph, const QVector<QPointF> &values, const QString &unit, const TrayStyle &style);
|
||||
QIcon telemetryIcon(bool graph, const QVector<QPointF> &values, const QString &unit, const TrayStyle &style,
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch());
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include "traypage.h"
|
||||
#include "colorbutton.h"
|
||||
#include "tooltip.h"
|
||||
#include <QGroupBox>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QColorDialog>
|
||||
@@ -19,7 +22,11 @@ TrayPage::TrayPage(QSettings &settings, const QVector<Sensor> &sensors, QWidget
|
||||
for (const auto &sensor : sensors) m_metric->addItem(sensor.name + " (" + sensor.unit + ")", sensor.id);
|
||||
m_mode->setCurrentIndex(std::max(0, m_mode->findData(m_values.value("tray/mode", "icon"))));
|
||||
m_metric->setCurrentIndex(std::max(0, m_metric->findData(m_values.value("tray/metric", "cpu-usage"))));
|
||||
form->addRow("Display:", m_mode); form->addRow("Reading:", m_metric);
|
||||
form->addRow("Display:", m_mode);
|
||||
auto *graphOptions = new QWidget; graphOptions->setObjectName("trayGraphOptions");
|
||||
auto *graphForm = new QFormLayout(graphOptions); graphForm->setContentsMargins(24, 0, 0, 0);
|
||||
graphForm->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
|
||||
form->addRow(graphOptions); form->addRow("Reading:", m_metric);
|
||||
auto *appearance = new QWidget; auto *colors = new QFormLayout(appearance); colors->setContentsMargins(0, 0, 0, 0);
|
||||
colors->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
|
||||
auto colorRow = [&](QFormLayout *target, const QString &label, const QString &key, const QColor &fallback, bool alpha = false) {
|
||||
@@ -57,6 +64,23 @@ TrayPage::TrayPage(QSettings &settings, const QVector<Sensor> &sensors, QWidget
|
||||
layout->addWidget(appearance);
|
||||
m_graphSettings = new QWidget; m_scaleLayout = new QFormLayout(m_graphSettings); m_scaleLayout->setContentsMargins(0, 0, 0, 0);
|
||||
m_scaleLayout->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
|
||||
m_history = new QComboBox; m_history->setObjectName("trayHistory");
|
||||
for (int seconds : {10, 15, 20, 30})
|
||||
m_history->addItem(QString::number(seconds) + " seconds", seconds * 1000);
|
||||
for (int minutes : {1, 2, 5})
|
||||
m_history->addItem(QString::number(minutes) + (minutes == 1 ? " minute" : " minutes"), minutes * 60000);
|
||||
const int savedHistory = m_history->findData(m_values.value("tray/historyMs", 60000));
|
||||
m_history->setCurrentIndex(savedHistory < 0 ? m_history->findData(60000) : savedHistory);
|
||||
m_history->setToolTip("Fixed spans end at now. Only retained samples are shown; missing history stays blank.");
|
||||
graphForm->addRow("History:", m_history);
|
||||
m_values.insert("tray/historyMs", m_history->currentData());
|
||||
connect(m_history, &QComboBox::currentIndexChanged, this, [this] {
|
||||
m_values.insert("tray/historyMs", m_history->currentData()); if (!m_loading) Q_EMIT settingsChanged();
|
||||
});
|
||||
m_reload.append([this] {
|
||||
const int saved = m_history->findData(m_values.value("tray/historyMs", 60000));
|
||||
m_history->setCurrentIndex(saved < 0 ? m_history->findData(60000) : saved);
|
||||
});
|
||||
m_minimum = new QDoubleSpinBox; m_minimum->setObjectName("trayMinimum");
|
||||
m_maximum = new QDoubleSpinBox; m_maximum->setObjectName("trayMaximum");
|
||||
m_scaleLayout->addRow("Graph minimum:", m_minimum); m_scaleLayout->addRow("Graph maximum:", m_maximum);
|
||||
@@ -66,21 +90,25 @@ TrayPage::TrayPage(QSettings &settings, const QVector<Sensor> &sensors, QWidget
|
||||
m_outside = new QComboBox; m_outside->setObjectName("trayOutside");
|
||||
m_outside->addItem("Draw along the inner edge", "clamp"); m_outside->addItem("Do not draw outside readings", "hide");
|
||||
m_outside->setCurrentIndex(std::max(0, m_outside->findData(m_values.value("tray/outside", "clamp"))));
|
||||
m_scaleLayout->addRow("Outside graph range:", m_outside);
|
||||
m_overflowColor = check(m_scaleLayout, "Use a different colour outside the range", "overflowColor", true);
|
||||
auto *outsideColor = colorRow(m_scaleLayout, "Outside-range colour:", "outsideColor", QColor("#f67400"));
|
||||
auto syncOutside = [this, outsideColor] {
|
||||
m_overflowColor->setEnabled(m_outside->currentData() == "clamp");
|
||||
outsideColor->setEnabled(m_overflowColor->isEnabled() && m_overflowColor->isChecked());
|
||||
graphForm->addRow("Out of range:", m_outside);
|
||||
auto *overflowOptions = new QWidget; overflowOptions->setObjectName("trayOverflowOptions");
|
||||
auto *overflowForm = new QFormLayout(overflowOptions); overflowForm->setContentsMargins(24, 0, 0, 0);
|
||||
overflowForm->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint); graphForm->addRow(overflowOptions);
|
||||
m_overflowColor = check(overflowForm, "Use a different colour outside the range", "overflowColor", true);
|
||||
auto *outsideColor = colorRow(overflowForm, "Colour:", "outsideColor", QColor("#f67400"));
|
||||
auto syncOutside = [this, outsideColor, overflowOptions] {
|
||||
overflowOptions->setVisible(m_outside->currentData() == "clamp");
|
||||
outsideColor->setEnabled(m_overflowColor->isChecked());
|
||||
};
|
||||
connect(m_overflowColor, &QCheckBox::toggled, this, syncOutside);
|
||||
connect(m_outside, &QComboBox::currentIndexChanged, this, [this, syncOutside] {
|
||||
m_values.insert("tray/outside", m_outside->currentData()); syncOutside(); if (!m_loading) Q_EMIT settingsChanged();
|
||||
});
|
||||
syncOutside(); layout->addWidget(m_graphSettings);
|
||||
auto syncMode = [this, appearance] {
|
||||
auto syncMode = [this, appearance, graphOptions] {
|
||||
const bool telemetry = mode() != "icon";
|
||||
m_metric->setEnabled(telemetry); appearance->setVisible(telemetry); m_graphSettings->setVisible(mode() == "graph");
|
||||
graphOptions->setVisible(mode() == "graph");
|
||||
};
|
||||
connect(m_mode, &QComboBox::currentIndexChanged, this, [this, syncMode] {
|
||||
m_values.insert("tray/mode", m_mode->currentData()); syncMode(); if (!m_loading) Q_EMIT settingsChanged();
|
||||
@@ -104,6 +132,31 @@ TrayPage::TrayPage(QSettings &settings, const QVector<Sensor> &sensors, QWidget
|
||||
m_outside->setCurrentIndex(std::max(0, m_outside->findData(m_values.value("tray/outside", "clamp"))));
|
||||
syncMode(); syncOutside();
|
||||
});
|
||||
auto *hover = new QGroupBox("Hover information"); auto *hoverForm = new QFormLayout(hover);
|
||||
auto *cpu = check(hoverForm, "CPU usage", "hover/cpu", true);
|
||||
auto *appOptions = new QWidget; auto *appForm = new QHBoxLayout(appOptions); appForm->setContentsMargins(24, 0, 0, 0);
|
||||
auto *topApps = new QSpinBox; topApps->setObjectName("hover/topApps"); topApps->setRange(0, 3);
|
||||
topApps->setValue(tooltipAppCount(m_values));
|
||||
m_values.remove("tray/hover/topApp"); m_values.insert("tray/hover/topApps", topApps->value());
|
||||
appForm->addWidget(topApps); appForm->addWidget(new QLabel("apps using the most CPU")); appForm->addStretch();
|
||||
hoverForm->addRow(appOptions);
|
||||
topApps->setToolTip("0 turns this off. Percentages use total CPU capacity; processes are grouped by application where identifiable.");
|
||||
m_reload.append([this, topApps] { topApps->setValue(tooltipAppCount(m_values)); });
|
||||
connect(topApps, &QSpinBox::valueChanged, this, [this](int count) {
|
||||
m_values.insert("tray/hover/topApps", count); if (!m_loading) Q_EMIT settingsChanged();
|
||||
});
|
||||
appOptions->setEnabled(cpu->isChecked()); connect(cpu, &QCheckBox::toggled, appOptions, &QWidget::setEnabled);
|
||||
check(hoverForm, "Battery", "hover/battery", true);
|
||||
check(hoverForm, "Fan", "hover/fan", true);
|
||||
hoverForm->addRow(new QLabel("Temperature:"));
|
||||
auto *temperatures = new QWidget; auto *tempForm = new QFormLayout(temperatures); tempForm->setContentsMargins(24, 0, 0, 0);
|
||||
for (const auto &sensor : tooltipSensors(sensors)) {
|
||||
const QString label = sensor.key == "memory" ? "Memory" : sensor.key == "board" ? "Mainboard" : sensor.name;
|
||||
auto *box = check(tempForm, label, "hover/temperature/" + sensor.key, !sensor.id.isEmpty());
|
||||
box->setEnabled(!sensor.id.isEmpty());
|
||||
if (sensor.id.isEmpty()) box->setToolTip("No matching temperature sensor detected.");
|
||||
}
|
||||
hoverForm->addRow(temperatures); layout->addWidget(hover);
|
||||
layout->addStretch();
|
||||
}
|
||||
void TrayPage::load(const QVariantMap &values)
|
||||
@@ -133,6 +186,7 @@ TrayStyle TrayPage::iconStyle() const
|
||||
return value.isValid() ? value : fallback;
|
||||
};
|
||||
TrayStyle s;
|
||||
s.historyMs = m_history->currentData().toLongLong();
|
||||
s.border = m_border->isChecked(); s.transparent = m_transparent->isChecked(); s.fill = m_fill->isChecked();
|
||||
s.clamp = m_outside->currentData() == "clamp"; s.overflowColor = m_overflowColor->isChecked();
|
||||
s.borderColor = color("borderColor", palette().color(QPalette::WindowText));
|
||||
|
||||
@@ -28,7 +28,7 @@ private:
|
||||
QList<std::function<void()>> m_reload;
|
||||
bool m_loading = false;
|
||||
QVector<Sensor> m_sensors;
|
||||
QComboBox *m_mode, *m_metric, *m_outside;
|
||||
QComboBox *m_mode, *m_metric, *m_outside, *m_history;
|
||||
QDoubleSpinBox *m_minimum, *m_maximum;
|
||||
QFormLayout *m_scaleLayout;
|
||||
QWidget *m_graphSettings;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "window.h"
|
||||
#include "legend.h"
|
||||
#include "fan.h"
|
||||
#include "tooltip.h"
|
||||
#include <QMessageBox>
|
||||
#include <KAuth/Action>
|
||||
#include <KAuth/ExecuteJob>
|
||||
@@ -22,6 +23,7 @@
|
||||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QSlider>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
@@ -30,10 +32,6 @@ ValueControl *spin(int minimum, int maximum, const QString &suffix) {
|
||||
auto *box = new ValueControl; box->setRange(minimum, maximum); box->setSuffix(suffix); return box;
|
||||
}
|
||||
|
||||
QString duration(double seconds) {
|
||||
if (!std::isfinite(seconds) || seconds < 60) return "less than a minute";
|
||||
return QString("%1 h %2 min").arg(int(seconds) / 3600).arg(int(seconds) / 60 % 60);
|
||||
}
|
||||
}
|
||||
|
||||
Window::Window() : m_settings("fedora-tools", "framework-laptop-tools"),
|
||||
@@ -157,6 +155,18 @@ QGroupBox *Window::sensorGroup(const QString &title, Chart *chart, const QVector
|
||||
QWidget *Window::monitorPage()
|
||||
{
|
||||
auto *page = new QWidget; auto *layout = new QVBoxLayout(page);
|
||||
auto *historyRow = new QHBoxLayout;
|
||||
auto *history = new QSlider(Qt::Horizontal); history->setObjectName("monitorHistory");
|
||||
// Logarithmic spacing, rounded to whole minutes for a readable selection.
|
||||
history->setRange(0, 1000);
|
||||
const auto savedMinutes = std::clamp(m_settings.value("monitor/historyMinutes", 1440).toInt(), 5, 1440);
|
||||
history->setValue(qRound(1000 * std::log(savedMinutes / 5.) / std::log(288.)));
|
||||
auto *span = new QLabel; span->setMinimumWidth(fontMetrics().horizontalAdvance("24 h 59 min"));
|
||||
auto *stretch = new QCheckBox("Stretch available data"); stretch->setObjectName("monitorStretch");
|
||||
stretch->setChecked(m_settings.value("monitor/stretch", true).toBool());
|
||||
stretch->setToolTip("Fit the available history until the selected time span has been collected.");
|
||||
historyRow->addWidget(new QLabel("History shown:")); historyRow->addWidget(history, 1); historyRow->addWidget(span);
|
||||
layout->addLayout(historyRow); layout->addWidget(stretch);
|
||||
m_frequencyChart = new Chart("MHz"); m_temperatureChart = new Chart("°C"); m_batteryChart = new Chart("%");
|
||||
layout->addWidget(sensorGroup("CPU and GPU frequency", m_frequencyChart, m_frequencies, false));
|
||||
m_fanChart = new Chart("RPM");
|
||||
@@ -167,6 +177,18 @@ QWidget *Window::monitorPage()
|
||||
{{"battery", "Charge level", {}, "%"}, {"battery-rate", "Charge / discharge rate", {}, "W"}}, false);
|
||||
layout->addWidget(batteryGroup);
|
||||
const QList<Chart *> charts{m_frequencyChart, m_fanChart, m_temperatureChart, m_batteryChart};
|
||||
auto updateHistory = [this, charts, history, stretch, span] {
|
||||
const int minutes = qRound(5 * std::pow(288., history->value() / 1000.));
|
||||
span->setText(minutes < 60 ? QString("%1 min").arg(minutes)
|
||||
: minutes % 60 ? QString("%1 h %2 min").arg(minutes / 60).arg(minutes % 60)
|
||||
: QString("%1 h").arg(minutes / 60));
|
||||
m_settings.setValue("monitor/historyMinutes", minutes);
|
||||
m_settings.setValue("monitor/stretch", stretch->isChecked());
|
||||
for (auto *chart : charts) chart->setHistoryWindow(qint64(minutes) * 60000, stretch->isChecked());
|
||||
};
|
||||
connect(history, &QSlider::valueChanged, this, updateHistory);
|
||||
connect(stretch, &QCheckBox::toggled, this, updateHistory);
|
||||
updateHistory();
|
||||
for (auto *source : charts) connect(source, &Chart::hovered, this, [charts](qint64 time) {
|
||||
for (auto *chart : charts) chart->setHoverTime(time);
|
||||
});
|
||||
@@ -260,15 +282,18 @@ QWidget *Window::trayPage()
|
||||
void Window::updateTray()
|
||||
{
|
||||
const auto metric = m_trayMetric;
|
||||
QVector<QPointF> history;
|
||||
if (metric.id == "cpu-usage") history = m_usageHistory;
|
||||
else if (metric.id == "battery" || metric.id == "battery-rate") history = m_batteryChart->history(metric.id);
|
||||
else history = (metric.unit == "MHz" ? m_frequencyChart : m_temperatureChart)->history(metric.id);
|
||||
if (m_trayMode == "icon") m_tray->setIcon(windowIcon());
|
||||
else m_tray->setIcon(telemetryIcon(m_trayMode == "graph", history, metric.unit, m_trayStyle));
|
||||
const auto value = m_values.constFind(metric.id);
|
||||
m_tray->setToolTip(metric.name + ": " + (value == m_values.cend() ? "—" : QString::number(*value, 'f', 1) + " " + metric.unit)
|
||||
+ "\n" + m_batteryText + "\n" + m_fanReadout->text());
|
||||
else {
|
||||
const auto now = QDateTime::currentMSecsSinceEpoch();
|
||||
const double since = now - (m_trayMode == "graph" ? m_trayStyle.historyMs : 0);
|
||||
QVector<QPointF> history;
|
||||
if (metric.id == "cpu-usage")
|
||||
for (auto it = historyStart(m_usageHistory, since); it != m_usageHistory.cend(); ++it) history.append(*it);
|
||||
else if (metric.id == "battery" || metric.id == "battery-rate") history = m_batteryChart->history(metric.id, since);
|
||||
else history = (metric.unit == "MHz" ? m_frequencyChart : m_temperatureChart)->history(metric.id, since);
|
||||
m_tray->setIcon(telemetryIcon(m_trayMode == "graph", history, metric.unit, m_trayStyle, now));
|
||||
}
|
||||
m_tray->setToolTip(trayTooltip(m_savedTray, m_values, tooltipSensors(m_temperatures), m_batteryText, m_topAppText));
|
||||
}
|
||||
QVariantMap Window::controlValue(const QString &key) const
|
||||
{
|
||||
@@ -487,20 +512,21 @@ void Window::sample()
|
||||
m_fanReadout->setText((rpm ? QString("%1 RPM").arg(*rpm, 0, 'f', 0) : "Fan speed unavailable")
|
||||
+ (pwm ? QString(" · Duty: %1%").arg(*pwm * 100 / 255, 0, 'f', 0) : " · Duty unavailable") + " · " + fanMode);
|
||||
QMap<QString, double> fans;
|
||||
if (rpm) fans["fan"] = *rpm;
|
||||
if (rpm) { fans["fan"] = *rpm; m_values["fan"] = *rpm; }
|
||||
if (pwm) m_values["fan-duty"] = *pwm * 100 / 255;
|
||||
m_fanChart->sample(fans, m_fastTimer.interval());
|
||||
const auto battery = batteryStatus();
|
||||
const auto usage = m_cpuUsage.sample(readText("/proc/stat"));
|
||||
if (usage) m_values["cpu-usage"] = *usage;
|
||||
m_usageHistory.append({double(QDateTime::currentMSecsSinceEpoch()), usage.value_or(NAN)});
|
||||
if (m_usageHistory.size() > 600) m_usageHistory.removeFirst();
|
||||
if (tooltipEnabled(m_savedTray, "cpu") && tooltipAppCount(m_savedTray) > 0) {
|
||||
m_topAppText.clear();
|
||||
for (const auto &app : m_processUsage.sample(m_cpuUsage.totalDelta()))
|
||||
m_topAppText << app.name + QString(": %1 %").arg(app.percent, 0, 'f', 1);
|
||||
} else { m_processUsage.reset(); m_topAppText.clear(); }
|
||||
appendSample(m_usageHistory, {double(QDateTime::currentMSecsSinceEpoch()), usage.value_or(NAN)});
|
||||
if (battery.contains("capacity")) m_values["battery"] = battery["capacity"].toDouble();
|
||||
const auto rate = batteryRate(battery); if (rate) m_values["battery-rate"] = *rate;
|
||||
m_batteryChart->setPowerState(false, onAcPower());
|
||||
QString text = battery.isEmpty() ? "Battery unavailable" : QString("Battery %1% · %2").arg(battery.value("capacity").toInt()).arg(battery.value("state").toString());
|
||||
if (battery.contains("watts")) text += QString(" · %1 W").arg(battery["watts"].toDouble(), 0, 'f', 1);
|
||||
if (battery.contains("health")) text += QString(" · Capacity / design %1%").arg(battery["health"].toDouble(), 0, 'f', 0);
|
||||
if (battery.contains("cycle_count")) text += QString(" · %1 cycles").arg(battery["cycle_count"].toInt());
|
||||
QStringList details;
|
||||
if (battery.contains("fullMWh")) details << QString("Capacity: %1 mWh").arg(battery["fullMWh"].toDouble(), 0, 'f', 0);
|
||||
if (battery.contains("health")) details << QString("Health: %1%").arg(battery["health"].toDouble(), 0, 'f', 0);
|
||||
@@ -510,18 +536,7 @@ void Window::sample()
|
||||
QDBusInterface properties("org.freedesktop.UPower", "/org/freedesktop/UPower/devices/DisplayDevice", "org.freedesktop.DBus.Properties", QDBusConnection::systemBus());
|
||||
properties.setTimeout(250);
|
||||
const QDBusReply<QVariantMap> response = properties.call("GetAll", "org.freedesktop.UPower.Device");
|
||||
if (response.isValid()) {
|
||||
if (battery["state"] == "Discharging" && response.value()["TimeToEmpty"].toLongLong() > 0)
|
||||
text += " · " + duration(response.value()["TimeToEmpty"].toDouble()) + " remaining (OS estimate)";
|
||||
else if (battery["state"] == "Charging" && response.value()["TimeToFull"].toLongLong() > 0 && effectiveLimit == 100)
|
||||
text += " · " + duration(response.value()["TimeToFull"].toDouble()) + " until full (OS estimate)";
|
||||
}
|
||||
if (battery["state"] == "Charging" && effectiveLimit < 100 && battery["current_now"].toDouble() > 0
|
||||
&& battery.contains("charge_now") && battery["charge_full"].toDouble() > 0) {
|
||||
const double remaining = battery["charge_full"].toDouble() * effectiveLimit / 100 - battery["charge_now"].toDouble();
|
||||
text += remaining > 0 ? " · about " + duration(remaining / battery["current_now"].toDouble() * 3600) + QString(" to %1% (at current rate)").arg(effectiveLimit) : " · charge limit reached";
|
||||
}
|
||||
m_batteryText = text;
|
||||
m_batteryText = batteryTooltip(battery, effectiveLimit, response.isValid() ? response.value() : QVariantMap{});
|
||||
updateTray();
|
||||
}
|
||||
void Window::sampleBattery()
|
||||
@@ -539,6 +554,7 @@ void Window::sleepChanged(bool sleeping)
|
||||
for (auto *chart : {m_frequencyChart, m_temperatureChart, m_batteryChart, m_fanChart})
|
||||
chart->setPowerState(sleeping, sleeping || chart != m_batteryChart ? std::nullopt : onAcPower());
|
||||
m_cpuUsage.reset();
|
||||
m_processUsage.reset(); m_topAppText.clear();
|
||||
if (!sleeping) { sample(); sampleBattery(); }
|
||||
}
|
||||
void Window::closeEvent(QCloseEvent *event)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "cpupage.h"
|
||||
#include "tray.h"
|
||||
#include "traypage.h"
|
||||
#include "processusage.h"
|
||||
#include <QMainWindow>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
@@ -49,7 +50,9 @@ private:
|
||||
QVector<Sensor> m_temperatures, m_frequencies;
|
||||
QMap<QString, double> m_values;
|
||||
CpuUsage m_cpuUsage;
|
||||
QVector<QPointF> m_usageHistory;
|
||||
ProcessUsage m_processUsage;
|
||||
QStringList m_topAppText;
|
||||
Samples m_usageHistory;
|
||||
Chart *m_frequencyChart, *m_temperatureChart, *m_batteryChart, *m_fanChart;
|
||||
QLabel *m_batteryDetails, *m_fanDetails;
|
||||
QLabel *m_message, *m_fanReadout, *m_firmwareReadout;
|
||||
|
||||
Reference in New Issue
Block a user