[plasma/union] /: Implement a create command in styletool and PackageHandler
Arjen Hiemstra <[email protected]> Wed, 5 Aug 2026 10:33:24 +0000 (UTC)
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 0b8f605d7dec63eddad9c299677bb139f339f258 by Arjen Hiemstra.
Committed on 05/08/2026 at 10:24.
Pushed by ahiemstra into branch 'master'.
Implement a create command in styletool and PackageHandler
This will create a barebones style package that contains everything that
is needed for a valid package of a certain input type. This makes it
easy to get started creating new styles.
M +7 -0 src/InputPlugin.h
M +67 -0 src/PackageHandler.cpp
M +25 -0 src/PackageHandler.h
M +49 -0 src/input/css/CssPlugin.cpp
M +2 -0 src/input/css/CssPlugin.h
M +84 -0 tools/styletool/styletool.cpp
https://invent.kde.org/plasma/union/-/commit/0b8f605d7dec63eddad9c299677bb139f339f258
diff --git a/src/InputPlugin.h b/src/InputPlugin.h
index 15aa0f7a..18cf2a11 100644
--- a/src/InputPlugin.h
+++ b/src/InputPlugin.h
@@ -3,6 +3,7 @@
#pragma once
+#include "PackageHandler.h"
#include "PluginRegistry.h"
#include "StylePackage.h"
@@ -12,6 +13,7 @@ namespace Union
{
class Style;
+
/*!
* \class Union::InputPlugin
* \inmodule core
@@ -38,6 +40,11 @@ public:
*/
virtual StylePackage::Error validatePackage(const StylePackage &package) = 0;
+ /*!
+ * Create any files required for the package to be valid.
+ */
+ virtual PackageHandler::Error createPackage(const StylePackage &package) = 0;
+
/*!
* Create an instance of a style from a package.
*
diff --git a/src/PackageHandler.cpp b/src/PackageHandler.cpp
index c93e3249..e544b163 100644
--- a/src/PackageHandler.cpp
+++ b/src/PackageHandler.cpp
@@ -71,6 +71,73 @@ QList<StylePackage> PackageHandler::allPackages()
return result;
}
+PackageHandler::Error PackageHandler::create(StylePackage &destination, const PackageHandler::CreateInfo &info)
+{
+ if (fs::exists(info.path)) {
+ return Error::PackageExists;
+ }
+
+ if (!fs::create_directories(info.path)) {
+ return Error::FilesystemError;
+ }
+
+ if (!fs::create_directories(info.path / "contents")) {
+ return Error::FilesystemError;
+ }
+
+ auto inputPlugin = InputPlugin::inputPlugin(info.inputType);
+ if (!inputPlugin) {
+ return Error::UnknownInputType;
+ }
+
+ QFile metaDataFile(info.path / "metadata.json");
+ if (!metaDataFile.open(QIODevice::WriteOnly)) {
+ return Error::FilesystemError;
+ }
+
+ QTextStream stream{&metaDataFile};
+ stream << "{\n";
+ stream << R"( "input-type": ")" << info.inputType << "\",\n";
+ stream << R"( "name": ")" << info.name << "\"";
+ if (!info.description.isEmpty()) {
+ stream << ",\n";
+ stream << R"( "description": ")" << info.description << "\"";
+ }
+
+ if (!info.version.isEmpty()) {
+ stream << ",\n";
+ stream << R"( "version": ")" << info.version << "\"";
+ }
+ if (!info.license.isEmpty()) {
+ stream << ",\n";
+ stream << R"( "license": ")" << info.license << "\"";
+ }
+ if (!info.url.isEmpty()) {
+ stream << ",\n";
+ stream << R"( "url": ")" << info.url.toString() << "\"";
+ }
+ if (!info.authors.isEmpty()) {
+ stream << ",\n";
+ stream << R"( "authors": [)" << "\n";
+ for (int i = 0; i < info.authors.size(); ++i) {
+ stream << " \"" << info.authors.at(i) << "\"";
+ if (i < info.authors.size() - 1) {
+ stream << ",";
+ }
+ stream << "\n";
+ }
+ stream << " ]";
+ }
+ stream << "\n";
+ stream << "}\n";
+
+ stream.flush();
+ metaDataFile.close();
+
+ destination = StylePackage{info.path};
+ return inputPlugin->createPackage(destination);
+}
+
PackageHandler::Error PackageHandler::install(const StylePackage &package)
{
if (!package.isValid()) {
diff --git a/src/PackageHandler.h b/src/PackageHandler.h
index a631243c..9c52447b 100644
--- a/src/PackageHandler.h
+++ b/src/PackageHandler.h
@@ -5,6 +5,8 @@
#include <filesystem>
+#include <QUrl>
+
#include "StylePackage.h"
#include "union_export.h"
@@ -43,6 +45,12 @@ public:
* \value NotAnUpdate
* The operation failed because the provided package is not considered
* to be an update for the existing installed package.
+ * \value PackageExists
+ * The operation failed because a package already exists at the
+ * location provided. Note that this differs from AlreadyInstalled in
+ * that this may also occur for non-install paths.
+ * \value UnknownInputType
+ * The operation failed because an unknown input type was requested.
*/
enum class Error {
None,
@@ -51,6 +59,19 @@ public:
FilesystemError,
NotInstalled,
NotAnUpdate,
+ PackageExists,
+ UnknownInputType,
+ };
+
+ struct CreateInfo {
+ std::filesystem::path path;
+ QString inputType;
+ QString name;
+ QString description;
+ QString version;
+ QString license;
+ QUrl url;
+ QStringList authors;
};
PackageHandler(const std::shared_ptr<PlatformPlugin> &platformPlugin);
@@ -66,6 +87,10 @@ public:
*/
QList<StylePackage> allPackages();
+ /*!
+ * Create a new package.
+ */
+ Error create(StylePackage &destination, const CreateInfo &info);
/*!
* Install a package.
*
diff --git a/src/input/css/CssPlugin.cpp b/src/input/css/CssPlugin.cpp
index 2e880924..0d2df655 100644
--- a/src/input/css/CssPlugin.cpp
+++ b/src/input/css/CssPlugin.cpp
@@ -3,6 +3,10 @@
#include "CssPlugin.h"
+#include <fstream>
+
+#include <QFile>
+
#include <Style.h>
#include <StylePackage.h>
@@ -26,6 +30,51 @@ Union::StylePackage::Error CssPlugin::validatePackage(const Union::StylePackage
return StylePackage::Error::None;
}
+Union::PackageHandler::Error CssPlugin::createPackage(const Union::StylePackage &package)
+{
+ auto path = package.path();
+
+ if (!fs::create_directories(path / "contents" / "css")) {
+ return Union::PackageHandler::Error::FilesystemError;
+ }
+
+ auto year = QDate::currentDate().year();
+ auto author = package.authors().isEmpty() ? u"Unknown"_s : package.authors().first();
+
+ {
+ std::ofstream stream{path / "contents" / "css" / "style.css", std::ios::out};
+ // REUSE-IgnoreStart
+ stream << "/* SPDX-License-Identifier: " << (package.license().isEmpty() ? "Unknown" : qPrintable(package.license())) << " */\n";
+ stream << "/* SPDX-FileCopyrightText: " << year << " " << qPrintable(author) << " */\n";
+ stream << "\n";
+ stream << "/* This is the main CSS file for the Union style " << qPrintable(package.name()) << " */\n";
+ stream << "\n";
+ stream << "/* It is recommended to not place any style rules in this file, but instead\n";
+ stream << " create separate files for different categories of rules and use @import\n";
+ stream << " here to include them in the style. */\n";
+ // REUSE-IgnoreEnd
+
+ if (stream.fail()) {
+ return Union::PackageHandler::Error::FilesystemError;
+ }
+ }
+
+ if (!fs::create_directories(path / "contents" / "images")) {
+ return Union::PackageHandler::Error::FilesystemError;
+ }
+
+ {
+ std::ofstream stream{path / "contents" / "images" / "README", std::ios::out};
+ stream << "This directory should contain any images your style uses.\n";
+
+ if (stream.fail()) {
+ return Union::PackageHandler::Error::FilesystemError;
+ }
+ }
+
+ return Union::PackageHandler::Error::None;
+}
+
std::shared_ptr<Union::Style> CssPlugin::createStyle(const Union::StylePackage &package) const
{
return Union::Style::create(package.path(), std::make_unique<CssLoader>());
diff --git a/src/input/css/CssPlugin.h b/src/input/css/CssPlugin.h
index 8a31ef95..d26636ad 100644
--- a/src/input/css/CssPlugin.h
+++ b/src/input/css/CssPlugin.h
@@ -21,5 +21,7 @@ public:
Union::StylePackage::Error validatePackage(const Union::StylePackage &package) override;
+ Union::PackageHandler::Error createPackage(const Union::StylePackage &package) override;
+
std::shared_ptr<Union::Style> createStyle(const Union::StylePackage &package) const override;
};
diff --git a/tools/styletool/styletool.cpp b/tools/styletool/styletool.cpp
index 7af2402f..d0f10e1e 100644
--- a/tools/styletool/styletool.cpp
+++ b/tools/styletool/styletool.cpp
@@ -40,6 +40,90 @@ int handleInstallCommand([[maybe_unused]] const QStringList &arguments)
int handleCreateCommand([[maybe_unused]] const QStringList &arguments)
{
+ auto parser = parseCommand(u"create"_s, arguments, [](QCommandLineParser *parser) {
+ parser->setApplicationDescription(u"Create a new style. Any missing information will be requested interactively."_s);
+ parser->addOptions({
+ {u"name"_s, u"The name of the style."_s, u"name"_s},
+ {u"id"_s, u"The ID of the style."_s, u"id"_s},
+ {u"author"_s, u"The author of the style."_s, u"author"_s},
+ {u"version"_s, u"The version of the style."_s, u"version"_s},
+ {u"license"_s, u"The license of the style."_s, u"license"_s},
+ {u"output-path"_s, u"The path where the style will be created. If not supplied, the current working directory will be used."_s, u"output-path"_s},
+ {u"input-type"_s, u"The Union input type of the style to create. Currently only supports CSS"_s, u"input-type"_s},
+ {u"install"_s, u"Install the style after creating it."_s},
+ });
+ });
+
+ Union::PackageHandler::CreateInfo info;
+
+ info.inputType = parser->value(u"input-type"_s);
+ if (info.inputType.isEmpty()) {
+ info.inputType = u"css"_s;
+ }
+
+ info.name = parser->value(u"name"_s);
+ readInput(info.name, u"Enter the name of the style:");
+
+ const auto nonIdCharacters = QRegularExpression(u"[^a-zA-Z0-9-_]"_s);
+ const auto suggestedId = info.name.toLower().replace(nonIdCharacters, u"-"_s);
+
+ auto id = parser->value(u"id"_s);
+
+ auto idValidationFunction = [&nonIdCharacters](const QString &input) {
+ if (input.contains(nonIdCharacters)) {
+ std::cout << "The ID can only contain the characters a-z A-Z 0-9 - _\n";
+ return false;
+ } else {
+ return true;
+ }
+ };
+
+ if (!idValidationFunction(id)) {
+ id = QString{};
+ }
+
+ readInput(id, u"Enter the ID of the style:", idValidationFunction, suggestedId);
+
+ info.description = parser->value(u"description"_s);
+ readInput(info.description, u"Enter the description of the style:");
+
+ auto author = parser->value(u"author"_s);
+ readInput(author, u"Enter the name of the author of the style:", u"Unknown Author"_s);
+ info.authors = {author};
+
+ info.version = parser->value(u"version"_s);
+ readInput(info.version, u"Enter the version of the style:", u"0.1"_s);
+
+ info.license = parser->value(u"license"_s);
+ readInput(info.license, u"Enter the license of the style:", u"BSD-2-Clause"_s);
+
+ std::cout << "Creating style:\n";
+ std::cout << " Input Type: " << qPrintable(info.inputType) << "\n";
+ std::cout << " ID: " << qPrintable(id) << "\n";
+ std::cout << " Name: " << qPrintable(info.name) << "\n";
+ std::cout << " Description: " << qPrintable(info.description) << "\n";
+ std::cout << " Author: " << qPrintable(info.authors.join(u", ")) << "\n";
+ std::cout << " License: " << qPrintable(info.license) << "\n";
+ std::cout << " Version: " << qPrintable(info.version) << "\n";
+
+ if (parser->isSet(u"output-path"_s)) {
+ info.path = fs::path(parser->value(u"output-path"_s).toStdString());
+ } else {
+ info.path = fs::current_path() / id.toStdString();
+ }
+
+ Union::StylePackage package;
+ auto handler = Union::StyleRegistry::instance()->packageHandler();
+ if (auto result = handler->create(package, info); result != Union::PackageHandler::Error::None) {
+ return printHandlerError(package, result);
+ }
+
+ std::cout << "Created style " << package.path() << "\n";
+
+ if (parser->isSet(u"install"_s)) {
+ return handleInstallCommand({QString::fromStdString(package.path())});
+ }
+
return 0;
}