From ed2bb7f92c79134d534b7c9be3b17996d6661929 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Mon, 7 Sep 2026 19:47:28 +0000 Subject: [PATCH] Discover external tool settings and refresh cached settings UI --- fedora-tools-settings/README.md | 14 +++++ .../fedora-tools-settings.spec | 31 +++++++++- fedora-tools-settings/src/fedoratoolskcm.cpp | 13 ++++ fedora-tools-settings/src/fedoratoolskcm.h | 1 + fedora-tools-settings/src/helper.cpp | 11 ++++ fedora-tools-settings/src/toolmodel.cpp | 62 ++++++++++++++++++- fedora-tools-settings/src/toolmodel.h | 4 ++ fedora-tools-settings/src/ui/main.qml | 5 ++ fedora-tools-settings/tests/CMakeLists.txt | 6 ++ .../tests/test-qmlresource.cpp | 30 +++++++++ .../tests/test-toolmodel.cpp | 28 +++++++++ 11 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 fedora-tools-settings/tests/test-qmlresource.cpp diff --git a/fedora-tools-settings/README.md b/fedora-tools-settings/README.md index daefd68..eb56747 100644 --- a/fedora-tools-settings/README.md +++ b/fedora-tools-settings/README.md @@ -19,6 +19,20 @@ operations run one at a time; other actions remain disabled until DNF finishes. The module currently configures task-group shortcut behavior, touchpad hold-tap timing and output, and the experimental fingerprint workaround. +Installed packages are discovered independently of repository metadata, so +locally installed tools appear even before they are published. Tools with their +own settings window can register it by installing +`/usr/share/fedora-tools/settings/PACKAGE-NAME.json`: + +```json +{"command": ["/usr/bin/example-tool", "--settings"]} +``` + +The Configure button launches this command as the current user, without a +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. + Installing this module does not install or activate any other tool. Tools remain usable without the module. diff --git a/fedora-tools-settings/fedora-tools-settings.spec b/fedora-tools-settings/fedora-tools-settings.spec index af1485d..55b8cc9 100644 --- a/fedora-tools-settings/fedora-tools-settings.spec +++ b/fedora-tools-settings/fedora-tools-settings.spec @@ -1,6 +1,6 @@ Name: fedora-tools-settings Version: 0.1.0 -Release: 7%{?dist} +Release: 11%{?dist} Summary: Plasma System Settings module for Fedora Tools License: MIT @@ -36,7 +36,15 @@ tools after administrator authorization. %build %cmake -%cmake_build +# RCC otherwise gives every revision built on the same changelog date the +# same timestamp, allowing Qt to reuse an older QML disk cache. Keep the +# resource stamp reproducible, but make it depend on the actual UI contents. +qml_checksum=$(find src/ui -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum) +( + unset SOURCE_DATE_EPOCH + export QT_RCC_SOURCE_DATE_OVERRIDE=$((16#${qml_checksum:0:8})) + %cmake_build +) %check %ctest @@ -48,6 +56,13 @@ install -Dpm 0644 %{SOURCE1} \ install -Dpm 0644 %{SOURCE2} \ %{buildroot}%{_docdir}/%{name}/README.md +%post +# Releases through 0.1.0-7 let privileged DNF processes inherit KAuth's +# restrictive umask. Restore DNF's standard permissions on files they created. +for file in environments.toml groups.toml modules.toml nevras.toml packages.toml system.toml; do + test ! -e "%{_prefix}/lib/sysimage/libdnf5/$file" || chmod 0644 "%{_prefix}/lib/sysimage/libdnf5/$file" +done + %files %license %{_licensedir}/%{name}/LICENSE %doc %{_docdir}/%{name}/README.md @@ -59,6 +74,18 @@ install -Dpm 0644 %{SOURCE2} \ %{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy %changelog +* Mon Sep 07 2026 fedora-tools contributors - 0.1.0-11 +- Invalidate cached QML whenever the embedded settings pages change + +* Sun Sep 06 2026 fedora-tools contributors - 0.1.0-10 +- Discover configuration applications registered by installed tools + +* Sun Sep 06 2026 fedora-tools contributors - 0.1.0-9 +- Open the Panel Actions widget's native per-panel configuration + +* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-8 +- Preserve standard permissions on DNF system-state files + * Sat Sep 05 2026 fedora-tools contributors - 0.1.0-7 - Use the DNF upgrade transaction for installed tools diff --git a/fedora-tools-settings/src/fedoratoolskcm.cpp b/fedora-tools-settings/src/fedoratoolskcm.cpp index 49f1bb8..73b2950 100644 --- a/fedora-tools-settings/src/fedoratoolskcm.cpp +++ b/fedora-tools-settings/src/fedoratoolskcm.cpp @@ -51,6 +51,19 @@ QAbstractItemModel *FedoraToolsKcm::tools() return &m_tools; } +void FedoraToolsKcm::configureExternalTool(const QString &packageName) +{ + QStringList command = m_tools.configurationCommand(packageName); + if (command.isEmpty()) { + setMessage(i18n("No configuration application is registered for %1.", packageName), true); + return; + } + const QString executable = command.takeFirst(); + if (!QProcess::startDetached(executable, command)) { + setMessage(i18n("Could not open settings for %1.", packageName), true); + } +} + bool FedoraToolsKcm::busy() const { return m_busy; diff --git a/fedora-tools-settings/src/fedoratoolskcm.h b/fedora-tools-settings/src/fedoratoolskcm.h index cdcb94b..9c2a92c 100644 --- a/fedora-tools-settings/src/fedoratoolskcm.h +++ b/fedora-tools-settings/src/fedoratoolskcm.h @@ -53,6 +53,7 @@ public: Q_INVOKABLE void refresh(bool refreshMetadata = false); Q_INVOKABLE void clearMessage(); + Q_INVOKABLE void configureExternalTool(const QString &packageName); Q_INVOKABLE void installTool(const QString &packageName); Q_INVOKABLE void removeTool(const QString &packageName); Q_INVOKABLE void saveShortcutSettings(bool startWithFirst, bool initialShiftOpensNew, bool shiftCyclesBackward); diff --git a/fedora-tools-settings/src/helper.cpp b/fedora-tools-settings/src/helper.cpp index dffd1f3..04b33d4 100644 --- a/fedora-tools-settings/src/helper.cpp +++ b/fedora-tools-settings/src/helper.cpp @@ -10,13 +10,23 @@ #include #include +#include + namespace { constexpr int commandTimeoutMs = 10 * 60 * 1000; +void setChildUmask(QProcess &process) +{ + process.setChildProcessModifier([]() { + umask(0022); + }); +} + KAuth::ActionReply commandReply(const QString &program, const QStringList &arguments) { QProcess process; + setChildUmask(process); process.setProcessChannelMode(QProcess::MergedChannels); process.start(program, arguments); if (!process.waitForStarted()) { @@ -52,6 +62,7 @@ KAuth::ActionReply commandReply(const QString &program, const QStringList &argum bool isPublishedTool(const QString &packageName) { QProcess process; + setChildUmask(process); process.start(QStringLiteral("/usr/bin/dnf5"), {QStringLiteral("--repo=fedora-tools"), QStringLiteral("-q"), diff --git a/fedora-tools-settings/src/toolmodel.cpp b/fedora-tools-settings/src/toolmodel.cpp index 741a6a1..62f3792 100644 --- a/fedora-tools-settings/src/toolmodel.cpp +++ b/fedora-tools-settings/src/toolmodel.cpp @@ -3,10 +3,55 @@ #include "toolmodel.h" #include +#include +#include +#include +#include +#include +#include +#include #include #include +namespace +{ +QStringList readConfigurationCommand(const QString &packageName) +{ + const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QStringLiteral("fedora-tools/settings/") + packageName + QStringLiteral(".json")); + if (path.isEmpty()) { + return {}; + } + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Cannot read tool configuration registration:" << path << file.errorString(); + return {}; + } + QJsonParseError error; + const auto document = QJsonDocument::fromJson(file.readAll(), &error); + const auto command = document.object().value(QStringLiteral("command")).toArray(); + if (error.error != QJsonParseError::NoError || command.isEmpty()) { + qWarning() << "Invalid tool configuration registration:" << path; + return {}; + } + QStringList result; + for (const auto &argument : command) { + if (!argument.isString()) { + qWarning() << "Invalid tool configuration argument:" << path; + return {}; + } + result.append(argument.toString()); + } + const QFileInfo executable(result.first()); + if (!executable.isAbsolute() || !executable.isExecutable() || !executable.isFile()) { + qWarning() << "Tool configuration executable is unavailable:" << result.first(); + return {}; + } + return result; +} +} + ToolModel::ToolModel(QObject *parent) : QAbstractListModel(parent) { @@ -43,9 +88,12 @@ QVariant ToolModel::data(const QModelIndex &index, int role) const return tool.updateAvailable; case ConfigurableRole: return tool.installed - && (tool.packageName == QStringLiteral("plasma-fingerprint-workaround") + && (!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(); default: return {}; } @@ -63,6 +111,7 @@ QHash ToolModel::roleNames() const {AvailableRole, "available"}, {UpdateAvailableRole, "updateAvailable"}, {ConfigurableRole, "configurable"}, + {ExternalConfigurationRole, "externalConfiguration"}, }; } @@ -79,6 +128,7 @@ void ToolModel::setPackages(const QList &installed, tool.architecture = package.architecture; tool.summary = package.summary; tool.installed = true; + tool.configurationCommand = readConfigurationCommand(package.name); } for (const PackageRecord &package : available) { @@ -116,3 +166,13 @@ bool ToolModel::mayRemove(const QString &packageName) const return tool.packageName == packageName && tool.installed; }); } + +QStringList ToolModel::configurationCommand(const QString &packageName) const +{ + for (const Tool &tool : m_tools) { + if (tool.packageName == packageName && tool.installed) { + return tool.configurationCommand; + } + } + return {}; +} diff --git a/fedora-tools-settings/src/toolmodel.h b/fedora-tools-settings/src/toolmodel.h index 116b85a..501b24f 100644 --- a/fedora-tools-settings/src/toolmodel.h +++ b/fedora-tools-settings/src/toolmodel.h @@ -6,6 +6,7 @@ #include #include +#include struct Tool { @@ -14,6 +15,7 @@ struct Tool QString availableVersion; QString architecture; QString summary; + QStringList configurationCommand; bool installed = false; bool available = false; bool updateAvailable = false; @@ -34,6 +36,7 @@ public: AvailableRole, UpdateAvailableRole, ConfigurableRole, + ExternalConfigurationRole, }; explicit ToolModel(QObject *parent = nullptr); @@ -47,6 +50,7 @@ public: const QSet &updates = {}); bool mayInstall(const QString &packageName) const; bool mayRemove(const QString &packageName) const; + QStringList configurationCommand(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 index e4ac736..950fd01 100644 --- a/fedora-tools-settings/src/ui/main.qml +++ b/fedora-tools-settings/src/ui/main.qml @@ -86,6 +86,7 @@ KCM.SimpleKCM { required property bool available required property bool updateAvailable required property bool configurable + required property bool externalConfiguration width: toolList.width @@ -138,6 +139,10 @@ KCM.SimpleKCM { enabled: !kcm.busy onClicked: { kcm.clearMessage() + if (toolCard.externalConfiguration) { + kcm.configureExternalTool(toolCard.packageName) + return + } root.settingsPage = toolCard.packageName if (root.settingsPage === "plasma-task-group-shortcuts") { shortcutPage.loadSettings() diff --git a/fedora-tools-settings/tests/CMakeLists.txt b/fedora-tools-settings/tests/CMakeLists.txt index c377988..16ac365 100644 --- a/fedora-tools-settings/tests/CMakeLists.txt +++ b/fedora-tools-settings/tests/CMakeLists.txt @@ -16,3 +16,9 @@ add_executable(test-toolmodel target_include_directories(test-toolmodel PRIVATE ../src) target_link_libraries(test-toolmodel PRIVATE Qt6::Core Qt6::Test) add_test(NAME toolmodel COMMAND test-toolmodel) + +add_executable(test-qmlresource test-qmlresource.cpp) +target_link_libraries(test-qmlresource PRIVATE Qt6::Core Qt6::Test) +target_compile_definitions(test-qmlresource PRIVATE KCM_PLUGIN_PATH="$") +add_dependencies(test-qmlresource kcm_fedora_tools) +add_test(NAME qmlresource COMMAND test-qmlresource) diff --git a/fedora-tools-settings/tests/test-qmlresource.cpp b/fedora-tools-settings/tests/test-qmlresource.cpp new file mode 100644 index 0000000..1ce9acd --- /dev/null +++ b/fedora-tools-settings/tests/test-qmlresource.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +#include + +class QmlResourceTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void packagedUiHasItsOwnCacheStamp() + { + QLibrary plugin(QString::fromUtf8(KCM_PLUGIN_PATH)); + QVERIFY2(plugin.load(), qPrintable(plugin.errorString())); + QFile ui(QStringLiteral(":/kcm/kcm_fedora_tools/main.qml")); + QVERIFY(ui.open(QIODevice::ReadOnly)); + const auto source = ui.readAll(); + QVERIFY(source.contains("configureExternalTool")); + QVERIFY(!source.contains("configurePanelActions")); + const auto stamp = QFileInfo(ui).lastModified().toSecsSinceEpoch(); + QVERIFY(stamp > 0); + const auto packageDate = qgetenv("SOURCE_DATE_EPOCH").toLongLong(); + if (packageDate > 0) { + QVERIFY2(stamp != packageDate, "QML resources must not share the RPM changelog timestamp across revisions"); + } + } +}; +QTEST_GUILESS_MAIN(QmlResourceTest) +#include "test-qmlresource.moc" diff --git a/fedora-tools-settings/tests/test-toolmodel.cpp b/fedora-tools-settings/tests/test-toolmodel.cpp index 277c74b..aeb89e2 100644 --- a/fedora-tools-settings/tests/test-toolmodel.cpp +++ b/fedora-tools-settings/tests/test-toolmodel.cpp @@ -3,12 +3,40 @@ #include "toolmodel.h" #include +#include +#include +#include class ToolModelTest : public QObject { Q_OBJECT private Q_SLOTS: + void discoversLocallyInstalledToolSettings() + { + QTemporaryDir directory; + QVERIFY(directory.isValid()); + const QByteArray original = qgetenv("XDG_DATA_HOME"); + qputenv("XDG_DATA_HOME", directory.path().toUtf8()); + QVERIFY(QDir(directory.path()).mkpath(QStringLiteral("fedora-tools/settings"))); + QFile registration(directory.filePath(QStringLiteral("fedora-tools/settings/local-tool.json"))); + QVERIFY(registration.open(QIODevice::WriteOnly)); + registration.write("{\"command\":[\"/bin/true\",\"--settings\"]}"); + registration.close(); + ToolModel model; + const PackageRecord package{QStringLiteral("local-tool"), QStringLiteral("1"), QStringLiteral("noarch"), QStringLiteral("Local tool")}; + model.setPackages({package}, {}); + QCOMPARE(model.rowCount(), 1); + QVERIFY(model.data(model.index(0), ToolModel::ConfigurableRole).toBool()); + QVERIFY(model.data(model.index(0), ToolModel::ExternalConfigurationRole).toBool()); + QVERIFY(!model.data(model.index(0), ToolModel::AvailableRole).toBool()); + QCOMPARE(model.configurationCommand(package.name), QStringList({QStringLiteral("/bin/true"), QStringLiteral("--settings")})); + // Repository entries alone must not expose locally registered commands. + model.setPackages({}, {package}); + QVERIFY(!model.data(model.index(0), ToolModel::ConfigurableRole).toBool()); + QVERIFY(model.configurationCommand(package.name).isEmpty()); + qputenv("XDG_DATA_HOME", original); + } void mergesInstalledAndAvailablePackages() { ToolModel model;