From d03c59768c680f052dff6e7a7918bbf990b0f743 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 15 Jan 2026 23:04:10 -0800 Subject: [PATCH 1/2] io/ipchandler: add signal listener support --- changelog/next.md | 2 + src/io/CMakeLists.txt | 2 +- src/io/ipc.cpp | 12 ++++ src/io/ipc.hpp | 25 ++++++-- src/io/ipccomm.cpp | 108 ++++++++++++++++++++++++++++++-- src/io/ipccomm.hpp | 50 +++++++++++++++ src/io/ipchandler.cpp | 120 +++++++++++++++++++++++++++++++++--- src/io/ipchandler.hpp | 82 +++++++++++++++++++++++- src/ipc/ipc.cpp | 6 ++ src/ipc/ipccommand.hpp | 1 + src/launch/command.cpp | 4 ++ src/launch/launch_p.hpp | 2 + src/launch/parsecommand.cpp | 11 ++++ 13 files changed, 405 insertions(+), 20 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 0cdff57..cab03e6 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -22,6 +22,7 @@ set shell id. - Added pipewire audio peak detection. - Added initial support for network management. - Added support for grabbing focus from popup windows. +- Added support for IPC signal listeners. ## Other Changes @@ -40,6 +41,7 @@ set shell id. - Fixed missing signals for system tray item title and description updates. - Fixed asynchronous loaders not working after reload. - Fixed asynchronous loaders not working before window creation. +- Fixed memory leak in IPC handlers. ## Packaging Changes diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 17628d3..991beaa 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -24,7 +24,7 @@ qt_add_qml_module(quickshell-io qs_add_module_deps_light(quickshell-io Quickshell) install_qml_module(quickshell-io) -target_link_libraries(quickshell-io PRIVATE Qt::Quick) +target_link_libraries(quickshell-io PRIVATE Qt::Quick quickshell-ipc) target_link_libraries(quickshell PRIVATE quickshell-ioplugin) qs_module_pch(quickshell-io) diff --git a/src/io/ipc.cpp b/src/io/ipc.cpp index 768299e..c381567 100644 --- a/src/io/ipc.cpp +++ b/src/io/ipc.cpp @@ -190,6 +190,14 @@ QString WirePropertyDefinition::toString() const { return "property " % this->name % ": " % this->type; } +QString WireSignalDefinition::toString() const { + if (this->rettype.isEmpty()) { + return "signal " % this->name % "()"; + } else { + return "signal " % this->name % "(" % this->retname % ": " % this->rettype % ')'; + } +} + QString WireTargetDefinition::toString() const { QString accum = "target " % this->name; @@ -201,6 +209,10 @@ QString WireTargetDefinition::toString() const { accum += "\n " % prop.toString(); } + for (const auto& sig: this->signalFunctions) { + accum += "\n " % sig.toString(); + } + return accum; } diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp index d2b865a..32486d6 100644 --- a/src/io/ipc.hpp +++ b/src/io/ipc.hpp @@ -146,14 +146,31 @@ struct WirePropertyDefinition { DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type); -struct WireTargetDefinition { +struct WireSignalDefinition { QString name; - QVector functions; - QVector properties; + QString retname; + QString rettype; [[nodiscard]] QString toString() const; }; -DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions, data.properties); +DEFINE_SIMPLE_DATASTREAM_OPS(WireSignalDefinition, data.name, data.retname, data.rettype); + +struct WireTargetDefinition { + QString name; + QVector functions; + QVector properties; + QVector signalFunctions; + + [[nodiscard]] QString toString() const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS( + WireTargetDefinition, + data.name, + data.functions, + data.properties, + data.signalFunctions +); } // namespace qs::io::ipc diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp index 6c7e4f6..03b688a 100644 --- a/src/io/ipccomm.cpp +++ b/src/io/ipccomm.cpp @@ -1,9 +1,11 @@ #include "ipccomm.hpp" +#include #include #include #include #include +#include #include #include @@ -18,10 +20,6 @@ using namespace qs::ipc; namespace qs::io::ipc::comm { -struct NoCurrentGeneration: std::monostate {}; -struct TargetNotFound: std::monostate {}; -struct EntryNotFound: std::monostate {}; - using QueryResponse = std::variant< std::monostate, NoCurrentGeneration, @@ -313,4 +311,106 @@ int getProperty(IpcClient* client, const QString& target, const QString& propert return -1; } +int listenToSignal(IpcClient* client, const QString& target, const QString& signal, bool once) { + if (target.isEmpty()) { + qCCritical(logBare) << "Target required to listen for signals."; + return -1; + } else if (signal.isEmpty()) { + qCCritical(logBare) << "Signal required to listen."; + return -1; + } + + client->sendMessage(IpcCommand(SignalListenCommand {.target = target, .signal = signal})); + + while (true) { + SignalListenResponse slot; + if (!client->waitForResponse(slot)) return -1; + + if (std::holds_alternative(slot)) { + auto& result = std::get(slot); + QTextStream(stdout) << result.response << Qt::endl; + if (once) return 0; + else continue; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Target not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Signal not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Not ready to accept queries yet."; + } else { + qCCritical(logIpc) << "Received invalid IPC response from" << client; + } + break; + } + + return -1; +} + +void SignalListenCommand::exec(qs::ipc::IpcServerConnection* conn) { + auto resp = conn->responseStream(); + + if (auto* generation = EngineGeneration::currentGeneration()) { + auto* registry = IpcHandlerRegistry::forGeneration(generation); + + auto* handler = registry->findHandler(this->target); + if (!handler) { + resp << TargetNotFound(); + return; + } + + auto* signal = handler->findSignal(this->signal); + if (!signal) { + resp << EntryNotFound(); + return; + } + + new RemoteSignalListener(conn, *this); + } else { + conn->respond(SignalListenResponse(NoCurrentGeneration())); + } +} + +RemoteSignalListener::RemoteSignalListener( + qs::ipc::IpcServerConnection* conn, + SignalListenCommand command +) + : conn(conn) + , command(std::move(command)) { + conn->setParent(this); + + QObject::connect( + IpcSignalRemoteListener::instance(), + &IpcSignalRemoteListener::triggered, + this, + &RemoteSignalListener::onSignal + ); + + QObject::connect( + conn, + &qs::ipc::IpcServerConnection::destroyed, + this, + &RemoteSignalListener::onConnDestroyed + ); + + qCDebug(logIpc) << "Remote listener created for" << this->command.target << this->command.signal + << ":" << this; +} + +RemoteSignalListener::~RemoteSignalListener() { + qCDebug(logIpc) << "Destroying remote listener" << this; +} + +void RemoteSignalListener::onSignal( + const QString& target, + const QString& signal, + const QString& value +) { + if (target != this->command.target || signal != this->command.signal) return; + qCDebug(logIpc) << "Remote signal" << signal << "triggered on" << target << "with value" << value; + + this->conn->respond(SignalListenResponse(SignalResponse {.response = value})); +} + +void RemoteSignalListener::onConnDestroyed() { this->deleteLater(); } + } // namespace qs::io::ipc::comm diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp index bc7dbf9..ac12979 100644 --- a/src/io/ipccomm.hpp +++ b/src/io/ipccomm.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include "../ipc/ipc.hpp" @@ -48,4 +50,52 @@ DEFINE_SIMPLE_DATASTREAM_OPS(StringPropReadCommand, data.target, data.property); int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property); +struct SignalListenCommand { + QString target; + QString signal; + + void exec(qs::ipc::IpcServerConnection* conn); +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(SignalListenCommand, data.target, data.signal); + +int listenToSignal( + qs::ipc::IpcClient* client, + const QString& target, + const QString& signal, + bool once +); + +struct NoCurrentGeneration: std::monostate {}; +struct TargetNotFound: std::monostate {}; +struct EntryNotFound: std::monostate {}; + +struct SignalResponse { + QString response; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(SignalResponse, data.response); + +using SignalListenResponse = std:: + variant; + +class RemoteSignalListener: public QObject { + Q_OBJECT; + +public: + explicit RemoteSignalListener(qs::ipc::IpcServerConnection* conn, SignalListenCommand command); + + ~RemoteSignalListener() override; + + Q_DISABLE_COPY_MOVE(RemoteSignalListener); + +private slots: + void onSignal(const QString& target, const QString& signal, const QString& value); + void onConnDestroyed(); + +private: + qs::ipc::IpcServerConnection* conn; + SignalListenCommand command; +}; + } // namespace qs::io::ipc::comm diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp index 5ffa0ad..e80cf4b 100644 --- a/src/io/ipchandler.cpp +++ b/src/io/ipchandler.cpp @@ -1,5 +1,7 @@ #include "ipchandler.hpp" #include +#include +#include #include #include @@ -139,6 +141,75 @@ WirePropertyDefinition IpcProperty::wireDef() const { return wire; } +WireSignalDefinition IpcSignal::wireDef() const { + WireSignalDefinition wire; + wire.name = this->signal.name(); + if (this->targetSlot != IpcSignalListener::SLOT_VOID) { + wire.retname = this->signal.parameterNames().value(0); + if (this->targetSlot == IpcSignalListener::SLOT_STRING) wire.rettype = "string"; + else if (this->targetSlot == IpcSignalListener::SLOT_INT) wire.rettype = "int"; + else if (this->targetSlot == IpcSignalListener::SLOT_BOOL) wire.rettype = "bool"; + else if (this->targetSlot == IpcSignalListener::SLOT_REAL) wire.rettype = "real"; + else if (this->targetSlot == IpcSignalListener::SLOT_COLOR) wire.rettype = "color"; + } + return wire; +} + +// NOLINTBEGIN (cppcoreguidelines-interfaces-global-init) +// clang-format off +const int IpcSignalListener::SLOT_VOID = IpcSignalListener::staticMetaObject.indexOfSlot("invokeVoid()"); +const int IpcSignalListener::SLOT_STRING = IpcSignalListener::staticMetaObject.indexOfSlot("invokeString(QString)"); +const int IpcSignalListener::SLOT_INT = IpcSignalListener::staticMetaObject.indexOfSlot("invokeInt(int)"); +const int IpcSignalListener::SLOT_BOOL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeBool(bool)"); +const int IpcSignalListener::SLOT_REAL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeReal(double)"); +const int IpcSignalListener::SLOT_COLOR = IpcSignalListener::staticMetaObject.indexOfSlot("invokeColor(QColor)"); +// clang-format on +// NOLINTEND + +bool IpcSignal::resolve(QString& error) { + if (this->signal.parameterCount() > 1) { + error = "Due to technical limitations, IPC signals can have at most one argument."; + return false; + } + + auto slot = IpcSignalListener::SLOT_VOID; + + if (this->signal.parameterCount() == 1) { + auto paramType = this->signal.parameterType(0); + if (paramType == QMetaType::QString) slot = IpcSignalListener::SLOT_STRING; + else if (paramType == QMetaType::Int) slot = IpcSignalListener::SLOT_INT; + else if (paramType == QMetaType::Bool) slot = IpcSignalListener::SLOT_BOOL; + else if (paramType == QMetaType::Double) slot = IpcSignalListener::SLOT_REAL; + else if (paramType == QMetaType::QColor) slot = IpcSignalListener::SLOT_COLOR; + else { + error = QString("Type of argument (%2: %3) cannot be used across IPC.") + .arg(this->signal.parameterNames().value(0)) + .arg(QMetaType(paramType).name()); + + return false; + } + } + + this->targetSlot = slot; + return true; +} + +void IpcSignal::connectListener(IpcHandler* handler) { + if (this->targetSlot == -1) { + qFatal() << "Tried to connect unresolved IPC signal"; + } + + this->listener = std::make_shared(this->signal.name()); + QMetaObject::connect(handler, this->signal.methodIndex(), this->listener.get(), this->targetSlot); + + QObject::connect( + this->listener.get(), + &IpcSignalListener::triggered, + handler, + &IpcHandler::onSignalTriggered + ); +} + IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) { for (const auto& arg: function.argumentTypes) { this->argumentSlots.emplace_back(arg); @@ -172,16 +243,28 @@ void IpcHandler::onPostReload() { // which should handle inheritance on the qml side. for (auto i = smeta.methodCount(); i != meta->methodCount(); i++) { const auto& method = meta->method(i); - if (method.methodType() != QMetaMethod::Slot) continue; + if (method.methodType() == QMetaMethod::Slot) { + auto ipcFunc = IpcFunction(method); + QString error; - auto ipcFunc = IpcFunction(method); - QString error; + if (!ipcFunc.resolve(error)) { + qmlWarning(this).nospace().noquote() + << "Error parsing function \"" << method.name() << "\": " << error; + } else { + this->functionMap.insert(method.name(), ipcFunc); + } + } else if (method.methodType() == QMetaMethod::Signal) { + qmlDebug(this) << "Signal detected: " << method.name(); + auto ipcSig = IpcSignal(method); + QString error; - if (!ipcFunc.resolve(error)) { - qmlWarning(this).nospace().noquote() - << "Error parsing function \"" << method.name() << "\": " << error; - } else { - this->functionMap.insert(method.name(), ipcFunc); + if (!ipcSig.resolve(error)) { + qmlWarning(this).nospace().noquote() + << "Error parsing signal \"" << method.name() << "\": " << error; + } else { + ipcSig.connectListener(this); + this->signalMap.emplace(method.name(), std::move(ipcSig)); + } } } @@ -222,6 +305,11 @@ IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generati return dynamic_cast(ext); } +void IpcHandler::onSignalTriggered(const QString& signal, const QString& value) const { + emit IpcSignalRemoteListener::instance() + -> triggered(this->registeredState.target, signal, value); +} + void IpcHandler::updateRegistration(bool destroying) { if (!this->complete) return; @@ -324,6 +412,10 @@ WireTargetDefinition IpcHandler::wireDef() const { wire.properties += prop.wireDef(); } + for (const auto& sig: this->signalMap.values()) { + wire.signalFunctions += sig.wireDef(); + } + return wire; } @@ -368,6 +460,13 @@ IpcProperty* IpcHandler::findProperty(const QString& name) { else return &*itr; } +IpcSignal* IpcHandler::findSignal(const QString& name) { + auto itr = this->signalMap.find(name); + + if (itr == this->signalMap.end()) return nullptr; + else return &*itr; +} + IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) { return this->handlers.value(target); } @@ -382,4 +481,9 @@ QVector IpcHandlerRegistry::wireTargets() const { return wire; } +IpcSignalRemoteListener* IpcSignalRemoteListener::instance() { + static auto* instance = new IpcSignalRemoteListener(); + return instance; +} + } // namespace qs::io::ipc diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index 4c5d9bc..eb274e3 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -1,8 +1,10 @@ #pragma once #include +#include #include +#include #include #include #include @@ -67,6 +69,54 @@ public: const IpcType* type = nullptr; }; +class IpcSignalListener: public QObject { + Q_OBJECT; + +public: + IpcSignalListener(QString signal): signal(std::move(signal)) {} + + static const int SLOT_VOID; + static const int SLOT_STRING; + static const int SLOT_INT; + static const int SLOT_BOOL; + static const int SLOT_REAL; + static const int SLOT_COLOR; + +signals: + void triggered(const QString& signal, const QString& value); + +private slots: + void invokeVoid() { this->triggered(this->signal, "void"); } + void invokeString(const QString& value) { this->triggered(this->signal, value); } + void invokeInt(int value) { this->triggered(this->signal, QString::number(value)); } + void invokeBool(bool value) { this->triggered(this->signal, value ? "true" : "false"); } + void invokeReal(double value) { this->triggered(this->signal, QString::number(value)); } + void invokeColor(QColor value) { this->triggered(this->signal, value.name(QColor::HexArgb)); } + +private: + QString signal; +}; + +class IpcHandler; + +class IpcSignal { +public: + explicit IpcSignal(QMetaMethod signal): signal(signal) {} + + bool resolve(QString& error); + + [[nodiscard]] WireSignalDefinition wireDef() const; + + QMetaMethod signal; + int targetSlot = -1; + + void connectListener(IpcHandler* handler); + +private: + void connectListener(QObject* handler, IpcSignalListener* listener) const; + std::shared_ptr listener; +}; + class IpcHandlerRegistry; ///! Handler for IPC message calls. @@ -100,6 +150,11 @@ class IpcHandlerRegistry; /// - `real` will be converted to a string and returned. /// - `color` will be converted to a hex string in the form `#AARRGGBB` and returned. /// +/// #### Signals +/// IPC handler signals can be observed remotely using `qs ipc wait` (one call) +/// and `qs ipc listen` (many calls). IPC signals may have zero or one argument, where +/// the argument is one of the types listed above, or no arguments for void. +/// /// #### Example /// The following example creates ipc functions to control and retrieve the appearance /// of a Rectangle. @@ -119,10 +174,18 @@ class IpcHandlerRegistry; /// /// function setColor(color: color): void { rect.color = color; } /// function getColor(): color { return rect.color; } +/// /// function setAngle(angle: real): void { rect.rotation = angle; } /// function getAngle(): real { return rect.rotation; } -/// function setRadius(radius: int): void { rect.radius = radius; } +/// +/// function setRadius(radius: int): void { +/// rect.radius = radius; +/// this.radiusChanged(radius); +/// } +/// /// function getRadius(): int { return rect.radius; } +/// +/// signal radiusChanged(newRadius: int); /// } /// } /// ``` @@ -136,6 +199,7 @@ class IpcHandlerRegistry; /// function getAngle(): real /// function setRadius(radius: int): void /// function getRadius(): int +/// signal radiusChanged(newRadius: int) /// ``` /// /// and then invoked using `qs ipc call`. @@ -179,14 +243,15 @@ public: QString listMembers(qsizetype indent); [[nodiscard]] IpcFunction* findFunction(const QString& name); [[nodiscard]] IpcProperty* findProperty(const QString& name); + [[nodiscard]] IpcSignal* findSignal(const QString& name); [[nodiscard]] WireTargetDefinition wireDef() const; signals: void enabledChanged(); void targetChanged(); -private slots: - //void handleIpcPropertyChange(); +public slots: + void onSignalTriggered(const QString& signal, const QString& value) const; private: void updateRegistration(bool destroying = false); @@ -204,6 +269,7 @@ private: QHash functionMap; QHash propertyMap; + QHash signalMap; friend class IpcHandlerRegistry; }; @@ -227,4 +293,14 @@ private: QHash> knownHandlers; }; +class IpcSignalRemoteListener: public QObject { + Q_OBJECT; + +public: + static IpcSignalRemoteListener* instance(); + +signals: + void triggered(const QString& target, const QString& signal, const QString& value); +}; + } // namespace qs::io::ipc diff --git a/src/ipc/ipc.cpp b/src/ipc/ipc.cpp index 0196359..32d8482 100644 --- a/src/ipc/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -61,6 +61,7 @@ IpcServerConnection::IpcServerConnection(QLocalSocket* socket, IpcServer* server void IpcServerConnection::onDisconnected() { qCInfo(logIpc) << "IPC connection disconnected" << this; + delete this; } void IpcServerConnection::onReadyRead() { @@ -84,6 +85,11 @@ void IpcServerConnection::onReadyRead() { ); if (!this->stream.commitTransaction()) return; + + // async connections reparent + if (dynamic_cast(this->parent()) != nullptr) { + delete this; + } } IpcClient::IpcClient(const QString& path) { diff --git a/src/ipc/ipccommand.hpp b/src/ipc/ipccommand.hpp index b221b46..105ce1e 100644 --- a/src/ipc/ipccommand.hpp +++ b/src/ipc/ipccommand.hpp @@ -16,6 +16,7 @@ using IpcCommand = std::variant< IpcKillCommand, qs::io::ipc::comm::QueryMetadataCommand, qs::io::ipc::comm::StringCallCommand, + qs::io::ipc::comm::SignalListenCommand, qs::io::ipc::comm::StringPropReadCommand>; } // namespace qs::ipc diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 1a58cb8..d867584 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -411,6 +411,10 @@ int ipcCommand(CommandState& cmd) { return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name); } else if (*cmd.ipc.getprop) { return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name); + } else if (*cmd.ipc.wait) { + return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, true); + } else if (*cmd.ipc.listen) { + return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, false); } else { QVector arguments; for (auto& arg: cmd.ipc.arguments) { diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index a186ddb..f666e7a 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -74,6 +74,8 @@ struct CommandState { CLI::App* show = nullptr; CLI::App* call = nullptr; CLI::App* getprop = nullptr; + CLI::App* wait = nullptr; + CLI::App* listen = nullptr; bool showOld = false; QStringOption target; QStringOption name; diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index 0776f58..fc43b6b 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include // NOLINT: Need to include this for impls of some CLI11 classes @@ -226,6 +227,16 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->allow_extra_args(); } + auto signalCmd = [&](std::string cmd, std::string desc) { + auto* scmd = sub->add_subcommand(std::move(cmd), std::move(desc)); + scmd->add_option("target", state.ipc.target, "The target to listen on."); + scmd->add_option("signal", state.ipc.name, "The signal to listen for."); + return scmd; + }; + + state.ipc.wait = signalCmd("wait", "Wait for one IpcHandler signal."); + state.ipc.listen = signalCmd("listen", "Listen for IpcHandler signals."); + { auto* prop = sub->add_subcommand("prop", "Manipulate IpcHandler properties.")->require_subcommand(); From 5eb6f51f4a2a84d3f0f3f7352253780730beee1b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 17 Jan 2026 03:05:50 -0800 Subject: [PATCH 2/2] core: add preprocessor for versioning --- CMakeLists.txt | 2 + changelog/next.md | 1 + src/build/build.hpp.in | 5 ++ src/core/CMakeLists.txt | 3 +- src/core/qmlglobal.cpp | 9 +++ src/core/qmlglobal.hpp | 15 +++++ src/core/rootwrapper.cpp | 30 ++++++++- src/core/scan.cpp | 133 +++++++++++++++++++++++++++++---------- src/core/scan.hpp | 8 +++ src/core/scanenv.cpp | 22 +++++++ src/core/scanenv.hpp | 17 +++++ src/launch/command.cpp | 4 +- 12 files changed, 209 insertions(+), 40 deletions(-) create mode 100644 src/core/scanenv.cpp create mode 100644 src/core/scanenv.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 81e896f..7633f4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/changelog/next.md b/changelog/next.md index cab03e6..30e998b 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -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 diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index 075abd1..66fb664 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -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@ diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index bbfb8c4..fb63f40 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 07238f6..03fb818 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -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); diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 1fc363b..3ca70be 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -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; } diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 25c46cc..1e75819 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -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()); diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 453b7dc..8ca1f51 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -1,9 +1,11 @@ #include "scan.hpp" #include +#include #include #include #include +#include #include #include #include @@ -15,6 +17,7 @@ #include #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(); - 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 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; } diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 2dc8c3c..29f8f6a 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -23,6 +23,14 @@ public: QVector scannedFiles; QHash fileIntercepts; + struct ScanError { + QString file; + QString message; + int line; + }; + + QVector scanErrors; + private: QDir rootPath; diff --git a/src/core/scanenv.cpp b/src/core/scanenv.cpp new file mode 100644 index 0000000..b8c514c --- /dev/null +++ b/src/core/scanenv.cpp @@ -0,0 +1,22 @@ +#include "scanenv.hpp" + +#include + +#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 diff --git a/src/core/scanenv.hpp b/src/core/scanenv.hpp new file mode 100644 index 0000000..0abde2e --- /dev/null +++ b/src/core/scanenv.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +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 diff --git a/src/launch/command.cpp b/src/launch/command.cpp index d867584..151fc24 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -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;