core: add preprocessor for versioning
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

This commit is contained in:
outfoxxed 2026-01-17 03:05:50 -08:00
parent d03c59768c
commit 5eb6f51f4a
No known key found for this signature in database
GPG key ID: 4C88A185FB89301E
12 changed files with 209 additions and 40 deletions

View file

@ -1,6 +1,8 @@
cmake_minimum_required(VERSION 3.20)
project(quickshell VERSION "0.2.1" LANGUAGES CXX C)
set(UNRELEASED_FEATURES)
set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

View file

@ -23,6 +23,7 @@ set shell id.
- Added initial support for network management.
- Added support for grabbing focus from popup windows.
- Added support for IPC signal listeners.
- Added Quickshell version checking and version gated preprocessing.
## Other Changes

View file

@ -1,6 +1,11 @@
#pragma once
// NOLINTBEGIN
#define QS_VERSION "@quickshell_VERSION@"
#define QS_VERSION_MAJOR @quickshell_VERSION_MAJOR@
#define QS_VERSION_MINOR @quickshell_VERSION_MINOR@
#define QS_VERSION_PATCH @quickshell_VERSION_PATCH@
#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@"
#define GIT_REVISION "@GIT_REVISION@"
#define DISTRIBUTOR "@DISTRIBUTOR@"
#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@

View file

@ -12,6 +12,7 @@ qt_add_library(quickshell-core STATIC
singleton.cpp
generation.cpp
scan.cpp
scanenv.cpp
qsintercept.cpp
incubator.cpp
lazyloader.cpp
@ -51,7 +52,7 @@ qt_add_qml_module(quickshell-core
install_qml_module(quickshell-core)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build)
qs_module_pch(quickshell-core SET large)

View file

@ -29,6 +29,7 @@
#include "paths.hpp"
#include "qmlscreen.hpp"
#include "rootwrapper.hpp"
#include "scanenv.hpp"
QuickshellSettings::QuickshellSettings() {
QObject::connect(
@ -313,6 +314,14 @@ QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback)
return IconImageProvider::requestString(icon, "", fallback);
}
bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor, const QStringList& features) {
return qs::scan::env::PreprocEnv::hasVersion(major, minor, features);
}
bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor) {
return QuickshellGlobal::hasVersion(major, minor, QStringList());
}
QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) {
auto* qsg = new QuickshellGlobal();
auto* generation = EngineGeneration::findEngineGeneration(engine);

View file

@ -217,6 +217,21 @@ public:
///
/// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`.
Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; }
/// Check if Quickshell's version is at least `major.minor` and the listed
/// unreleased features are available. If Quickshell is newer than the given version
/// it is assumed that all unreleased features are present. The unreleased feature list
/// may be omitted.
///
/// > [!NOTE] You can feature gate code blocks using Quickshell's preprocessor which
/// > has the same function available.
/// >
/// > ```qml
/// > //@ if hasVersion(0, 3, ["feature"])
/// > ...
/// > //@ endif
/// > ```
Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor, const QStringList& features);
Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor);
void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; }
[[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; }

View file

@ -63,9 +63,6 @@ void RootWrapper::reloadGraph(bool hard) {
qs::core::QmlToolingSupport::updateTooling(rootPath, scanner);
this->configDirWatcher.addPath(rootPath.path());
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
generation->wrapper = this;
// todo: move into EngineGeneration
if (this->generation != nullptr) {
qInfo() << "Reloading configuration...";
@ -74,6 +71,33 @@ void RootWrapper::reloadGraph(bool hard) {
QDir::setCurrent(this->originalWorkingDirectory);
if (!scanner.scanErrors.isEmpty()) {
qCritical() << "Failed to load configuration";
QString errorString = "Failed to load configuration";
for (auto& error: scanner.scanErrors) {
const auto& file = error.file;
QString rel;
if (file.startsWith(rootPath.path() % '/')) {
rel = '@' % file.sliced(rootPath.path().length() + 1);
} else {
rel = file;
}
auto msg = " error in " % rel % '[' % QString::number(error.line) % ":0]: " % error.message;
errorString += '\n' % msg;
qCritical().noquote() << msg;
}
if (this->generation != nullptr && this->generation->qsgInstance != nullptr) {
emit this->generation->qsgInstance->reloadFailed(errorString);
}
return;
}
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
generation->wrapper = this;
QUrl url;
url.setScheme("qs");
url.setPath("@/qs/" % rootFile.fileName());

View file

@ -1,9 +1,11 @@
#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>
@ -15,6 +17,7 @@
#include <qtextstream.h>
#include "logcat.hpp"
#include "scanenv.hpp"
QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
@ -115,51 +118,113 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna
auto stream = QTextStream(&file);
auto imports = QVector<QString>();
while (!stream.atEnd()) {
auto line = stream.readLine().trimmed();
if (!singleton && line == "pragma Singleton") {
singleton = true;
} else if (!internal && line == "//@ pragma Internal") {
internal = 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;
bool inHeader = false;
auto ifScopes = QVector<bool>();
bool sourceMasked = false;
int lineNum = 0;
QString overrideText;
bool isOverridden = false;
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;
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;
}
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);
}
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 = true;
}
} else if (line.contains('{')) break;
}
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;
}

View file

@ -23,6 +23,14 @@ public:
QVector<QString> scannedFiles;
QHash<QString, QString> fileIntercepts;
struct ScanError {
QString file;
QString message;
int line;
};
QVector<ScanError> scanErrors;
private:
QDir rootPath;

22
src/core/scanenv.cpp Normal file
View file

@ -0,0 +1,22 @@
#include "scanenv.hpp"
#include <qcontainerfwd.h>
#include "build.hpp"
namespace qs::scan::env {
bool PreprocEnv::hasVersion(int major, int minor, const QStringList& features) {
if (QS_VERSION_MAJOR > major) return true;
if (QS_VERSION_MAJOR == major && QS_VERSION_MINOR > minor) return true;
auto availFeatures = QString(QS_UNRELEASED_FEATURES).split(';');
for (const auto& feature: features) {
if (!availFeatures.contains(feature)) return false;
}
return QS_VERSION_MAJOR == major && QS_VERSION_MINOR == minor;
}
} // namespace qs::scan::env

17
src/core/scanenv.hpp Normal file
View file

@ -0,0 +1,17 @@
#pragma once
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qtmetamacros.h>
namespace qs::scan::env {
class PreprocEnv: public QObject {
Q_OBJECT;
public:
Q_INVOKABLE static bool
hasVersion(int major, int minor, const QStringList& features = QStringList());
};
} // namespace qs::scan::env

View file

@ -519,8 +519,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
}
if (state.misc.printVersion) {
qCInfo(logBare).noquote().nospace()
<< "quickshell 0.2.1, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR;
qCInfo(logBare).noquote().nospace() << "quickshell " << QS_VERSION << ", revision "
<< GIT_REVISION << ", distributed by: " << DISTRIBUTOR;
if (state.log.verbosity > 1) {
qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;