mirror of
https://git.outfoxxed.me/quickshell/quickshell.git
synced 2026-02-23 03:33:57 +11:00
Some checks failed
Build / Nix (push) Has been cancelled
Build / Nix-1 (push) Has been cancelled
Build / Nix-2 (push) Has been cancelled
Build / Nix-3 (push) Has been cancelled
Build / Nix-4 (push) Has been cancelled
Build / Nix-5 (push) Has been cancelled
Build / Nix-6 (push) Has been cancelled
Build / Nix-7 (push) Has been cancelled
Build / Nix-8 (push) Has been cancelled
Build / Nix-9 (push) Has been cancelled
Build / Nix-10 (push) Has been cancelled
Build / Nix-11 (push) Has been cancelled
Build / Nix-12 (push) Has been cancelled
Build / Nix-13 (push) Has been cancelled
Build / Nix-14 (push) Has been cancelled
Build / Nix-15 (push) Has been cancelled
Build / Nix-16 (push) Has been cancelled
Build / Nix-17 (push) Has been cancelled
Build / Nix-18 (push) Has been cancelled
Build / Nix-19 (push) Has been cancelled
Build / Nix-20 (push) Has been cancelled
Build / Nix-21 (push) Has been cancelled
Build / Nix-22 (push) Has been cancelled
Build / Nix-23 (push) Has been cancelled
Build / Nix-24 (push) Has been cancelled
Build / Nix-25 (push) Has been cancelled
Build / Nix-26 (push) Has been cancelled
Build / Nix-27 (push) Has been cancelled
Build / Nix-28 (push) Has been cancelled
Build / Nix-29 (push) Has been cancelled
Build / Nix-30 (push) Has been cancelled
Build / Nix-31 (push) Has been cancelled
Build / Nix-32 (push) Has been cancelled
Build / Nix-33 (push) Has been cancelled
Build / Archlinux (push) Has been cancelled
Lint / Lint (push) Has been cancelled
353 lines
10 KiB
C++
353 lines
10 KiB
C++
#include "scan.hpp"
|
|
#include <cmath>
|
|
#include <utility>
|
|
|
|
#include <qcontainerfwd.h>
|
|
#include <qdir.h>
|
|
#include <qfileinfo.h>
|
|
#include <qjsengine.h>
|
|
#include <qjsonarray.h>
|
|
#include <qjsondocument.h>
|
|
#include <qjsonobject.h>
|
|
#include <qjsonvalue.h>
|
|
#include <qlogging.h>
|
|
#include <qloggingcategory.h>
|
|
#include <qpair.h>
|
|
#include <qstring.h>
|
|
#include <qtextstream.h>
|
|
|
|
#include "logcat.hpp"
|
|
#include "scanenv.hpp"
|
|
|
|
QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
|
|
|
|
void QmlScanner::scanDir(const QDir& dir) {
|
|
if (this->scannedDirs.contains(dir)) return;
|
|
this->scannedDirs.push_back(dir);
|
|
|
|
const auto& path = dir.path();
|
|
|
|
qCDebug(logQmlScanner) << "Scanning directory" << path;
|
|
|
|
struct Entry {
|
|
QString name;
|
|
bool singleton = false;
|
|
bool internal = false;
|
|
};
|
|
|
|
bool seenQmldir = false;
|
|
auto entries = QVector<Entry>();
|
|
|
|
for (auto& name: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
|
|
if (name == "qmldir") {
|
|
qCDebug(
|
|
logQmlScanner
|
|
) << "Found qmldir file, qmldir synthesization will be disabled for directory"
|
|
<< path;
|
|
seenQmldir = true;
|
|
} else if (name.at(0).isUpper() && name.endsWith(".qml")) {
|
|
auto& entry = entries.emplaceBack();
|
|
|
|
if (this->scanQmlFile(dir.filePath(name), entry.singleton, entry.internal)) {
|
|
entry.name = name;
|
|
} else {
|
|
entries.pop_back();
|
|
}
|
|
} else if (name.at(0).isUpper() && name.endsWith(".qml.json")) {
|
|
if (this->scanQmlJson(dir.filePath(name))) {
|
|
entries.push_back({
|
|
.name = name.first(name.length() - 5),
|
|
.singleton = true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!seenQmldir) {
|
|
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path;
|
|
|
|
QString qmldir;
|
|
auto stream = QTextStream(&qmldir);
|
|
|
|
// cant derive a module name if not in shell path
|
|
if (path.startsWith(this->rootPath.path())) {
|
|
auto end = path.sliced(this->rootPath.path().length());
|
|
|
|
// verify we have a valid module name.
|
|
for (auto& c: end) {
|
|
if (c == '/') c = '.';
|
|
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
|
|| c == '_')
|
|
{
|
|
} else {
|
|
qCWarning(logQmlScanner) << "Module path contains invalid characters for a module name: "
|
|
<< path.sliced(this->rootPath.path().length());
|
|
goto skipadd;
|
|
}
|
|
}
|
|
|
|
stream << "module qs" << end << '\n';
|
|
skipadd:;
|
|
} else {
|
|
qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder.";
|
|
}
|
|
|
|
for (const auto& entry: entries) {
|
|
if (entry.internal) stream << "internal ";
|
|
if (entry.singleton) stream << "singleton ";
|
|
stream << entry.name.sliced(0, entry.name.length() - 4) << " 1.0 " << entry.name << '\n';
|
|
}
|
|
|
|
qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir);
|
|
this->fileIntercepts.insert(QDir(path).filePath("qmldir"), qmldir);
|
|
}
|
|
}
|
|
|
|
bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& internal) {
|
|
if (this->scannedFiles.contains(path)) return false;
|
|
this->scannedFiles.push_back(path);
|
|
|
|
qCDebug(logQmlScanner) << "Scanning qml file" << path;
|
|
|
|
auto file = QFile(path);
|
|
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
|
qCWarning(logQmlScanner) << "Failed to open file" << path;
|
|
return false;
|
|
}
|
|
|
|
auto stream = QTextStream(&file);
|
|
auto imports = QVector<QString>();
|
|
|
|
bool inHeader = true;
|
|
auto ifScopes = QVector<bool>();
|
|
bool sourceMasked = false;
|
|
int lineNum = 0;
|
|
QString overrideText;
|
|
bool isOverridden = false;
|
|
|
|
auto pragmaEngine = QJSEngine();
|
|
pragmaEngine.globalObject().setPrototype(
|
|
pragmaEngine.newQObject(new qs::scan::env::PreprocEnv())
|
|
);
|
|
|
|
auto postError = [&, this](QString error) {
|
|
this->scanErrors.append({.file = path, .message = std::move(error), .line = lineNum});
|
|
};
|
|
|
|
while (!stream.atEnd()) {
|
|
++lineNum;
|
|
bool hideMask = false;
|
|
auto rawLine = stream.readLine();
|
|
auto line = rawLine.trimmed();
|
|
if (!sourceMasked && inHeader) {
|
|
if (!singleton && line == "pragma Singleton") {
|
|
singleton = true;
|
|
} else if (line.startsWith("import")) {
|
|
// we dont care about "import qs" as we always load the root folder
|
|
if (auto importCursor = line.indexOf(" qs."); importCursor != -1) {
|
|
importCursor += 4;
|
|
QString path;
|
|
|
|
while (importCursor != line.length()) {
|
|
auto c = line.at(importCursor);
|
|
if (c == '.') c = '/';
|
|
else if (c == ' ') break;
|
|
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
|
|| c == '_')
|
|
{
|
|
} else {
|
|
qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line;
|
|
goto next;
|
|
}
|
|
|
|
path.append(c);
|
|
importCursor += 1;
|
|
}
|
|
|
|
imports.append(this->rootPath.filePath(path));
|
|
} else if (auto startQuot = line.indexOf('"');
|
|
startQuot != -1 && line.length() >= startQuot + 3)
|
|
{
|
|
auto endQuot = line.indexOf('"', startQuot + 1);
|
|
if (endQuot == -1) continue;
|
|
|
|
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
|
|
imports.push_back(name);
|
|
}
|
|
} else if (!internal && line == "//@ pragma Internal") {
|
|
internal = true;
|
|
} else if (line.contains('{')) {
|
|
inHeader = false;
|
|
}
|
|
}
|
|
|
|
if (line.startsWith("//@ if ")) {
|
|
auto code = line.sliced(7);
|
|
auto value = pragmaEngine.evaluate(code, path, 1234);
|
|
bool mask = true;
|
|
|
|
if (value.isError()) {
|
|
postError(QString("Evaluating if: %0").arg(value.toString()));
|
|
} else if (!value.isBool()) {
|
|
postError(QString("If expression \"%0\" is not a boolean").arg(value.toString()));
|
|
} else if (value.toBool()) {
|
|
mask = false;
|
|
}
|
|
if (!sourceMasked && mask) hideMask = true;
|
|
mask = sourceMasked || mask; // cant unmask if a nested if passes
|
|
ifScopes.append(mask);
|
|
if (mask) isOverridden = true;
|
|
sourceMasked = mask;
|
|
} else if (line.startsWith("//@ endif")) {
|
|
if (ifScopes.isEmpty()) {
|
|
postError("endif without matching if");
|
|
} else {
|
|
ifScopes.pop_back();
|
|
|
|
if (ifScopes.isEmpty()) sourceMasked = false;
|
|
else sourceMasked = ifScopes.last();
|
|
}
|
|
}
|
|
|
|
if (!hideMask && sourceMasked) overrideText.append("// MASKED: " % rawLine % '\n');
|
|
else overrideText.append(rawLine % '\n');
|
|
|
|
next:;
|
|
}
|
|
|
|
if (!ifScopes.isEmpty()) {
|
|
postError("unclosed preprocessor if block");
|
|
}
|
|
|
|
file.close();
|
|
|
|
if (isOverridden) {
|
|
this->fileIntercepts.insert(path, overrideText);
|
|
}
|
|
|
|
if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) {
|
|
qCDebug(logQmlScanner) << "Found imports" << imports;
|
|
}
|
|
|
|
auto currentdir = QDir(QFileInfo(path).absolutePath());
|
|
|
|
// the root can never be a singleton so it dosent matter if we skip it
|
|
this->scanDir(currentdir);
|
|
|
|
for (auto& import: imports) {
|
|
QString ipath;
|
|
if (import.startsWith("root:")) {
|
|
auto path = import.sliced(5);
|
|
if (path.startsWith('/')) path = path.sliced(1);
|
|
ipath = this->rootPath.filePath(path);
|
|
} else {
|
|
ipath = currentdir.filePath(import);
|
|
}
|
|
|
|
auto pathInfo = QFileInfo(ipath);
|
|
auto cpath = pathInfo.absoluteFilePath();
|
|
|
|
if (!pathInfo.exists()) {
|
|
qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path;
|
|
continue;
|
|
}
|
|
|
|
if (!pathInfo.isDir()) {
|
|
qCDebug(logQmlScanner) << "Ignoring non-directory import" << ipath << "from" << path;
|
|
continue;
|
|
}
|
|
|
|
if (import.endsWith(".js")) this->scannedFiles.push_back(cpath);
|
|
else this->scanDir(cpath);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void QmlScanner::scanQmlRoot(const QString& path) {
|
|
bool singleton = false;
|
|
bool internal = false;
|
|
this->scanQmlFile(path, singleton, internal);
|
|
}
|
|
|
|
bool QmlScanner::scanQmlJson(const QString& path) {
|
|
qCDebug(logQmlScanner) << "Scanning qml.json file" << path;
|
|
|
|
auto file = QFile(path);
|
|
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
|
qCWarning(logQmlScanner) << "Failed to open file" << path;
|
|
return false;
|
|
}
|
|
|
|
auto data = file.readAll();
|
|
|
|
// Importing this makes CI builds fail for some reason.
|
|
QJsonParseError error; // NOLINT (misc-include-cleaner)
|
|
auto json = QJsonDocument::fromJson(data, &error);
|
|
|
|
if (error.error != QJsonParseError::NoError) {
|
|
qCCritical(logQmlScanner).nospace()
|
|
<< "Failed to parse qml.json file at " << path << ": " << error.errorString();
|
|
return false;
|
|
}
|
|
|
|
const QString body =
|
|
"pragma Singleton\nimport QtQuick as Q\n\n" % QmlScanner::jsonToQml(json.object()).second;
|
|
|
|
qCDebug(logQmlScanner) << "Synthesized qml file for" << path << qPrintable("\n" + body);
|
|
|
|
this->fileIntercepts.insert(path.first(path.length() - 5), body);
|
|
this->scannedFiles.push_back(path);
|
|
return true;
|
|
}
|
|
|
|
QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int indent) {
|
|
if (value.isObject()) {
|
|
const auto& object = value.toObject();
|
|
|
|
auto valIter = object.constBegin();
|
|
|
|
QString accum = "Q.QtObject {\n";
|
|
for (const auto& key: object.keys()) {
|
|
const auto& val = *valIter++;
|
|
auto [type, repr] = QmlScanner::jsonToQml(val, indent + 2);
|
|
accum += QString(' ').repeated(indent + 2) % "readonly property " % type % ' ' % key % ": "
|
|
% repr % ";\n";
|
|
}
|
|
|
|
accum += QString(' ').repeated(indent) % '}';
|
|
return qMakePair(QStringLiteral("Q.QtObject"), accum);
|
|
} else if (value.isArray()) {
|
|
return qMakePair(
|
|
QStringLiteral("var"),
|
|
QJsonDocument(value.toArray()).toJson(QJsonDocument::Compact)
|
|
);
|
|
} else if (value.isString()) {
|
|
const auto& str = value.toString();
|
|
|
|
if (str.startsWith('#') && (str.length() == 4 || str.length() == 7 || str.length() == 9)) {
|
|
for (auto c: str.sliced(1)) {
|
|
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
|
goto noncolor;
|
|
}
|
|
}
|
|
|
|
return qMakePair(QStringLiteral("Q.color"), '"' % str % '"');
|
|
}
|
|
|
|
noncolor:
|
|
return qMakePair(QStringLiteral("string"), '"' % QString(str).replace("\"", "\\\"") % '"');
|
|
} else if (value.isDouble()) {
|
|
auto num = value.toDouble();
|
|
double whole = 0;
|
|
if (std::modf(num, &whole) == 0.0) {
|
|
return qMakePair(QStringLiteral("int"), QString::number(static_cast<int>(whole)));
|
|
} else {
|
|
return qMakePair(QStringLiteral("real"), QString::number(num));
|
|
}
|
|
} else if (value.isBool()) {
|
|
return qMakePair(QStringLiteral("bool"), value.toBool() ? "true" : "false");
|
|
} else {
|
|
return qMakePair(QStringLiteral("var"), "null");
|
|
}
|
|
}
|