File indexing completed on 2024-12-01 06:42:32
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 }