[plasma/union] tools: tools: Add styletool as a helper CLI for working with styles
Arjen Hiemstra <[email protected]> Wed, 5 Aug 2026 10:33:24 +0000 (UTC)
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 97ad2c5aba4b2ff9ba4763f4d4bc98da55561559 by Arjen Hiemstra. Committed on 05/08/2026 at 10:24. Pushed by ahiemstra into branch 'master'. tools: Add styletool as a helper CLI for working with styles This allows listing, installing and other operations on style packages. M +1 -0 tools/CMakeLists.txt A +9 -0 tools/styletool/CMakeLists.txt A +149 -0 tools/styletool/Common.h [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)] A +164 -0 tools/styletool/styletool.cpp [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)] https://invent.kde.org/plasma/union/-/commit/97ad2c5aba4b2ff9ba4763f4d4bc98da55561559 diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index ad064a8f..b324409a 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -2,3 +2,4 @@ # SPDX-FileCopyrightText: 2024 Arjen Hiemstra <[email protected]> add_subdirectory(ruleinspector) +add_subdirectory(styletool) diff --git a/tools/styletool/CMakeLists.txt b/tools/styletool/CMakeLists.txt new file mode 100644 index 00000000..4ac2c698 --- /dev/null +++ b/tools/styletool/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: BSD-2-Clause +# SPDX-FileCopyrightText: 2026 Arjen Hiemstra <[email protected]> + +add_executable(union-styletool) +target_sources(union-styletool PRIVATE styletool.cpp) + +target_link_libraries(union-styletool PRIVATE Union::Union Qt::Core) + +install(TARGETS union-styletool ${KDE_INSTALL_DEFAULT_ARGS}) diff --git a/tools/styletool/Common.h b/tools/styletool/Common.h new file mode 100644 index 00000000..adc95662 --- /dev/null +++ b/tools/styletool/Common.h @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +// SPDX-FileCopyrightText: 2026 Arjen Hiemstra <[email protected]> + +#include <filesystem> +#include <iostream> + +#include <QCommandLineParser> +#include <QCoreApplication> +#include <QFile> +#include <QStandardPaths> +#include <QTextStream> + +#include <PackageHandler.h> +#include <StylePackage.h> + +using namespace Qt::StringLiterals; + +namespace fs = std::filesystem; + +void showCommandHelp(QCommandLineParser *parser, const QString &command, const QString &errorMessage = {}, int exitCode = 0) +{ + auto help = parser->helpText(); + help.replace(u"union-styletool"_s, u"union-styletool " + command); + + if (!errorMessage.isEmpty()) { + help.prepend(errorMessage + u"\n"); + parser->showMessageAndExit(QCommandLineParser::MessageType::Error, help, exitCode); + } else { + parser->showMessageAndExit(QCommandLineParser::MessageType::Information, help, exitCode); + } +} + +[[nodiscard]] std::unique_ptr<QCommandLineParser> +parseCommand(const QString &command, const QStringList &arguments, std::function<void(QCommandLineParser *)> customiseFunction) +{ + auto commandParser = std::make_unique<QCommandLineParser>(); + commandParser->addOption({u"help"_s, u"Displays help for the current command."_s}); + + customiseFunction(commandParser.get()); + + QStringList argumentsWithApplication = {QCoreApplication::applicationName()}; + argumentsWithApplication.append(arguments); + + if (!commandParser->parse(argumentsWithApplication)) { + QString errorMessage; + if (!commandParser->unknownOptionNames().isEmpty()) { + errorMessage = u"Unknown option for command "_s + command + u": " + commandParser->unknownOptionNames().first(); + } + + showCommandHelp(commandParser.get(), command, errorMessage, 1); + } + + if (commandParser->isSet(u"help"_s)) { + showCommandHelp(commandParser.get(), command); + } + + return commandParser; +} + +void readInput(QString &output, QStringView message, std::function<bool(QString)> validationFunction, QString defaultValue = {}) +{ + Q_ASSERT(!message.isEmpty()); + + if (output.isEmpty()) { + QTextStream inputStream(stdin); + QString input; + while (input.isEmpty()) { + std::cout << message.toUtf8().data(); + if (!defaultValue.isEmpty()) { + std::cout << " [Enter to use: " << qPrintable(defaultValue) << "]" << std::endl; + } else { + std::cout << std::endl; + } + + inputStream.readLineInto(&input, 1024); + + if (input.isEmpty() && !defaultValue.isEmpty()) { + input = defaultValue; + } + + if (!validationFunction(input)) { + input = QString{}; + } + } + + output = input; + } +} + +void readInput(QString &output, QStringView message, QString defaultValue = {}) +{ + readInput( + output, + message, + [](const QString &) { + return true; + }, + defaultValue); +} + +int printPackageError(const Union::StylePackage &package) +{ + auto path = package.path(); + switch (package.error()) { + case Union::StylePackage::Error::NotFound: + std::cerr << path << " does not exist.\n"; + return 1; + case Union::StylePackage::Error::MissingFiles: + std::cerr << path << " is an invalid style because it is missing required files.\n"; + return 2; + case Union::StylePackage::Error::InvalidMetaData: + std::cerr << path << " is an invalid style because its metadata could not be read.\n"; + return 3; + case Union::StylePackage::Error::UnknownInputType: + std::cerr << path << " is an invalid style because it uses an unknown input type.\n"; + return 4; + case Union::StylePackage::Error::None: + return 0; + } + + return 0; +} + +int printHandlerError(const Union::StylePackage &package, Union::PackageHandler::Error error) +{ + auto path = package.path(); + switch (error) { + case Union::PackageHandler::Error::InvalidPackage: + std::cerr << path << "is an invalid style:\n"; + printPackageError(package); + return 5; + case Union::PackageHandler::Error::AlreadyInstalled: + std::cerr << "A style with ID " << qPrintable(package.id()) << " is already installed.\n"; + return 6; + case Union::PackageHandler::Error::FilesystemError: + std::cerr << "A filesystem error occurred.\n"; + return 7; + case Union::PackageHandler::Error::NotInstalled: + std::cerr << "The style " << qPrintable(package.id()) << " is not installed.\n"; + return 8; + case Union::PackageHandler::Error::PackageExists: + std::cerr << "A style already exists at " << package.path() << "\n"; + return 9; + case Union::PackageHandler::Error::None: + break; + } + + return 0; +} diff --git a/tools/styletool/styletool.cpp b/tools/styletool/styletool.cpp new file mode 100644 index 00000000..027be97e --- /dev/null +++ b/tools/styletool/styletool.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +// SPDX-FileCopyrightText: 2026 Arjen Hiemstra <[email protected]> + +#include <condition_variable> +#include <mutex> + +#include <QStandardPaths> + +#include <StyleRegistry.h> + +#include "Common.h" + +int handleInstallCommand([[maybe_unused]] const QStringList &arguments) +{ + return 0; +} + +int handleCreateCommand([[maybe_unused]] const QStringList &arguments) +{ + return 0; +} + +int handleInspectCommand([[maybe_unused]] const QStringList &arguments) +{ + auto parser = parseCommand(u"inspect"_s, arguments, [](QCommandLineParser *parser) { + parser->setApplicationDescription(u"Inspect details of an installed Union style."_s); + parser->addPositionalArgument(u"<path or id>"_s, u"The path of a style or the ID of an installed style to inspect."_s); + }); + + if (parser->positionalArguments().size() != 1) { + showCommandHelp(parser.get(), u"inspect"_s, u"Invalid arguments for command \"inspect\""_s, 1); + } + + auto pathOrId = parser->positionalArguments().first(); + + auto handler = Union::StyleRegistry::instance()->packageHandler(); + + auto package = handler->package(pathOrId); + if (!package.isValid()) { + package = Union::StylePackage{fs::absolute(fs::path(pathOrId.toStdString()))}; + } + + if (!package.isValid()) { + return printPackageError(package); + } + + std::cout << "Path: " << package.path().string() << "\n"; + std::cout << "Input Type: " << qPrintable(package.inputType()) << "\n"; + std::cout << "Name: " << qPrintable(package.name()) << "\n"; + std::cout << "Description: " << qPrintable(package.description()) << "\n"; + std::cout << "Version: " << qPrintable(package.version()) << "\n"; + std::cout << "License: " << qPrintable(package.license()) << "\n"; + std::cout << "Authors: " << qPrintable(package.authors().join(u", ")) << "\n"; + + return 0; +} + +int handleListCommand([[maybe_unused]] const QStringList &arguments) +{ + auto parser = parseCommand(u"list"_s, arguments, [](QCommandLineParser *parser) { + parser->setApplicationDescription(u"List all the installed Union styles."_s); + parser->addOption({u"hidden"_s, u"Include hidden packages."_s}); + parser->addOption({u"details"_s, u"Show details information about each package."_s}); + }); + + auto handler = Union::StyleRegistry::instance()->packageHandler(); + auto packages = handler->allPackages(); + if (packages.isEmpty()) { + std::cerr << "No styles could be found.\n"; + return 1; + } + + std::ranges::stable_sort(packages, [](const Union::StylePackage &first, const Union::StylePackage &second) { + return first.id() < second.name(); + }); + + for (const auto &package : packages) { + if (!parser->isSet(u"details"_s)) { + std::cout << "- " << qPrintable(package.id()) << ": " << qPrintable(package.name()) << "\n"; + } else { + std::cout << "- Path: " << package.path().string() << "\n"; + std::cout << " Input Type: " << qPrintable(package.inputType()) << "\n"; + std::cout << " Name: " << qPrintable(package.name()) << "\n"; + std::cout << " Description: " << qPrintable(package.description()) << "\n"; + std::cout << " Version: " << qPrintable(package.version()) << "\n"; + std::cout << " License: " << qPrintable(package.license()) << "\n"; + std::cout << " Authors: " << qPrintable(package.authors().join(u", ")) << "\n"; + std::cout << std::endl; + } + } + + return 0; +} + +int handleUpdateCommand([[maybe_unused]] const QStringList &arguments) +{ + return 0; +} + +int handleUninstallCommand([[maybe_unused]] const QStringList &arguments) +{ + return 0; +} + +int main(int argc, char **argv) +{ + QCoreApplication application(argc, argv); + QCoreApplication::setApplicationVersion(u"1.0"_s); + + QCommandLineParser parser; + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments); + auto versionOption = parser.addVersionOption(); + auto helpOption = parser.addHelpOption(); + + auto commandHelp = u"The command to execute.\nAvailable commands:\n"_s; + commandHelp += u" create Create a new style.\n"_s; + commandHelp += u" inspect Inspect details of an installed style.\n"_s; + commandHelp += u" install Install a new style.\n"_s; + commandHelp += u" list List installed styles.\n"_s; + commandHelp += u" update Update an installed style.\n"_s; + commandHelp += u" uninstall Uninstall an installed style.\n"_s; + + parser.addPositionalArgument(u"<command>"_s, commandHelp); + + auto result = parser.parse(application.arguments()); + if (!result && parser.unknownOptionNames().isEmpty()) { + parser.showHelp(1); + } + + if (parser.isSet(versionOption)) { + parser.showVersion(); + } + + if (parser.isSet(helpOption)) { + parser.showHelp(0); + } + + if (parser.positionalArguments().isEmpty()) { + parser.showHelp(1); + } + + auto remaining = parser.positionalArguments(); + auto command = remaining.takeFirst().toLower(); + + int commandResult = 0; + + if (command == u"create") { + commandResult = handleCreateCommand(remaining); + } else if (command == u"inspect") { + commandResult = handleInspectCommand(remaining); + } else if (command == u"install") { + commandResult = handleInstallCommand(remaining); + } else if (command == u"list") { + commandResult = handleListCommand(remaining); + } else if (command == u"update") { + commandResult = handleUpdateCommand(remaining); + } else if (command == u"uninstall") { + commandResult = handleUninstallCommand(remaining); + } else { + parser.showMessageAndExit(QCommandLineParser::MessageType::Error, u"Unknown command: "_s + command, 1); + } + + return commandResult; +}