From 471419108a5c26d44c3c5b170b535bd74ab41cc5 Mon Sep 17 00:00:00 2001 From: Ala Alkhafaji <3akevdev@gmail.com> Date: Thu, 13 Nov 2025 19:43:11 +0100 Subject: [PATCH 1/2] i3/ipc: implement IPC listener to receive arbitrary events --- src/x11/i3/ipc/CMakeLists.txt | 2 + src/x11/i3/ipc/connection.cpp | 449 +++++----------------------------- src/x11/i3/ipc/connection.hpp | 82 +------ src/x11/i3/ipc/controller.cpp | 351 ++++++++++++++++++++++++++ src/x11/i3/ipc/controller.hpp | 94 +++++++ src/x11/i3/ipc/listener.cpp | 43 ++++ src/x11/i3/ipc/listener.hpp | 57 +++++ src/x11/i3/ipc/monitor.cpp | 4 +- src/x11/i3/ipc/monitor.hpp | 7 +- src/x11/i3/ipc/qml.cpp | 31 +-- src/x11/i3/ipc/qml.hpp | 1 + src/x11/i3/ipc/workspace.cpp | 4 +- src/x11/i3/ipc/workspace.hpp | 5 +- src/x11/i3/module.md | 2 + 14 files changed, 654 insertions(+), 478 deletions(-) create mode 100644 src/x11/i3/ipc/controller.cpp create mode 100644 src/x11/i3/ipc/controller.hpp create mode 100644 src/x11/i3/ipc/listener.cpp create mode 100644 src/x11/i3/ipc/listener.hpp diff --git a/src/x11/i3/ipc/CMakeLists.txt b/src/x11/i3/ipc/CMakeLists.txt index 27a44841..c228ae33 100644 --- a/src/x11/i3/ipc/CMakeLists.txt +++ b/src/x11/i3/ipc/CMakeLists.txt @@ -3,6 +3,8 @@ qt_add_library(quickshell-i3-ipc STATIC qml.cpp workspace.cpp monitor.cpp + controller.cpp + listener.cpp ) qt_add_qml_module(quickshell-i3-ipc diff --git a/src/x11/i3/ipc/connection.cpp b/src/x11/i3/ipc/connection.cpp index c5d2db2e..8103d119 100644 --- a/src/x11/i3/ipc/connection.cpp +++ b/src/x11/i3/ipc/connection.cpp @@ -1,4 +1,4 @@ -#include +#include "connection.hpp" #include #include #include @@ -23,11 +23,6 @@ #include #include "../../../core/logcat.hpp" -#include "../../../core/model.hpp" -#include "../../../core/qmlscreen.hpp" -#include "connection.hpp" -#include "monitor.hpp" -#include "workspace.hpp" namespace qs::i3::ipc { @@ -36,31 +31,43 @@ QS_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); QS_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); } // namespace -void I3Ipc::makeRequest(const QByteArray& request) { - if (!this->valid) { - qCWarning(logI3IpcEvents) << "IPC connection is not open, ignoring request."; - return; - } - this->liveEventSocket.write(request); - this->liveEventSocket.flush(); -} - -void I3Ipc::dispatch(const QString& payload) { - auto message = I3Ipc::buildRequestMessage(EventCode::RunCommand, payload.toLocal8Bit()); +QString I3IpcEvent::type() const { return I3IpcEvent::eventToString(this->mCode); } +QString I3IpcEvent::data() const { return QString::fromUtf8(this->mData.toJson()); } - this->makeRequest(message); +EventCode I3IpcEvent::intToEvent(quint32 raw) { + if ((EventCode::Workspace <= raw && raw <= EventCode::Input) + || (EventCode::RunCommand <= raw && raw <= EventCode::GetTree)) + { + return static_cast(raw); + } else { + return EventCode::Unknown; + } } -QByteArray I3Ipc::buildRequestMessage(EventCode cmd, const QByteArray& payload) { - auto payloadLength = static_cast(payload.length()); +QString I3IpcEvent::eventToString(EventCode event) { + switch (event) { + case EventCode::RunCommand: return "run_command"; break; + case EventCode::GetWorkspaces: return "get_workspaces"; break; + case EventCode::Subscribe: return "subscribe"; break; + case EventCode::GetOutputs: return "get_outputs"; break; + case EventCode::GetTree: return "get_tree"; break; - auto type = QByteArray(std::bit_cast>(cmd).data(), 4); - auto len = QByteArray(std::bit_cast>(payloadLength).data(), 4); + case EventCode::Output: return "output"; break; + case EventCode::Workspace: return "workspace"; break; + case EventCode::Mode: return "mode"; break; + case EventCode::Window: return "window"; break; + case EventCode::BarconfigUpdate: return "barconfig_update"; break; + case EventCode::Binding: return "binding"; break; + case EventCode::Shutdown: return "shutdown"; break; + case EventCode::Tick: return "tick"; break; + case EventCode::BarStateUpdate: return "bar_state_update"; break; + case EventCode::Input: return "input"; break; - return MAGIC.data() + len + type + payload; + default: return "unknown"; break; + } } -I3Ipc::I3Ipc() { +I3Ipc::I3Ipc(const QList& events): mEvents(events) { auto sock = qEnvironmentVariable("I3SOCK"); if (sock.isEmpty()) { @@ -74,36 +81,50 @@ I3Ipc::I3Ipc() { } } - this->bFocusedWorkspace.setBinding([this]() -> I3Workspace* { - if (!this->bFocusedMonitor) return nullptr; - return this->bFocusedMonitor->bindableActiveWorkspace().value(); - }); - this->mSocketPath = sock; // clang-format off - QObject::connect(&this->liveEventSocket, &QLocalSocket::errorOccurred, this, &I3Ipc::eventSocketError); - QObject::connect(&this->liveEventSocket, &QLocalSocket::stateChanged, this, &I3Ipc::eventSocketStateChanged); - QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady); - QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe); + QObject::connect(&this->liveEventSocket, &QLocalSocket::errorOccurred, this, &I3Ipc::eventSocketError); + QObject::connect(&this->liveEventSocket, &QLocalSocket::stateChanged, this, &I3Ipc::eventSocketStateChanged); + QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady); + QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe); // clang-format on this->liveEventSocketDs.setDevice(&this->liveEventSocket); this->liveEventSocketDs.setByteOrder(static_cast(QSysInfo::ByteOrder)); +} + +void I3Ipc::makeRequest(const QByteArray& request) { + if (!this->valid) { + qCWarning(logI3IpcEvents) << "IPC connection is not open, ignoring request."; + return; + } + this->liveEventSocket.write(request); + this->liveEventSocket.flush(); +} + +void I3Ipc::dispatch(const QString& payload) { + auto message = I3Ipc::buildRequestMessage(EventCode::RunCommand, payload.toLocal8Bit()); - this->liveEventSocket.connectToServer(this->mSocketPath); + this->makeRequest(message); +} + +QByteArray I3Ipc::buildRequestMessage(EventCode cmd, const QByteArray& payload) { + auto payloadLength = static_cast(payload.length()); + + auto type = QByteArray(std::bit_cast>(cmd).data(), 4); + auto len = QByteArray(std::bit_cast>(payloadLength).data(), 4); + + return MAGIC.data() + len + type + payload; } void I3Ipc::subscribe() { - auto payload = QByteArray(R"(["workspace","output"])"); + auto jsonArray = QJsonArray::fromStringList(this->mEvents); + const QJsonDocument jsonDoc(jsonArray); + auto payload = jsonDoc.toJson(QJsonDocument::Compact); auto message = I3Ipc::buildRequestMessage(EventCode::Subscribe, payload); this->makeRequest(message); - - // Workspaces must be refreshed before monitors or no focus will be - // detected on launch. - this->refreshWorkspaces(); - this->refreshMonitors(); } void I3Ipc::eventSocketReady() { @@ -111,15 +132,16 @@ void I3Ipc::eventSocketReady() { this->event.mCode = type; this->event.mData = data; - this->onEvent(&this->event); emit this->rawEvent(&this->event); } } +void I3Ipc::connect() { this->liveEventSocket.connectToServer(this->mSocketPath); } + void I3Ipc::reconnectIPC() { qCWarning(logI3Ipc) << "Fatal IPC error occured, recreating connection"; this->liveEventSocket.disconnectFromServer(); - this->liveEventSocket.connectToServer(this->mSocketPath); + this->connect(); } QVector I3Ipc::parseResponse() { @@ -193,347 +215,4 @@ void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { QString I3Ipc::socketPath() const { return this->mSocketPath; } -void I3Ipc::setFocusedMonitor(I3Monitor* monitor) { - auto* oldMonitor = this->bFocusedMonitor.value(); - if (monitor == oldMonitor) return; - - if (oldMonitor != nullptr) { - QObject::disconnect(oldMonitor, nullptr, this, nullptr); - } - - if (monitor != nullptr) { - QObject::connect(monitor, &QObject::destroyed, this, &I3Ipc::onFocusedMonitorDestroyed); - } - - this->bFocusedMonitor = monitor; -} - -void I3Ipc::onFocusedMonitorDestroyed() { this->bFocusedMonitor = nullptr; } - -I3Ipc* I3Ipc::instance() { - static I3Ipc* instance = nullptr; // NOLINT - - if (instance == nullptr) { - instance = new I3Ipc(); - } - - return instance; -} - -void I3Ipc::refreshWorkspaces() { - this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetWorkspaces)); -} - -void I3Ipc::handleGetWorkspacesEvent(I3IpcEvent* event) { - auto data = event->mData; - - auto workspaces = data.array(); - - const auto& mList = this->mWorkspaces.valueList(); - auto names = QVector(); - - qCDebug(logI3Ipc) << "There are" << workspaces.toVariantList().length() << "workspaces"; - for (auto entry: workspaces) { - auto object = entry.toObject().toVariantMap(); - auto name = object["name"].toString(); - - auto workspaceIter = std::ranges::find_if(mList, [name](I3Workspace* m) { - return m->bindableName().value() == name; - }); - - auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; - auto existed = workspace != nullptr; - - if (workspace == nullptr) { - workspace = new I3Workspace(this); - } - - workspace->updateFromObject(object); - - if (!existed) { - this->mWorkspaces.insertObjectSorted(workspace, &I3Ipc::compareWorkspaces); - } - - if (!this->bFocusedWorkspace && object.value("focused").value()) { - this->bFocusedMonitor = workspace->bindableMonitor().value(); - } - - names.push_back(name); - } - - auto removedWorkspaces = QVector(); - - for (auto* workspace: mList) { - if (!names.contains(workspace->bindableName().value())) { - removedWorkspaces.push_back(workspace); - } - } - - qCDebug(logI3Ipc) << "Removing" << removedWorkspaces.length() << "deleted workspaces."; - - for (auto* workspace: removedWorkspaces) { - this->mWorkspaces.removeObject(workspace); - delete workspace; - } -} - -void I3Ipc::refreshMonitors() { - this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetOutputs)); -} - -void I3Ipc::handleGetOutputsEvent(I3IpcEvent* event) { - auto data = event->mData; - - auto monitors = data.array(); - const auto& mList = this->mMonitors.valueList(); - auto names = QVector(); - - qCDebug(logI3Ipc) << "There are" << monitors.toVariantList().length() << "monitors"; - - for (auto elem: monitors) { - auto object = elem.toObject().toVariantMap(); - auto name = object["name"].toString(); - - auto monitorIter = std::ranges::find_if(mList, [name](I3Monitor* m) { - return m->bindableName().value() == name; - }); - - auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; - auto existed = monitor != nullptr; - - if (monitor == nullptr) { - monitor = new I3Monitor(this); - } - - monitor->updateFromObject(object); - - if (monitor->bindableFocused().value()) { - this->setFocusedMonitor(monitor); - } - - if (!existed) { - this->mMonitors.insertObject(monitor); - } - - names.push_back(name); - } - - auto removedMonitors = QVector(); - - for (auto* monitor: mList) { - if (!names.contains(monitor->bindableName().value())) { - removedMonitors.push_back(monitor); - } - } - - qCDebug(logI3Ipc) << "Removing" << removedMonitors.length() << "disconnected monitors."; - - for (auto* monitor: removedMonitors) { - this->mMonitors.removeObject(monitor); - delete monitor; - } -} - -void I3Ipc::onEvent(I3IpcEvent* event) { - switch (event->mCode) { - case EventCode::Workspace: this->handleWorkspaceEvent(event); return; - case EventCode::Output: - /// I3 only sends an "unspecified" event, so we have to query the data changes ourselves - qCInfo(logI3Ipc) << "Refreshing Monitors..."; - this->refreshMonitors(); - return; - case EventCode::Subscribe: qCInfo(logI3Ipc) << "Connected to IPC"; return; - case EventCode::GetOutputs: this->handleGetOutputsEvent(event); return; - case EventCode::GetWorkspaces: this->handleGetWorkspacesEvent(event); return; - case EventCode::RunCommand: I3Ipc::handleRunCommand(event); return; - case EventCode::Unknown: - qCWarning(logI3Ipc) << "Unknown event:" << event->type() << event->data(); - return; - default: qCWarning(logI3Ipc) << "Unhandled event:" << event->type(); - } -} - -void I3Ipc::handleRunCommand(I3IpcEvent* event) { - for (auto r: event->mData.array()) { - auto obj = r.toObject(); - const bool success = obj["success"].toBool(); - - if (!success) { - const QString error = obj["error"].toString(); - qCWarning(logI3Ipc) << "Error occured while running command:" << error; - } - } -} - -void I3Ipc::handleWorkspaceEvent(I3IpcEvent* event) { - // If a workspace doesn't exist, and is being switch to, no focus change event is emited, - // only the init one, which does not contain the previously focused workspace - auto change = event->mData["change"]; - - if (change == "init") { - qCInfo(logI3IpcEvents) << "New workspace has been created"; - - auto workspaceData = event->mData["current"]; - - auto* workspace = this->findWorkspaceByID(workspaceData["id"].toInt(-1)); - auto existed = workspace != nullptr; - - if (!existed) { - workspace = new I3Workspace(this); - } - - if (workspaceData.isObject()) { - workspace->updateFromObject(workspaceData.toObject().toVariantMap()); - } - - if (!existed) { - this->mWorkspaces.insertObjectSorted(workspace, &I3Ipc::compareWorkspaces); - qCInfo(logI3Ipc) << "Added workspace" << workspace->bindableName().value() << "to list"; - } - } else if (change == "focus") { - auto oldData = event->mData["old"]; - auto newData = event->mData["current"]; - auto oldName = oldData["name"].toString(); - auto newName = newData["name"].toString(); - - qCInfo(logI3IpcEvents) << "Focus changed: " << oldName << "->" << newName; - - if (auto* oldWorkspace = this->findWorkspaceByName(oldName)) { - oldWorkspace->updateFromObject(oldData.toObject().toVariantMap()); - } - - auto* newWorkspace = this->findWorkspaceByName(newName); - - if (newWorkspace == nullptr) { - newWorkspace = new I3Workspace(this); - } - - newWorkspace->updateFromObject(newData.toObject().toVariantMap()); - - if (newWorkspace->bindableMonitor().value()) { - auto* monitor = newWorkspace->bindableMonitor().value(); - monitor->setFocusedWorkspace(newWorkspace); - this->bFocusedMonitor = monitor; - } - } else if (change == "empty") { - auto name = event->mData["current"]["name"].toString(); - - auto* oldWorkspace = this->findWorkspaceByName(name); - - if (oldWorkspace != nullptr) { - qCInfo(logI3Ipc) << "Deleting" << oldWorkspace->bindableId().value() << name; - - if (this->bFocusedWorkspace == oldWorkspace) { - this->bFocusedMonitor->setFocusedWorkspace(nullptr); - } - - this->workspaces()->removeObject(oldWorkspace); - - delete oldWorkspace; - } else { - qCInfo(logI3Ipc) << "Workspace" << name << "has already been deleted"; - } - } else if (change == "move" || change == "rename" || change == "urgent") { - auto name = event->mData["current"]["name"].toString(); - - auto* workspace = this->findWorkspaceByName(name); - - if (workspace != nullptr) { - auto data = event->mData["current"].toObject().toVariantMap(); - - workspace->updateFromObject(data); - } else { - qCWarning(logI3Ipc) << "Workspace" << name << "doesn't exist"; - } - } else if (change == "reload") { - qCInfo(logI3Ipc) << "Refreshing Workspaces..."; - this->refreshWorkspaces(); - } -} - -I3Monitor* I3Ipc::monitorFor(QuickshellScreenInfo* screen) { - if (screen == nullptr) return nullptr; - - return this->findMonitorByName(screen->name()); -} - -I3Workspace* I3Ipc::findWorkspaceByID(qint32 id) { - auto list = this->mWorkspaces.valueList(); - auto workspaceIter = - std::ranges::find_if(list, [id](I3Workspace* m) { return m->bindableId().value() == id; }); - - return workspaceIter == list.end() ? nullptr : *workspaceIter; -} - -I3Workspace* I3Ipc::findWorkspaceByName(const QString& name) { - auto list = this->mWorkspaces.valueList(); - auto workspaceIter = std::ranges::find_if(list, [name](I3Workspace* m) { - return m->bindableName().value() == name; - }); - - return workspaceIter == list.end() ? nullptr : *workspaceIter; -} - -I3Monitor* I3Ipc::findMonitorByName(const QString& name, bool createIfMissing) { - auto list = this->mMonitors.valueList(); - auto monitorIter = std::ranges::find_if(list, [name](I3Monitor* m) { - return m->bindableName().value() == name; - }); - - if (monitorIter != list.end()) { - return *monitorIter; - } else if (createIfMissing) { - qCDebug(logI3Ipc) << "Monitor" << name << "requested before creation, performing early init"; - auto* monitor = new I3Monitor(this); - monitor->updateInitial(name); - this->mMonitors.insertObject(monitor); - return monitor; - } else { - return nullptr; - } -} - -ObjectModel* I3Ipc::monitors() { return &this->mMonitors; } -ObjectModel* I3Ipc::workspaces() { return &this->mWorkspaces; } - -bool I3Ipc::compareWorkspaces(I3Workspace* a, I3Workspace* b) { - return a->bindableNumber().value() > b->bindableNumber().value(); -} - -QString I3IpcEvent::type() const { return I3IpcEvent::eventToString(this->mCode); } -QString I3IpcEvent::data() const { return QString::fromUtf8(this->mData.toJson()); } - -EventCode I3IpcEvent::intToEvent(quint32 raw) { - if ((EventCode::Workspace <= raw && raw <= EventCode::Input) - || (EventCode::RunCommand <= raw && raw <= EventCode::GetTree)) - { - return static_cast(raw); - } else { - return EventCode::Unknown; - } -} - -QString I3IpcEvent::eventToString(EventCode event) { - switch (event) { - case EventCode::RunCommand: return "run_command"; break; - case EventCode::GetWorkspaces: return "get_workspaces"; break; - case EventCode::Subscribe: return "subscribe"; break; - case EventCode::GetOutputs: return "get_outputs"; break; - case EventCode::GetTree: return "get_tree"; break; - - case EventCode::Output: return "output"; break; - case EventCode::Workspace: return "workspace"; break; - case EventCode::Mode: return "mode"; break; - case EventCode::Window: return "window"; break; - case EventCode::BarconfigUpdate: return "barconfig_update"; break; - case EventCode::Binding: return "binding"; break; - case EventCode::Shutdown: return "shutdown"; break; - case EventCode::Tick: return "tick"; break; - case EventCode::BarStateUpdate: return "bar_state_update"; break; - case EventCode::Input: return "input"; break; - - default: return "unknown"; break; - } -} - } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/connection.hpp b/src/x11/i3/ipc/connection.hpp index af480c55..6100f7ed 100644 --- a/src/x11/i3/ipc/connection.hpp +++ b/src/x11/i3/ipc/connection.hpp @@ -1,28 +1,14 @@ #pragma once #include +#include #include -#include #include #include -#include -#include #include #include #include -#include "../../../core/model.hpp" -#include "../../../core/qmlscreen.hpp" - -namespace qs::i3::ipc { - -class I3Workspace; -class I3Monitor; -} // namespace qs::i3::ipc - -Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Workspace*); -Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Monitor*); - namespace qs::i3::ipc { constexpr std::string MAGIC = "i3-ipc"; @@ -54,9 +40,7 @@ using Event = std::tuple; class I3IpcEvent: public QObject { Q_OBJECT; - /// The name of the event Q_PROPERTY(QString type READ type CONSTANT); - /// The payload of the event in JSON format. Q_PROPERTY(QString data READ data CONSTANT); QML_NAMED_ELEMENT(I3Event); @@ -75,90 +59,48 @@ class I3IpcEvent: public QObject { static QString eventToString(EventCode event); }; +/// Base class that manages the IPC socket, subscriptions and event reception. class I3Ipc: public QObject { Q_OBJECT; public: - static I3Ipc* instance(); + explicit I3Ipc(const QList& events); [[nodiscard]] QString socketPath() const; void makeRequest(const QByteArray& request); void dispatch(const QString& payload); + void connect(); - static QByteArray buildRequestMessage(EventCode cmd, const QByteArray& payload = QByteArray()); - - I3Workspace* findWorkspaceByName(const QString& name); - I3Monitor* findMonitorByName(const QString& name, bool createIfMissing = false); - I3Workspace* findWorkspaceByID(qint32 id); - - void setFocusedMonitor(I3Monitor* monitor); - - void refreshWorkspaces(); - void refreshMonitors(); - - I3Monitor* monitorFor(QuickshellScreenInfo* screen); - - [[nodiscard]] QBindable bindableFocusedMonitor() const { - return &this->bFocusedMonitor; - }; - - [[nodiscard]] QBindable bindableFocusedWorkspace() const { - return &this->bFocusedWorkspace; - }; - - [[nodiscard]] ObjectModel* monitors(); - [[nodiscard]] ObjectModel* workspaces(); + [[nodiscard]] QByteArray static buildRequestMessage( + EventCode cmd, + const QByteArray& payload = QByteArray() + ); signals: void connected(); void rawEvent(I3IpcEvent* event); - void focusedWorkspaceChanged(); - void focusedMonitorChanged(); -private slots: +protected slots: void eventSocketError(QLocalSocket::LocalSocketError error) const; void eventSocketStateChanged(QLocalSocket::LocalSocketState state); void eventSocketReady(); void subscribe(); - void onFocusedMonitorDestroyed(); - -private: - explicit I3Ipc(); - - void onEvent(I3IpcEvent* event); - - void handleWorkspaceEvent(I3IpcEvent* event); - void handleGetWorkspacesEvent(I3IpcEvent* event); - void handleGetOutputsEvent(I3IpcEvent* event); - static void handleRunCommand(I3IpcEvent* event); - static bool compareWorkspaces(I3Workspace* a, I3Workspace* b); - +protected: void reconnectIPC(); - QVector> parseResponse(); QLocalSocket liveEventSocket; QDataStream liveEventSocketDs; QString mSocketPath; - bool valid = false; - ObjectModel mMonitors {this}; - ObjectModel mWorkspaces {this}; - I3IpcEvent event {this}; - Q_OBJECT_BINDABLE_PROPERTY(I3Ipc, I3Monitor*, bFocusedMonitor, &I3Ipc::focusedMonitorChanged); - - Q_OBJECT_BINDABLE_PROPERTY( - I3Ipc, - I3Workspace*, - bFocusedWorkspace, - &I3Ipc::focusedWorkspaceChanged - ); +private: + QList mEvents; }; } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/controller.cpp b/src/x11/i3/ipc/controller.cpp new file mode 100644 index 00000000..25a227b1 --- /dev/null +++ b/src/x11/i3/ipc/controller.cpp @@ -0,0 +1,351 @@ +#include "controller.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/logcat.hpp" +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" +#include "workspace.hpp" + +namespace qs::i3::ipc { + +namespace { +QS_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); +QS_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); +} // namespace + +I3IpcController::I3IpcController(): I3Ipc({"workspace", "output"}) { + // bind focused workspace to focused monitor's active workspace + this->bFocusedWorkspace.setBinding([this]() -> I3Workspace* { + if (!this->bFocusedMonitor) return nullptr; + return this->bFocusedMonitor->bindableActiveWorkspace().value(); + }); + + // clang-format off + QObject::connect(this, &I3Ipc::rawEvent, this, &I3IpcController::onEvent); + QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3IpcController::onConnected); + // clang-format on +} + +void I3IpcController::onConnected() { + // Workspaces must be refreshed before monitors or no focus will be + // detected on launch. + this->refreshWorkspaces(); + this->refreshMonitors(); +} + +void I3IpcController::setFocusedMonitor(I3Monitor* monitor) { + auto* oldMonitor = this->bFocusedMonitor.value(); + if (monitor == oldMonitor) return; + + if (oldMonitor != nullptr) { QObject::disconnect(oldMonitor, nullptr, this, nullptr); } + + if (monitor != nullptr) { + QObject::connect( + monitor, + &QObject::destroyed, + this, + &I3IpcController::onFocusedMonitorDestroyed + ); + } + + this->bFocusedMonitor = monitor; +} + +void I3IpcController::onFocusedMonitorDestroyed() { this->bFocusedMonitor = nullptr; } + +I3IpcController* I3IpcController::instance() { + static I3IpcController* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new I3IpcController(); + instance->connect(); + } + + return instance; +} + +void I3IpcController::refreshWorkspaces() { + this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetWorkspaces)); +} + +void I3IpcController::handleGetWorkspacesEvent(I3IpcEvent* event) { + auto data = event->mData; + + auto workspaces = data.array(); + + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); + + qCDebug(logI3Ipc) << "There are" << workspaces.toVariantList().length() << "workspaces"; + for (auto entry: workspaces) { + auto object = entry.toObject().toVariantMap(); + auto name = object["name"].toString(); + + auto workspaceIter = std::ranges::find_if(mList, [name](I3Workspace* m) { + return m->bindableName().value() == name; + }); + + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; + + if (workspace == nullptr) { workspace = new I3Workspace(this); } + + workspace->updateFromObject(object); + + if (!existed) { + this->mWorkspaces.insertObjectSorted(workspace, &I3IpcController::compareWorkspaces); + } + + if (!this->bFocusedWorkspace && object.value("focused").value()) { + this->bFocusedMonitor = workspace->bindableMonitor().value(); + } + + names.push_back(name); + } + + auto removedWorkspaces = QVector(); + + for (auto* workspace: mList) { + if (!names.contains(workspace->bindableName().value())) { + removedWorkspaces.push_back(workspace); + } + } + + qCDebug(logI3Ipc) << "Removing" << removedWorkspaces.length() << "deleted workspaces."; + + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } +} + +void I3IpcController::refreshMonitors() { + this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetOutputs)); +} + +void I3IpcController::handleGetOutputsEvent(I3IpcEvent* event) { + auto data = event->mData; + + auto monitors = data.array(); + const auto& mList = this->mMonitors.valueList(); + auto names = QVector(); + + qCDebug(logI3Ipc) << "There are" << monitors.toVariantList().length() << "monitors"; + + for (auto elem: monitors) { + auto object = elem.toObject().toVariantMap(); + auto name = object["name"].toString(); + + auto monitorIter = std::ranges::find_if(mList, [name](I3Monitor* m) { + return m->bindableName().value() == name; + }); + + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; + + if (monitor == nullptr) { monitor = new I3Monitor(this); } + + monitor->updateFromObject(object); + + if (monitor->bindableFocused().value()) { this->setFocusedMonitor(monitor); } + + if (!existed) { this->mMonitors.insertObject(monitor); } + + names.push_back(name); + } + + auto removedMonitors = QVector(); + + for (auto* monitor: mList) { + if (!names.contains(monitor->bindableName().value())) { removedMonitors.push_back(monitor); } + } + + qCDebug(logI3Ipc) << "Removing" << removedMonitors.length() << "disconnected monitors."; + + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + delete monitor; + } +} + +void I3IpcController::onEvent(I3IpcEvent* event) { + switch (event->mCode) { + case EventCode::Workspace: this->handleWorkspaceEvent(event); return; + case EventCode::Output: + /// I3 only sends an "unspecified" event, so we have to query the data changes ourselves + qCInfo(logI3Ipc) << "Refreshing Monitors..."; + this->refreshMonitors(); + return; + case EventCode::Subscribe: qCInfo(logI3Ipc) << "Connected to IPC"; return; + case EventCode::GetOutputs: this->handleGetOutputsEvent(event); return; + case EventCode::GetWorkspaces: this->handleGetWorkspacesEvent(event); return; + case EventCode::RunCommand: I3IpcController::handleRunCommand(event); return; + case EventCode::Unknown: + qCWarning(logI3Ipc) << "Unknown event:" << event->type() << event->data(); + return; + default: qCWarning(logI3Ipc) << "Unhandled event:" << event->type(); + } +} + +void I3IpcController::handleRunCommand(I3IpcEvent* event) { + for (auto r: event->mData.array()) { + auto obj = r.toObject(); + const bool success = obj["success"].toBool(); + + if (!success) { + const QString error = obj["error"].toString(); + qCWarning(logI3Ipc) << "Error occured while running command:" << error; + } + } +} + +void I3IpcController::handleWorkspaceEvent(I3IpcEvent* event) { + // If a workspace doesn't exist, and is being switch to, no focus change event is emited, + // only the init one, which does not contain the previously focused workspace + auto change = event->mData["change"]; + + if (change == "init") { + qCInfo(logI3IpcEvents) << "New workspace has been created"; + + auto workspaceData = event->mData["current"]; + + auto* workspace = this->findWorkspaceByID(workspaceData["id"].toInt(-1)); + auto existed = workspace != nullptr; + + if (!existed) { workspace = new I3Workspace(this); } + + if (workspaceData.isObject()) { + workspace->updateFromObject(workspaceData.toObject().toVariantMap()); + } + + if (!existed) { + this->mWorkspaces.insertObjectSorted(workspace, &I3IpcController::compareWorkspaces); + qCInfo(logI3Ipc) << "Added workspace" << workspace->bindableName().value() << "to list"; + } + } else if (change == "focus") { + auto oldData = event->mData["old"]; + auto newData = event->mData["current"]; + auto oldName = oldData["name"].toString(); + auto newName = newData["name"].toString(); + + qCInfo(logI3IpcEvents) << "Focus changed: " << oldName << "->" << newName; + + if (auto* oldWorkspace = this->findWorkspaceByName(oldName)) { + oldWorkspace->updateFromObject(oldData.toObject().toVariantMap()); + } + + auto* newWorkspace = this->findWorkspaceByName(newName); + + if (newWorkspace == nullptr) { newWorkspace = new I3Workspace(this); } + + newWorkspace->updateFromObject(newData.toObject().toVariantMap()); + + if (newWorkspace->bindableMonitor().value()) { + auto* monitor = newWorkspace->bindableMonitor().value(); + monitor->setFocusedWorkspace(newWorkspace); + this->bFocusedMonitor = monitor; + } + } else if (change == "empty") { + auto name = event->mData["current"]["name"].toString(); + + auto* oldWorkspace = this->findWorkspaceByName(name); + + if (oldWorkspace != nullptr) { + qCInfo(logI3Ipc) << "Deleting" << oldWorkspace->bindableId().value() << name; + + if (this->bFocusedWorkspace == oldWorkspace) { + this->bFocusedMonitor->setFocusedWorkspace(nullptr); + } + + this->workspaces()->removeObject(oldWorkspace); + + delete oldWorkspace; + } else { + qCInfo(logI3Ipc) << "Workspace" << name << "has already been deleted"; + } + } else if (change == "move" || change == "rename" || change == "urgent") { + auto name = event->mData["current"]["name"].toString(); + + auto* workspace = this->findWorkspaceByName(name); + + if (workspace != nullptr) { + auto data = event->mData["current"].toObject().toVariantMap(); + + workspace->updateFromObject(data); + } else { + qCWarning(logI3Ipc) << "Workspace" << name << "doesn't exist"; + } + } else if (change == "reload") { + qCInfo(logI3Ipc) << "Refreshing Workspaces..."; + this->refreshWorkspaces(); + } +} + +I3Monitor* I3IpcController::monitorFor(QuickshellScreenInfo* screen) { + if (screen == nullptr) return nullptr; + + return this->findMonitorByName(screen->name()); +} + +I3Workspace* I3IpcController::findWorkspaceByID(qint32 id) { + auto list = this->mWorkspaces.valueList(); + auto workspaceIter = + std::ranges::find_if(list, [id](I3Workspace* m) { return m->bindableId().value() == id; }); + + return workspaceIter == list.end() ? nullptr : *workspaceIter; +} + +I3Workspace* I3IpcController::findWorkspaceByName(const QString& name) { + auto list = this->mWorkspaces.valueList(); + auto workspaceIter = std::ranges::find_if(list, [name](I3Workspace* m) { + return m->bindableName().value() == name; + }); + + return workspaceIter == list.end() ? nullptr : *workspaceIter; +} + +I3Monitor* I3IpcController::findMonitorByName(const QString& name, bool createIfMissing) { + auto list = this->mMonitors.valueList(); + auto monitorIter = std::ranges::find_if(list, [name](I3Monitor* m) { + return m->bindableName().value() == name; + }); + + if (monitorIter != list.end()) { + return *monitorIter; + } else if (createIfMissing) { + qCDebug(logI3Ipc) << "Monitor" << name << "requested before creation, performing early init"; + auto* monitor = new I3Monitor(this); + monitor->updateInitial(name); + this->mMonitors.insertObject(monitor); + return monitor; + } else { + return nullptr; + } +} + +ObjectModel* I3IpcController::monitors() { return &this->mMonitors; } +ObjectModel* I3IpcController::workspaces() { return &this->mWorkspaces; } + +bool I3IpcController::compareWorkspaces(I3Workspace* a, I3Workspace* b) { + return a->bindableNumber().value() > b->bindableNumber().value(); +} + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/controller.hpp b/src/x11/i3/ipc/controller.hpp new file mode 100644 index 00000000..464f6f65 --- /dev/null +++ b/src/x11/i3/ipc/controller.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" + +namespace qs::i3::ipc { + +class I3Workspace; +class I3Monitor; +} // namespace qs::i3::ipc + +Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Workspace*); +Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Monitor*); + +namespace qs::i3::ipc { + +/// I3/Sway IPC controller that manages workspaces and monitors +class I3IpcController: public I3Ipc { + Q_OBJECT; + +public: + static I3IpcController* instance(); + + I3Workspace* findWorkspaceByName(const QString& name); + I3Monitor* findMonitorByName(const QString& name, bool createIfMissing = false); + I3Workspace* findWorkspaceByID(qint32 id); + + void setFocusedMonitor(I3Monitor* monitor); + + void refreshWorkspaces(); + void refreshMonitors(); + + I3Monitor* monitorFor(QuickshellScreenInfo* screen); + + [[nodiscard]] QBindable bindableFocusedMonitor() const { + return &this->bFocusedMonitor; + }; + + [[nodiscard]] QBindable bindableFocusedWorkspace() const { + return &this->bFocusedWorkspace; + }; + + [[nodiscard]] ObjectModel* monitors(); + [[nodiscard]] ObjectModel* workspaces(); + +signals: + void focusedWorkspaceChanged(); + void focusedMonitorChanged(); + +private slots: + void onFocusedMonitorDestroyed(); + + void onEvent(I3IpcEvent* event); + void onConnected(); + +private: + explicit I3IpcController(); + + void handleWorkspaceEvent(I3IpcEvent* event); + void handleGetWorkspacesEvent(I3IpcEvent* event); + void handleGetOutputsEvent(I3IpcEvent* event); + static void handleRunCommand(I3IpcEvent* event); + static bool compareWorkspaces(I3Workspace* a, I3Workspace* b); + + ObjectModel mMonitors {this}; + ObjectModel mWorkspaces {this}; + + Q_OBJECT_BINDABLE_PROPERTY( + I3IpcController, + I3Monitor*, + bFocusedMonitor, + &I3IpcController::focusedMonitorChanged + ); + + Q_OBJECT_BINDABLE_PROPERTY( + I3IpcController, + I3Workspace*, + bFocusedWorkspace, + &I3IpcController::focusedWorkspaceChanged + ); +}; + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/listener.cpp b/src/x11/i3/ipc/listener.cpp new file mode 100644 index 00000000..c26023fb --- /dev/null +++ b/src/x11/i3/ipc/listener.cpp @@ -0,0 +1,43 @@ +#include "listener.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::i3::ipc { + +I3IpcListener::~I3IpcListener() { delete this->i3Ipc; } + +void I3IpcListener::onPostReload() { this->startListening(); } + +QList I3IpcListener::subscriptions() const { return this->mSubscriptions; } +void I3IpcListener::setSubscriptions(QList subscriptions) { + if (this->mSubscriptions == subscriptions) return; + this->mSubscriptions = std::move(subscriptions); + + emit this->subscriptionsChanged(); + this->startListening(); +} + +void I3IpcListener::startListening() { + if (this->i3Ipc != nullptr || this->mSubscriptions.isEmpty()) return; + + this->i3Ipc = new I3Ipc(this->mSubscriptions); + + // clang-format off + QObject::connect(this->i3Ipc, &I3Ipc::rawEvent, this, &I3IpcListener::receiveEvent); + // clang-format on + + this->i3Ipc->connect(); +} + +void I3IpcListener::receiveEvent(I3IpcEvent* event) { emit this->ipcEvent(event); } + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/listener.hpp b/src/x11/i3/ipc/listener.hpp new file mode 100644 index 00000000..01d44ea8 --- /dev/null +++ b/src/x11/i3/ipc/listener.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include // NOLINT +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/doc.hpp" +#include "../../../core/generation.hpp" +#include "../../../core/qmlglobal.hpp" +#include "../../../core/reload.hpp" +#include "connection.hpp" + +namespace qs::i3::ipc { + +///! I3/Sway IPC event listener +class I3IpcListener: public PostReloadHook { + Q_OBJECT; + // clang-format off + /// List of [I3/Sway events](https://man.archlinux.org/man/sway-ipc.7.en#EVENTS) to subscribe to. + Q_PROPERTY(QList subscriptions READ subscriptions WRITE setSubscriptions NOTIFY subscriptionsChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit I3IpcListener(QObject* parent = nullptr): PostReloadHook(parent) {} + ~I3IpcListener() override; + Q_DISABLE_COPY_MOVE(I3IpcListener); + + void onPostReload() override; + + [[nodiscard]] QList subscriptions() const; + void setSubscriptions(QList subscriptions); + +signals: + void ipcEvent(I3IpcEvent* event); + void subscriptionsChanged(); + +private: + void startListening(); + void receiveEvent(I3IpcEvent* event); + + QList mSubscriptions; + I3Ipc* i3Ipc = nullptr; +}; + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/monitor.cpp b/src/x11/i3/ipc/monitor.cpp index 1bc593cd..fb0ec869 100644 --- a/src/x11/i3/ipc/monitor.cpp +++ b/src/x11/i3/ipc/monitor.cpp @@ -7,12 +7,12 @@ #include #include -#include "connection.hpp" +#include "controller.hpp" #include "workspace.hpp" namespace qs::i3::ipc { -I3Monitor::I3Monitor(I3Ipc* ipc): QObject(ipc), ipc(ipc) { +I3Monitor::I3Monitor(I3IpcController* ipc): QObject(ipc), ipc(ipc) { // clang-format off this->bFocused.setBinding([this]() { return this->ipc->bindableFocusedMonitor().value() == this; }); // clang-format on diff --git a/src/x11/i3/ipc/monitor.hpp b/src/x11/i3/ipc/monitor.hpp index 00269a1b..cd348b1a 100644 --- a/src/x11/i3/ipc/monitor.hpp +++ b/src/x11/i3/ipc/monitor.hpp @@ -4,6 +4,7 @@ #include #include "connection.hpp" +#include "controller.hpp" namespace qs::i3::ipc { @@ -39,10 +40,10 @@ class I3Monitor: public QObject { Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); // clang-format on QML_ELEMENT; - QML_UNCREATABLE("I3Monitors must be retrieved from the I3Ipc object."); + QML_UNCREATABLE("I3Monitors must be retrieved from the I3IpcController object."); public: - explicit I3Monitor(I3Ipc* ipc); + explicit I3Monitor(I3IpcController* ipc); [[nodiscard]] QBindable bindableId() { return &this->bId; } [[nodiscard]] QBindable bindableName() { return &this->bName; } @@ -79,7 +80,7 @@ class I3Monitor: public QObject { void focusedChanged(); private: - I3Ipc* ipc; + I3IpcController* ipc; QVariantMap mLastIpcObject; diff --git a/src/x11/i3/ipc/qml.cpp b/src/x11/i3/ipc/qml.cpp index 28041612..d835cbd5 100644 --- a/src/x11/i3/ipc/qml.cpp +++ b/src/x11/i3/ipc/qml.cpp @@ -7,46 +7,49 @@ #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" #include "connection.hpp" +#include "controller.hpp" #include "workspace.hpp" namespace qs::i3::ipc { I3IpcQml::I3IpcQml() { - auto* instance = I3Ipc::instance(); + auto* instance = I3IpcController::instance(); // clang-format off QObject::connect(instance, &I3Ipc::rawEvent, this, &I3IpcQml::rawEvent); QObject::connect(instance, &I3Ipc::connected, this, &I3IpcQml::connected); - QObject::connect(instance, &I3Ipc::focusedWorkspaceChanged, this, &I3IpcQml::focusedWorkspaceChanged); - QObject::connect(instance, &I3Ipc::focusedMonitorChanged, this, &I3IpcQml::focusedMonitorChanged); + QObject::connect(instance, &I3IpcController::focusedWorkspaceChanged, this, &I3IpcQml::focusedWorkspaceChanged); + QObject::connect(instance, &I3IpcController::focusedMonitorChanged, this, &I3IpcQml::focusedMonitorChanged); // clang-format on } -void I3IpcQml::dispatch(const QString& request) { I3Ipc::instance()->dispatch(request); } -void I3IpcQml::refreshMonitors() { I3Ipc::instance()->refreshMonitors(); } -void I3IpcQml::refreshWorkspaces() { I3Ipc::instance()->refreshWorkspaces(); } -QString I3IpcQml::socketPath() { return I3Ipc::instance()->socketPath(); } -ObjectModel* I3IpcQml::monitors() { return I3Ipc::instance()->monitors(); } -ObjectModel* I3IpcQml::workspaces() { return I3Ipc::instance()->workspaces(); } +void I3IpcQml::dispatch(const QString& request) { I3IpcController::instance()->dispatch(request); } +void I3IpcQml::refreshMonitors() { I3IpcController::instance()->refreshMonitors(); } +void I3IpcQml::refreshWorkspaces() { I3IpcController::instance()->refreshWorkspaces(); } +QString I3IpcQml::socketPath() { return I3IpcController::instance()->socketPath(); } +ObjectModel* I3IpcQml::monitors() { return I3IpcController::instance()->monitors(); } +ObjectModel* I3IpcQml::workspaces() { + return I3IpcController::instance()->workspaces(); +} QBindable I3IpcQml::bindableFocusedWorkspace() { - return I3Ipc::instance()->bindableFocusedWorkspace(); + return I3IpcController::instance()->bindableFocusedWorkspace(); } QBindable I3IpcQml::bindableFocusedMonitor() { - return I3Ipc::instance()->bindableFocusedMonitor(); + return I3IpcController::instance()->bindableFocusedMonitor(); } I3Workspace* I3IpcQml::findWorkspaceByName(const QString& name) { - return I3Ipc::instance()->findWorkspaceByName(name); + return I3IpcController::instance()->findWorkspaceByName(name); } I3Monitor* I3IpcQml::findMonitorByName(const QString& name) { - return I3Ipc::instance()->findMonitorByName(name); + return I3IpcController::instance()->findMonitorByName(name); } I3Monitor* I3IpcQml::monitorFor(QuickshellScreenInfo* screen) { - return I3Ipc::instance()->monitorFor(screen); + return I3IpcController::instance()->monitorFor(screen); } } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/qml.hpp b/src/x11/i3/ipc/qml.hpp index 804e42a0..2e7c81c0 100644 --- a/src/x11/i3/ipc/qml.hpp +++ b/src/x11/i3/ipc/qml.hpp @@ -7,6 +7,7 @@ #include "../../../core/doc.hpp" #include "../../../core/qmlscreen.hpp" #include "connection.hpp" +#include "controller.hpp" namespace qs::i3::ipc { diff --git a/src/x11/i3/ipc/workspace.cpp b/src/x11/i3/ipc/workspace.cpp index 7d0b7305..03fadc26 100644 --- a/src/x11/i3/ipc/workspace.cpp +++ b/src/x11/i3/ipc/workspace.cpp @@ -7,12 +7,12 @@ #include #include -#include "connection.hpp" +#include "controller.hpp" #include "monitor.hpp" namespace qs::i3::ipc { -I3Workspace::I3Workspace(I3Ipc* ipc): QObject(ipc), ipc(ipc) { +I3Workspace::I3Workspace(I3IpcController* ipc): QObject(ipc), ipc(ipc) { Qt::beginPropertyUpdateGroup(); this->bActive.setBinding([this]() { diff --git a/src/x11/i3/ipc/workspace.hpp b/src/x11/i3/ipc/workspace.hpp index c9cd0298..f540545d 100644 --- a/src/x11/i3/ipc/workspace.hpp +++ b/src/x11/i3/ipc/workspace.hpp @@ -5,6 +5,7 @@ #include #include "connection.hpp" +#include "controller.hpp" namespace qs::i3::ipc { @@ -40,7 +41,7 @@ class I3Workspace: public QObject { QML_UNCREATABLE("I3Workspaces must be retrieved from the I3 object."); public: - I3Workspace(I3Ipc* ipc); + I3Workspace(I3IpcController* ipc); /// Activate the workspace. /// @@ -72,7 +73,7 @@ class I3Workspace: public QObject { void lastIpcObjectChanged(); private: - I3Ipc* ipc; + I3IpcController* ipc; QVariantMap mLastIpcObject; diff --git a/src/x11/i3/module.md b/src/x11/i3/module.md index 10afb98d..1dc6180f 100644 --- a/src/x11/i3/module.md +++ b/src/x11/i3/module.md @@ -2,8 +2,10 @@ name = "Quickshell.I3" description = "I3 specific Quickshell types" headers = [ "ipc/connection.hpp", + "ipc/controller.hpp", "ipc/qml.hpp", "ipc/workspace.hpp", "ipc/monitor.hpp", + "ipc/listener.hpp", ] ----- From 0bc2c4047c695bb053d5b2d0e94cf07d20621eac Mon Sep 17 00:00:00 2001 From: Ala Alkhafaji <3akevdev@gmail.com> Date: Thu, 13 Nov 2025 20:05:56 +0100 Subject: [PATCH 2/2] i3/ipc: add usage example for I3IpcListener --- src/x11/i3/ipc/listener.hpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/x11/i3/ipc/listener.hpp b/src/x11/i3/ipc/listener.hpp index 01d44ea8..a495c385 100644 --- a/src/x11/i3/ipc/listener.hpp +++ b/src/x11/i3/ipc/listener.hpp @@ -24,6 +24,15 @@ namespace qs::i3::ipc { ///! I3/Sway IPC event listener +/// #### Example +/// ```qml +/// I3IpcListener { +/// subscriptions: ["input"] +/// onIpcEvent: function (event) { +/// handleInputEvent(event.data) +/// } +/// } +/// ``` class I3IpcListener: public PostReloadHook { Q_OBJECT; // clang-format off