Add Framework laptop controls and monitoring with hardware-aware discovery

This commit is contained in:
ajp_anton
2026-09-11 10:29:27 +00:00
parent 787231cb03
commit 8719402d97
50 changed files with 3611 additions and 9 deletions
+6 -1
View File
@@ -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
+6
View File
@@ -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).");
}
+1
View File
@@ -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"));
+10 -3
View File
@@ -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();
+2
View File
@@ -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);
+4
View File
@@ -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;