diff --git a/README.md b/README.md index 5de9492..853156c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ sudo dnf install touchpad-hold-tap sudo dnf install plasma-always-show-unlock sudo dnf install plasma-task-group-shortcuts sudo dnf install plasma-fingerprint-workaround +sudo dnf install fedora-tools-settings ``` ## Compatibility @@ -29,6 +30,13 @@ systems, architectures, or distributions may be limited. ## 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 Experimental. Recognizes a one-finger hold followed by a quick tap from a diff --git a/fedora-tools-settings/CMakeLists.txt b/fedora-tools-settings/CMakeLists.txt new file mode 100644 index 0000000..175ac0d --- /dev/null +++ b/fedora-tools-settings/CMakeLists.txt @@ -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() diff --git a/fedora-tools-settings/README.md b/fedora-tools-settings/README.md new file mode 100644 index 0000000..aeb2178 --- /dev/null +++ b/fedora-tools-settings/README.md @@ -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. diff --git a/fedora-tools-settings/fedora-tools-settings.spec b/fedora-tools-settings/fedora-tools-settings.spec new file mode 100644 index 0000000..bc5a870 --- /dev/null +++ b/fedora-tools-settings/fedora-tools-settings.spec @@ -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 diff --git a/fedora-tools-settings/src/CMakeLists.txt b/fedora-tools-settings/src/CMakeLists.txt new file mode 100644 index 0000000..3dd3505 --- /dev/null +++ b/fedora-tools-settings/src/CMakeLists.txt @@ -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 +) diff --git a/fedora-tools-settings/src/fedoratoolskcm.cpp b/fedora-tools-settings/src/fedoratoolskcm.cpp new file mode 100644 index 0000000..d448ee4 --- /dev/null +++ b/fedora-tools-settings/src/fedoratoolskcm.cpp @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: MIT + +#include "fedoratoolskcm.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +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 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(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" diff --git a/fedora-tools-settings/src/fedoratoolskcm.h b/fedora-tools-settings/src/fedoratoolskcm.h new file mode 100644 index 0000000..4250f80 --- /dev/null +++ b/fedora-tools-settings/src/fedoratoolskcm.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT + +#pragma once + +#include "packageutils.h" +#include "toolmodel.h" + +#include + +#include +#include +#include + +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 m_installedPackages; + QList m_availablePackages; + QSet m_updates; + bool m_refreshMetadata = false; + bool m_busy = false; + bool m_error = false; + bool m_fingerprintEnabled = false; + QString m_message; +}; diff --git a/fedora-tools-settings/src/helper.cpp b/fedora-tools-settings/src/helper.cpp new file mode 100644 index 0000000..7f86ae8 --- /dev/null +++ b/fedora-tools-settings/src/helper.cpp @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT + +#include "packageutils.h" + +#include +#include + +#include +#include +#include + +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" diff --git a/fedora-tools-settings/src/kcm_fedora_tools.json b/fedora-tools-settings/src/kcm_fedora_tools.json new file mode 100644 index 0000000..aeaf97f --- /dev/null +++ b/fedora-tools-settings/src/kcm_fedora_tools.json @@ -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" +} diff --git a/fedora-tools-settings/src/packageutils.cpp b/fedora-tools-settings/src/packageutils.cpp new file mode 100644 index 0000000..6b3cdf1 --- /dev/null +++ b/fedora-tools-settings/src/packageutils.cpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +#include "packageutils.h" + +#include + +#include + +QList parsePackageRecords(const QByteArray &output) +{ + QList records; + + for (const QByteArray &line : output.split('\n')) { + if (line.isEmpty()) { + continue; + } + + const QList 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(); +} diff --git a/fedora-tools-settings/src/packageutils.h b/fedora-tools-settings/src/packageutils.h new file mode 100644 index 0000000..c969754 --- /dev/null +++ b/fedora-tools-settings/src/packageutils.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +struct PackageRecord +{ + QString name; + QString version; + QString architecture; + QString summary; +}; + +QList parsePackageRecords(const QByteArray &output); +bool isValidPackageName(const QString &name); diff --git a/fedora-tools-settings/src/se.ajpanton.fedoratools.actions b/fedora-tools-settings/src/se.ajpanton.fedoratools.actions new file mode 100644 index 0000000..973c5ea --- /dev/null +++ b/fedora-tools-settings/src/se.ajpanton.fedoratools.actions @@ -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 diff --git a/fedora-tools-settings/src/toolmodel.cpp b/fedora-tools-settings/src/toolmodel.cpp new file mode 100644 index 0000000..49178db --- /dev/null +++ b/fedora-tools-settings/src/toolmodel.cpp @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT + +#include "toolmodel.h" + +#include + +#include +#include + +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 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 &installed, + const QList &available, + const QSet &updates) +{ + QMap 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 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; + }); +} diff --git a/fedora-tools-settings/src/toolmodel.h b/fedora-tools-settings/src/toolmodel.h new file mode 100644 index 0000000..16af48d --- /dev/null +++ b/fedora-tools-settings/src/toolmodel.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +#pragma once + +#include "packageutils.h" + +#include +#include + +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 roleNames() const override; + + void setPackages(const QList &installed, + const QList &available, + const QSet &updates = {}); + bool mayInstall(const QString &packageName) const; + +private: + QList m_tools; +}; diff --git a/fedora-tools-settings/src/ui/main.qml b/fedora-tools-settings/src/ui/main.qml new file mode 100644 index 0000000..313c2f0 --- /dev/null +++ b/fedora-tools-settings/src/ui/main.qml @@ -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) + } +} diff --git a/fedora-tools-settings/tests/CMakeLists.txt b/fedora-tools-settings/tests/CMakeLists.txt new file mode 100644 index 0000000..c377988 --- /dev/null +++ b/fedora-tools-settings/tests/CMakeLists.txt @@ -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) diff --git a/fedora-tools-settings/tests/test-packageutils.cpp b/fedora-tools-settings/tests/test-packageutils.cpp new file mode 100644 index 0000000..eea4615 --- /dev/null +++ b/fedora-tools-settings/tests/test-packageutils.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +#include "packageutils.h" + +#include + +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" diff --git a/fedora-tools-settings/tests/test-toolmodel.cpp b/fedora-tools-settings/tests/test-toolmodel.cpp new file mode 100644 index 0000000..40694e1 --- /dev/null +++ b/fedora-tools-settings/tests/test-toolmodel.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT + +#include "toolmodel.h" + +#include + +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" diff --git a/plasma-always-show-unlock/plasma-always-show-unlock.spec b/plasma-always-show-unlock/plasma-always-show-unlock.spec index 88ccaa2..796abe3 100644 --- a/plasma-always-show-unlock/plasma-always-show-unlock.spec +++ b/plasma-always-show-unlock/plasma-always-show-unlock.spec @@ -1,6 +1,6 @@ Name: plasma-always-show-unlock Version: 0.1.0 -Release: 8%{?dist} +Release: 9%{?dist} Summary: Always show the Plasma lock-screen unlock prompt License: MIT AND GPL-2.0-or-later @@ -18,6 +18,7 @@ BuildRequires: patch Requires: bash Requires: patch Requires: plasma-desktop >= 6.7.4 +Provides: fedora-tools-tool %description 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 %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 - Keep the prompt visible without continuously restarting authentication - Retry authentication after resume and the automatic-lock grace period diff --git a/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec b/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec index b60abc9..94dab15 100644 --- a/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec +++ b/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec @@ -1,6 +1,6 @@ Name: plasma-fingerprint-workaround Version: 0.1.0 -Release: 1%{?dist} +Release: 2%{?dist} Summary: Opt-in installer for a patched Fedora KScreenLocker License: MIT @@ -21,6 +21,7 @@ Requires: dnf5 Requires: grep Requires: rpm Requires: sed +Provides: fedora-tools-tool %description Provides an explicit, reversible installer for an experimental KScreenLocker @@ -50,5 +51,8 @@ install -D -m 0644 %{SOURCE3} \ %{_datadir}/plasma-fingerprint-workaround/payload.conf %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 - Initial package diff --git a/plasma-task-group-shortcuts/plasma-task-group-shortcuts.spec b/plasma-task-group-shortcuts/plasma-task-group-shortcuts.spec index 066c515..6d64408 100644 --- a/plasma-task-group-shortcuts/plasma-task-group-shortcuts.spec +++ b/plasma-task-group-shortcuts/plasma-task-group-shortcuts.spec @@ -1,6 +1,6 @@ Name: plasma-task-group-shortcuts Version: 0.1.0 -Release: 12%{?dist} +Release: 13%{?dist} Summary: Application-group shortcuts for the Plasma Task Manager License: MIT @@ -22,6 +22,7 @@ BuildRequires: plasma-workspace-devel >= 6.7 BuildRequires: qt6-qtbase-devel BuildRequires: qt6-qtwayland-devel Requires: plasma-workspace >= 6.7 +Provides: fedora-tools-tool %description 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 %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 - Initial package diff --git a/scripts/build-fedora-tools-settings-rpm b/scripts/build-fedora-tools-settings-rpm new file mode 100755 index 0000000..082d634 --- /dev/null +++ b/scripts/build-fedora-tools-settings-rpm @@ -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" diff --git a/touchpad-hold-tap/touchpad-hold-tap.spec b/touchpad-hold-tap/touchpad-hold-tap.spec index b4fbdbc..fa21551 100644 --- a/touchpad-hold-tap/touchpad-hold-tap.spec +++ b/touchpad-hold-tap/touchpad-hold-tap.spec @@ -1,6 +1,6 @@ Name: touchpad-hold-tap Version: 0.1.0 -Release: 5%{?dist} +Release: 6%{?dist} Summary: Hold-tap middle-click gesture for touchpads License: MIT @@ -12,6 +12,7 @@ Source3: README.md BuildRequires: lua Requires: libinput >= 1.30 +Provides: fedora-tools-tool %description 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 %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 - Run state-machine tests during RPM builds and package the README