Discover external tool settings and refresh cached settings UI
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
# 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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -10,13 +10,23 @@
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
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"),
|
||||
|
||||
@@ -3,10 +3,55 @@
|
||||
#include "toolmodel.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
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<int, QByteArray> ToolModel::roleNames() const
|
||||
{AvailableRole, "available"},
|
||||
{UpdateAvailableRole, "updateAvailable"},
|
||||
{ConfigurableRole, "configurable"},
|
||||
{ExternalConfigurationRole, "externalConfiguration"},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,6 +128,7 @@ void ToolModel::setPackages(const QList<PackageRecord> &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 {};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
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<QString> &updates = {});
|
||||
bool mayInstall(const QString &packageName) const;
|
||||
bool mayRemove(const QString &packageName) const;
|
||||
QStringList configurationCommand(const QString &packageName) const;
|
||||
|
||||
private:
|
||||
QList<Tool> m_tools;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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="$<TARGET_FILE:kcm_fedora_tools>")
|
||||
add_dependencies(test-qmlresource kcm_fedora_tools)
|
||||
add_test(NAME qmlresource COMMAND test-qmlresource)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QLibrary>
|
||||
#include <QTest>
|
||||
|
||||
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"
|
||||
@@ -3,12 +3,40 @@
|
||||
#include "toolmodel.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user