File indexing completed on 2024-12-08 03:37:54

0001 /* SPDX-FileCopyrightText: 2023 Noah Davis <noahadvs@gmail.com>
0002  * SPDX-License-Identifier: LGPL-2.0-or-later
0003  */
0004 
0005 #include <QCommandLineOption>
0006 #include <QCommandLineParser>
0007 #include <QCoreApplication>
0008 #include <QDebug>
0009 #include <QDir>
0010 #include <QDirIterator>
0011 #include <QFile>
0012 #include <QMimeDatabase>
0013 #include <QRegularExpression>
0014 #include <QXmlStreamReader>
0015 #include <QXmlStreamWriter>
0016 
0017 using namespace Qt::StringLiterals;
0018 using QRE = QRegularExpression;
0019 
0020 // Prevent massive build log files
0021 QString elideString(const QString &string)
0022 {
0023     static const auto ellipsis = "…"_L1;
0024     static constexpr auto limit = 100000;
0025     if (string.size() > limit) {
0026         return string.first(qBound(0, limit - ellipsis.size(), string.size())) + ellipsis;
0027     }
0028     return string;
0029 }
0030 
0031 QString convertStylesheet(QString stylesheet)
0032 {
0033     const auto patternOptions = QRE::MultilineOption | QRE::DotMatchesEverythingOption;
0034     // Remove whitespace
0035     stylesheet.remove(QRE(u"\\s"_s, patternOptions));
0036     // class, color
0037     QMap<QString, QString> classColorMap;
0038     // TODO: Support color values other than hexadecimal, maybe properties other than "color"
0039     QRE regex(u"\\.ColorScheme-(\\S+){color:(#[0-9a-fA-F]+);}"_s,
0040               patternOptions);
0041     auto matchIt = regex.globalMatch(stylesheet);
0042     while (matchIt.hasNext()) {
0043         auto match = matchIt.next();
0044         auto classString = match.captured(1);
0045         auto colorString = match.captured(2);
0046         if (classString == "Text"_L1) {
0047             colorString = u"#fcfcfc"_s;
0048         } else if (classString == "Background"_L1) {
0049             colorString = u"#2a2e32"_s;
0050         }
0051         classColorMap.insert(match.captured(1), colorString);
0052     }
0053 
0054     QString output;
0055     for (auto it = classColorMap.cbegin(); it != classColorMap.cend(); ++it) {
0056         output += ".ColorScheme-%1 { color: %2; } "_L1.arg(it.key(), it.value());
0057     }
0058     return output;
0059 }
0060 
0061 int main(int argc, char **argv)
0062 {
0063     QCoreApplication app(argc, argv);
0064 
0065     QCommandLineParser commandLineParser;
0066     commandLineParser.setApplicationDescription(u"Takes light theme icons and makes modified copies of them with dark theme stylesheets."_s);
0067     commandLineParser.addPositionalArgument(u"inputs"_s, u"Input folders (separated by spaces)"_s, u"inputs..."_s);
0068     commandLineParser.addPositionalArgument(u"output"_s, u"Output folder (will be created if not existing)"_s, u"output"_s);
0069     commandLineParser.addHelpOption();
0070     commandLineParser.addVersionOption();
0071 
0072     commandLineParser.process(app);
0073 
0074     const auto &positionalArguments = commandLineParser.positionalArguments();
0075     if (positionalArguments.isEmpty()) {
0076         qWarning() << "The arguments are missing.";
0077         return 1;
0078     }
0079 
0080     QFileInfo outputDirInfo(positionalArguments.last());
0081     if (outputDirInfo.exists() && !outputDirInfo.isDir()) {
0082         qWarning() << positionalArguments.last() << "is not a folder.";
0083         return 1;
0084     }
0085 
0086     QList<QDir> inputDirs;
0087     QStringList ignoredArgs;
0088     for (int i = 0; i < positionalArguments.size() - 1; ++i) {
0089         QFileInfo inputDirInfo(positionalArguments[i]);
0090         if (!inputDirInfo.isDir()) {
0091             ignoredArgs << positionalArguments[i];
0092             continue;
0093         }
0094         inputDirs << inputDirInfo.absoluteFilePath();
0095     }
0096 
0097     if (inputDirs.isEmpty()) {
0098         qWarning() << "None of the input arguments could be used.";
0099         return 1;
0100     }
0101 
0102     if (!ignoredArgs.isEmpty()) {
0103         // Using the arg instead of path or filename so the user sees what they typed.
0104         qWarning() << "The following input arguments were ignored:";
0105         qWarning().noquote() << elideString(ignoredArgs.join("\n"_L1));
0106     }
0107 
0108     bool wasAnyFileWritten = false;
0109     QStringList unreadFiles;
0110     QStringList unwrittenFiles;
0111     QStringList xmlReadErrorFiles;
0112     QStringList xmlWriteErrorFiles;
0113     for (auto &inputDir : std::as_const(inputDirs)) {
0114         QDirIterator dirIt(inputDir, QDirIterator::Subdirectories);
0115         while (dirIt.hasNext()) {
0116             auto inputFileInfo = dirIt.nextFileInfo();
0117             const auto inputFilePath = inputFileInfo.absoluteFilePath();
0118 
0119             // Skip non-files, symlinks, non-svgs and existing breeze dark icons
0120             if (!inputFileInfo.isFile() || inputFileInfo.isSymLink()
0121                 || !inputFilePath.endsWith(".svg"_L1)
0122                 || QFileInfo::exists(QString{inputFilePath}.replace("/icons/"_L1,
0123                                                                     "/icons-dark/"_L1))
0124             ) {
0125                 continue;
0126             }
0127 
0128             QFile inputFile(inputFilePath);
0129             if (!inputFile.open(QIODevice::ReadOnly)) {
0130                 unreadFiles.append("\""_L1 + inputFile.fileName() + "\": "_L1 + inputFile.errorString());
0131                 continue;
0132             }
0133             const auto inputData = inputFile.readAll();
0134             inputFile.close();
0135 
0136             // Skip any icons that don't have the stylesheet
0137             if (!inputData.contains("current-color-scheme")) {
0138                 continue;
0139             }
0140 
0141             QDir outputDir = outputDirInfo.absoluteFilePath();
0142             const auto outputFilePath = outputDir.absoluteFilePath(QString{inputFilePath}.remove(QRE(u".*/icons/"_s)));
0143             QFileInfo outputFileInfo(outputFilePath);
0144             outputDir = outputFileInfo.dir();
0145             if (!outputDir.exists()) {
0146                 QDir::root().mkpath(outputDir.absolutePath());
0147             }
0148             QFile outputFile(outputFilePath);
0149             if (!outputFile.open(QIODevice::WriteOnly)) {
0150                 unwrittenFiles.append("\""_L1 + outputFile.fileName() + "\": "_L1 + outputFile.errorString());
0151                 continue;
0152             }
0153 
0154             QXmlStreamReader reader(inputData);
0155             reader.setNamespaceProcessing(false);
0156             QByteArray outputData;
0157             QXmlStreamWriter writer(&outputData);
0158             writer.setAutoFormatting(true);
0159 
0160             while (!reader.atEnd() && !reader.hasError() && !writer.hasError()) {
0161                 reader.readNext();
0162                 writer.writeCurrentToken(reader);
0163                 if (!reader.isStartElement() || reader.qualifiedName() != "style"_L1
0164                     || reader.attributes().value("id"_L1) != "current-color-scheme"_L1
0165                 ) {
0166                     continue;
0167                 }
0168                 reader.readNext();
0169                 if (!reader.isCharacters()) {
0170                     writer.writeCurrentToken(reader);
0171                     continue;
0172                 }
0173                 writer.writeCharacters(convertStylesheet(reader.text().toString()));
0174             }
0175 
0176             if (reader.hasError()) {
0177                 xmlReadErrorFiles.append("\""_L1 + inputFile.fileName() + "\": "_L1 + reader.errorString());
0178             }
0179             if (writer.hasError()) {
0180                 xmlWriteErrorFiles.append("\""_L1 + outputFile.fileName() + "\""_L1);
0181             }
0182 
0183             auto bytesWritten = outputFile.write(outputData);
0184             outputFile.close();
0185             wasAnyFileWritten |= bytesWritten > 0;
0186         }
0187     }
0188 
0189     if (!unreadFiles.empty()) {
0190         qWarning() << "Input file open errors:";
0191         qWarning().noquote() << elideString(unreadFiles.join("\n"_L1));
0192     }
0193     if (!unwrittenFiles.empty()) {
0194         qWarning() << "Output file open errors:";
0195         qWarning().noquote() << elideString(unwrittenFiles.join("\n"_L1));
0196     }
0197     if (!xmlReadErrorFiles.empty()) {
0198         qWarning() << "Input XML read errors:";
0199         qWarning().noquote() << elideString(xmlReadErrorFiles.join("\n"_L1));
0200     }
0201     if (!xmlWriteErrorFiles.empty()) {
0202         qWarning() << "Output XML write errors:";
0203         qWarning().noquote() << elideString(xmlWriteErrorFiles.join("\n"_L1));
0204     }
0205 
0206     return wasAnyFileWritten ? 0 : 1;
0207 }