Publish Framework tray enhancements and idle fingerprint retry
This commit is contained in:
@@ -65,9 +65,12 @@ display is turned off or the system resumes from sleep.
|
||||
The package applies a narrow patch to Plasma 6.7's lock-screen QML and reapplies
|
||||
it after `plasma-desktop` updates. An unknown future layout is left unchanged
|
||||
instead of being modified speculatively. Removing the package restores Plasma's
|
||||
original behaviour. It retries lock-screen authentication after resume and the
|
||||
automatic-lock grace period, but does not modify `kscreenlocker` authentication
|
||||
code or `fprintd`.
|
||||
original behaviour. It retries lock-screen authentication after resume, the
|
||||
automatic-lock grace period and idle fingerprint timeouts. Failed scans do not
|
||||
trigger automatic retries. It does not modify `kscreenlocker` authentication
|
||||
code or restart `fprintd`; retry support depends on the installed KScreenLocker.
|
||||
An intermittent missing fingerprint prompt can still occur; pressing a key may
|
||||
restore it. The idle retry is a workaround, not a complete fix for that issue.
|
||||
|
||||
### Plasma task-group shortcuts
|
||||
|
||||
|
||||
@@ -17,12 +17,19 @@ Successful saves become the new Undo baseline. If a hardware operation fails,
|
||||
earlier operations may already have applied; remaining edits stay pending.
|
||||
CPU slider bounds are read from Linux's hardware-frequency limits.
|
||||
|
||||
The Monitor page has CPU/GPU frequency, fan RPM, temperature and battery graphs.
|
||||
The Monitor page has CPU/GPU frequency, power, fan RPM, temperature and battery graphs.
|
||||
The battery heading shows full capacity, health and cycle count when available.
|
||||
Capacity calculated from charge capacity and nominal voltage is approximate.
|
||||
Temperature labels distinguish CPU-area/board sensors from die readings;
|
||||
raw sysfs paths remain available in tooltips. CPU frequency is Linux's reported
|
||||
average across CPU policies, not an instantaneous measurement of every core.
|
||||
average across readable CPU policies, not an instantaneous measurement of every
|
||||
core or a utilisation-weighted average. On detected Intel hybrid CPUs, separate
|
||||
P-core and E-core averages are shown by default; the combined average remains
|
||||
selectable. Each group also offers its highest reported frequency at each sample,
|
||||
disabled by default. These are readings, not hardware limits or historical peaks.
|
||||
All averages and maxima are also available as tray readings. Core membership uses
|
||||
Linux's hybrid CPU lists, not clock-speed guesses. Without reliable grouping,
|
||||
only the combined average is shown. AMD core-type grouping is not yet supported.
|
||||
GPU GT domains are shown separately rather than assuming they are identical.
|
||||
Click a coloured legend to toggle its series. Hidden series remain in the legend,
|
||||
greyed out and crossed out. Main temperature sensors are shown first; additional
|
||||
@@ -75,8 +82,10 @@ Sampling choices are 0.5/1/2/4 seconds for frequencies, power, fan and temperatu
|
||||
15/30/60/120 seconds for the battery graph. Tray autostart is optional.
|
||||
Closing the window leaves monitoring in the tray; Quit exits the application.
|
||||
The Tray icon tab supports one to ten independently configured icons, each showing
|
||||
an application icon, a history graph, or a number. Settings cards wrap to fit the
|
||||
window. New icons default to the application icon; your existing configuration
|
||||
an application icon, a history graph, or a number. Number mode offers an optional
|
||||
second reading, displayed beneath the first; **None** keeps a single number.
|
||||
Settings cards wrap to fit the window. New icons default to the application icon;
|
||||
your existing configuration
|
||||
becomes Icon 1. Reducing the count keeps hidden drafts until Save and Apply;
|
||||
saving retains only visible icons. The Move icon arrows swap all settings with
|
||||
the adjacent icon while keeping the position numbers fixed. Undo restores the
|
||||
@@ -106,6 +115,11 @@ choice. Hover information is configured separately: CPU usage; battery level
|
||||
and remaining/full energy in mWh, followed by signed power and time to the charge
|
||||
target; fan RPM and duty percentage; and the main CPU, memory, NVMe, battery and
|
||||
board temperatures. Multiple temperatures have a heading and indented lines.
|
||||
Detected power sensors have individual hover checkboxes, off by default. Multiple
|
||||
selected power readings are grouped under a heading; battery power remains part
|
||||
of the Battery option. Power, frequency and temperature readings use one decimal
|
||||
between -10 and 10 (exclusive), and otherwise round to the nearest integer.
|
||||
Numeric tray frequencies remain in GHz; graph readings retain their labelled units.
|
||||
Missing sensors are unavailable in the settings page. Batteries reporting only
|
||||
charge are converted to mWh using nominal voltage.
|
||||
Choose zero to three top CPU applications, displayed below CPU usage in descending
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: framework-laptop-tools
|
||||
Version: 0.1.0
|
||||
Release: 25%{?dist}
|
||||
Release: 28%{?dist}
|
||||
Summary: Hardware controls and monitoring for Framework Laptop 13 Pro
|
||||
License: MIT
|
||||
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
||||
@@ -56,6 +56,16 @@ install -Dpm 0644 %{SOURCE1} %{buildroot}%{_licensedir}/%{name}/LICENSE
|
||||
%{_unitdir}/framework-laptop-tools-fan-resume.service
|
||||
%{_unitdir}/framework-laptop-tools-cpu.service
|
||||
%changelog
|
||||
* Fri Sep 18 2026 fedora-tools contributors - 0.1.0-28
|
||||
- Add an optional second numeric tray reading and P/E-core maximum frequencies
|
||||
|
||||
* Fri Sep 18 2026 fedora-tools contributors - 0.1.0-27
|
||||
- Show fractional power, frequency and temperature readings below ten
|
||||
- Add selectable power sensors to each tray icon's hover information
|
||||
|
||||
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-26
|
||||
- Add detected P-core and E-core frequency averages to Monitor and tray readings
|
||||
|
||||
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-25
|
||||
- Add multiple tray icons, opacity controls and component power monitoring
|
||||
- Refine graph columns, live hover readings and sampling-interval changes
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include "chart.h"
|
||||
#include "graphdata.h"
|
||||
#include "readingformat.h"
|
||||
#include <QPainter>
|
||||
#include <QMouseEvent>
|
||||
#include <QToolTip>
|
||||
@@ -230,7 +231,8 @@ QString Chart::readingAt(qint64 time) const
|
||||
if (next != points.cbegin()) interval = std::max(interval, intervalAt((next - 1)->x()));
|
||||
const bool valid = !sleeping && closest && std::abs(closest->x() - time) <= interval * 1.5
|
||||
&& std::isfinite(closest->y()) && !crossesSleep(std::min(closest->x(), double(time)), std::max(closest->x(), double(time)));
|
||||
lines.append(it->name + ": " + (valid ? number(closest->y()) + " " + it->unit : "—"));
|
||||
const bool adaptive = it->unit == "W" || it->unit == "MHz" || it->unit == "°C";
|
||||
lines.append(it->name + ": " + (valid ? (adaptive ? formatReading(closest->y()) : number(closest->y())) + " " + it->unit : "—"));
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -72,7 +72,26 @@ QVector<Sensor> temperatureSensors(const QString &sys)
|
||||
QVector<Sensor> frequencySensors(const QString &sys)
|
||||
{
|
||||
QVector<Sensor> sensors;
|
||||
sensors.append({"cpu", "CPU average (reported)", sys + "/devices/system/cpu/cpufreq", "MHz", .001});
|
||||
const QString root = sys + "/devices/system/cpu/cpufreq";
|
||||
const auto groups = cpuPolicyGroups(sys);
|
||||
for (const auto &group : {QString("p"), QString("e")}) {
|
||||
if (!groups.contains(group)) continue;
|
||||
Sensor sensor{"cpu/" + group, group.toUpper() + "-cores average (reported)", root, "MHz", .001, true};
|
||||
for (const auto &policy : groups[group])
|
||||
sensor.frequencyPaths.append(root + "/" + policy + "/scaling_cur_freq");
|
||||
sensors.append(sensor);
|
||||
}
|
||||
// Keep the original ID for saved tray selections and combined-average history.
|
||||
sensors.append({"cpu", "CPU average (reported)", root, "MHz", .001, groups.isEmpty()});
|
||||
const int groupCount = sensors.size() - 1;
|
||||
for (int i = 0; i < groupCount; ++i) {
|
||||
auto sensor = sensors[i];
|
||||
sensor.id += "/max";
|
||||
sensor.name = sensor.id.section('/', 1, 1).toUpper() + "-cores maximum (reported)";
|
||||
sensor.primary = false;
|
||||
sensor.maximum = true;
|
||||
sensors.append(sensor);
|
||||
}
|
||||
const QDir drm(sys + "/class/drm");
|
||||
for (const auto &card : drm.entryList({"card*"}, QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
QDirIterator files(drm.filePath(card + "/device"), {"act_freq"}, QDir::Files,
|
||||
@@ -80,22 +99,27 @@ QVector<Sensor> frequencySensors(const QString &sys)
|
||||
while (files.hasNext()) {
|
||||
const QString path = files.next();
|
||||
const QString relative = path.mid(drm.filePath(card + "/device/").size());
|
||||
sensors.append({card + "/" + relative, "GPU " + card + " " + relative.section('/', 1, 1), path, "MHz", 1});
|
||||
sensors.append({card + "/" + relative, "GPU " + card + " " + relative.section('/', 1, 1), path, "MHz", 1, true});
|
||||
}
|
||||
}
|
||||
return sensors;
|
||||
}
|
||||
std::optional<double> sensorValue(const Sensor &sensor)
|
||||
{
|
||||
if (sensor.id == "cpu" || !sensor.frequencyPaths.isEmpty()) {
|
||||
QStringList paths = sensor.frequencyPaths;
|
||||
if (sensor.id == "cpu") {
|
||||
const QDir dir(sensor.path);
|
||||
double total = 0;
|
||||
int count = 0;
|
||||
for (const auto &policy : dir.entryList({"policy*"}, QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
const auto n = readNumber(dir.filePath(policy + "/scaling_cur_freq"));
|
||||
if (n) { total += *n; ++count; }
|
||||
for (const auto &policy : dir.entryList({"policy*"}, QDir::Dirs | QDir::NoDotAndDotDot))
|
||||
paths.append(dir.filePath(policy + "/scaling_cur_freq"));
|
||||
}
|
||||
return count ? std::optional<double>(total / count * sensor.scale) : std::nullopt;
|
||||
double total = 0, maximum = 0;
|
||||
int count = 0;
|
||||
for (const auto &path : paths) {
|
||||
const auto n = readNumber(path);
|
||||
if (n) { total += *n; maximum = std::max(maximum, *n); ++count; }
|
||||
}
|
||||
return count ? std::optional<double>((sensor.maximum ? maximum : total / count) * sensor.scale) : std::nullopt;
|
||||
}
|
||||
if (sensor.unit == "°C") {
|
||||
const QString fault = sensor.path.chopped(6) + "_fault";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVariantMap>
|
||||
#include <QVector>
|
||||
#include <optional>
|
||||
@@ -9,6 +10,8 @@ struct Sensor {
|
||||
QString id, name, path, unit;
|
||||
double scale = 1;
|
||||
bool primary = false;
|
||||
QStringList frequencyPaths;
|
||||
bool maximum = false;
|
||||
};
|
||||
QString readText(const QString &path);
|
||||
std::optional<double> readNumber(const QString &path);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <cmath>
|
||||
|
||||
inline QString formatReading(double value)
|
||||
{
|
||||
if (!std::isfinite(value)) return QStringLiteral("—");
|
||||
const bool fractional = std::abs(value) < 10;
|
||||
const auto text = QString::number(fractional ? value : std::round(value), 'f', fractional ? 1 : 0);
|
||||
return text == "-0.0" ? QStringLiteral("0.0") : text;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include "tooltip.h"
|
||||
#include "readingformat.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
@@ -15,9 +16,9 @@ QVector<TooltipSensor> tooltipSensors(const QVector<Sensor> &sensors)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool tooltipEnabled(const QVariantMap &settings, const QString &key)
|
||||
bool tooltipEnabled(const QVariantMap &settings, const QString &key, bool fallback)
|
||||
{
|
||||
return settings.value("tray/hover/" + key, true).toBool();
|
||||
return settings.value("tray/hover/" + key, fallback).toBool();
|
||||
}
|
||||
int tooltipAppCount(const QVariantMap &settings)
|
||||
{
|
||||
@@ -36,7 +37,7 @@ QString batteryTooltip(const QVariantMap &battery, int limit, const QVariantMap
|
||||
};
|
||||
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");
|
||||
text += "\n\u2003" + (watts ? (*watts > 0 ? "+" : "") + formatReading(*watts) + " W" : "— W");
|
||||
const bool charging = battery["state"] == "Charging", discharging = battery["state"] == "Discharging";
|
||||
double seconds = 0;
|
||||
bool approximate = false;
|
||||
@@ -60,7 +61,8 @@ QString batteryTooltip(const QVariantMap &battery, int limit, const QVariantMap
|
||||
return text;
|
||||
}
|
||||
QString trayTooltip(const QVariantMap &settings, const QMap<QString, double> &values,
|
||||
const QVector<TooltipSensor> &temperatures, const QString &battery, const QStringList &topApps)
|
||||
const QVector<TooltipSensor> &temperatures, const QString &battery, const QStringList &topApps,
|
||||
const QVector<Sensor> &powerSensors)
|
||||
{
|
||||
const auto reading = [&](const QString &id, int decimals) {
|
||||
const auto it = values.constFind(id);
|
||||
@@ -75,10 +77,16 @@ QString trayTooltip(const QVariantMap &settings, const QMap<QString, double> &va
|
||||
}
|
||||
if (tooltipEnabled(settings, "battery")) lines << battery;
|
||||
if (tooltipEnabled(settings, "fan")) lines << "Fan speed: " + reading("fan", 0) + " RPM (" + reading("fan-duty", 0) + " %)";
|
||||
QStringList power;
|
||||
for (const auto &sensor : powerSensors)
|
||||
if (tooltipEnabled(settings, sensor.id, false))
|
||||
power << sensor.name + ": " + formatReading(values.value(sensor.id, NAN)) + " W";
|
||||
if (power.size() > 1) lines << "Power consumption:";
|
||||
for (const auto &line : power) lines << (power.size() > 1 ? "\u2003" : "") + line;
|
||||
QStringList temps;
|
||||
for (const auto &sensor : temperatures)
|
||||
if (!sensor.id.isEmpty() && tooltipEnabled(settings, "temperature/" + sensor.key))
|
||||
temps << sensor.name + " " + reading(sensor.id, 0) + "°C";
|
||||
temps << sensor.name + " " + formatReading(values.value(sensor.id, NAN)) + "°C";
|
||||
if (temps.size() > 1) lines << "Temperatures:";
|
||||
for (const auto &temp : temps) lines << (temps.size() > 1 ? "\u2003" : "") + temp;
|
||||
return lines.join('\n');
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
struct TooltipSensor { QString key, name, id; };
|
||||
QVector<TooltipSensor> tooltipSensors(const QVector<Sensor> &sensors);
|
||||
bool tooltipEnabled(const QVariantMap &settings, const QString &key);
|
||||
bool tooltipEnabled(const QVariantMap &settings, const QString &key, bool fallback = true);
|
||||
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);
|
||||
const QVector<TooltipSensor> &temperatures, const QString &battery, const QStringList &topApps,
|
||||
const QVector<Sensor> &powerSensors = {});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include "tray.h"
|
||||
#include "graphdata.h"
|
||||
#include "readingformat.h"
|
||||
#include <QPainter>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
@@ -30,7 +31,8 @@ QRectF trayPlotRect(bool border)
|
||||
// Two-pixel border; reserve space for the graph's vertical padding 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, qint64 now)
|
||||
QIcon telemetryIcon(bool graph, const QVector<QPointF> &values, const QString &unit, const TrayStyle &style, qint64 now,
|
||||
std::optional<TrayReading> second)
|
||||
{
|
||||
QPixmap pixmap(64, 64); pixmap.fill(style.backgroundColor);
|
||||
QPainter p(&pixmap); p.setRenderHint(QPainter::Antialiasing);
|
||||
@@ -67,11 +69,28 @@ QIcon telemetryIcon(bool graph, const QVector<QPointF> &values, const QString &u
|
||||
}
|
||||
p.restore();
|
||||
} else {
|
||||
const QString value = !std::isfinite(latest) ? "—" : unit == "MHz"
|
||||
? QString::number(latest / 1000, 'f', 1) : QString::number(std::round(latest), 'f', 0);
|
||||
const bool stacked = !graph && second.has_value();
|
||||
const auto drawNumber = [&](const TrayReading &reading, const QRectF &rect) {
|
||||
const bool adaptive = reading.unit == "MHz" || reading.unit == "W" || reading.unit == "°C";
|
||||
const QString value = !std::isfinite(reading.value) ? "—" : adaptive
|
||||
? formatReading(reading.unit == "MHz" ? reading.value / 1000 : reading.value)
|
||||
: QString::number(std::round(reading.value), 'f', 0);
|
||||
QFont font; font.setBold(true); font.setPixelSize(42);
|
||||
while (QFontMetrics(font).horizontalAdvance(value) > area.width() && font.pixelSize() > 12) font.setPixelSize(font.pixelSize() - 1);
|
||||
p.setFont(font); p.drawText(area, Qt::AlignCenter, value);
|
||||
while (font.pixelSize() > 12 && (QFontMetrics(font).horizontalAdvance(value) > rect.width()
|
||||
|| (stacked && QFontMetrics(font).tightBoundingRect(value).height() > rect.height() - 2)))
|
||||
font.setPixelSize(font.pixelSize() - 1);
|
||||
p.setFont(font);
|
||||
if (stacked) {
|
||||
// Centre the visible glyphs, not the font's extra line spacing.
|
||||
const auto bounds = QFontMetricsF(font).tightBoundingRect(value);
|
||||
p.drawText(rect.center() - bounds.center(), value);
|
||||
} else p.drawText(rect, Qt::AlignCenter, value);
|
||||
};
|
||||
if (stacked) {
|
||||
const double height = area.height() / 2;
|
||||
drawNumber({latest, unit}, QRectF(area.left(), area.top(), area.width(), height));
|
||||
drawNumber(*second, QRectF(area.left(), area.top() + height, area.width(), height));
|
||||
} else drawNumber({latest, unit}, area);
|
||||
}
|
||||
return QIcon(pixmap);
|
||||
}
|
||||
|
||||
@@ -23,5 +23,9 @@ struct TrayStyle {
|
||||
qint64 historyMs = 60000;
|
||||
};
|
||||
QRectF trayPlotRect(bool border);
|
||||
struct TrayReading {
|
||||
double value;
|
||||
QString unit;
|
||||
};
|
||||
QIcon telemetryIcon(bool graph, const QVector<QPointF> &values, const QString &unit, const TrayStyle &style,
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch());
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch(), std::optional<TrayReading> second = {});
|
||||
|
||||
@@ -53,6 +53,11 @@ TrayPage::TrayPage(const QVariantMap &values, const QVector<Sensor> &sensors, QW
|
||||
auto *graphForm = new QFormLayout(graphOptions); graphForm->setContentsMargins(24, 0, 0, 0);
|
||||
graphForm->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
|
||||
form->addRow(graphOptions); form->addRow("Reading:", m_metric);
|
||||
m_secondMetric = new QComboBox; m_secondMetric->setObjectName("traySecondMetric");
|
||||
m_secondMetric->addItem("None", "");
|
||||
for (const auto &sensor : sensors) m_secondMetric->addItem(sensor.name + " (" + sensor.unit + ")", sensor.id);
|
||||
m_secondMetric->setCurrentIndex(std::max(0, m_secondMetric->findData(m_values.value("tray/secondMetric", ""))));
|
||||
form->addRow("Second reading:", m_secondMetric);
|
||||
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) {
|
||||
@@ -132,10 +137,11 @@ TrayPage::TrayPage(const QVariantMap &values, const QVector<Sensor> &sensors, QW
|
||||
m_values.insert("tray/outside", m_outside->currentData()); syncOutside(); if (!m_loading) Q_EMIT settingsChanged();
|
||||
});
|
||||
syncOutside(); layout->addWidget(m_graphSettings);
|
||||
auto syncMode = [this, appearance, graphOptions] {
|
||||
auto syncMode = [this, form, appearance, graphOptions] {
|
||||
const bool telemetry = mode() != "icon";
|
||||
m_metric->setEnabled(telemetry); appearance->setVisible(telemetry); m_graphSettings->setVisible(mode() == "graph");
|
||||
graphOptions->setVisible(mode() == "graph");
|
||||
form->setRowVisible(m_secondMetric, mode() == "number");
|
||||
};
|
||||
connect(m_mode, &QComboBox::currentIndexChanged, this, [this, syncMode] {
|
||||
m_values.insert("tray/mode", m_mode->currentData()); syncMode(); if (!m_loading) Q_EMIT settingsChanged();
|
||||
@@ -143,6 +149,9 @@ TrayPage::TrayPage(const QVariantMap &values, const QVector<Sensor> &sensors, QW
|
||||
connect(m_metric, &QComboBox::currentIndexChanged, this, [this] {
|
||||
m_values.insert("tray/metric", m_metric->currentData()); loadScale(); if (!m_loading) Q_EMIT settingsChanged();
|
||||
});
|
||||
connect(m_secondMetric, &QComboBox::currentIndexChanged, this, [this] {
|
||||
m_values.insert("tray/secondMetric", m_secondMetric->currentData()); if (!m_loading) Q_EMIT settingsChanged();
|
||||
});
|
||||
connect(m_minimum, &QDoubleSpinBox::valueChanged, this, [this](double value) {
|
||||
m_maximum->setMinimum(value + 1); m_values.insert(scaleKey("minimum"), value); if (!m_loading) Q_EMIT settingsChanged();
|
||||
});
|
||||
@@ -152,10 +161,12 @@ TrayPage::TrayPage(const QVariantMap &values, const QVector<Sensor> &sensors, QW
|
||||
loadScale(); syncMode();
|
||||
m_values.insert("tray/mode", m_mode->currentData());
|
||||
m_values.insert("tray/metric", m_metric->currentData());
|
||||
m_values.insert("tray/secondMetric", m_secondMetric->currentData());
|
||||
m_values.insert("tray/outside", m_outside->currentData());
|
||||
m_reload.append([this, syncMode, syncOutside] {
|
||||
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"))));
|
||||
m_secondMetric->setCurrentIndex(std::max(0, m_secondMetric->findData(m_values.value("tray/secondMetric", ""))));
|
||||
m_outside->setCurrentIndex(std::max(0, m_outside->findData(m_values.value("tray/outside", "clamp"))));
|
||||
syncMode(); syncOutside();
|
||||
});
|
||||
@@ -178,6 +189,15 @@ TrayPage::TrayPage(const QVariantMap &values, const QVector<Sensor> &sensors, QW
|
||||
appOptions->setEnabled(cpu->isChecked()); connect(cpu, &QCheckBox::toggled, appOptions, &QWidget::setEnabled);
|
||||
check(hoverForm, "Battery", "hover/battery", true);
|
||||
check(hoverForm, "Fan", "hover/fan", true);
|
||||
auto *powerOptions = new QWidget;
|
||||
auto *powerForm = new QFormLayout(powerOptions); powerForm->setContentsMargins(24, 0, 0, 0);
|
||||
for (const auto &sensor : sensors)
|
||||
if (sensor.unit == "W" && sensor.id.startsWith("power/"))
|
||||
check(powerForm, sensor.name, "hover/" + sensor.id, false);
|
||||
if (powerForm->rowCount()) {
|
||||
hoverForm->addRow(new QLabel("Power consumption:"));
|
||||
hoverForm->addRow(powerOptions);
|
||||
} else delete powerOptions;
|
||||
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)) {
|
||||
@@ -205,7 +225,8 @@ void TrayPage::loadScale()
|
||||
const bool frequency = unit == "MHz", temperature = unit == "°C", rate = unit == "W";
|
||||
m_scaleLayout->setRowVisible(m_minimum, temperature || rate); m_scaleLayout->setRowVisible(m_maximum, temperature || frequency || rate);
|
||||
for (auto *spin : {m_minimum, m_maximum}) { spin->setDecimals(0); spin->setRange(rate ? -300 : temperature ? -50 : 0, frequency ? 20000 : 300); spin->setSuffix(" " + unit); }
|
||||
double maximum = frequency ? (metric().id == "cpu" ? cpuLimits().value("high", 5000).toDouble() : 3000) : rate ? 75 : 100;
|
||||
const bool cpuFrequency = metric().id == "cpu" || metric().id.startsWith("cpu/");
|
||||
double maximum = frequency ? (cpuFrequency ? cpuLimits("/sys", metric().id.section('/', 1, 1)).value("high", 5000).toDouble() : 3000) : rate ? 75 : 100;
|
||||
const double low = m_values.value(scaleKey("minimum"), rate && !metric().id.startsWith("power/") ? -75 : 0).toDouble();
|
||||
maximum = m_values.value(scaleKey("maximum"), maximum).toDouble();
|
||||
m_minimum->setValue(low); m_maximum->setMinimum(m_minimum->value() + 1); m_maximum->setValue(maximum);
|
||||
@@ -345,7 +366,7 @@ QVector<TrayConfiguration> TrayIconsPage::configurations() const
|
||||
QVector<TrayConfiguration> result;
|
||||
for (int i = 0; i < m_count->value(); ++i) {
|
||||
const auto *page = m_pages[i];
|
||||
result.append({page->mode(), page->metric(), page->iconStyle(), page->draft()});
|
||||
result.append({page->mode(), page->metric(), page->iconStyle(), page->draft(), page->secondMetric()});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ struct TrayConfiguration {
|
||||
Sensor metric;
|
||||
TrayStyle style;
|
||||
QVariantMap values;
|
||||
std::optional<Sensor> secondMetric;
|
||||
};
|
||||
|
||||
class TrayPage : public QWidget {
|
||||
@@ -28,6 +29,10 @@ public:
|
||||
TrayPage(const QVariantMap &values, const QVector<Sensor> &sensors, QWidget *parent = nullptr);
|
||||
QString mode() const { return m_mode->currentData().toString(); }
|
||||
Sensor metric() const { return m_sensors[m_metric->currentIndex()]; }
|
||||
std::optional<Sensor> secondMetric() const {
|
||||
return m_secondMetric->currentIndex() > 0
|
||||
? std::optional<Sensor>(m_sensors[m_secondMetric->currentIndex() - 1]) : std::nullopt;
|
||||
}
|
||||
TrayStyle iconStyle() const;
|
||||
QVariantMap draft() const { return m_values; }
|
||||
void load(const QVariantMap &values);
|
||||
@@ -40,7 +45,7 @@ private:
|
||||
QList<std::function<void()>> m_reload;
|
||||
bool m_loading = false;
|
||||
QVector<Sensor> m_sensors;
|
||||
QComboBox *m_mode, *m_metric, *m_outside, *m_history;
|
||||
QComboBox *m_mode, *m_metric, *m_secondMetric, *m_outside, *m_history;
|
||||
QDoubleSpinBox *m_minimum, *m_maximum;
|
||||
QFormLayout *m_scaleLayout;
|
||||
QWidget *m_graphSettings;
|
||||
|
||||
@@ -119,7 +119,8 @@ QGroupBox *Window::sensorGroup(const QString &title, Chart *chart, const QVector
|
||||
QVector<Sensor> main, extra;
|
||||
for (const auto &sensor : sensors) {
|
||||
chart->addSeries(sensor.id, sensor.name, sensor.unit);
|
||||
chart->setSelected(sensor.id, m_settings.value("series/" + sensor.id, !temperatures || sensor.primary).toBool());
|
||||
const bool selectedByDefault = (temperatures || sensor.unit == "MHz") ? sensor.primary : true;
|
||||
chart->setSelected(sensor.id, m_settings.value("series/" + sensor.id, selectedByDefault).toBool());
|
||||
(temperatures && !sensor.primary ? extra : main).append(sensor);
|
||||
}
|
||||
auto makeLegend = [&](const QVector<Sensor> &entries) {
|
||||
@@ -310,15 +311,24 @@ void Window::updateTray()
|
||||
if (config.mode == "icon") tray.icon->setIcon(windowIcon());
|
||||
else {
|
||||
const double since = now - (config.mode == "graph" ? config.style.historyMs : 0);
|
||||
const auto historyFor = [&](const Sensor &sensor) {
|
||||
if (sensor.id == "cpu-usage") {
|
||||
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 if (metric.id.startsWith("power/")) history = m_powerChart->history(metric.id, since);
|
||||
else history = (metric.unit == "MHz" ? m_frequencyChart : m_temperatureChart)->history(metric.id, since);
|
||||
tray.icon->setIcon(telemetryIcon(config.mode == "graph", history, metric.unit, config.style, now));
|
||||
return history;
|
||||
}
|
||||
tray.icon->setToolTip(trayTooltip(config.values, m_values, temperatures, m_batteryText, m_topAppText));
|
||||
if (sensor.id == "battery" || sensor.id == "battery-rate") return m_batteryChart->history(sensor.id, since);
|
||||
if (sensor.id.startsWith("power/")) return m_powerChart->history(sensor.id, since);
|
||||
return (sensor.unit == "MHz" ? m_frequencyChart : m_temperatureChart)->history(sensor.id, since);
|
||||
};
|
||||
std::optional<TrayReading> second;
|
||||
if (config.mode == "number" && config.secondMetric) {
|
||||
const auto history = historyFor(*config.secondMetric);
|
||||
second = TrayReading{history.isEmpty() ? NAN : history.last().y(), config.secondMetric->unit};
|
||||
}
|
||||
tray.icon->setIcon(telemetryIcon(config.mode == "graph", historyFor(metric), metric.unit, config.style, now, second));
|
||||
}
|
||||
tray.icon->setToolTip(trayTooltip(config.values, m_values, temperatures, m_batteryText, m_topAppText, m_powerMonitor.sensors()));
|
||||
}
|
||||
}
|
||||
QVariantMap Window::controlValue(const QString &key) const
|
||||
@@ -531,7 +541,8 @@ void Window::sample()
|
||||
sampleSensors(m_frequencies, m_frequencyChart); sampleSensors(m_temperatures, m_temperatureChart);
|
||||
const auto power = m_powerMonitor.sample(m_fastTimer.interval());
|
||||
m_powerChart->sample(power, m_fastTimer.interval());
|
||||
m_values.insert(power);
|
||||
for (const auto &sensor : m_powerMonitor.sensors())
|
||||
m_values[sensor.id] = power.value(sensor.id, NAN);
|
||||
const QString ec = ecHwmon();
|
||||
const auto rpm = readNumber(ec + "/fan1_input");
|
||||
const auto mode = readNumber(ec + "/pwm1_enable");
|
||||
|
||||
@@ -15,6 +15,54 @@ class HardwareTest : public QObject {
|
||||
QFile file(path); QVERIFY(file.open(QIODevice::WriteOnly)); QCOMPARE(file.write(value), value.size());
|
||||
}
|
||||
private Q_SLOTS:
|
||||
void coreGroupFrequencies() {
|
||||
QTemporaryDir root;
|
||||
const QString base = root.path() + "/devices/system/cpu/cpufreq/";
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const auto policy = base + "policy" + QString::number(i) + "/";
|
||||
put(policy + "related_cpus", QByteArray::number(i));
|
||||
put(policy + "scaling_cur_freq", QByteArray::number((i + 1) * 1000000));
|
||||
}
|
||||
// No core-type metadata: retain the original combined reading.
|
||||
auto sensors = frequencySensors(root.path());
|
||||
QCOMPARE(sensors.size(), 1);
|
||||
QCOMPARE(sensors[0].id, QString("cpu"));
|
||||
QVERIFY(sensors[0].primary);
|
||||
QCOMPARE(sensorValue(sensors[0]), std::optional<double>(2000));
|
||||
put(root.path() + "/bus/event_source/devices/cpu_core/cpus", "2");
|
||||
put(root.path() + "/bus/event_source/devices/cpu_atom/cpus", "0-1");
|
||||
sensors = frequencySensors(root.path());
|
||||
QCOMPARE(sensors.size(), 5);
|
||||
QCOMPARE(sensors[0].id, QString("cpu/p"));
|
||||
QCOMPARE(sensors[1].id, QString("cpu/e"));
|
||||
QCOMPARE(sensors[2].id, QString("cpu"));
|
||||
QVERIFY(sensors[0].primary && sensors[1].primary && !sensors[2].primary);
|
||||
QCOMPARE(sensorValue(sensors[0]), std::optional<double>(3000));
|
||||
QCOMPARE(sensorValue(sensors[1]), std::optional<double>(1500));
|
||||
QCOMPARE(sensorValue(sensors[2]), std::optional<double>(2000));
|
||||
QCOMPARE(sensors[3].id, QString("cpu/p/max"));
|
||||
QCOMPARE(sensors[4].id, QString("cpu/e/max"));
|
||||
QVERIFY(!sensors[3].primary && !sensors[4].primary);
|
||||
QCOMPARE(sensorValue(sensors[3]), std::optional<double>(3000));
|
||||
QCOMPARE(sensorValue(sensors[4]), std::optional<double>(2000));
|
||||
put(base + "policy0/scaling_cur_freq", "4000000");
|
||||
QCOMPARE(sensorValue(sensors[4]), std::optional<double>(4000));
|
||||
put(base + "policy0/scaling_cur_freq", "1000000");
|
||||
QCOMPARE(sensorValue(sensors[4]), std::optional<double>(2000)); // Not a historical peak.
|
||||
// Missing readings aren't zeroes, and membership isn't guessed by speed.
|
||||
put(base + "policy0/scaling_cur_freq", "unavailable");
|
||||
QCOMPARE(sensorValue(sensors[1]), std::optional<double>(2000));
|
||||
QCOMPARE(sensorValue(sensors[4]), std::optional<double>(2000));
|
||||
put(base + "policy1/scaling_cur_freq", "unavailable");
|
||||
QVERIFY(!sensorValue(sensors[1]));
|
||||
QVERIFY(!sensorValue(sensors[4]));
|
||||
QCOMPARE(sensorValue(sensors[2]), std::optional<double>(3000));
|
||||
put(base + "policy2/related_cpus", "1 2");
|
||||
sensors = frequencySensors(root.path());
|
||||
QCOMPARE(sensors.size(), 1); // A policy spanning types cannot be split.
|
||||
QVERIFY(sensors[0].primary);
|
||||
}
|
||||
|
||||
void energyCounters() {
|
||||
EnergyCounter counter;
|
||||
const quint64 range = 262143328850ULL;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <QSlider>
|
||||
#include "graphdata.h"
|
||||
#include "tooltip.h"
|
||||
#include "readingformat.h"
|
||||
#include "processusage.h"
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
@@ -33,6 +34,112 @@
|
||||
class WindowTest : public QObject {
|
||||
Q_OBJECT
|
||||
private Q_SLOTS:
|
||||
void readingFormat_data() {
|
||||
QTest::addColumn<double>("value"); QTest::addColumn<QString>("text");
|
||||
QTest::newRow("zero") << 0. << QString("0.0");
|
||||
QTest::newRow("small") << 1.26 << QString("1.3");
|
||||
QTest::newRow("negative") << -1.26 << QString("-1.3");
|
||||
QTest::newRow("negative zero") << -0.01 << QString("0.0");
|
||||
QTest::newRow("whole below ten") << 9. << QString("9.0");
|
||||
QTest::newRow("round to boundary") << 9.96 << QString("10.0");
|
||||
QTest::newRow("ten") << 10. << QString("10");
|
||||
QTest::newRow("minus ten") << -10. << QString("-10");
|
||||
QTest::newRow("round up") << 12.6 << QString("13");
|
||||
QTest::newRow("round down") << 12.4 << QString("12");
|
||||
QTest::newRow("negative round") << -12.5 << QString("-13");
|
||||
QTest::newRow("unavailable") << double(NAN) << QString("—");
|
||||
QTest::newRow("infinite") << double(INFINITY) << QString("—");
|
||||
}
|
||||
void readingFormat() {
|
||||
QFETCH(double, value); QFETCH(QString, text);
|
||||
QCOMPARE(formatReading(value), text);
|
||||
}
|
||||
void trayNumberPrecision() {
|
||||
TrayStyle style; style.border = false;
|
||||
style.backgroundColor = Qt::black; style.lineColor = Qt::white;
|
||||
const auto render = [&](double value, const QString &unit) {
|
||||
return telemetryIcon(false, {{0, value}}, unit, style, 0).pixmap(64, 64).toImage();
|
||||
};
|
||||
QCOMPARE(render(1250, "MHz"), render(1.25, "W")); // GHz in the icon.
|
||||
QCOMPARE(render(1.25, "W"), render(1.25, "°C"));
|
||||
QVERIFY(render(1.2, "W") != render(1.4, "W"));
|
||||
QCOMPARE(render(12.6, "W"), render(13, "%"));
|
||||
QCOMPARE(render(10000, "MHz"), render(10, "%"));
|
||||
QVERIFY(render(-1.2, "W") != render(1.2, "W"));
|
||||
}
|
||||
void stackedTrayNumbers() {
|
||||
TrayStyle style; style.border = false;
|
||||
style.backgroundColor = Qt::black; style.lineColor = Qt::white;
|
||||
const auto render = [&](double first, double second, const QString &unit = "W") {
|
||||
return telemetryIcon(false, {{0, first}}, "W", style, 0, TrayReading{second, unit}).pixmap(64, 64).toImage();
|
||||
};
|
||||
const auto both = render(1.2, 3.4);
|
||||
const auto changedTop = render(5.6, 3.4), changedBottom = render(1.2, 7.8);
|
||||
QVERIFY(both.copy(0, 0, 64, 32) != changedTop.copy(0, 0, 64, 32));
|
||||
QCOMPARE(both.copy(0, 32, 64, 32), changedTop.copy(0, 32, 64, 32));
|
||||
QCOMPARE(both.copy(0, 0, 64, 32), changedBottom.copy(0, 0, 64, 32));
|
||||
QVERIFY(both.copy(0, 32, 64, 32) != changedBottom.copy(0, 32, 64, 32));
|
||||
QCOMPARE(both, render(1.2, 3400, "MHz"));
|
||||
QVERIFY(both != render(1.2, NAN)); // Missing is a dash, not removal of the second row.
|
||||
// Both rows use their available height with small margins.
|
||||
for (int row = 0; row < 2; ++row) {
|
||||
int first = 64, last = -1;
|
||||
for (int y = row * 32; y < (row + 1) * 32; ++y)
|
||||
for (int x = 0; x < 64; ++x)
|
||||
if (qRed(both.pixel(x, y)) > 128) { first = std::min(first, y); last = std::max(last, y); }
|
||||
QVERIFY(first <= row * 32 + 5); QVERIFY(last >= row * 32 + 26);
|
||||
}
|
||||
const auto single = telemetryIcon(false, {{0, 1.2}}, "W", style, 0).pixmap(64, 64).toImage();
|
||||
QCOMPARE(single, telemetryIcon(false, {{0, 1.2}}, "W", style, 0, std::nullopt).pixmap(64, 64).toImage());
|
||||
const QVector<QPointF> history{{0, 1.2}, {1000, 3.4}};
|
||||
QCOMPARE(telemetryIcon(true, history, "W", style, 1000).pixmap(64, 64).toImage(),
|
||||
telemetryIcon(true, history, "W", style, 1000, TrayReading{90, "%"}).pixmap(64, 64).toImage());
|
||||
}
|
||||
void secondTrayReading() {
|
||||
const QVector<Sensor> sensors{{"cpu-usage", "CPU usage", {}, "%"}, {"cpu/p/max", "P-cores maximum", {}, "MHz"}};
|
||||
TrayPage page(QVariantMap{{"tray/mode", "number"}}, sensors);
|
||||
auto *second = page.findChild<QComboBox *>("traySecondMetric");
|
||||
auto *mode = page.findChild<QComboBox *>("trayDisplayMode");
|
||||
QVERIFY(second); QCOMPARE(second->currentText(), QString("None")); QVERIFY(!page.secondMetric());
|
||||
QVERIFY(!second->isHidden());
|
||||
const auto original = page.draft();
|
||||
second->setCurrentIndex(second->findData("cpu/p/max"));
|
||||
QVERIFY(page.secondMetric()); QCOMPARE(page.secondMetric()->id, QString("cpu/p/max"));
|
||||
const auto chosen = page.draft();
|
||||
TrayPage restored(chosen, sensors);
|
||||
QCOMPARE(restored.secondMetric()->id, QString("cpu/p/max"));
|
||||
mode->setCurrentIndex(mode->findData("graph")); QVERIFY(second->isHidden());
|
||||
mode->setCurrentIndex(mode->findData("number")); QVERIFY(!second->isHidden());
|
||||
QCOMPARE(page.secondMetric()->id, QString("cpu/p/max"));
|
||||
page.load(original); QVERIFY(!page.secondMetric());
|
||||
page.load(chosen); QCOMPARE(page.secondMetric()->id, QString("cpu/p/max"));
|
||||
TrayPage missing(chosen, {sensors[0]}); QVERIFY(!missing.secondMetric());
|
||||
}
|
||||
void powerHoverOptions() {
|
||||
const QVector<Sensor> sensors{{"power/rapl/0", "CPU package", {}, "W"},
|
||||
{"power/rapl/0:0", "CPU cores", {}, "W"}, {"battery-rate", "Battery rate", {}, "W"}};
|
||||
QVariantMap settings{{"tray/hover/cpu", false}, {"tray/hover/fan", false}, {"tray/hover/battery", false}};
|
||||
const QVector<Sensor> power{sensors[0], sensors[1]};
|
||||
const QMap<QString, double> values{{power[0].id, 4.25}, {power[1].id, 12.6}};
|
||||
TrayPage page(settings, sensors);
|
||||
auto *package = page.findChild<QCheckBox *>("hover/" + power[0].id);
|
||||
auto *cores = page.findChild<QCheckBox *>("hover/" + power[1].id);
|
||||
QVERIFY(package); QVERIFY(cores);
|
||||
QVERIFY(!package->isChecked()); QVERIFY(!cores->isChecked());
|
||||
QVERIFY(!page.findChild<QCheckBox *>("hover/battery-rate"));
|
||||
QVERIFY(trayTooltip(page.draft(), values, {}, {}, {}, power).isEmpty());
|
||||
const auto saved = page.draft();
|
||||
package->setChecked(true);
|
||||
QCOMPARE(trayTooltip(page.draft(), values, {}, {}, {}, power), QString("CPU package: 4.3 W"));
|
||||
cores->setChecked(true);
|
||||
QCOMPARE(trayTooltip(page.draft(), values, {}, {}, {}, power), QString("Power consumption:\n\u2003CPU package: 4.3 W\n\u2003CPU cores: 13 W"));
|
||||
QCOMPARE(trayTooltip(page.draft(), {}, {}, {}, {}, power), QString("Power consumption:\n\u2003CPU package: — W\n\u2003CPU cores: — W"));
|
||||
const auto chosen = page.draft();
|
||||
page.load(saved); QVERIFY(!package->isChecked()); QVERIFY(!cores->isChecked());
|
||||
page.load(chosen); QVERIFY(package->isChecked()); QVERIFY(cores->isChecked());
|
||||
TrayPage restored(chosen, sensors);
|
||||
QCOMPARE(trayTooltip(restored.draft(), values, {}, {}, {}, power), trayTooltip(chosen, values, {}, {}, {}, power));
|
||||
}
|
||||
void hoverFormatting() {
|
||||
const QVector<Sensor> sensors{{"cros_ec/peci-temp", "CPU", {}, "°C"}, {"spd5118/temp1", "Memory", {}, "°C"},
|
||||
{"nvme/Composite", "NVMe", {}, "°C"}};
|
||||
@@ -52,14 +159,15 @@ private Q_SLOTS:
|
||||
settings["tray/hover/temperature/memory"] = false; settings["tray/hover/temperature/nvme"] = false;
|
||||
QCOMPARE(trayTooltip(settings, readings, temperatures, {}, apps), QString("CPU 47°C"));
|
||||
QCOMPARE(trayTooltip(settings, {}, temperatures, {}, {}), QString("CPU —°C"));
|
||||
QCOMPARE(trayTooltip(settings, {{sensors[0].id, 3.2}}, temperatures, {}, {}), QString("CPU 3.2°C"));
|
||||
settings["tray/hover/temperature/cpu"] = false;
|
||||
QVERIFY(trayTooltip(settings, readings, temperatures, {}, {}).isEmpty());
|
||||
QVariantMap battery{{"state", "Discharging"}, {"capacity", 75}, {"watts", 12.5}, {"remainingMWh", 56250}, {"fullMWh", 75000}};
|
||||
QCOMPARE(batteryTooltip(battery, 80, {{"TimeToEmpty", 5400}}), QString("Battery 75% · 56 250 mWh / 75 000 mWh\n\u2003-12.5 W · 1 h 30 min to 0%"));
|
||||
QCOMPARE(batteryTooltip(battery, 80, {{"TimeToEmpty", 5400}}), QString("Battery 75% · 56 250 mWh / 75 000 mWh\n\u2003-13 W · 1 h 30 min to 0%"));
|
||||
battery["state"] = "Charging";
|
||||
QCOMPARE(batteryTooltip(battery, 100, {{"TimeToFull", 3600}}), QString("Battery 75% · 56 250 mWh / 75 000 mWh\n\u2003+12.5 W · 1 h 0 min to 100%"));
|
||||
QCOMPARE(batteryTooltip(battery, 100, {{"TimeToFull", 3600}}), QString("Battery 75% · 56 250 mWh / 75 000 mWh\n\u2003+13 W · 1 h 0 min to 100%"));
|
||||
battery["charge_full"] = 10000; battery["charge_now"] = 7500; battery["current_now"] = 1000;
|
||||
QCOMPARE(batteryTooltip(battery, 80, {}), QString("Battery 75% · 56 250 mWh / 75 000 mWh\n\u2003+12.5 W · ≈0 h 30 min to 80%"));
|
||||
QCOMPARE(batteryTooltip(battery, 80, {}), QString("Battery 75% · 56 250 mWh / 75 000 mWh\n\u2003+13 W · ≈0 h 30 min to 80%"));
|
||||
battery["current_now"] = 0;
|
||||
QVERIFY(batteryTooltip(battery, 80, {}).contains("Time remaining unavailable"));
|
||||
battery["capacity"] = 80;
|
||||
@@ -296,6 +404,7 @@ private Q_SLOTS:
|
||||
first->findChild<QComboBox *>("trayHistory")->setCurrentIndex(0);
|
||||
first->findChild<QCheckBox *>("border")->setChecked(false);
|
||||
last->findChild<QComboBox *>("trayDisplayMode")->setCurrentIndex(2);
|
||||
last->findChild<QComboBox *>("traySecondMetric")->setCurrentIndex(1);
|
||||
last->findChild<QSpinBox *>("hover/topApps")->setValue(3);
|
||||
const auto firstValues = first->draft(), lastValues = last->draft();
|
||||
const auto original = page.draft();
|
||||
@@ -304,6 +413,8 @@ private Q_SLOTS:
|
||||
QSignalSpy changed(&page, &TrayIconsPage::settingsChanged);
|
||||
cards[2]->findChild<QToolButton *>("moveIconLeft")->click();
|
||||
QCOMPARE(page.configurations()[1].values, lastValues); QCOMPARE(changed.count(), 1);
|
||||
QVERIFY(page.configurations()[1].secondMetric);
|
||||
QCOMPARE(page.configurations()[1].secondMetric->id, QString("cpu-usage"));
|
||||
cards[0]->findChild<QToolButton *>("moveIconRight")->click();
|
||||
QCOMPARE(page.configurations()[0].values, lastValues);
|
||||
QCOMPARE(page.configurations()[1].values, firstValues);
|
||||
@@ -411,7 +522,7 @@ private Q_SLOTS:
|
||||
for (int i = 0; i <= 7200; ++i) chart.sample({{"hidden", double(i)}}, 500, start + i * 500);
|
||||
QCOMPARE(chart.history("hidden").size(), 7201); // Not capped at 600, even when hidden.
|
||||
chart.setSelected("hidden", true);
|
||||
QVERIFY(chart.readingAt(start + 500).contains("hidden: 1 MHz"));
|
||||
QVERIFY(chart.readingAt(start + 500).contains("hidden: 1.0 MHz"));
|
||||
const auto end = double(start + 3600000);
|
||||
chart.setHistoryWindow(historyRetentionMs, true);
|
||||
QCOMPARE(chart.timeRange(), qMakePair(double(start), end));
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import QtQml
|
||||
|
||||
Timer {
|
||||
id: retry
|
||||
|
||||
required property QtObject auth
|
||||
required property int fingerprintType
|
||||
property bool enabled: true
|
||||
readonly property bool fingerprintActive: (auth.authenticatorTypes & fingerprintType) !== 0
|
||||
property bool sawInfo: false
|
||||
property bool sawError: false
|
||||
|
||||
interval: 6000
|
||||
|
||||
function reset() {
|
||||
stop();
|
||||
sawInfo = false;
|
||||
sawError = false;
|
||||
}
|
||||
|
||||
onEnabledChanged: {
|
||||
if (!enabled) {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
onFingerprintActiveChanged: {
|
||||
if (fingerprintActive) {
|
||||
reset();
|
||||
} else if (enabled && sawInfo && !sawError) {
|
||||
// An idle conversation ended. Consume the retry until another
|
||||
// conversation actually supplies fingerprint instructions.
|
||||
sawInfo = false;
|
||||
restart();
|
||||
}
|
||||
}
|
||||
onTriggered: {
|
||||
if (enabled && !fingerprintActive) {
|
||||
auth.startAuthenticating();
|
||||
}
|
||||
}
|
||||
|
||||
property Connections authenticationSignals: Connections {
|
||||
target: retry.auth
|
||||
function onNoninteractiveInfo(kind, source) {
|
||||
if (retry.enabled && (kind & retry.fingerprintType)) {
|
||||
retry.sawInfo = true;
|
||||
}
|
||||
}
|
||||
function onNoninteractiveError(kind, source) {
|
||||
if (kind & retry.fingerprintType) {
|
||||
// Do not automatically replenish failed scan attempts.
|
||||
retry.sawError = true;
|
||||
retry.stop();
|
||||
}
|
||||
}
|
||||
function onFailed(kind, source) {
|
||||
if (kind === 0) {
|
||||
retry.reset();
|
||||
}
|
||||
}
|
||||
function onSucceeded() {
|
||||
retry.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@
|
||||
onUiVisibleChanged: {
|
||||
if (uiVisible) {
|
||||
Window.window.requestActivate();
|
||||
@@ -135,6 +143,18 @@
|
||||
@@ -135,6 +143,24 @@
|
||||
}
|
||||
authenticator.startAuthenticating();
|
||||
}
|
||||
@@ -72,11 +72,17 @@
|
||||
+ interval: 6000
|
||||
+ running: root.viewVisible
|
||||
+ onTriggered: authenticator.startAuthenticating()
|
||||
+ }
|
||||
+ FedoraToolsFingerprintRetry {
|
||||
+ auth: authenticator
|
||||
+ fingerprintType: ScreenLocker.Authenticator.Fingerprint
|
||||
+ enabled: root.viewVisible && !authenticator.unlocked
|
||||
+ && !graceLockTimer.running && !resumeAuthenticationTimer.running
|
||||
+ }
|
||||
onBlockUIChanged: {
|
||||
if (blockUI) {
|
||||
fadeoutTimer.running = false;
|
||||
@@ -194,6 +214,6 @@
|
||||
@@ -194,6 +220,6 @@
|
||||
WallpaperFader {
|
||||
anchors.fill: parent
|
||||
- state: lockScreenRoot.uiVisible ? "on" : "off"
|
||||
@@ -84,7 +90,7 @@
|
||||
source: wallpaper
|
||||
mainStack: mainStack
|
||||
footer: footer
|
||||
@@ -211,7 +231,7 @@
|
||||
@@ -211,7 +237,7 @@
|
||||
samples: 15
|
||||
spread: 0.2
|
||||
color : Qt.rgba(0, 0, 0, 0.7)
|
||||
@@ -93,6 +99,6 @@
|
||||
Behavior on opacity {
|
||||
OpacityAnimator {
|
||||
duration: Kirigami.Units.veryLongDuration * 2
|
||||
@@ -265 +285 @@
|
||||
@@ -265 +291 @@
|
||||
- lockScreenUiVisible: lockScreenRoot.uiVisible
|
||||
+ lockScreenUiVisible: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: plasma-always-show-unlock
|
||||
Version: 0.1.0
|
||||
Release: 10%{?dist}
|
||||
Release: 11%{?dist}
|
||||
Summary: Immediately show the Plasma unlock prompt without requiring input
|
||||
|
||||
License: MIT AND GPL-2.0-or-later
|
||||
@@ -11,10 +11,13 @@ Source2: test-plasma-always-show-unlock
|
||||
Source3: LockScreenUi.qml
|
||||
Source4: LICENSE
|
||||
Source5: README.md
|
||||
Source6: FedoraToolsFingerprintRetry.qml
|
||||
Source7: tst_fingerprintretry.qml
|
||||
|
||||
BuildArch: noarch
|
||||
BuildRequires: bash
|
||||
BuildRequires: patch
|
||||
BuildRequires: qt6-qtdeclarative-devel
|
||||
Requires: bash
|
||||
Requires: patch
|
||||
Requires: plasma-desktop >= 6.7.4
|
||||
@@ -31,6 +34,11 @@ patch after plasma-desktop updates and leaves unknown future layouts unchanged.
|
||||
|
||||
%check
|
||||
bash %{SOURCE2} %{SOURCE0} %{SOURCE1} %{SOURCE3}
|
||||
mkdir -p qml-tests/tests
|
||||
cp %{SOURCE6} qml-tests/
|
||||
cp %{SOURCE7} qml-tests/tests/
|
||||
QT_QPA_PLATFORM=offscreen QT_QUICK_BACKEND=software \
|
||||
qmltestrunner-qt6 -input qml-tests/tests
|
||||
|
||||
%install
|
||||
install -Dpm 0755 %{SOURCE0} \
|
||||
@@ -41,6 +49,15 @@ install -Dpm 0644 %{SOURCE4} \
|
||||
%{buildroot}%{_licensedir}/%{name}/LICENSE
|
||||
install -Dpm 0644 %{SOURCE5} \
|
||||
%{buildroot}%{_docdir}/%{name}/README.md
|
||||
install -Dpm 0644 %{SOURCE6} \
|
||||
%{buildroot}%{_datadir}/plasma/shells/org.kde.plasma.desktop/contents/lockscreen/FedoraToolsFingerprintRetry.qml
|
||||
|
||||
%pre
|
||||
# Revert using the installed helper and patch, before RPM replaces either.
|
||||
# The new patch must be applied to the original QML, not an older patched copy.
|
||||
if [ -x %{_libexecdir}/plasma-always-show-unlock ]; then
|
||||
%{_libexecdir}/plasma-always-show-unlock revert || exit 1
|
||||
fi
|
||||
|
||||
%preun
|
||||
if [ "$1" -eq 0 ]; then
|
||||
@@ -55,8 +72,14 @@ fi
|
||||
%doc %{_docdir}/%{name}/README.md
|
||||
%{_libexecdir}/plasma-always-show-unlock
|
||||
%{_datadir}/plasma-always-show-unlock/LockScreenUi.patch
|
||||
%{_datadir}/plasma/shells/org.kde.plasma.desktop/contents/lockscreen/FedoraToolsFingerprintRetry.qml
|
||||
|
||||
%changelog
|
||||
* Mon Sep 14 2026 fedora-tools contributors - 0.1.0-11
|
||||
- Retry idle fingerprint conversations without polling or restarting fprintd
|
||||
- Preserve failed-scan handling and test timeout recovery in QML
|
||||
- Revert the installed patch before upgrading to a new revision
|
||||
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-10
|
||||
- Clarify the package summary
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ run_tool status >/dev/null
|
||||
grep -q 'function onViewVisibleChanged()' "$target"
|
||||
grep -q 'id: resumeAuthenticationTimer' "$target"
|
||||
grep -q 'id: postGraceAuthenticationTimer' "$target"
|
||||
grep -q 'FedoraToolsFingerprintRetry {' "$target"
|
||||
grep -q 'fingerprintType: ScreenLocker.Authenticator.Fingerprint' "$target"
|
||||
grep -q 'interval: 1500' "$target"
|
||||
grep -q 'interval: 6000' "$target"
|
||||
grep -q 'running: root.viewVisible' "$target"
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import QtQml
|
||||
import QtTest
|
||||
import ".."
|
||||
|
||||
TestCase {
|
||||
id: testCase
|
||||
name: "FingerprintRetry"
|
||||
property alias authenticator: auth
|
||||
|
||||
QtObject {
|
||||
id: auth
|
||||
property int authenticatorTypes: 0
|
||||
property int starts: 0
|
||||
signal noninteractiveInfo(int kind, QtObject source)
|
||||
signal noninteractiveError(int kind, QtObject source)
|
||||
signal failed(int kind, QtObject source)
|
||||
signal succeeded()
|
||||
function startAuthenticating() { ++starts; }
|
||||
}
|
||||
|
||||
FedoraToolsFingerprintRetry {
|
||||
id: retry
|
||||
auth: testCase.authenticator
|
||||
fingerprintType: 1
|
||||
}
|
||||
|
||||
function initTestCase() {
|
||||
compare(retry.interval, 6000);
|
||||
compare(retry.repeat, false);
|
||||
}
|
||||
|
||||
function init() {
|
||||
retry.enabled = false;
|
||||
auth.authenticatorTypes = 0;
|
||||
auth.starts = 0;
|
||||
retry.interval = 20;
|
||||
retry.enabled = true;
|
||||
}
|
||||
|
||||
function beginFingerprint() {
|
||||
auth.authenticatorTypes = 1;
|
||||
auth.noninteractiveInfo(1, auth);
|
||||
}
|
||||
|
||||
function test_idleTimeoutRetriesWithoutInput() {
|
||||
beginFingerprint();
|
||||
wait(50);
|
||||
compare(auth.starts, 0); // Never restart a working conversation.
|
||||
auth.authenticatorTypes = 0;
|
||||
verify(retry.running);
|
||||
tryCompare(auth, "starts", 1);
|
||||
wait(60);
|
||||
compare(auth.starts, 1); // No polling while the reader stays unavailable.
|
||||
|
||||
beginFingerprint();
|
||||
auth.authenticatorTypes = 0;
|
||||
tryCompare(auth, "starts", 2); // Later idle timeouts can also recover.
|
||||
}
|
||||
|
||||
function test_noReaderOrNoInstructions() {
|
||||
wait(60);
|
||||
compare(auth.starts, 0);
|
||||
auth.authenticatorTypes = 1;
|
||||
auth.authenticatorTypes = 0;
|
||||
wait(60);
|
||||
compare(auth.starts, 0);
|
||||
}
|
||||
|
||||
function test_failedScansDoNotGetAutomaticNewAttempts() {
|
||||
beginFingerprint();
|
||||
auth.noninteractiveError(1, auth);
|
||||
auth.noninteractiveInfo(1, auth); // Another instruction or timeout message.
|
||||
auth.failed(1, auth);
|
||||
auth.authenticatorTypes = 0;
|
||||
wait(60);
|
||||
compare(auth.starts, 0);
|
||||
}
|
||||
|
||||
function test_unavailableRetryDoesNotLoop() {
|
||||
beginFingerprint();
|
||||
auth.authenticatorTypes = 0;
|
||||
tryCompare(auth, "starts", 1);
|
||||
auth.authenticatorTypes = 1; // PAM marks active before opening the reader.
|
||||
auth.failed(1, auth);
|
||||
auth.authenticatorTypes = 0;
|
||||
wait(60);
|
||||
compare(auth.starts, 1);
|
||||
}
|
||||
|
||||
function test_smartcardDoesNotArmOrCancelFingerprintRetry() {
|
||||
auth.authenticatorTypes = 2;
|
||||
auth.noninteractiveInfo(2, auth);
|
||||
auth.authenticatorTypes = 0;
|
||||
wait(60);
|
||||
compare(auth.starts, 0);
|
||||
|
||||
beginFingerprint();
|
||||
auth.noninteractiveError(2, auth);
|
||||
auth.authenticatorTypes = 2;
|
||||
tryCompare(auth, "starts", 1);
|
||||
}
|
||||
|
||||
function test_cancelPendingRetry_data() {
|
||||
return [ {tag: "manual restart"}, {tag: "success"},
|
||||
{tag: "password failure"}, {tag: "hidden or suspended"} ];
|
||||
}
|
||||
|
||||
function test_cancelPendingRetry(data) {
|
||||
beginFingerprint();
|
||||
auth.authenticatorTypes = 0;
|
||||
verify(retry.running);
|
||||
switch (data.tag) {
|
||||
case "manual restart": auth.authenticatorTypes = 1; break;
|
||||
case "success": auth.succeeded(); break;
|
||||
case "password failure": auth.failed(0, auth); break;
|
||||
case "hidden or suspended": retry.enabled = false; break;
|
||||
}
|
||||
wait(60);
|
||||
compare(auth.starts, 0);
|
||||
}
|
||||
|
||||
function test_passwordFailureBeforeAvailabilityChange() {
|
||||
beginFingerprint();
|
||||
auth.failed(0, auth);
|
||||
auth.authenticatorTypes = 0;
|
||||
wait(60);
|
||||
compare(auth.starts, 0);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ endif()
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Test)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Qml Test)
|
||||
find_package(ECM REQUIRED NO_MODULE)
|
||||
list(APPEND CMAKE_MODULE_PATH "${ECM_MODULE_PATH}")
|
||||
include(ECMQtDeclareLoggingCategory)
|
||||
@@ -25,8 +25,9 @@ ecm_qt_declare_logging_category(sources
|
||||
add_executable(retrytest ${sources})
|
||||
target_include_directories(retrytest PRIVATE
|
||||
${CMAKE_CURRENT_BINARY_DIR} ${KSCREENLOCKER_SOURCE}/greeter)
|
||||
target_compile_definitions(retrytest PRIVATE HAVE_PAM_FAIL_DELAY)
|
||||
target_link_libraries(retrytest PRIVATE Qt6::Core Qt6::Test)
|
||||
target_compile_definitions(retrytest PRIVATE HAVE_PAM_FAIL_DELAY
|
||||
RETRY_QML="${CMAKE_CURRENT_SOURCE_DIR}/../../../plasma-always-show-unlock/FedoraToolsFingerprintRetry.qml")
|
||||
target_link_libraries(retrytest PRIVATE Qt6::Core Qt6::Qml Qt6::Test)
|
||||
enable_testing()
|
||||
add_test(NAME fingerprint-retry COMMAND retrytest)
|
||||
set_tests_properties(fingerprint-retry PROPERTIES TIMEOUT 30)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
#include <QQmlComponent>
|
||||
#include <QQmlEngine>
|
||||
#include <security/pam_appl.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -38,9 +40,21 @@ int pam_authenticate(pam_handle_t *handle, int)
|
||||
if (handle->service == "transient" && handle->attempts == 1) {
|
||||
return PAM_AUTHINFO_UNAVAIL;
|
||||
}
|
||||
if (handle->service == "timeout" && handle->attempts == 1) {
|
||||
if (handle->service == "attempt-limit" && handle->attempts == 1) {
|
||||
return PAM_MAXTRIES;
|
||||
}
|
||||
if (handle->service == "idle" && handle->attempts == 1) {
|
||||
pam_message message{PAM_TEXT_INFO, "Test fingerprint instruction"};
|
||||
const pam_message *messages = &message;
|
||||
pam_response *response = nullptr;
|
||||
const auto &conversation = handle->conversation;
|
||||
const int result = conversation.conv(1, &messages, &response, conversation.appdata_ptr);
|
||||
if (response) {
|
||||
std::free(response->resp);
|
||||
std::free(response);
|
||||
}
|
||||
return result == PAM_SUCCESS ? PAM_AUTHINFO_UNAVAIL : result;
|
||||
}
|
||||
pam_message message{PAM_PROMPT_ECHO_OFF, "Test credential:"};
|
||||
const pam_message *messages = &message;
|
||||
pam_response *response = nullptr;
|
||||
@@ -61,11 +75,60 @@ class RetryTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private Q_SLOTS:
|
||||
void idleFingerprintRetriesThroughQml_data()
|
||||
{
|
||||
QTest::addColumn<bool>("automaticRetry");
|
||||
QTest::newRow("automatic retry") << true;
|
||||
QTest::newRow("without the new retry") << false;
|
||||
}
|
||||
|
||||
void idleFingerprintRetriesThroughQml()
|
||||
{
|
||||
QFETCH(bool, automaticRetry);
|
||||
qmlRegisterUncreatableType<PamAuthenticator>("org.kde.kscreenlocker", 1, 0, "Authenticator", "Test instance only");
|
||||
auto password = std::make_unique<PamAuthenticator>(QStringLiteral("password"), QStringLiteral("test"));
|
||||
auto fingerprint = std::make_unique<PamAuthenticator>(QStringLiteral("idle"), QStringLiteral("test"), PamAuthenticator::Fingerprint);
|
||||
QSignalSpy passwordPrompts(password.get(), &PamAuthenticator::promptForSecret);
|
||||
QSignalSpy fingerprintPrompts(fingerprint.get(), &PamAuthenticator::promptForSecret);
|
||||
std::vector<std::unique_ptr<PamAuthenticator>> others;
|
||||
others.push_back(std::move(fingerprint));
|
||||
PamAuthenticators authenticators(std::move(password), std::move(others));
|
||||
QSignalSpy successes(&authenticators, &PamAuthenticators::succeeded);
|
||||
|
||||
QQmlEngine engine;
|
||||
QQmlComponent component(&engine, QUrl::fromLocalFile(QStringLiteral(RETRY_QML)));
|
||||
QVERIFY2(component.isReady(), qPrintable(component.errorString()));
|
||||
std::unique_ptr<QObject> retry(component.createWithInitialProperties({
|
||||
{QStringLiteral("auth"), QVariant::fromValue(&authenticators)},
|
||||
{QStringLiteral("fingerprintType"), int(PamAuthenticator::Fingerprint)},
|
||||
{QStringLiteral("interval"), 50},
|
||||
{QStringLiteral("enabled"), automaticRetry},
|
||||
}));
|
||||
QVERIFY2(retry, qPrintable(component.errorString()));
|
||||
|
||||
authenticators.startAuthenticating();
|
||||
QTRY_COMPARE(passwordPrompts.count(), 1);
|
||||
if (!automaticRetry) {
|
||||
QTest::qWait(200);
|
||||
QCOMPARE(fingerprintPrompts.count(), 0);
|
||||
// Reproduce today's recovery via Ctrl without any daemon restart.
|
||||
authenticators.startAuthenticating();
|
||||
}
|
||||
// With the helper enabled, no simulated input or manual start is needed.
|
||||
QTRY_COMPARE_WITH_TIMEOUT(fingerprintPrompts.count(), 1, 2000);
|
||||
QCOMPARE(passwordPrompts.count(), 1);
|
||||
QVERIFY(!authenticators.isUnlocked());
|
||||
QCOMPARE(successes.count(), 0);
|
||||
authenticators.respond("correct");
|
||||
QTRY_COMPARE(successes.count(), 1);
|
||||
QVERIFY(authenticators.isUnlocked());
|
||||
}
|
||||
|
||||
void retryWithActivePassword_data()
|
||||
{
|
||||
QTest::addColumn<QString>("service");
|
||||
QTest::newRow("temporary unavailability") << QStringLiteral("transient");
|
||||
QTest::newRow("attempt timeout") << QStringLiteral("timeout");
|
||||
QTest::newRow("exhausted attempts, manual retry") << QStringLiteral("attempt-limit");
|
||||
}
|
||||
|
||||
void retryWithActivePassword()
|
||||
|
||||
@@ -21,6 +21,8 @@ install -m 0644 \
|
||||
"$topdir/SOURCES/LockScreenUi.qml"
|
||||
install -m 0644 "$repo_root/LICENSE" "$topdir/SOURCES/LICENSE"
|
||||
install -m 0644 "$repo_root/README.md" "$topdir/SOURCES/README.md"
|
||||
install -m 0644 "$tool_dir/FedoraToolsFingerprintRetry.qml" "$topdir/SOURCES/FedoraToolsFingerprintRetry.qml"
|
||||
install -m 0644 "$tool_dir/tests/tst_fingerprintretry.qml" "$topdir/SOURCES/tst_fingerprintretry.qml"
|
||||
|
||||
rpmbuild \
|
||||
--define "_topdir $topdir" \
|
||||
|
||||
@@ -18,6 +18,8 @@ bash touchpad-hold-tap/tests/test-touchpad-hold-tap-config "$plugin" touchpad-ho
|
||||
bash plasma-always-show-unlock/tests/test-plasma-always-show-unlock \
|
||||
plasma-always-show-unlock/plasma-always-show-unlock \
|
||||
plasma-always-show-unlock/LockScreenUi.patch plasma-always-show-unlock/tests/LockScreenUi.qml
|
||||
QT_QPA_PLATFORM=offscreen QT_QUICK_BACKEND=software \
|
||||
qmltestrunner-qt6 -input plasma-always-show-unlock/tests
|
||||
bash plasma-fingerprint-workaround/tests/test-controller \
|
||||
plasma-fingerprint-workaround/plasma-fingerprint-workaround plasma-fingerprint-workaround/payload.conf
|
||||
|
||||
|
||||
Reference in New Issue
Block a user