BreezeIcons

generate-symbolic-dark.cpp
1/* SPDX-FileCopyrightText: 2023 Noah Davis <noahadvs@gmail.com>
2 * SPDX-License-Identifier: LGPL-2.0-or-later
3 */
4
5#include <QCommandLineOption>
6#include <QCommandLineParser>
7#include <QCoreApplication>
8#include <QDebug>
9#include <QDir>
10#include <QDirIterator>
11#include <QFile>
12#include <QMimeDatabase>
13#include <QRegularExpression>
14#include <QXmlStreamReader>
15#include <QXmlStreamWriter>
16
17using namespace Qt::StringLiterals;
19
20// Prevent massive build log files
21QString elideString(const QString &string)
22{
23 static const auto ellipsis = "…"_L1;
24 static constexpr auto limit = 100000;
25 if (string.size() > limit) {
26 return string.first(qBound(0, limit - ellipsis.size(), string.size())) + ellipsis;
27 }
28 return string;
29}
30
31QString convertStylesheet(QString stylesheet)
32{
33 const auto patternOptions = QRE::MultilineOption | QRE::DotMatchesEverythingOption;
34 // Remove whitespace
35 stylesheet.remove(QRE(u"\\s"_s, patternOptions));
36 // class, color
37 QMap<QString, QString> classColorMap;
38 // TODO: Support color values other than hexadecimal, maybe properties other than "color"
39 QRE regex(u"\\.ColorScheme-(\\S+?){color:(#[0-9a-fA-F]+);}"_s, patternOptions);
40 auto matchIt = regex.globalMatch(stylesheet);
41 while (matchIt.hasNext()) {
42 auto match = matchIt.next();
43 auto classString = match.captured(1);
44 auto colorString = match.captured(2);
45 if (classString == "Text"_L1) {
46 colorString = u"#fcfcfc"_s;
47 } else if (classString == "Background"_L1) {
48 colorString = u"#2a2e32"_s;
49 }
50 classColorMap.insert(match.captured(1), colorString);
51 }
52
53 QString output;
54 for (auto it = classColorMap.cbegin(); it != classColorMap.cend(); ++it) {
55 output += ".ColorScheme-%1 { color: %2; } "_L1.arg(it.key(), it.value());
56 }
57 return output;
58}
59
60int main(int argc, char **argv)
61{
62 QCoreApplication app(argc, argv);
63
64 QCommandLineParser commandLineParser;
65 commandLineParser.setApplicationDescription(u"Takes light theme icons and makes modified copies of them with dark theme stylesheets."_s);
66 commandLineParser.addPositionalArgument(u"inputs"_s, u"Input folders (separated by spaces)"_s, u"inputs..."_s);
67 commandLineParser.addPositionalArgument(u"output"_s, u"Output folder (will be created if not existing)"_s, u"output"_s);
68 commandLineParser.addHelpOption();
69 commandLineParser.addVersionOption();
70
71 commandLineParser.process(app);
72
73 const auto &positionalArguments = commandLineParser.positionalArguments();
74 if (positionalArguments.isEmpty()) {
75 qWarning() << "The arguments are missing.";
76 return 1;
77 }
78
79 QFileInfo outputDirInfo(positionalArguments.last());
80 if (outputDirInfo.exists() && !outputDirInfo.isDir()) {
81 qWarning() << positionalArguments.last() << "is not a folder.";
82 return 1;
83 }
84
85 QList<QDir> inputDirs;
86 QStringList ignoredArgs;
87 for (int i = 0; i < positionalArguments.size() - 1; ++i) {
88 QFileInfo inputDirInfo(positionalArguments[i]);
89 if (!inputDirInfo.isDir()) {
90 ignoredArgs << positionalArguments[i];
91 continue;
92 }
93 inputDirs << inputDirInfo.absoluteFilePath();
94 }
95
96 if (inputDirs.isEmpty()) {
97 qWarning() << "None of the input arguments could be used.";
98 return 1;
99 }
100
101 if (!ignoredArgs.isEmpty()) {
102 // Using the arg instead of path or filename so the user sees what they typed.
103 qWarning() << "The following input arguments were ignored:";
104 qWarning().noquote() << elideString(ignoredArgs.join("\n"_L1));
105 }
106
107 bool wasAnyFileWritten = false;
108 QStringList unreadFiles;
109 QStringList unwrittenFiles;
110 QStringList xmlReadErrorFiles;
111 QStringList xmlWriteErrorFiles;
112 for (auto &inputDir : std::as_const(inputDirs)) {
114 while (dirIt.hasNext()) {
115 auto inputFileInfo = dirIt.nextFileInfo();
116 const auto inputFilePath = inputFileInfo.absoluteFilePath();
117
118 // Skip non-files, symlinks, non-svgs and existing breeze dark icons
119 if (!inputFileInfo.isFile() || inputFileInfo.isSymLink() || !inputFilePath.endsWith(".svg"_L1)
120 || QFileInfo::exists(QString{inputFilePath}.replace("/icons/"_L1, "/icons-dark/"_L1))) {
121 continue;
122 }
123
124 QFile inputFile(inputFilePath);
125 if (!inputFile.open(QIODevice::ReadOnly)) {
126 unreadFiles.append("\""_L1 + inputFile.fileName() + "\": "_L1 + inputFile.errorString());
127 continue;
128 }
129 const auto inputData = inputFile.readAll();
130 inputFile.close();
131
132 // Skip any icons that don't have the stylesheet
133 if (!inputData.contains("current-color-scheme")) {
134 continue;
135 }
136
137 QDir outputDir = outputDirInfo.absoluteFilePath();
138 const auto outputFilePath = outputDir.absoluteFilePath(QString{inputFilePath}.remove(QRE(u".*/icons/"_s)));
139 QFileInfo outputFileInfo(outputFilePath);
140 outputDir = outputFileInfo.dir();
141 if (!outputDir.exists()) {
142 QDir::root().mkpath(outputDir.absolutePath());
143 }
144 QFile outputFile(outputFilePath);
145 if (!outputFile.open(QIODevice::WriteOnly)) {
146 unwrittenFiles.append("\""_L1 + outputFile.fileName() + "\": "_L1 + outputFile.errorString());
147 continue;
148 }
149
150 QXmlStreamReader reader(inputData);
151 reader.setNamespaceProcessing(false);
152 QByteArray outputData;
153 QXmlStreamWriter writer(&outputData);
154 writer.setAutoFormatting(true);
155
156 while (!reader.atEnd() && !reader.hasError() && !writer.hasError()) {
157 reader.readNext();
158 writer.writeCurrentToken(reader);
159 if (!reader.isStartElement() || reader.qualifiedName() != "style"_L1 || reader.attributes().value("id"_L1) != "current-color-scheme"_L1) {
160 continue;
161 }
162 reader.readNext();
163 if (!reader.isCharacters()) {
164 writer.writeCurrentToken(reader);
165 continue;
166 }
167 writer.writeCharacters(convertStylesheet(reader.text().toString()));
168 }
169
170 if (reader.hasError()) {
171 xmlReadErrorFiles.append("\""_L1 + inputFile.fileName() + "\": "_L1 + reader.errorString());
172 }
173 if (writer.hasError()) {
174 xmlWriteErrorFiles.append("\""_L1 + outputFile.fileName() + "\""_L1);
175 }
176
177 auto bytesWritten = outputFile.write(outputData);
178 outputFile.close();
179 wasAnyFileWritten |= bytesWritten > 0;
180 }
181 }
182
183 if (!unreadFiles.empty()) {
184 qWarning() << "Input file open errors:";
185 qWarning().noquote() << elideString(unreadFiles.join("\n"_L1));
186 }
187 if (!unwrittenFiles.empty()) {
188 qWarning() << "Output file open errors:";
189 qWarning().noquote() << elideString(unwrittenFiles.join("\n"_L1));
190 }
191 if (!xmlReadErrorFiles.empty()) {
192 qWarning() << "Input XML read errors:";
193 qWarning().noquote() << elideString(xmlReadErrorFiles.join("\n"_L1));
194 }
195 if (!xmlWriteErrorFiles.empty()) {
196 qWarning() << "Output XML write errors:";
197 qWarning().noquote() << elideString(xmlWriteErrorFiles.join("\n"_L1));
198 }
199
200 return wasAnyFileWritten ? 0 : 1;
201}
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QCommandLineOption addHelpOption()
void addPositionalArgument(const QString &name, const QString &description, const QString &syntax)
QCommandLineOption addVersionOption()
QStringList positionalArguments() const const
void process(const QCoreApplication &app)
void setApplicationDescription(const QString &description)
QString absoluteFilePath(const QString &fileName) const const
QString absolutePath() const const
bool exists() const const
bool mkpath(const QString &dirPath) const const
QDir root()
bool exists() const const
void append(QList< T > &&value)
bool empty() const const
bool isEmpty() const const
const_iterator cbegin() const const
const_iterator cend() const const
iterator insert(const Key &key, const T &value)
QString arg(Args &&... args) const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString join(QChar separator) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 13 2024 11:52:12 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.