Add Framework laptop controls and monitoring with hardware-aware discovery
This commit is contained in:
@@ -16,7 +16,7 @@ package names that independently resolve to that capability in the
|
||||
|
||||
Installed tools can also be configured or removed from the module. Package
|
||||
operations run one at a time; other actions remain disabled until DNF finishes.
|
||||
The module currently configures task-group shortcut behavior, touchpad hold-tap
|
||||
The module currently configures task-group shortcut behaviour, touchpad hold-tap
|
||||
timing and output, and the experimental fingerprint workaround.
|
||||
|
||||
Installed packages are discovered independently of repository metadata, so
|
||||
@@ -33,6 +33,11 @@ shell or administrator privileges. This registration needs no tool-specific
|
||||
change to the settings module. The package must provide `fedora-tools-tool`;
|
||||
arbitrary installed applications are not included in this list.
|
||||
|
||||
Hardware-specific compatibility checks hide Framework Laptop Tools from the
|
||||
available list on unvalidated models. A manually installed copy remains
|
||||
visible as incompatible, with removal available but configuration and updates
|
||||
disabled. Its own privileged helper also checks the model before any change.
|
||||
|
||||
Installing this module does not install or activate any other tool. Tools
|
||||
remain usable without the module.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: fedora-tools-settings
|
||||
Version: 0.1.0
|
||||
Release: 11%{?dist}
|
||||
Release: 12%{?dist}
|
||||
Summary: Plasma System Settings module for Fedora Tools
|
||||
|
||||
License: MIT
|
||||
@@ -29,7 +29,7 @@ Requires: rpm
|
||||
%description
|
||||
Adds a Fedora Tools page to KDE Plasma System Settings. It discovers tools from
|
||||
the configured RPM repository, reports installation state, and can install
|
||||
tools after administrator authorization.
|
||||
tools after administrator authorisation.
|
||||
|
||||
%prep
|
||||
%autosetup
|
||||
@@ -74,6 +74,9 @@ done
|
||||
%{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy
|
||||
|
||||
%changelog
|
||||
* Tue Sep 08 2026 fedora-tools contributors - 0.1.0-12
|
||||
- Hide unsupported Framework hardware tools and flag incompatible local installs
|
||||
|
||||
* Mon Sep 07 2026 fedora-tools contributors - 0.1.0-11
|
||||
- Invalidate cached QML whenever the embedded settings pages change
|
||||
|
||||
|
||||
@@ -125,6 +125,12 @@ public Q_SLOTS:
|
||||
KAuth::ActionReply installpackage(const QVariantMap &arguments)
|
||||
{
|
||||
const QString packageName = arguments.value(QStringLiteral("packageName")).toString();
|
||||
const QString incompatible = packageCompatibilityError(packageName);
|
||||
if (!incompatible.isEmpty()) {
|
||||
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
|
||||
reply.addData(QStringLiteral("message"), incompatible);
|
||||
return reply;
|
||||
}
|
||||
if (!isValidPackageName(packageName) || !isPublishedTool(packageName)) {
|
||||
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
|
||||
reply.addData(QStringLiteral("message"), QStringLiteral("The requested package is not a published Fedora tool."));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "packageutils.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QFile>
|
||||
|
||||
#include <utility>
|
||||
|
||||
@@ -38,3 +39,15 @@ bool isValidPackageName(const QString &name)
|
||||
static const QRegularExpression expression(QStringLiteral("^[a-z0-9][a-z0-9+._-]*$"));
|
||||
return expression.match(name).hasMatch();
|
||||
}
|
||||
|
||||
QString packageCompatibilityError(const QString &name, const QString &sys)
|
||||
{
|
||||
if (name != QLatin1String("framework-laptop-tools")) return {};
|
||||
auto read = [](const QString &path) {
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::ReadOnly) ? QString::fromUtf8(file.readAll()).trimmed() : QString();
|
||||
};
|
||||
if (read(sys + QStringLiteral("/class/dmi/id/sys_vendor")) == QLatin1String("Framework")
|
||||
&& read(sys + QStringLiteral("/class/dmi/id/product_name")) == QLatin1String("Laptop 13 Pro (Intel Core Ultra Series 3)")) return {};
|
||||
return QStringLiteral("Incompatible: requires Framework Laptop 13 Pro (Intel Core Ultra Series 3).");
|
||||
}
|
||||
|
||||
@@ -16,3 +16,4 @@ struct PackageRecord
|
||||
|
||||
QList<PackageRecord> parsePackageRecords(const QByteArray &output);
|
||||
bool isValidPackageName(const QString &name);
|
||||
QString packageCompatibilityError(const QString &name, const QString &sys = QStringLiteral("/sys"));
|
||||
|
||||
@@ -87,13 +87,15 @@ QVariant ToolModel::data(const QModelIndex &index, int role) const
|
||||
case UpdateAvailableRole:
|
||||
return tool.updateAvailable;
|
||||
case ConfigurableRole:
|
||||
return tool.installed
|
||||
return tool.installed && tool.compatibilityError.isEmpty()
|
||||
&& (!tool.configurationCommand.isEmpty()
|
||||
|| tool.packageName == QStringLiteral("plasma-fingerprint-workaround")
|
||||
|| tool.packageName == QStringLiteral("plasma-task-group-shortcuts")
|
||||
|| tool.packageName == QStringLiteral("touchpad-hold-tap"));
|
||||
case ExternalConfigurationRole:
|
||||
return tool.installed && !tool.configurationCommand.isEmpty();
|
||||
case CompatibilityErrorRole:
|
||||
return tool.compatibilityError;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@@ -112,6 +114,7 @@ QHash<int, QByteArray> ToolModel::roleNames() const
|
||||
{UpdateAvailableRole, "updateAvailable"},
|
||||
{ConfigurableRole, "configurable"},
|
||||
{ExternalConfigurationRole, "externalConfiguration"},
|
||||
{CompatibilityErrorRole, "compatibilityError"},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,9 +132,12 @@ void ToolModel::setPackages(const QList<PackageRecord> &installed,
|
||||
tool.summary = package.summary;
|
||||
tool.installed = true;
|
||||
tool.configurationCommand = readConfigurationCommand(package.name);
|
||||
tool.compatibilityError = packageCompatibilityError(package.name);
|
||||
}
|
||||
|
||||
for (const PackageRecord &package : available) {
|
||||
const QString incompatible = packageCompatibilityError(package.name);
|
||||
if (!incompatible.isEmpty() && !tools.contains(package.name)) continue;
|
||||
Tool &tool = tools[package.name];
|
||||
tool.packageName = package.name;
|
||||
tool.availableVersion = package.version;
|
||||
@@ -139,8 +145,9 @@ void ToolModel::setPackages(const QList<PackageRecord> &installed,
|
||||
if (!tool.installed) {
|
||||
tool.summary = package.summary;
|
||||
}
|
||||
tool.available = true;
|
||||
tool.updateAvailable = tool.installed && updates.contains(package.name);
|
||||
tool.compatibilityError = incompatible;
|
||||
tool.available = incompatible.isEmpty();
|
||||
tool.updateAvailable = tool.available && tool.installed && updates.contains(package.name);
|
||||
}
|
||||
|
||||
QList<Tool> merged = tools.values();
|
||||
|
||||
@@ -16,6 +16,7 @@ struct Tool
|
||||
QString architecture;
|
||||
QString summary;
|
||||
QStringList configurationCommand;
|
||||
QString compatibilityError;
|
||||
bool installed = false;
|
||||
bool available = false;
|
||||
bool updateAvailable = false;
|
||||
@@ -37,6 +38,7 @@ public:
|
||||
UpdateAvailableRole,
|
||||
ConfigurableRole,
|
||||
ExternalConfigurationRole,
|
||||
CompatibilityErrorRole,
|
||||
};
|
||||
|
||||
explicit ToolModel(QObject *parent = nullptr);
|
||||
|
||||
@@ -87,6 +87,7 @@ KCM.SimpleKCM {
|
||||
required property bool updateAvailable
|
||||
required property bool configurable
|
||||
required property bool externalConfiguration
|
||||
required property string compatibilityError
|
||||
|
||||
width: toolList.width
|
||||
|
||||
@@ -121,6 +122,9 @@ KCM.SimpleKCM {
|
||||
Controls.Label {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
if (toolCard.compatibilityError.length > 0) {
|
||||
return toolCard.compatibilityError
|
||||
}
|
||||
if (toolCard.updateAvailable) {
|
||||
return i18n("Installed %1; %2 is available", toolCard.version, toolCard.availableVersion)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,28 @@
|
||||
#include "packageutils.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <QTemporaryDir>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
class PackageUtilsTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void frameworkCompatibility() {
|
||||
QTemporaryDir root;
|
||||
QVERIFY(packageCompatibilityError(QStringLiteral("touchpad-hold-tap"), root.path()).isEmpty());
|
||||
QVERIFY(!packageCompatibilityError(QStringLiteral("framework-laptop-tools"), root.path()).isEmpty());
|
||||
QVERIFY(QDir().mkpath(root.path() + QStringLiteral("/class/dmi/id")));
|
||||
QFile vendor(root.path() + QStringLiteral("/class/dmi/id/sys_vendor"));
|
||||
QVERIFY(vendor.open(QIODevice::WriteOnly)); vendor.write("Framework\n"); vendor.close();
|
||||
QFile product(root.path() + QStringLiteral("/class/dmi/id/product_name"));
|
||||
QVERIFY(product.open(QIODevice::WriteOnly)); product.write("Laptop 13 Pro (Intel Core Ultra Series 3)\n"); product.close();
|
||||
QVERIFY(packageCompatibilityError(QStringLiteral("framework-laptop-tools"), root.path()).isEmpty());
|
||||
QVERIFY(product.open(QIODevice::WriteOnly | QIODevice::Truncate)); product.write("Laptop 13\n"); product.close();
|
||||
QVERIFY(!packageCompatibilityError(QStringLiteral("framework-laptop-tools"), root.path()).isEmpty());
|
||||
}
|
||||
void parsesQueryOutput()
|
||||
{
|
||||
const auto records = parsePackageRecords(
|
||||
|
||||
@@ -12,6 +12,18 @@ class ToolModelTest : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void incompatibleFrameworkIsOnlyListedWhenInstalled() {
|
||||
if (packageCompatibilityError(QStringLiteral("framework-laptop-tools")).isEmpty()) QSKIP("Requires a non-Framework test host");
|
||||
const PackageRecord package{QStringLiteral("framework-laptop-tools"), QStringLiteral("1"), QStringLiteral("x86_64"), QStringLiteral("Hardware controls")};
|
||||
ToolModel model; model.setPackages({}, {package});
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
model.setPackages({package}, {package});
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QVERIFY(!model.data(model.index(0), ToolModel::CompatibilityErrorRole).toString().isEmpty());
|
||||
QVERIFY(!model.mayInstall(package.name));
|
||||
QVERIFY(model.mayRemove(package.name));
|
||||
QVERIFY(!model.data(model.index(0), ToolModel::ConfigurableRole).toBool());
|
||||
}
|
||||
void discoversLocallyInstalledToolSettings()
|
||||
{
|
||||
QTemporaryDir directory;
|
||||
|
||||
Reference in New Issue
Block a user