Add Fedora Tools settings module

This commit is contained in:
ajp_anton
2026-09-05 02:53:11 +00:00
parent 19df3c3a75
commit d6f30c5735
23 changed files with 1354 additions and 4 deletions
+8
View File
@@ -18,6 +18,7 @@ sudo dnf install touchpad-hold-tap
sudo dnf install plasma-always-show-unlock sudo dnf install plasma-always-show-unlock
sudo dnf install plasma-task-group-shortcuts sudo dnf install plasma-task-group-shortcuts
sudo dnf install plasma-fingerprint-workaround sudo dnf install plasma-fingerprint-workaround
sudo dnf install fedora-tools-settings
``` ```
## Compatibility ## Compatibility
@@ -29,6 +30,13 @@ systems, architectures, or distributions may be limited.
## Tools ## Tools
### Fedora Tools settings
The optional `fedora-tools-settings` package adds a **Fedora Tools** page to
Plasma System Settings. It discovers tools from this RPM repository, shows
which are installed, and can install available tools after administrator
authorization. Individual tools do not require the settings module.
### Touchpad hold-tap ### Touchpad hold-tap
Experimental. Recognizes a one-finger hold followed by a quick tap from a Experimental. Recognizes a one-finger hold followed by a quick tap from a
+24
View File
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: MIT
cmake_minimum_required(VERSION 3.16)
project(fedora-tools-settings VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(ECM 6.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(CTest)
include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
find_package(Qt6 6.8 REQUIRED COMPONENTS Core Quick Test)
find_package(KF6 6.0 REQUIRED COMPONENTS Auth CoreAddons I18n KCMUtils)
add_subdirectory(src)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
+22
View File
@@ -0,0 +1,22 @@
# Fedora Tools settings
This optional KDE Configuration Module lists tools published in the configured
`fedora-tools` RPM repository. It appears as **Fedora Tools** in Plasma System
Settings and can also be opened directly:
```bash
kcmshell6 kcm_fedora_tools
```
Packages advertise themselves with the RPM capability `fedora-tools-tool`.
The module uses DNF's repository metadata for discovery and delegates package
installation to a narrowly scoped KAuth helper. The helper accepts only valid
package names that independently resolve to that capability in the
`fedora-tools` repository.
Installing this module does not install or activate any other tool. Tools
remain usable without the module.
The fingerprint-workaround page can enable the tested build, select a locally
built RPM, allow an intentional version change, and restore Fedora's official
KScreenLocker package. These operations require administrator authentication.
@@ -0,0 +1,62 @@
Name: fedora-tools-settings
Version: 0.1.0
Release: 1%{?dist}
Summary: Plasma System Settings module for Fedora Tools
License: MIT
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
Source0: %{name}-%{version}.tar.gz
Source1: LICENSE
Source2: README.md
BuildRequires: cmake
BuildRequires: extra-cmake-modules
BuildRequires: gcc-c++
BuildRequires: kf6-kauth-devel
BuildRequires: kf6-kcmutils-devel
BuildRequires: kf6-ki18n-devel
BuildRequires: kf6-kirigami-devel
BuildRequires: qt6-qtdeclarative-devel
Requires: dnf5
Requires: kf6-kauth
Requires: kf6-kcmutils
Requires: kf6-kirigami
Requires: plasma-systemsettings
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.
%prep
%autosetup
%build
%cmake
%cmake_build
%check
%ctest
%install
%cmake_install
install -Dpm 0644 %{SOURCE1} \
%{buildroot}%{_licensedir}/%{name}/LICENSE
install -Dpm 0644 %{SOURCE2} \
%{buildroot}%{_docdir}/%{name}/README.md
%files
%license %{_licensedir}/%{name}/LICENSE
%doc %{_docdir}/%{name}/README.md
%{_libdir}/qt6/plugins/plasma/kcms/systemsettings/kcm_fedora_tools.so
%{_libexecdir}/kf6/kauth/fedora-tools-settings-helper
%{_datadir}/applications/kcm_fedora_tools.desktop
%{_datadir}/dbus-1/system-services/se.ajpanton.fedoratools.service
%{_datadir}/dbus-1/system.d/se.ajpanton.fedoratools.conf
%{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy
%changelog
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-1
- Initial package
+42
View File
@@ -0,0 +1,42 @@
# SPDX-License-Identifier: MIT
kcmutils_add_qml_kcm(kcm_fedora_tools
SOURCES
fedoratoolskcm.cpp
fedoratoolskcm.h
packageutils.cpp
packageutils.h
toolmodel.cpp
toolmodel.h
)
target_link_libraries(kcm_fedora_tools PRIVATE
Qt6::Core
Qt6::Quick
KF6::AuthCore
KF6::CoreAddons
KF6::I18n
KF6::KCMUtilsQuick
)
add_executable(fedora-tools-settings-helper
helper.cpp
packageutils.cpp
packageutils.h
)
target_link_libraries(fedora-tools-settings-helper PRIVATE
Qt6::Core
KF6::AuthCore
)
install(TARGETS fedora-tools-settings-helper
DESTINATION ${KAUTH_HELPER_INSTALL_DIR}
)
kauth_install_helper_files(
fedora-tools-settings-helper
se.ajpanton.fedoratools
root
)
kauth_install_actions(
se.ajpanton.fedoratools
se.ajpanton.fedoratools.actions
)
@@ -0,0 +1,323 @@
// SPDX-License-Identifier: MIT
#include "fedoratoolskcm.h"
#include <KAuth/Action>
#include <KAuth/ExecuteJob>
#include <KJob>
#include <KLocalizedString>
#include <KPluginFactory>
#include <QTimer>
#include <algorithm>
#include <utility>
namespace
{
const QString packageFormat = QStringLiteral("%{name}\t%{evr}\t%{arch}\t%{summary}\n");
const QString helperId = QStringLiteral("se.ajpanton.fedoratools");
}
K_PLUGIN_CLASS_WITH_JSON(FedoraToolsKcm, "kcm_fedora_tools.json")
FedoraToolsKcm::FedoraToolsKcm(QObject *parent, const KPluginMetaData &data)
: KQuickConfigModule(parent, data)
, m_tools(this)
{
setButtons(NoAdditionalButton);
connect(&m_query, &QProcess::finished, this, &FedoraToolsKcm::queryFinished);
connect(&m_query, &QProcess::errorOccurred, this, [this](QProcess::ProcessError processError) {
if (processError != QProcess::FailedToStart) {
return;
}
setMessage(i18n("Could not start %1.", m_query.program()), true);
m_stage = QueryStage::Idle;
setBusy(false);
});
QTimer::singleShot(0, this, [this] {
refresh();
});
}
QAbstractItemModel *FedoraToolsKcm::tools()
{
return &m_tools;
}
bool FedoraToolsKcm::busy() const
{
return m_busy;
}
QString FedoraToolsKcm::message() const
{
return m_message;
}
bool FedoraToolsKcm::error() const
{
return m_error;
}
bool FedoraToolsKcm::fingerprintEnabled() const
{
return m_fingerprintEnabled;
}
void FedoraToolsKcm::refresh(bool refreshMetadata)
{
if (m_busy) {
return;
}
m_refreshMetadata = refreshMetadata;
m_installedPackages.clear();
m_availablePackages.clear();
m_updates.clear();
setMessage({});
setBusy(true);
startQuery(QStringLiteral("/usr/bin/rpm"),
{QStringLiteral("-q"),
QStringLiteral("--whatprovides"),
QStringLiteral("fedora-tools-tool"),
QStringLiteral("--qf"),
packageFormat},
QueryStage::InstalledProviders);
}
void FedoraToolsKcm::clearMessage()
{
setMessage({});
}
void FedoraToolsKcm::installTool(const QString &packageName)
{
if (m_busy || !m_tools.mayInstall(packageName)) {
return;
}
runAuthorizedAction(QStringLiteral("installpackage"),
{{QStringLiteral("packageName"), packageName}},
i18n("%1 is now installed.", packageName),
true);
}
void FedoraToolsKcm::enableFingerprintWorkaround(const QUrl &rpmFile, bool force)
{
if (m_busy) {
return;
}
if (!rpmFile.isEmpty() && !rpmFile.isLocalFile()) {
setMessage(i18n("Select a local RPM file."), true);
return;
}
QVariantMap arguments{
{QStringLiteral("operation"), QStringLiteral("enable")},
{QStringLiteral("force"), force},
};
if (rpmFile.isLocalFile()) {
arguments.insert(QStringLiteral("rpmPath"), rpmFile.toLocalFile());
}
runAuthorizedAction(QStringLiteral("fingerprintworkaround"),
arguments,
i18n("The fingerprint workaround was enabled. Reboot before testing it."),
false);
}
void FedoraToolsKcm::disableFingerprintWorkaround()
{
if (m_busy) {
return;
}
runAuthorizedAction(QStringLiteral("fingerprintworkaround"),
{{QStringLiteral("operation"), QStringLiteral("disable")}},
i18n("The fingerprint workaround was disabled. Reboot before testing the lock screen."),
false);
}
void FedoraToolsKcm::startQuery(const QString &program, const QStringList &arguments, QueryStage stage)
{
m_stage = stage;
m_query.start(program, arguments);
}
void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
const QByteArray output = m_query.readAllStandardOutput();
const QByteArray errorOutput = m_query.readAllStandardError();
if (exitStatus != QProcess::NormalExit) {
setMessage(i18n("Package discovery terminated unexpectedly."), true);
m_stage = QueryStage::Idle;
setBusy(false);
return;
}
if (m_stage == QueryStage::InstalledProviders) {
m_installedPackages = parsePackageRecords(output);
QStringList arguments{QStringLiteral("--repo=fedora-tools"), QStringLiteral("-q")};
if (m_refreshMetadata) {
arguments.append(QStringLiteral("--refresh"));
}
arguments.append({QStringLiteral("repoquery"),
QStringLiteral("--available"),
QStringLiteral("--latest-limit=1"),
QStringLiteral("--whatprovides=fedora-tools-tool"),
QStringLiteral("--queryformat"),
packageFormat});
startQuery(QStringLiteral("/usr/bin/dnf5"), arguments, QueryStage::Available);
return;
}
if (m_stage == QueryStage::Available) {
if (exitCode != 0) {
m_tools.setPackages(m_installedPackages, {});
const QString details = QString::fromUtf8(errorOutput).trimmed();
setMessage(details.isEmpty() ? i18n("The Fedora Tools repository is unavailable.") : details, true);
updateFingerprintStatus();
m_stage = QueryStage::Idle;
setBusy(false);
return;
}
m_availablePackages = parsePackageRecords(output);
if (m_availablePackages.isEmpty()) {
finishRefresh();
return;
}
startQuery(QStringLiteral("/usr/bin/dnf5"),
{QStringLiteral("--repo=fedora-tools"),
QStringLiteral("-q"),
QStringLiteral("repoquery"),
QStringLiteral("--available"),
QStringLiteral("--upgrades"),
QStringLiteral("--whatprovides=fedora-tools-tool"),
QStringLiteral("--queryformat"),
packageFormat},
QueryStage::Updates);
return;
}
if (m_stage == QueryStage::Updates) {
if (exitCode == 0) {
for (const PackageRecord &package : parsePackageRecords(output)) {
m_updates.insert(package.name);
}
}
QStringList packageNames;
for (const PackageRecord &package : std::as_const(m_availablePackages)) {
packageNames.append(package.name);
}
QStringList arguments{QStringLiteral("-q"), QStringLiteral("--qf"), packageFormat};
arguments.append(packageNames);
startQuery(QStringLiteral("/usr/bin/rpm"), arguments, QueryStage::KnownInstalled);
return;
}
if (m_stage == QueryStage::KnownInstalled) {
const QList<PackageRecord> knownInstalled = parsePackageRecords(output);
for (const PackageRecord &package : knownInstalled) {
auto existing = std::find_if(m_installedPackages.begin(), m_installedPackages.end(), [&package](const PackageRecord &candidate) {
return candidate.name == package.name;
});
if (existing == m_installedPackages.end()) {
m_installedPackages.append(package);
} else {
*existing = package;
}
}
finishRefresh();
}
}
void FedoraToolsKcm::finishRefresh()
{
m_tools.setPackages(m_installedPackages, m_availablePackages, m_updates);
updateFingerprintStatus();
m_stage = QueryStage::Idle;
setBusy(false);
}
void FedoraToolsKcm::runAuthorizedAction(const QString &name,
const QVariantMap &arguments,
const QString &successMessage,
bool refreshTools)
{
setMessage({});
setBusy(true);
KAuth::Action action(helperId + QLatin1Char('.') + name);
action.setHelperId(helperId);
action.setArguments(arguments);
action.setTimeout(10 * 60 * 1000);
KAuth::ExecuteJob *job = action.execute();
connect(job, &KJob::result, this, [this, successMessage, refreshTools](KJob *finishedJob) {
actionFinished(finishedJob, successMessage, refreshTools);
});
job->start();
}
void FedoraToolsKcm::actionFinished(KJob *job, const QString &successMessage, bool refreshTools)
{
auto *executeJob = qobject_cast<KAuth::ExecuteJob *>(job);
if (job->error()) {
const QString helperMessage = executeJob ? executeJob->data().value(QStringLiteral("message")).toString() : QString();
const QString errorMessage = helperMessage.isEmpty() ? job->errorString() : helperMessage;
setMessage(errorMessage, true);
setBusy(false);
return;
}
if (refreshTools) {
setBusy(false);
refresh();
} else {
updateFingerprintStatus();
setBusy(false);
}
setMessage(successMessage);
}
void FedoraToolsKcm::setBusy(bool busy)
{
if (m_busy == busy) {
return;
}
m_busy = busy;
Q_EMIT busyChanged();
}
void FedoraToolsKcm::setMessage(const QString &message, bool error)
{
if (m_message == message && m_error == error) {
return;
}
m_message = message;
m_error = error;
Q_EMIT messageChanged();
}
void FedoraToolsKcm::updateFingerprintStatus()
{
QProcess process;
process.start(QStringLiteral("/usr/bin/plasma-fingerprint-workaround"), {QStringLiteral("status")});
const bool finished = process.waitForFinished(2000);
const bool enabled = finished && process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0
&& process.readAllStandardOutput().startsWith("Fingerprint workaround: enabled");
if (m_fingerprintEnabled != enabled) {
m_fingerprintEnabled = enabled;
Q_EMIT fingerprintEnabledChanged();
}
}
#include "fedoratoolskcm.moc"
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT
#pragma once
#include "packageutils.h"
#include "toolmodel.h"
#include <KQuickConfigModule>
#include <QProcess>
#include <QSet>
#include <QUrl>
class KJob;
class FedoraToolsKcm : public KQuickConfigModule
{
Q_OBJECT
Q_PROPERTY(QAbstractItemModel *tools READ tools CONSTANT)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
Q_PROPERTY(QString message READ message NOTIFY messageChanged)
Q_PROPERTY(bool error READ error NOTIFY messageChanged)
Q_PROPERTY(bool fingerprintEnabled READ fingerprintEnabled NOTIFY fingerprintEnabledChanged)
public:
FedoraToolsKcm(QObject *parent, const KPluginMetaData &data);
QAbstractItemModel *tools();
bool busy() const;
QString message() const;
bool error() const;
bool fingerprintEnabled() const;
Q_INVOKABLE void refresh(bool refreshMetadata = false);
Q_INVOKABLE void clearMessage();
Q_INVOKABLE void installTool(const QString &packageName);
Q_INVOKABLE void enableFingerprintWorkaround(const QUrl &rpmFile, bool force);
Q_INVOKABLE void disableFingerprintWorkaround();
Q_SIGNALS:
void busyChanged();
void messageChanged();
void fingerprintEnabledChanged();
private:
enum class QueryStage {
Idle,
InstalledProviders,
Available,
Updates,
KnownInstalled,
};
void startQuery(const QString &program, const QStringList &arguments, QueryStage stage);
void queryFinished(int exitCode, QProcess::ExitStatus exitStatus);
void finishRefresh();
void runAuthorizedAction(const QString &name, const QVariantMap &arguments, const QString &successMessage, bool refreshTools);
void actionFinished(KJob *job, const QString &successMessage, bool refreshTools);
void setBusy(bool busy);
void setMessage(const QString &message, bool error = false);
void updateFingerprintStatus();
ToolModel m_tools;
QProcess m_query;
QueryStage m_stage = QueryStage::Idle;
QList<PackageRecord> m_installedPackages;
QList<PackageRecord> m_availablePackages;
QSet<QString> m_updates;
bool m_refreshMetadata = false;
bool m_busy = false;
bool m_error = false;
bool m_fingerprintEnabled = false;
QString m_message;
};
+134
View File
@@ -0,0 +1,134 @@
// SPDX-License-Identifier: MIT
#include "packageutils.h"
#include <KAuth/ActionReply>
#include <KAuth/HelperSupport>
#include <QFileInfo>
#include <QObject>
#include <QProcess>
namespace
{
constexpr int commandTimeoutMs = 10 * 60 * 1000;
KAuth::ActionReply commandReply(const QString &program, const QStringList &arguments)
{
QProcess process;
process.setProcessChannelMode(QProcess::MergedChannels);
process.start(program, arguments);
if (!process.waitForStarted()) {
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
reply.addData(QStringLiteral("message"), QStringLiteral("Could not start %1.").arg(program));
return reply;
}
if (!process.waitForFinished(commandTimeoutMs)) {
process.kill();
process.waitForFinished();
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
reply.addData(QStringLiteral("message"), QStringLiteral("The operation timed out."));
return reply;
}
QString output = QString::fromUtf8(process.readAll()).trimmed();
if (output.size() > 16 * 1024) {
output = output.right(16 * 1024);
}
if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply(process.exitCode());
reply.addData(QStringLiteral("message"), output.isEmpty() ? QStringLiteral("The operation failed.") : output);
return reply;
}
KAuth::ActionReply reply = KAuth::ActionReply::SuccessReply();
reply.addData(QStringLiteral("output"), output);
return reply;
}
bool isPublishedTool(const QString &packageName)
{
QProcess process;
process.start(QStringLiteral("/usr/bin/dnf5"),
{QStringLiteral("--repo=fedora-tools"),
QStringLiteral("-q"),
QStringLiteral("repoquery"),
QStringLiteral("--available"),
QStringLiteral("--whatprovides=fedora-tools-tool"),
QStringLiteral("--queryformat"),
QStringLiteral("%{name}\n"),
packageName});
if (!process.waitForFinished(60 * 1000) || process.exitCode() != 0) {
return false;
}
for (const QByteArray &line : process.readAllStandardOutput().split('\n')) {
if (QString::fromUtf8(line) == packageName) {
return true;
}
}
return false;
}
}
class FedoraToolsHelper : public QObject
{
Q_OBJECT
public Q_SLOTS:
KAuth::ActionReply installpackage(const QVariantMap &arguments)
{
const QString packageName = arguments.value(QStringLiteral("packageName")).toString();
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."));
return reply;
}
return commandReply(QStringLiteral("/usr/bin/dnf5"),
{QStringLiteral("install"),
QStringLiteral("--assumeyes"),
QStringLiteral("--from-repo=fedora-tools"),
packageName});
}
KAuth::ActionReply fingerprintworkaround(const QVariantMap &arguments)
{
const QString operation = arguments.value(QStringLiteral("operation")).toString();
if (operation != QLatin1String("enable") && operation != QLatin1String("disable")) {
return KAuth::ActionReply::InvalidActionReply();
}
const QString controller = QStringLiteral("/usr/bin/plasma-fingerprint-workaround");
if (!QFileInfo(controller).isExecutable()) {
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
reply.addData(QStringLiteral("message"), QStringLiteral("The fingerprint workaround controller is not installed."));
return reply;
}
QStringList commandArguments{operation};
if (operation == QLatin1String("enable")) {
const QString rpmPath = arguments.value(QStringLiteral("rpmPath")).toString();
if (!rpmPath.isEmpty()) {
if (!QFileInfo(rpmPath).isAbsolute() || !QFileInfo(rpmPath).isFile()) {
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
reply.addData(QStringLiteral("message"), QStringLiteral("The selected RPM does not exist."));
return reply;
}
commandArguments.append({QStringLiteral("--rpm"), rpmPath});
}
if (arguments.value(QStringLiteral("force")).toBool()) {
commandArguments.append(QStringLiteral("--force"));
}
}
commandArguments.append(QStringLiteral("--yes"));
return commandReply(controller, commandArguments);
}
};
KAUTH_HELPER_MAIN("se.ajpanton.fedoratools", FedoraToolsHelper)
#include "helper.moc"
@@ -0,0 +1,17 @@
{
"KPlugin": {
"Authors": [
{
"Name": "fedora-tools contributors"
}
],
"Description": "Install and configure small Fedora tools",
"Icon": "preferences-system",
"License": "MIT",
"Name": "Fedora Tools",
"Version": "0.1.0",
"Website": "https://git.ajpanton.se/ajp_anton/fedora-tools"
},
"X-KDE-Keywords": "Fedora,tools,utilities",
"X-KDE-System-Settings-Parent-Category": "system-administration"
}
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
#include "packageutils.h"
#include <QRegularExpression>
#include <utility>
QList<PackageRecord> parsePackageRecords(const QByteArray &output)
{
QList<PackageRecord> records;
for (const QByteArray &line : output.split('\n')) {
if (line.isEmpty()) {
continue;
}
const QList<QByteArray> fields = line.split('\t');
if (fields.size() < 4) {
continue;
}
PackageRecord record;
record.name = QString::fromUtf8(fields.at(0));
record.version = QString::fromUtf8(fields.at(1));
record.architecture = QString::fromUtf8(fields.at(2));
record.summary = QString::fromUtf8(fields.mid(3).join('\t'));
if (isValidPackageName(record.name)) {
records.append(std::move(record));
}
}
return records;
}
bool isValidPackageName(const QString &name)
{
static const QRegularExpression expression(QStringLiteral("^[a-z0-9][a-z0-9+._-]*$"));
return expression.match(name).hasMatch();
}
+18
View File
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
#pragma once
#include <QByteArray>
#include <QList>
#include <QString>
struct PackageRecord
{
QString name;
QString version;
QString architecture;
QString summary;
};
QList<PackageRecord> parsePackageRecords(const QByteArray &output);
bool isValidPackageName(const QString &name);
@@ -0,0 +1,13 @@
[Domain]
Name=Fedora Tools
Icon=preferences-system
[se.ajpanton.fedoratools.installpackage]
Name=Install a Fedora tool
Description=Install a package from the Fedora Tools repository
Policy=auth_admin
[se.ajpanton.fedoratools.fingerprintworkaround]
Name=Change the Plasma fingerprint workaround
Description=Install or remove the experimental patched KScreenLocker package
Policy=auth_admin
+109
View File
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: MIT
#include "toolmodel.h"
#include <QMap>
#include <algorithm>
#include <utility>
ToolModel::ToolModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int ToolModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_tools.size();
}
QVariant ToolModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_tools.size()) {
return {};
}
const Tool &tool = m_tools.at(index.row());
switch (role) {
case PackageNameRole:
return tool.packageName;
case VersionRole:
return tool.version;
case AvailableVersionRole:
return tool.availableVersion;
case ArchitectureRole:
return tool.architecture;
case SummaryRole:
return tool.summary;
case InstalledRole:
return tool.installed;
case AvailableRole:
return tool.available;
case UpdateAvailableRole:
return tool.updateAvailable;
case ConfigurableRole:
return tool.installed && tool.packageName == QStringLiteral("plasma-fingerprint-workaround");
default:
return {};
}
}
QHash<int, QByteArray> ToolModel::roleNames() const
{
return {
{PackageNameRole, "packageName"},
{VersionRole, "version"},
{AvailableVersionRole, "availableVersion"},
{ArchitectureRole, "architecture"},
{SummaryRole, "summary"},
{InstalledRole, "installed"},
{AvailableRole, "available"},
{UpdateAvailableRole, "updateAvailable"},
{ConfigurableRole, "configurable"},
};
}
void ToolModel::setPackages(const QList<PackageRecord> &installed,
const QList<PackageRecord> &available,
const QSet<QString> &updates)
{
QMap<QString, Tool> tools;
for (const PackageRecord &package : installed) {
Tool &tool = tools[package.name];
tool.packageName = package.name;
tool.version = package.version;
tool.architecture = package.architecture;
tool.summary = package.summary;
tool.installed = true;
}
for (const PackageRecord &package : available) {
Tool &tool = tools[package.name];
tool.packageName = package.name;
tool.availableVersion = package.version;
tool.architecture = package.architecture;
tool.summary = package.summary;
tool.available = true;
tool.updateAvailable = tool.installed && updates.contains(package.name);
}
QList<Tool> merged = tools.values();
std::ranges::sort(merged, [](const Tool &left, const Tool &right) {
if (left.installed != right.installed) {
return left.installed > right.installed;
}
return left.packageName < right.packageName;
});
beginResetModel();
m_tools = std::move(merged);
endResetModel();
}
bool ToolModel::mayInstall(const QString &packageName) const
{
return std::ranges::any_of(m_tools, [&packageName](const Tool &tool) {
return tool.packageName == packageName && tool.available;
});
}
+52
View File
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
#pragma once
#include "packageutils.h"
#include <QAbstractListModel>
#include <QSet>
struct Tool
{
QString packageName;
QString version;
QString availableVersion;
QString architecture;
QString summary;
bool installed = false;
bool available = false;
bool updateAvailable = false;
};
class ToolModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Role {
PackageNameRole = Qt::UserRole + 1,
VersionRole,
AvailableVersionRole,
ArchitectureRole,
SummaryRole,
InstalledRole,
AvailableRole,
UpdateAvailableRole,
ConfigurableRole,
};
explicit ToolModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = {}) const override;
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
void setPackages(const QList<PackageRecord> &installed,
const QList<PackageRecord> &available,
const QSet<QString> &updates = {});
bool mayInstall(const QString &packageName) const;
private:
QList<Tool> m_tools;
};
+268
View File
@@ -0,0 +1,268 @@
// SPDX-License-Identifier: MIT
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Dialogs as Dialogs
import QtQuick.Layouts
import org.kde.kcmutils as KCM
import org.kde.kirigami as Kirigami
KCM.SimpleKCM {
id: root
property bool showingFingerprintSettings: false
implicitWidth: Kirigami.Units.gridUnit * 36
implicitHeight: Kirigami.Units.gridUnit * 30
StackLayout {
anchors.fill: parent
currentIndex: root.showingFingerprintSettings ? 1 : 0
ColumnLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: kcm.message.length > 0
text: kcm.message
type: kcm.error ? Kirigami.MessageType.Error : Kirigami.MessageType.Positive
}
RowLayout {
Layout.fillWidth: true
Controls.Label {
Layout.fillWidth: true
text: i18n("Tools published in the configured Fedora Tools repository")
wrapMode: Text.WordWrap
}
Controls.Button {
text: i18n("Refresh")
icon.name: "view-refresh"
enabled: !kcm.busy
onClicked: kcm.refresh(true)
}
}
Controls.BusyIndicator {
Layout.alignment: Qt.AlignHCenter
visible: kcm.busy
running: visible
}
ListView {
id: toolList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: Kirigami.Units.smallSpacing
model: kcm.tools
delegate: Kirigami.AbstractCard {
id: toolCard
required property string packageName
required property string version
required property string availableVersion
required property string architecture
required property string summary
required property bool installed
required property bool available
required property bool updateAvailable
required property bool configurable
width: toolList.width
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.Icon {
source: "applications-system"
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: width
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Controls.Label {
Layout.fillWidth: true
text: toolCard.packageName
font.bold: true
elide: Text.ElideRight
}
Controls.Label {
Layout.fillWidth: true
text: toolCard.summary
wrapMode: Text.WordWrap
color: Kirigami.Theme.disabledTextColor
}
Controls.Label {
Layout.fillWidth: true
text: {
if (toolCard.updateAvailable) {
return i18n("Installed %1; %2 is available", toolCard.version, toolCard.availableVersion)
}
if (toolCard.installed) {
return i18n("Installed: %1", toolCard.version)
}
return i18n("Available: %1", toolCard.availableVersion)
}
color: Kirigami.Theme.disabledTextColor
}
}
Controls.Button {
visible: toolCard.configurable
text: i18n("Configure…")
enabled: !kcm.busy
onClicked: root.showingFingerprintSettings = true
}
Controls.Button {
visible: toolCard.available && (!toolCard.installed || toolCard.updateAvailable)
text: toolCard.installed ? i18n("Update") : i18n("Install")
enabled: !kcm.busy
onClicked: kcm.installTool(toolCard.packageName)
}
}
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - Kirigami.Units.gridUnit * 4
visible: toolList.count === 0 && !kcm.busy
text: i18n("No Fedora tools were found")
explanation: i18n("Check that the fedora-tools repository is installed and enabled.")
icon.name: "package-x-generic"
}
}
Controls.Label {
Layout.fillWidth: true
text: i18n("Availability is filtered for this system's architecture. DNF validates dependencies and the transaction before installation.")
wrapMode: Text.WordWrap
color: Kirigami.Theme.disabledTextColor
}
}
ColumnLayout {
spacing: Kirigami.Units.largeSpacing
RowLayout {
Layout.fillWidth: true
Controls.ToolButton {
text: i18n("Back to tools")
icon.name: "go-previous"
enabled: !kcm.busy
onClicked: {
kcm.clearMessage()
root.showingFingerprintSettings = false
}
}
Kirigami.Heading {
Layout.fillWidth: true
text: i18n("Fingerprint workaround")
level: 2
}
}
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: true
type: kcm.message.length > 0
? (kcm.error ? Kirigami.MessageType.Error : Kirigami.MessageType.Positive)
: Kirigami.MessageType.Warning
text: kcm.message.length > 0
? kcm.message
: i18n("This experimental workaround replaces Fedora's security-sensitive KScreenLocker package with an unaudited local build. Review its source before enabling it on another system.")
}
Kirigami.FormLayout {
Layout.fillWidth: true
Controls.Label {
Kirigami.FormData.label: i18n("Status:")
text: kcm.fingerprintEnabled ? i18n("Enabled") : i18n("Disabled")
}
Controls.CheckBox {
id: forceVersion
Kirigami.FormData.label: i18n("Version:")
text: i18n("Allow a different KScreenLocker base version")
enabled: !kcm.busy
}
}
Controls.Label {
Layout.fillWidth: true
text: i18n("Use the tested published build, or select an RPM built from the workaround spec. A version mismatch requires the option above and may downgrade KScreenLocker.")
wrapMode: Text.WordWrap
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
visible: kcm.busy
Controls.BusyIndicator {
running: parent.visible
}
Controls.Label {
text: i18n("Applying change…")
}
}
RowLayout {
Layout.fillWidth: true
Controls.Button {
text: i18n("Enable tested build")
icon.name: "security-high"
enabled: !kcm.busy
onClicked: kcm.enableFingerprintWorkaround("", forceVersion.checked)
}
Controls.Button {
text: i18n("Install RPM from disk…")
icon.name: "document-open"
enabled: !kcm.busy
onClicked: rpmDialog.open()
}
Item {
Layout.fillWidth: true
}
Controls.Button {
text: i18n("Disable workaround")
enabled: !kcm.busy && kcm.fingerprintEnabled
onClicked: kcm.disableFingerprintWorkaround()
}
}
Item {
Layout.fillHeight: true
}
}
}
Dialogs.FileDialog {
id: rpmDialog
title: i18n("Select a patched KScreenLocker RPM")
nameFilters: [i18n("RPM packages (*.rpm)")]
onAccepted: kcm.enableFingerprintWorkaround(selectedFile, forceVersion.checked)
}
}
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: MIT
add_executable(test-packageutils
test-packageutils.cpp
../src/packageutils.cpp
)
target_include_directories(test-packageutils PRIVATE ../src)
target_link_libraries(test-packageutils PRIVATE Qt6::Core Qt6::Test)
add_test(NAME packageutils COMMAND test-packageutils)
add_executable(test-toolmodel
test-toolmodel.cpp
../src/toolmodel.cpp
../src/packageutils.cpp
)
target_include_directories(test-toolmodel PRIVATE ../src)
target_link_libraries(test-toolmodel PRIVATE Qt6::Core Qt6::Test)
add_test(NAME toolmodel COMMAND test-toolmodel)
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
#include "packageutils.h"
#include <QTest>
class PackageUtilsTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void parsesQueryOutput()
{
const auto records = parsePackageRecords(
"touchpad-hold-tap\t0.1.0-6.fc44\tnoarch\tTouchpad gesture\n"
"broken line\n");
QCOMPARE(records.size(), 1);
QCOMPARE(records.first().name, QStringLiteral("touchpad-hold-tap"));
QCOMPARE(records.first().version, QStringLiteral("0.1.0-6.fc44"));
QCOMPARE(records.first().summary, QStringLiteral("Touchpad gesture"));
}
void validatesPackageNames()
{
QVERIFY(isValidPackageName(QStringLiteral("plasma-task-group-shortcuts")));
QVERIFY(!isValidPackageName(QStringLiteral("--installroot=/tmp")));
QVERIFY(!isValidPackageName(QStringLiteral("tool; reboot")));
QVERIFY(!isValidPackageName(QStringLiteral("Tool")));
}
};
QTEST_MAIN(PackageUtilsTest)
#include "test-packageutils.moc"
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
#include "toolmodel.h"
#include <QTest>
class ToolModelTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void mergesInstalledAndAvailablePackages()
{
ToolModel model;
const PackageRecord installed{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-5.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")};
const PackageRecord available{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-6.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")};
model.setPackages({installed}, {available}, {QStringLiteral("touchpad-hold-tap")});
QCOMPARE(model.rowCount(), 1);
const QModelIndex index = model.index(0);
QCOMPARE(model.data(index, ToolModel::InstalledRole).toBool(), true);
QCOMPARE(model.data(index, ToolModel::AvailableRole).toBool(), true);
QCOMPARE(model.data(index, ToolModel::UpdateAvailableRole).toBool(), true);
QCOMPARE(model.mayInstall(QStringLiteral("touchpad-hold-tap")), true);
}
void doesNotGuessUpdatesFromDifferentVersionStrings()
{
ToolModel model;
const PackageRecord installed{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-7.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")};
const PackageRecord available{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-6.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")};
model.setPackages({installed}, {available});
QCOMPARE(model.data(model.index(0), ToolModel::UpdateAvailableRole).toBool(), false);
}
void makesFingerprintControllerConfigurable()
{
ToolModel model;
const PackageRecord package{QStringLiteral("plasma-fingerprint-workaround"), QStringLiteral("0.1.0-2.fc44"), QStringLiteral("noarch"), QStringLiteral("Fingerprint")};
model.setPackages({package}, {package});
QCOMPARE(model.data(model.index(0), ToolModel::ConfigurableRole).toBool(), true);
}
};
QTEST_MAIN(ToolModelTest)
#include "test-toolmodel.moc"
@@ -1,6 +1,6 @@
Name: plasma-always-show-unlock Name: plasma-always-show-unlock
Version: 0.1.0 Version: 0.1.0
Release: 8%{?dist} Release: 9%{?dist}
Summary: Always show the Plasma lock-screen unlock prompt Summary: Always show the Plasma lock-screen unlock prompt
License: MIT AND GPL-2.0-or-later License: MIT AND GPL-2.0-or-later
@@ -18,6 +18,7 @@ BuildRequires: patch
Requires: bash Requires: bash
Requires: patch Requires: patch
Requires: plasma-desktop >= 6.7.4 Requires: plasma-desktop >= 6.7.4
Provides: fedora-tools-tool
%description %description
Keeps KDE Plasma's unlock prompt visible when the screen is locked, after the Keeps KDE Plasma's unlock prompt visible when the screen is locked, after the
@@ -56,6 +57,9 @@ fi
%{_datadir}/plasma-always-show-unlock/LockScreenUi.patch %{_datadir}/plasma-always-show-unlock/LockScreenUi.patch
%changelog %changelog
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-9
- Advertise the tool to Fedora Tools settings
* Thu Sep 03 2026 fedora-tools contributors - 0.1.0-8 * Thu Sep 03 2026 fedora-tools contributors - 0.1.0-8
- Keep the prompt visible without continuously restarting authentication - Keep the prompt visible without continuously restarting authentication
- Retry authentication after resume and the automatic-lock grace period - Retry authentication after resume and the automatic-lock grace period
@@ -1,6 +1,6 @@
Name: plasma-fingerprint-workaround Name: plasma-fingerprint-workaround
Version: 0.1.0 Version: 0.1.0
Release: 1%{?dist} Release: 2%{?dist}
Summary: Opt-in installer for a patched Fedora KScreenLocker Summary: Opt-in installer for a patched Fedora KScreenLocker
License: MIT License: MIT
@@ -21,6 +21,7 @@ Requires: dnf5
Requires: grep Requires: grep
Requires: rpm Requires: rpm
Requires: sed Requires: sed
Provides: fedora-tools-tool
%description %description
Provides an explicit, reversible installer for an experimental KScreenLocker Provides an explicit, reversible installer for an experimental KScreenLocker
@@ -50,5 +51,8 @@ install -D -m 0644 %{SOURCE3} \
%{_datadir}/plasma-fingerprint-workaround/payload.conf %{_datadir}/plasma-fingerprint-workaround/payload.conf
%changelog %changelog
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-2
- Advertise the tool to Fedora Tools settings
* Fri Sep 04 2026 Anton - 0.1.0-1 * Fri Sep 04 2026 Anton - 0.1.0-1
- Initial package - Initial package
@@ -1,6 +1,6 @@
Name: plasma-task-group-shortcuts Name: plasma-task-group-shortcuts
Version: 0.1.0 Version: 0.1.0
Release: 12%{?dist} Release: 13%{?dist}
Summary: Application-group shortcuts for the Plasma Task Manager Summary: Application-group shortcuts for the Plasma Task Manager
License: MIT License: MIT
@@ -22,6 +22,7 @@ BuildRequires: plasma-workspace-devel >= 6.7
BuildRequires: qt6-qtbase-devel BuildRequires: qt6-qtbase-devel
BuildRequires: qt6-qtwayland-devel BuildRequires: qt6-qtwayland-devel
Requires: plasma-workspace >= 6.7 Requires: plasma-workspace >= 6.7
Provides: fedora-tools-tool
%description %description
Changes Plasma's Meta+number shortcuts to address application groups while Changes Plasma's Meta+number shortcuts to address application groups while
@@ -53,5 +54,8 @@ install -Dpm 0644 %{SOURCE2} \
%{_sysconfdir}/xdg/autostart/se.ajpanton.plasma-task-group-shortcuts-autostart.desktop %{_sysconfdir}/xdg/autostart/se.ajpanton.plasma-task-group-shortcuts-autostart.desktop
%changelog %changelog
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-13
- Advertise the tool to Fedora Tools settings
* Fri Sep 04 2026 fedora-tools contributors - 0.1.0-12 * Fri Sep 04 2026 fedora-tools contributors - 0.1.0-12
- Initial package - Initial package
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
set -euo pipefail
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
tool_dir="$repo_root/fedora-tools-settings"
topdir="$repo_root/rpmbuild"
source_dir="$topdir/SOURCES/fedora-tools-settings-0.1.0"
mkdir -p "$topdir"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
rm -rf -- "$source_dir"
mkdir -p "$source_dir"
cp -a "$tool_dir/CMakeLists.txt" "$tool_dir/src" "$tool_dir/tests" "$source_dir/"
tar -C "$topdir/SOURCES" -czf \
"$topdir/SOURCES/fedora-tools-settings-0.1.0.tar.gz" \
fedora-tools-settings-0.1.0
install -m 0644 "$repo_root/LICENSE" "$topdir/SOURCES/LICENSE"
install -m 0644 "$tool_dir/README.md" "$topdir/SOURCES/README.md"
rpmbuild \
--define "_topdir $topdir" \
-bb "$tool_dir/fedora-tools-settings.spec"
+5 -1
View File
@@ -1,6 +1,6 @@
Name: touchpad-hold-tap Name: touchpad-hold-tap
Version: 0.1.0 Version: 0.1.0
Release: 5%{?dist} Release: 6%{?dist}
Summary: Hold-tap middle-click gesture for touchpads Summary: Hold-tap middle-click gesture for touchpads
License: MIT License: MIT
@@ -12,6 +12,7 @@ Source3: README.md
BuildRequires: lua BuildRequires: lua
Requires: libinput >= 1.30 Requires: libinput >= 1.30
Provides: fedora-tools-tool
%description %description
A libinput Lua plugin that recognizes a one-finger hold followed by a quick tap A libinput Lua plugin that recognizes a one-finger hold followed by a quick tap
@@ -39,6 +40,9 @@ install -Dpm 0644 %{SOURCE3} \
%{_libdir}/libinput/plugins/90-touchpad-hold-tap.lua %{_libdir}/libinput/plugins/90-touchpad-hold-tap.lua
%changelog %changelog
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-6
- Advertise the tool to Fedora Tools settings
* Sun Aug 30 2026 fedora-tools contributors - 0.1.0-5 * Sun Aug 30 2026 fedora-tools contributors - 0.1.0-5
- Run state-machine tests during RPM builds and package the README - Run state-machine tests during RPM builds and package the README