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
|
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
|
it after `plasma-desktop` updates. An unknown future layout is left unchanged
|
||||||
instead of being modified speculatively. Removing the package restores Plasma's
|
instead of being modified speculatively. Removing the package restores Plasma's
|
||||||
original behaviour. It retries lock-screen authentication after resume and the
|
original behaviour. It retries lock-screen authentication after resume, the
|
||||||
automatic-lock grace period, but does not modify `kscreenlocker` authentication
|
automatic-lock grace period and idle fingerprint timeouts. Failed scans do not
|
||||||
code or `fprintd`.
|
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
|
### 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.
|
earlier operations may already have applied; remaining edits stay pending.
|
||||||
CPU slider bounds are read from Linux's hardware-frequency limits.
|
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.
|
The battery heading shows full capacity, health and cycle count when available.
|
||||||
Capacity calculated from charge capacity and nominal voltage is approximate.
|
Capacity calculated from charge capacity and nominal voltage is approximate.
|
||||||
Temperature labels distinguish CPU-area/board sensors from die readings;
|
Temperature labels distinguish CPU-area/board sensors from die readings;
|
||||||
raw sysfs paths remain available in tooltips. CPU frequency is Linux's reported
|
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.
|
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,
|
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
|
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.
|
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.
|
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
|
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
|
an application icon, a history graph, or a number. Number mode offers an optional
|
||||||
window. New icons default to the application icon; your existing configuration
|
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;
|
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
|
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
|
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
|
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
|
target; fan RPM and duty percentage; and the main CPU, memory, NVMe, battery and
|
||||||
board temperatures. Multiple temperatures have a heading and indented lines.
|
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
|
Missing sensors are unavailable in the settings page. Batteries reporting only
|
||||||
charge are converted to mWh using nominal voltage.
|
charge are converted to mWh using nominal voltage.
|
||||||
Choose zero to three top CPU applications, displayed below CPU usage in descending
|
Choose zero to three top CPU applications, displayed below CPU usage in descending
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Name: framework-laptop-tools
|
Name: framework-laptop-tools
|
||||||
Version: 0.1.0
|
Version: 0.1.0
|
||||||
Release: 25%{?dist}
|
Release: 28%{?dist}
|
||||||
Summary: Hardware controls and monitoring for Framework Laptop 13 Pro
|
Summary: Hardware controls and monitoring for Framework Laptop 13 Pro
|
||||||
License: MIT
|
License: MIT
|
||||||
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
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-fan-resume.service
|
||||||
%{_unitdir}/framework-laptop-tools-cpu.service
|
%{_unitdir}/framework-laptop-tools-cpu.service
|
||||||
%changelog
|
%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
|
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-25
|
||||||
- Add multiple tray icons, opacity controls and component power monitoring
|
- Add multiple tray icons, opacity controls and component power monitoring
|
||||||
- Refine graph columns, live hover readings and sampling-interval changes
|
- Refine graph columns, live hover readings and sampling-interval changes
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
#include "chart.h"
|
#include "chart.h"
|
||||||
#include "graphdata.h"
|
#include "graphdata.h"
|
||||||
|
#include "readingformat.h"
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QToolTip>
|
#include <QToolTip>
|
||||||
@@ -230,7 +231,8 @@ QString Chart::readingAt(qint64 time) const
|
|||||||
if (next != points.cbegin()) interval = std::max(interval, intervalAt((next - 1)->x()));
|
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
|
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)));
|
&& 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');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,26 @@ QVector<Sensor> temperatureSensors(const QString &sys)
|
|||||||
QVector<Sensor> frequencySensors(const QString &sys)
|
QVector<Sensor> frequencySensors(const QString &sys)
|
||||||
{
|
{
|
||||||
QVector<Sensor> sensors;
|
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");
|
const QDir drm(sys + "/class/drm");
|
||||||
for (const auto &card : drm.entryList({"card*"}, QDir::Dirs | QDir::NoDotAndDotDot)) {
|
for (const auto &card : drm.entryList({"card*"}, QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||||
QDirIterator files(drm.filePath(card + "/device"), {"act_freq"}, QDir::Files,
|
QDirIterator files(drm.filePath(card + "/device"), {"act_freq"}, QDir::Files,
|
||||||
@@ -80,22 +99,27 @@ QVector<Sensor> frequencySensors(const QString &sys)
|
|||||||
while (files.hasNext()) {
|
while (files.hasNext()) {
|
||||||
const QString path = files.next();
|
const QString path = files.next();
|
||||||
const QString relative = path.mid(drm.filePath(card + "/device/").size());
|
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;
|
return sensors;
|
||||||
}
|
}
|
||||||
std::optional<double> sensorValue(const Sensor &sensor)
|
std::optional<double> sensorValue(const Sensor &sensor)
|
||||||
{
|
{
|
||||||
|
if (sensor.id == "cpu" || !sensor.frequencyPaths.isEmpty()) {
|
||||||
|
QStringList paths = sensor.frequencyPaths;
|
||||||
if (sensor.id == "cpu") {
|
if (sensor.id == "cpu") {
|
||||||
const QDir dir(sensor.path);
|
const QDir dir(sensor.path);
|
||||||
double total = 0;
|
for (const auto &policy : dir.entryList({"policy*"}, QDir::Dirs | QDir::NoDotAndDotDot))
|
||||||
int count = 0;
|
paths.append(dir.filePath(policy + "/scaling_cur_freq"));
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
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") {
|
if (sensor.unit == "°C") {
|
||||||
const QString fault = sensor.path.chopped(6) + "_fault";
|
const QString fault = sensor.path.chopped(6) + "_fault";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
#include <QVariantMap>
|
#include <QVariantMap>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
@@ -9,6 +10,8 @@ struct Sensor {
|
|||||||
QString id, name, path, unit;
|
QString id, name, path, unit;
|
||||||
double scale = 1;
|
double scale = 1;
|
||||||
bool primary = false;
|
bool primary = false;
|
||||||
|
QStringList frequencyPaths;
|
||||||
|
bool maximum = false;
|
||||||
};
|
};
|
||||||
QString readText(const QString &path);
|
QString readText(const QString &path);
|
||||||
std::optional<double> readNumber(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
|
// SPDX-License-Identifier: MIT
|
||||||
#include "tooltip.h"
|
#include "tooltip.h"
|
||||||
|
#include "readingformat.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@@ -15,9 +16,9 @@ QVector<TooltipSensor> tooltipSensors(const QVector<Sensor> &sensors)
|
|||||||
}
|
}
|
||||||
return result;
|
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)
|
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";
|
text += " · " + energy("remainingMWh") + " mWh / " + energy("fullMWh") + " mWh";
|
||||||
const auto watts = batteryRate(battery);
|
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";
|
const bool charging = battery["state"] == "Charging", discharging = battery["state"] == "Discharging";
|
||||||
double seconds = 0;
|
double seconds = 0;
|
||||||
bool approximate = false;
|
bool approximate = false;
|
||||||
@@ -60,7 +61,8 @@ QString batteryTooltip(const QVariantMap &battery, int limit, const QVariantMap
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
QString trayTooltip(const QVariantMap &settings, const QMap<QString, double> &values,
|
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 reading = [&](const QString &id, int decimals) {
|
||||||
const auto it = values.constFind(id);
|
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, "battery")) lines << battery;
|
||||||
if (tooltipEnabled(settings, "fan")) lines << "Fan speed: " + reading("fan", 0) + " RPM (" + reading("fan-duty", 0) + " %)";
|
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;
|
QStringList temps;
|
||||||
for (const auto &sensor : temperatures)
|
for (const auto &sensor : temperatures)
|
||||||
if (!sensor.id.isEmpty() && tooltipEnabled(settings, "temperature/" + sensor.key))
|
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:";
|
if (temps.size() > 1) lines << "Temperatures:";
|
||||||
for (const auto &temp : temps) lines << (temps.size() > 1 ? "\u2003" : "") + temp;
|
for (const auto &temp : temps) lines << (temps.size() > 1 ? "\u2003" : "") + temp;
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
|
|
||||||
struct TooltipSensor { QString key, name, id; };
|
struct TooltipSensor { QString key, name, id; };
|
||||||
QVector<TooltipSensor> tooltipSensors(const QVector<Sensor> &sensors);
|
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);
|
int tooltipAppCount(const QVariantMap &settings);
|
||||||
QString batteryTooltip(const QVariantMap &battery, int limit, const QVariantMap &upower);
|
QString batteryTooltip(const QVariantMap &battery, int limit, const QVariantMap &upower);
|
||||||
QString trayTooltip(const QVariantMap &settings, const QMap<QString, double> &values,
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
#include "tray.h"
|
#include "tray.h"
|
||||||
#include "graphdata.h"
|
#include "graphdata.h"
|
||||||
|
#include "readingformat.h"
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -30,7 +31,8 @@ QRectF trayPlotRect(bool border)
|
|||||||
// Two-pixel border; reserve space for the graph's vertical padding inside it.
|
// 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);
|
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);
|
QPixmap pixmap(64, 64); pixmap.fill(style.backgroundColor);
|
||||||
QPainter p(&pixmap); p.setRenderHint(QPainter::Antialiasing);
|
QPainter p(&pixmap); p.setRenderHint(QPainter::Antialiasing);
|
||||||
@@ -67,11 +69,28 @@ QIcon telemetryIcon(bool graph, const QVector<QPointF> &values, const QString &u
|
|||||||
}
|
}
|
||||||
p.restore();
|
p.restore();
|
||||||
} else {
|
} else {
|
||||||
const QString value = !std::isfinite(latest) ? "—" : unit == "MHz"
|
const bool stacked = !graph && second.has_value();
|
||||||
? QString::number(latest / 1000, 'f', 1) : QString::number(std::round(latest), 'f', 0);
|
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);
|
QFont font; font.setBold(true); font.setPixelSize(42);
|
||||||
while (QFontMetrics(font).horizontalAdvance(value) > area.width() && font.pixelSize() > 12) font.setPixelSize(font.pixelSize() - 1);
|
while (font.pixelSize() > 12 && (QFontMetrics(font).horizontalAdvance(value) > rect.width()
|
||||||
p.setFont(font); p.drawText(area, Qt::AlignCenter, value);
|
|| (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);
|
return QIcon(pixmap);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,5 +23,9 @@ struct TrayStyle {
|
|||||||
qint64 historyMs = 60000;
|
qint64 historyMs = 60000;
|
||||||
};
|
};
|
||||||
QRectF trayPlotRect(bool border);
|
QRectF trayPlotRect(bool border);
|
||||||
|
struct TrayReading {
|
||||||
|
double value;
|
||||||
|
QString unit;
|
||||||
|
};
|
||||||
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());
|
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);
|
auto *graphForm = new QFormLayout(graphOptions); graphForm->setContentsMargins(24, 0, 0, 0);
|
||||||
graphForm->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
|
graphForm->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
|
||||||
form->addRow(graphOptions); form->addRow("Reading:", m_metric);
|
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);
|
auto *appearance = new QWidget; auto *colors = new QFormLayout(appearance); colors->setContentsMargins(0, 0, 0, 0);
|
||||||
colors->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
|
colors->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
|
||||||
auto colorRow = [&](QFormLayout *target, const QString &label, const QString &key, const QColor &fallback, bool alpha = false) {
|
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();
|
m_values.insert("tray/outside", m_outside->currentData()); syncOutside(); if (!m_loading) Q_EMIT settingsChanged();
|
||||||
});
|
});
|
||||||
syncOutside(); layout->addWidget(m_graphSettings);
|
syncOutside(); layout->addWidget(m_graphSettings);
|
||||||
auto syncMode = [this, appearance, graphOptions] {
|
auto syncMode = [this, form, appearance, graphOptions] {
|
||||||
const bool telemetry = mode() != "icon";
|
const bool telemetry = mode() != "icon";
|
||||||
m_metric->setEnabled(telemetry); appearance->setVisible(telemetry); m_graphSettings->setVisible(mode() == "graph");
|
m_metric->setEnabled(telemetry); appearance->setVisible(telemetry); m_graphSettings->setVisible(mode() == "graph");
|
||||||
graphOptions->setVisible(mode() == "graph");
|
graphOptions->setVisible(mode() == "graph");
|
||||||
|
form->setRowVisible(m_secondMetric, mode() == "number");
|
||||||
};
|
};
|
||||||
connect(m_mode, &QComboBox::currentIndexChanged, this, [this, syncMode] {
|
connect(m_mode, &QComboBox::currentIndexChanged, this, [this, syncMode] {
|
||||||
m_values.insert("tray/mode", m_mode->currentData()); syncMode(); if (!m_loading) Q_EMIT settingsChanged();
|
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] {
|
connect(m_metric, &QComboBox::currentIndexChanged, this, [this] {
|
||||||
m_values.insert("tray/metric", m_metric->currentData()); loadScale(); if (!m_loading) Q_EMIT settingsChanged();
|
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) {
|
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();
|
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();
|
loadScale(); syncMode();
|
||||||
m_values.insert("tray/mode", m_mode->currentData());
|
m_values.insert("tray/mode", m_mode->currentData());
|
||||||
m_values.insert("tray/metric", m_metric->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_values.insert("tray/outside", m_outside->currentData());
|
||||||
m_reload.append([this, syncMode, syncOutside] {
|
m_reload.append([this, syncMode, syncOutside] {
|
||||||
m_mode->setCurrentIndex(std::max(0, m_mode->findData(m_values.value("tray/mode", "icon"))));
|
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_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"))));
|
m_outside->setCurrentIndex(std::max(0, m_outside->findData(m_values.value("tray/outside", "clamp"))));
|
||||||
syncMode(); syncOutside();
|
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);
|
appOptions->setEnabled(cpu->isChecked()); connect(cpu, &QCheckBox::toggled, appOptions, &QWidget::setEnabled);
|
||||||
check(hoverForm, "Battery", "hover/battery", true);
|
check(hoverForm, "Battery", "hover/battery", true);
|
||||||
check(hoverForm, "Fan", "hover/fan", 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:"));
|
hoverForm->addRow(new QLabel("Temperature:"));
|
||||||
auto *temperatures = new QWidget; auto *tempForm = new QFormLayout(temperatures); tempForm->setContentsMargins(24, 0, 0, 0);
|
auto *temperatures = new QWidget; auto *tempForm = new QFormLayout(temperatures); tempForm->setContentsMargins(24, 0, 0, 0);
|
||||||
for (const auto &sensor : tooltipSensors(sensors)) {
|
for (const auto &sensor : tooltipSensors(sensors)) {
|
||||||
@@ -205,7 +225,8 @@ void TrayPage::loadScale()
|
|||||||
const bool frequency = unit == "MHz", temperature = unit == "°C", rate = unit == "W";
|
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);
|
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); }
|
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();
|
const double low = m_values.value(scaleKey("minimum"), rate && !metric().id.startsWith("power/") ? -75 : 0).toDouble();
|
||||||
maximum = m_values.value(scaleKey("maximum"), maximum).toDouble();
|
maximum = m_values.value(scaleKey("maximum"), maximum).toDouble();
|
||||||
m_minimum->setValue(low); m_maximum->setMinimum(m_minimum->value() + 1); m_maximum->setValue(maximum);
|
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;
|
QVector<TrayConfiguration> result;
|
||||||
for (int i = 0; i < m_count->value(); ++i) {
|
for (int i = 0; i < m_count->value(); ++i) {
|
||||||
const auto *page = m_pages[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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ struct TrayConfiguration {
|
|||||||
Sensor metric;
|
Sensor metric;
|
||||||
TrayStyle style;
|
TrayStyle style;
|
||||||
QVariantMap values;
|
QVariantMap values;
|
||||||
|
std::optional<Sensor> secondMetric;
|
||||||
};
|
};
|
||||||
|
|
||||||
class TrayPage : public QWidget {
|
class TrayPage : public QWidget {
|
||||||
@@ -28,6 +29,10 @@ public:
|
|||||||
TrayPage(const QVariantMap &values, const QVector<Sensor> &sensors, QWidget *parent = nullptr);
|
TrayPage(const QVariantMap &values, const QVector<Sensor> &sensors, QWidget *parent = nullptr);
|
||||||
QString mode() const { return m_mode->currentData().toString(); }
|
QString mode() const { return m_mode->currentData().toString(); }
|
||||||
Sensor metric() const { return m_sensors[m_metric->currentIndex()]; }
|
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;
|
TrayStyle iconStyle() const;
|
||||||
QVariantMap draft() const { return m_values; }
|
QVariantMap draft() const { return m_values; }
|
||||||
void load(const QVariantMap &values);
|
void load(const QVariantMap &values);
|
||||||
@@ -40,7 +45,7 @@ private:
|
|||||||
QList<std::function<void()>> m_reload;
|
QList<std::function<void()>> m_reload;
|
||||||
bool m_loading = false;
|
bool m_loading = false;
|
||||||
QVector<Sensor> m_sensors;
|
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;
|
QDoubleSpinBox *m_minimum, *m_maximum;
|
||||||
QFormLayout *m_scaleLayout;
|
QFormLayout *m_scaleLayout;
|
||||||
QWidget *m_graphSettings;
|
QWidget *m_graphSettings;
|
||||||
|
|||||||
@@ -119,7 +119,8 @@ QGroupBox *Window::sensorGroup(const QString &title, Chart *chart, const QVector
|
|||||||
QVector<Sensor> main, extra;
|
QVector<Sensor> main, extra;
|
||||||
for (const auto &sensor : sensors) {
|
for (const auto &sensor : sensors) {
|
||||||
chart->addSeries(sensor.id, sensor.name, sensor.unit);
|
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);
|
(temperatures && !sensor.primary ? extra : main).append(sensor);
|
||||||
}
|
}
|
||||||
auto makeLegend = [&](const QVector<Sensor> &entries) {
|
auto makeLegend = [&](const QVector<Sensor> &entries) {
|
||||||
@@ -310,15 +311,24 @@ void Window::updateTray()
|
|||||||
if (config.mode == "icon") tray.icon->setIcon(windowIcon());
|
if (config.mode == "icon") tray.icon->setIcon(windowIcon());
|
||||||
else {
|
else {
|
||||||
const double since = now - (config.mode == "graph" ? config.style.historyMs : 0);
|
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;
|
QVector<QPointF> history;
|
||||||
if (metric.id == "cpu-usage")
|
|
||||||
for (auto it = historyStart(m_usageHistory, since); it != m_usageHistory.cend(); ++it) history.append(*it);
|
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);
|
return history;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
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
|
QVariantMap Window::controlValue(const QString &key) const
|
||||||
@@ -531,7 +541,8 @@ void Window::sample()
|
|||||||
sampleSensors(m_frequencies, m_frequencyChart); sampleSensors(m_temperatures, m_temperatureChart);
|
sampleSensors(m_frequencies, m_frequencyChart); sampleSensors(m_temperatures, m_temperatureChart);
|
||||||
const auto power = m_powerMonitor.sample(m_fastTimer.interval());
|
const auto power = m_powerMonitor.sample(m_fastTimer.interval());
|
||||||
m_powerChart->sample(power, 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 QString ec = ecHwmon();
|
||||||
const auto rpm = readNumber(ec + "/fan1_input");
|
const auto rpm = readNumber(ec + "/fan1_input");
|
||||||
const auto mode = readNumber(ec + "/pwm1_enable");
|
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());
|
QFile file(path); QVERIFY(file.open(QIODevice::WriteOnly)); QCOMPARE(file.write(value), value.size());
|
||||||
}
|
}
|
||||||
private Q_SLOTS:
|
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() {
|
void energyCounters() {
|
||||||
EnergyCounter counter;
|
EnergyCounter counter;
|
||||||
const quint64 range = 262143328850ULL;
|
const quint64 range = 262143328850ULL;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
#include "graphdata.h"
|
#include "graphdata.h"
|
||||||
#include "tooltip.h"
|
#include "tooltip.h"
|
||||||
|
#include "readingformat.h"
|
||||||
#include "processusage.h"
|
#include "processusage.h"
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
@@ -33,6 +34,112 @@
|
|||||||
class WindowTest : public QObject {
|
class WindowTest : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private Q_SLOTS:
|
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() {
|
void hoverFormatting() {
|
||||||
const QVector<Sensor> sensors{{"cros_ec/peci-temp", "CPU", {}, "°C"}, {"spd5118/temp1", "Memory", {}, "°C"},
|
const QVector<Sensor> sensors{{"cros_ec/peci-temp", "CPU", {}, "°C"}, {"spd5118/temp1", "Memory", {}, "°C"},
|
||||||
{"nvme/Composite", "NVMe", {}, "°C"}};
|
{"nvme/Composite", "NVMe", {}, "°C"}};
|
||||||
@@ -52,14 +159,15 @@ private Q_SLOTS:
|
|||||||
settings["tray/hover/temperature/memory"] = false; settings["tray/hover/temperature/nvme"] = false;
|
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, readings, temperatures, {}, apps), QString("CPU 47°C"));
|
||||||
QCOMPARE(trayTooltip(settings, {}, temperatures, {}, {}), QString("CPU —°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;
|
settings["tray/hover/temperature/cpu"] = false;
|
||||||
QVERIFY(trayTooltip(settings, readings, temperatures, {}, {}).isEmpty());
|
QVERIFY(trayTooltip(settings, readings, temperatures, {}, {}).isEmpty());
|
||||||
QVariantMap battery{{"state", "Discharging"}, {"capacity", 75}, {"watts", 12.5}, {"remainingMWh", 56250}, {"fullMWh", 75000}};
|
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";
|
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;
|
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;
|
battery["current_now"] = 0;
|
||||||
QVERIFY(batteryTooltip(battery, 80, {}).contains("Time remaining unavailable"));
|
QVERIFY(batteryTooltip(battery, 80, {}).contains("Time remaining unavailable"));
|
||||||
battery["capacity"] = 80;
|
battery["capacity"] = 80;
|
||||||
@@ -296,6 +404,7 @@ private Q_SLOTS:
|
|||||||
first->findChild<QComboBox *>("trayHistory")->setCurrentIndex(0);
|
first->findChild<QComboBox *>("trayHistory")->setCurrentIndex(0);
|
||||||
first->findChild<QCheckBox *>("border")->setChecked(false);
|
first->findChild<QCheckBox *>("border")->setChecked(false);
|
||||||
last->findChild<QComboBox *>("trayDisplayMode")->setCurrentIndex(2);
|
last->findChild<QComboBox *>("trayDisplayMode")->setCurrentIndex(2);
|
||||||
|
last->findChild<QComboBox *>("traySecondMetric")->setCurrentIndex(1);
|
||||||
last->findChild<QSpinBox *>("hover/topApps")->setValue(3);
|
last->findChild<QSpinBox *>("hover/topApps")->setValue(3);
|
||||||
const auto firstValues = first->draft(), lastValues = last->draft();
|
const auto firstValues = first->draft(), lastValues = last->draft();
|
||||||
const auto original = page.draft();
|
const auto original = page.draft();
|
||||||
@@ -304,6 +413,8 @@ private Q_SLOTS:
|
|||||||
QSignalSpy changed(&page, &TrayIconsPage::settingsChanged);
|
QSignalSpy changed(&page, &TrayIconsPage::settingsChanged);
|
||||||
cards[2]->findChild<QToolButton *>("moveIconLeft")->click();
|
cards[2]->findChild<QToolButton *>("moveIconLeft")->click();
|
||||||
QCOMPARE(page.configurations()[1].values, lastValues); QCOMPARE(changed.count(), 1);
|
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();
|
cards[0]->findChild<QToolButton *>("moveIconRight")->click();
|
||||||
QCOMPARE(page.configurations()[0].values, lastValues);
|
QCOMPARE(page.configurations()[0].values, lastValues);
|
||||||
QCOMPARE(page.configurations()[1].values, firstValues);
|
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);
|
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.
|
QCOMPARE(chart.history("hidden").size(), 7201); // Not capped at 600, even when hidden.
|
||||||
chart.setSelected("hidden", true);
|
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);
|
const auto end = double(start + 3600000);
|
||||||
chart.setHistoryWindow(historyRetentionMs, true);
|
chart.setHistoryWindow(historyRetentionMs, true);
|
||||||
QCOMPARE(chart.timeRange(), qMakePair(double(start), end));
|
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: {
|
onUiVisibleChanged: {
|
||||||
if (uiVisible) {
|
if (uiVisible) {
|
||||||
Window.window.requestActivate();
|
Window.window.requestActivate();
|
||||||
@@ -135,6 +143,18 @@
|
@@ -135,6 +143,24 @@
|
||||||
}
|
}
|
||||||
authenticator.startAuthenticating();
|
authenticator.startAuthenticating();
|
||||||
}
|
}
|
||||||
@@ -72,11 +72,17 @@
|
|||||||
+ interval: 6000
|
+ interval: 6000
|
||||||
+ running: root.viewVisible
|
+ running: root.viewVisible
|
||||||
+ onTriggered: authenticator.startAuthenticating()
|
+ onTriggered: authenticator.startAuthenticating()
|
||||||
|
+ }
|
||||||
|
+ FedoraToolsFingerprintRetry {
|
||||||
|
+ auth: authenticator
|
||||||
|
+ fingerprintType: ScreenLocker.Authenticator.Fingerprint
|
||||||
|
+ enabled: root.viewVisible && !authenticator.unlocked
|
||||||
|
+ && !graceLockTimer.running && !resumeAuthenticationTimer.running
|
||||||
+ }
|
+ }
|
||||||
onBlockUIChanged: {
|
onBlockUIChanged: {
|
||||||
if (blockUI) {
|
if (blockUI) {
|
||||||
fadeoutTimer.running = false;
|
fadeoutTimer.running = false;
|
||||||
@@ -194,6 +214,6 @@
|
@@ -194,6 +220,6 @@
|
||||||
WallpaperFader {
|
WallpaperFader {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
- state: lockScreenRoot.uiVisible ? "on" : "off"
|
- state: lockScreenRoot.uiVisible ? "on" : "off"
|
||||||
@@ -84,7 +90,7 @@
|
|||||||
source: wallpaper
|
source: wallpaper
|
||||||
mainStack: mainStack
|
mainStack: mainStack
|
||||||
footer: footer
|
footer: footer
|
||||||
@@ -211,7 +231,7 @@
|
@@ -211,7 +237,7 @@
|
||||||
samples: 15
|
samples: 15
|
||||||
spread: 0.2
|
spread: 0.2
|
||||||
color : Qt.rgba(0, 0, 0, 0.7)
|
color : Qt.rgba(0, 0, 0, 0.7)
|
||||||
@@ -93,6 +99,6 @@
|
|||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
OpacityAnimator {
|
OpacityAnimator {
|
||||||
duration: Kirigami.Units.veryLongDuration * 2
|
duration: Kirigami.Units.veryLongDuration * 2
|
||||||
@@ -265 +285 @@
|
@@ -265 +291 @@
|
||||||
- lockScreenUiVisible: lockScreenRoot.uiVisible
|
- lockScreenUiVisible: lockScreenRoot.uiVisible
|
||||||
+ lockScreenUiVisible: true
|
+ lockScreenUiVisible: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Name: plasma-always-show-unlock
|
Name: plasma-always-show-unlock
|
||||||
Version: 0.1.0
|
Version: 0.1.0
|
||||||
Release: 10%{?dist}
|
Release: 11%{?dist}
|
||||||
Summary: Immediately show the Plasma unlock prompt without requiring input
|
Summary: Immediately show the Plasma unlock prompt without requiring input
|
||||||
|
|
||||||
License: MIT AND GPL-2.0-or-later
|
License: MIT AND GPL-2.0-or-later
|
||||||
@@ -11,10 +11,13 @@ Source2: test-plasma-always-show-unlock
|
|||||||
Source3: LockScreenUi.qml
|
Source3: LockScreenUi.qml
|
||||||
Source4: LICENSE
|
Source4: LICENSE
|
||||||
Source5: README.md
|
Source5: README.md
|
||||||
|
Source6: FedoraToolsFingerprintRetry.qml
|
||||||
|
Source7: tst_fingerprintretry.qml
|
||||||
|
|
||||||
BuildArch: noarch
|
BuildArch: noarch
|
||||||
BuildRequires: bash
|
BuildRequires: bash
|
||||||
BuildRequires: patch
|
BuildRequires: patch
|
||||||
|
BuildRequires: qt6-qtdeclarative-devel
|
||||||
Requires: bash
|
Requires: bash
|
||||||
Requires: patch
|
Requires: patch
|
||||||
Requires: plasma-desktop >= 6.7.4
|
Requires: plasma-desktop >= 6.7.4
|
||||||
@@ -31,6 +34,11 @@ patch after plasma-desktop updates and leaves unknown future layouts unchanged.
|
|||||||
|
|
||||||
%check
|
%check
|
||||||
bash %{SOURCE2} %{SOURCE0} %{SOURCE1} %{SOURCE3}
|
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
|
||||||
install -Dpm 0755 %{SOURCE0} \
|
install -Dpm 0755 %{SOURCE0} \
|
||||||
@@ -41,6 +49,15 @@ install -Dpm 0644 %{SOURCE4} \
|
|||||||
%{buildroot}%{_licensedir}/%{name}/LICENSE
|
%{buildroot}%{_licensedir}/%{name}/LICENSE
|
||||||
install -Dpm 0644 %{SOURCE5} \
|
install -Dpm 0644 %{SOURCE5} \
|
||||||
%{buildroot}%{_docdir}/%{name}/README.md
|
%{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
|
%preun
|
||||||
if [ "$1" -eq 0 ]; then
|
if [ "$1" -eq 0 ]; then
|
||||||
@@ -55,8 +72,14 @@ fi
|
|||||||
%doc %{_docdir}/%{name}/README.md
|
%doc %{_docdir}/%{name}/README.md
|
||||||
%{_libexecdir}/plasma-always-show-unlock
|
%{_libexecdir}/plasma-always-show-unlock
|
||||||
%{_datadir}/plasma-always-show-unlock/LockScreenUi.patch
|
%{_datadir}/plasma-always-show-unlock/LockScreenUi.patch
|
||||||
|
%{_datadir}/plasma/shells/org.kde.plasma.desktop/contents/lockscreen/FedoraToolsFingerprintRetry.qml
|
||||||
|
|
||||||
%changelog
|
%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
|
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-10
|
||||||
- Clarify the package summary
|
- Clarify the package summary
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ run_tool status >/dev/null
|
|||||||
grep -q 'function onViewVisibleChanged()' "$target"
|
grep -q 'function onViewVisibleChanged()' "$target"
|
||||||
grep -q 'id: resumeAuthenticationTimer' "$target"
|
grep -q 'id: resumeAuthenticationTimer' "$target"
|
||||||
grep -q 'id: postGraceAuthenticationTimer' "$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: 1500' "$target"
|
||||||
grep -q 'interval: 6000' "$target"
|
grep -q 'interval: 6000' "$target"
|
||||||
grep -q 'running: root.viewVisible' "$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 23)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_AUTOMOC 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)
|
find_package(ECM REQUIRED NO_MODULE)
|
||||||
list(APPEND CMAKE_MODULE_PATH "${ECM_MODULE_PATH}")
|
list(APPEND CMAKE_MODULE_PATH "${ECM_MODULE_PATH}")
|
||||||
include(ECMQtDeclareLoggingCategory)
|
include(ECMQtDeclareLoggingCategory)
|
||||||
@@ -25,8 +25,9 @@ ecm_qt_declare_logging_category(sources
|
|||||||
add_executable(retrytest ${sources})
|
add_executable(retrytest ${sources})
|
||||||
target_include_directories(retrytest PRIVATE
|
target_include_directories(retrytest PRIVATE
|
||||||
${CMAKE_CURRENT_BINARY_DIR} ${KSCREENLOCKER_SOURCE}/greeter)
|
${CMAKE_CURRENT_BINARY_DIR} ${KSCREENLOCKER_SOURCE}/greeter)
|
||||||
target_compile_definitions(retrytest PRIVATE HAVE_PAM_FAIL_DELAY)
|
target_compile_definitions(retrytest PRIVATE HAVE_PAM_FAIL_DELAY
|
||||||
target_link_libraries(retrytest PRIVATE Qt6::Core Qt6::Test)
|
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()
|
enable_testing()
|
||||||
add_test(NAME fingerprint-retry COMMAND retrytest)
|
add_test(NAME fingerprint-retry COMMAND retrytest)
|
||||||
set_tests_properties(fingerprint-retry PROPERTIES TIMEOUT 30)
|
set_tests_properties(fingerprint-retry PROPERTIES TIMEOUT 30)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <QSignalSpy>
|
#include <QSignalSpy>
|
||||||
#include <QTest>
|
#include <QTest>
|
||||||
|
#include <QQmlComponent>
|
||||||
|
#include <QQmlEngine>
|
||||||
#include <security/pam_appl.h>
|
#include <security/pam_appl.h>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -38,9 +40,21 @@ int pam_authenticate(pam_handle_t *handle, int)
|
|||||||
if (handle->service == "transient" && handle->attempts == 1) {
|
if (handle->service == "transient" && handle->attempts == 1) {
|
||||||
return PAM_AUTHINFO_UNAVAIL;
|
return PAM_AUTHINFO_UNAVAIL;
|
||||||
}
|
}
|
||||||
if (handle->service == "timeout" && handle->attempts == 1) {
|
if (handle->service == "attempt-limit" && handle->attempts == 1) {
|
||||||
return PAM_MAXTRIES;
|
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:"};
|
pam_message message{PAM_PROMPT_ECHO_OFF, "Test credential:"};
|
||||||
const pam_message *messages = &message;
|
const pam_message *messages = &message;
|
||||||
pam_response *response = nullptr;
|
pam_response *response = nullptr;
|
||||||
@@ -61,11 +75,60 @@ class RetryTest : public QObject
|
|||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private Q_SLOTS:
|
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()
|
void retryWithActivePassword_data()
|
||||||
{
|
{
|
||||||
QTest::addColumn<QString>("service");
|
QTest::addColumn<QString>("service");
|
||||||
QTest::newRow("temporary unavailability") << QStringLiteral("transient");
|
QTest::newRow("temporary unavailability") << QStringLiteral("transient");
|
||||||
QTest::newRow("attempt timeout") << QStringLiteral("timeout");
|
QTest::newRow("exhausted attempts, manual retry") << QStringLiteral("attempt-limit");
|
||||||
}
|
}
|
||||||
|
|
||||||
void retryWithActivePassword()
|
void retryWithActivePassword()
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ install -m 0644 \
|
|||||||
"$topdir/SOURCES/LockScreenUi.qml"
|
"$topdir/SOURCES/LockScreenUi.qml"
|
||||||
install -m 0644 "$repo_root/LICENSE" "$topdir/SOURCES/LICENSE"
|
install -m 0644 "$repo_root/LICENSE" "$topdir/SOURCES/LICENSE"
|
||||||
install -m 0644 "$repo_root/README.md" "$topdir/SOURCES/README.md"
|
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 \
|
rpmbuild \
|
||||||
--define "_topdir $topdir" \
|
--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 \
|
bash plasma-always-show-unlock/tests/test-plasma-always-show-unlock \
|
||||||
plasma-always-show-unlock/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
|
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 \
|
bash plasma-fingerprint-workaround/tests/test-controller \
|
||||||
plasma-fingerprint-workaround/plasma-fingerprint-workaround plasma-fingerprint-workaround/payload.conf
|
plasma-fingerprint-workaround/plasma-fingerprint-workaround plasma-fingerprint-workaround/payload.conf
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user