From fae96f1a5b7f53de9b7e40e5b53c0b7a2e97b1d7 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 20 Sep 2026 22:04:49 -0700 Subject: [PATCH] hyprland/ipc: support workspace addresses --- changelog/next.md | 4 + src/wayland/hyprland/ipc/connection.cpp | 79 ++++++++++--------- src/wayland/hyprland/ipc/connection.hpp | 6 +- src/wayland/hyprland/ipc/monitor.cpp | 5 +- src/wayland/hyprland/ipc/workspace.cpp | 24 ++++-- src/wayland/hyprland/ipc/workspace.hpp | 14 +++- .../hyprland/test/manual/workspaces.qml | 2 +- 7 files changed, 86 insertions(+), 48 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index c64b8f3..58762d7 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -1,3 +1,7 @@ +## Other Changes + +- Added support for Hyprland's new workspace address format. + ## Bug Fixes - Fixed main process crashes on pam subprocess misbehavior. diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 998bd8e..942270a 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -188,12 +188,12 @@ void HyprlandIpc::makeRequest( }; auto errorCallback = - [this, requestSocket, request, callback](QLocalSocket::LocalSocketError error) { - qCWarning(logHyprlandIpc) << "Error making request:" << error << "request:" << request; - QObject::disconnect(requestSocket, nullptr, this, nullptr); - requestSocket->deleteLater(); - callback(false, {}); - }; + [this, requestSocket, request, callback](QLocalSocket::LocalSocketError error) { + qCWarning(logHyprlandIpc) << "Error making request:" << error << "request:" << request; + QObject::disconnect(requestSocket, nullptr, this, nullptr); + requestSocket->deleteLater(); + callback(false, {}); + }; QObject::connect(requestSocket, &QLocalSocket::connected, this, connectedCallback); QObject::connect(requestSocket, &QLocalSocket::errorOccurred, this, errorCallback); @@ -307,7 +307,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { const auto& mList = this->mMonitors.valueList(); auto name = QString::fromUtf8(event->data); - auto monitorIter = std::ranges::find_if(mList, [name](HyprlandMonitor* m) { + auto monitorIter = std::ranges::find_if(mList, [&](HyprlandMonitor* m) { return m->bindableName().value() == name; }); @@ -332,10 +332,10 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { } else if (event->name == "createworkspacev2") { auto args = event->parseView(2); - auto id = args.at(0).toInt(); + auto address = QString::fromUtf8(args.at(0)); auto name = QString::fromUtf8(args.at(1)); - qCDebug(logHyprlandIpc) << "Workspace created with id" << id << "name" << name; + qCDebug(logHyprlandIpc) << "Workspace created with address" << address << "name" << name; auto* workspace = this->findWorkspaceByName(name, false); auto existed = workspace != nullptr; @@ -344,7 +344,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { workspace = new HyprlandWorkspace(this); } - workspace->updateInitial(id, name); + workspace->updateInitial(address, name); if (!existed) { this->refreshWorkspaces(false); @@ -353,17 +353,17 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { } else if (event->name == "destroyworkspacev2") { auto args = event->parseView(2); - auto id = args.at(0).toInt(); + auto address = QString::fromUtf8(args.at(0)); auto name = QString::fromUtf8(args.at(1)); const auto& mList = this->mWorkspaces.valueList(); - auto workspaceIter = std::ranges::find_if(mList, [id](HyprlandWorkspace* m) { - return m->bindableId().value() == id; + auto workspaceIter = std::ranges::find_if(mList, [&](HyprlandWorkspace* m) { + return m->bindableAddress().value() == address; }); if (workspaceIter == mList.end()) { - qCWarning(logHyprlandIpc) << "Got removal for workspace id" << id << "name" << name + qCWarning(logHyprlandIpc) << "Got removal for workspace id" << address << "name" << name << "which was not previously tracked."; return; } @@ -371,7 +371,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { auto index = workspaceIter - mList.begin(); auto* workspace = *workspaceIter; - qCDebug(logHyprlandIpc) << "Workspace removed with id" << id << "name" << name; + qCDebug(logHyprlandIpc) << "Workspace removed with id" << address << "name" << name; this->mWorkspaces.removeAt(index); // workspaces have not been observed to be referenced after deletion @@ -400,10 +400,10 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { this->setFocusedMonitor(monitor); monitor->setActiveWorkspace(workspace); qCDebug(logHyprlandIpc) << "Monitor" << name << "focused with workspace" - << (workspace ? workspace->bindableId().value() : -1); + << (workspace ? workspace->bindableAddress().value() : ""); } else if (event->name == "workspacev2") { auto args = event->parseView(2); - auto id = args.at(0).toInt(); + auto id = QString::fromUtf8(args.at(0)); auto name = QString::fromUtf8(args.at(1)); if (this->bFocusedMonitor != nullptr) { @@ -414,7 +414,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { } } else if (event->name == "moveworkspacev2") { auto args = event->parseView(3); - auto id = args.at(0).toInt(); + auto id = QString::fromUtf8(args.at(0)); auto name = QString::fromUtf8(args.at(1)); auto monitorName = QString::fromUtf8(args.at(2)); @@ -425,18 +425,18 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { workspace->setMonitor(monitor); } else if (event->name == "renameworkspace") { auto args = event->parseView(2); - auto id = args.at(0).toInt(); + auto address = QString::fromUtf8(args.at(0)); auto name = QString::fromUtf8(args.at(1)); const auto& mList = this->mWorkspaces.valueList(); - auto workspaceIter = std::ranges::find_if(mList, [id](HyprlandWorkspace* m) { - return m->bindableId().value() == id; + auto workspaceIter = std::ranges::find_if(mList, [&](HyprlandWorkspace* m) { + return m->bindableAddress().value() == address; }); if (workspaceIter == mList.end()) return; - qCDebug(logHyprlandIpc) << "Workspace with id" << id << "renamed from" + qCDebug(logHyprlandIpc) << "Workspace with address" << address << "renamed from" << (*workspaceIter)->bindableName().value() << "to" << name; (*workspaceIter)->bindableName().setValue(name); @@ -488,7 +488,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { if (!ok) return; const auto& mList = this->mToplevels.valueList(); - auto toplevelIter = std::ranges::find_if(mList, [windowAddress](HyprlandToplevel* m) { + auto toplevelIter = std::ranges::find_if(mList, [&](HyprlandToplevel* m) { return m->address() == windowAddress; }); @@ -580,14 +580,17 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { } } -HyprlandWorkspace* -HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id) { +HyprlandWorkspace* HyprlandIpc::findWorkspaceByName( + const QString& name, + bool createIfMissing, + const QString& address +) { const auto& mList = this->mWorkspaces.valueList(); HyprlandWorkspace* workspace = nullptr; - if (id != -1) { + if (!address.isEmpty()) { auto workspaceIter = std::ranges::find_if(mList, [&](HyprlandWorkspace* m) { - return m->bindableId().value() == id; + return m->bindableAddress().value() == address; }); workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; @@ -605,10 +608,11 @@ HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint return workspace; } else if (createIfMissing) { qCDebug(logHyprlandIpc) << "Workspace" << name - << "requested before creation, performing early init with id" << id; + << "requested before creation, performing early init with address" + << address; auto* workspace = new HyprlandWorkspace(this); - workspace->updateInitial(id, name); + workspace->updateInitial(address, name); this->mWorkspaces.insertObjectSorted(workspace, &HyprlandIpc::compareWorkspaces); return workspace; } else { @@ -628,15 +632,16 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) { auto json = QJsonDocument::fromJson(resp).array(); const auto& mList = this->mWorkspaces.valueList(); - auto ids = QVector(); + auto addresses = QVector(); for (auto entry: json) { auto object = entry.toObject().toVariantMap(); - auto id = object.value("id").toInt(); + auto address = object.value("address").toString(); + if (address.isEmpty()) address = QString::number(object.value("id").toInt()); auto workspaceIter = std::ranges::find_if(mList, [&](HyprlandWorkspace* m) { - return m->bindableId().value() == id; + return m->bindableAddress().value() == address; }); // Only fall back to name-based filtering as a last resort, for workspaces where @@ -645,7 +650,7 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) { auto name = object.value("name").toString(); workspaceIter = std::ranges::find_if(mList, [&](HyprlandWorkspace* m) { - return m->bindableId().value() == -1 && m->bindableName().value() == name; + return m->bindableAddress().value().isEmpty() && m->bindableName().value() == name; }); } @@ -663,14 +668,14 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) { this->mWorkspaces.insertObjectSorted(workspace, &HyprlandIpc::compareWorkspaces); } - ids.push_back(id); + addresses.push_back(address); } if (canCreate) { auto removedWorkspaces = QVector(); for (auto* workspace: mList) { - if (!ids.contains(workspace->bindableId().value())) { + if (!addresses.contains(workspace->bindableAddress().value())) { removedWorkspaces.push_back(workspace); } } @@ -752,7 +757,7 @@ HyprlandMonitor* HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) { const auto& mList = this->mMonitors.valueList(); - auto monitorIter = std::ranges::find_if(mList, [name](HyprlandMonitor* m) { + auto monitorIter = std::ranges::find_if(mList, [&](HyprlandMonitor* m) { return m->bindableName().value() == name; }); @@ -820,7 +825,7 @@ void HyprlandIpc::refreshMonitors(bool canCreate) { auto object = entry.toObject().toVariantMap(); auto name = object.value("name").toString(); - auto monitorIter = std::ranges::find_if(mList, [name](HyprlandMonitor* m) { + auto monitorIter = std::ranges::find_if(mList, [&](HyprlandMonitor* m) { return m->bindableName().value() == name; }); diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp index 717e665..8d79184 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -102,7 +102,11 @@ public: [[nodiscard]] ObjectModel* toplevels(); // No byId because these preemptively create objects. The given id is set if created. - HyprlandWorkspace* findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id = -1); + HyprlandWorkspace* findWorkspaceByName( + const QString& name, + bool createIfMissing, + const QString& address = QString() + ); HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1); HyprlandToplevel* findToplevelByAddress(quint64 address, bool createIfMissing); diff --git a/src/wayland/hyprland/ipc/monitor.cpp b/src/wayland/hyprland/ipc/monitor.cpp index ddf3706..7519a92 100644 --- a/src/wayland/hyprland/ipc/monitor.cpp +++ b/src/wayland/hyprland/ipc/monitor.cpp @@ -28,7 +28,9 @@ void HyprlandMonitor::updateInitial(qint32 id, const QString& name, const QStrin void HyprlandMonitor::updateFromObject(QVariantMap object) { auto activeWorkspaceObj = object.value("activeWorkspace").value(); + auto activeWorkspaceAddress = activeWorkspaceObj.value("address").value(); auto activeWorkspaceId = activeWorkspaceObj.value("id").value(); + if (activeWorkspaceAddress.isEmpty()) activeWorkspaceAddress = QString::number(activeWorkspaceId); auto activeWorkspaceName = activeWorkspaceObj.value("name").value(); auto focused = object.value("focused").value(); @@ -46,7 +48,8 @@ void HyprlandMonitor::updateFromObject(QVariantMap object) { if (this->bActiveWorkspace == nullptr || this->bActiveWorkspace->bindableName().value() != activeWorkspaceName) { - auto* workspace = this->ipc->findWorkspaceByName(activeWorkspaceName, true, activeWorkspaceId); + auto* workspace = + this->ipc->findWorkspaceByName(activeWorkspaceName, true, activeWorkspaceAddress); workspace->setMonitor(this); this->setActiveWorkspace(workspace); } diff --git a/src/wayland/hyprland/ipc/workspace.cpp b/src/wayland/hyprland/ipc/workspace.cpp index 44377c0..a527d82 100644 --- a/src/wayland/hyprland/ipc/workspace.cpp +++ b/src/wayland/hyprland/ipc/workspace.cpp @@ -18,11 +18,18 @@ QVariantMap HyprlandWorkspace::lastIpcObject() const { return this->mLastIpcObje HyprlandWorkspace::HyprlandWorkspace(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) { Qt::beginPropertyUpdateGroup(); - this->bActive.setBinding([this]() { + this->bId.setBinding([this] { + auto idOk = false; + auto id = this->bAddress.value().toInt(&idOk); + if (!idOk) id = -1; + return id; + }); + + this->bActive.setBinding([this] { return this->bMonitor.value() && this->bMonitor->bindableActiveWorkspace().value() == this; }); - this->bFocused.setBinding([this]() { + this->bFocused.setBinding([this] { return this->ipc->bindableFocusedWorkspace().value() == this; }); @@ -35,9 +42,9 @@ HyprlandWorkspace::HyprlandWorkspace(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) { Qt::endPropertyUpdateGroup(); } -void HyprlandWorkspace::updateInitial(qint32 id, const QString& name) { +void HyprlandWorkspace::updateInitial(const QString& address, const QString& name) { Qt::beginPropertyUpdateGroup(); - this->bId = id; + this->bAddress = address; this->bName = name; Qt::endPropertyUpdateGroup(); } @@ -47,11 +54,14 @@ void HyprlandWorkspace::updateFromObject(QVariantMap object) { auto monitorName = object.value("monitor").value(); auto hasFullscreen = object.value("hasfullscreen").value(); - auto initial = this->bId == -1; + auto initial = this->bAddress.value().isEmpty(); - // ID cannot be updated after creation + // Address cannot be updated after creation if (initial) { - this->bId = object.value("id").value(); + auto address = object.value("address").value(); + auto id = object.value("id").value(); + if (address.isEmpty()) address = QString::number(id); + this->bAddress = address; } // No events we currently handle give a workspace id but not a name, diff --git a/src/wayland/hyprland/ipc/workspace.hpp b/src/wayland/hyprland/ipc/workspace.hpp index a0b09cf..86e0426 100644 --- a/src/wayland/hyprland/ipc/workspace.hpp +++ b/src/wayland/hyprland/ipc/workspace.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -18,6 +19,13 @@ class HyprlandMonitor; class HyprlandWorkspace: public QObject { Q_OBJECT; // clang-format off + // > [!NOTE] On Hyprland versions prior to 0.57, this property is set to @@id as a string for compatibility. + // > + // > Prefer this property over @@id for new code. + Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress); + // > [!NOTE] On Hyprland versions 0.57 and later, this property is set to @@address as an integer for compatibility. + // > + // > After 0.57, this property will equal `-1` for non-numbered workspaces. Q_PROPERTY(qint32 id READ default NOTIFY idChanged BINDABLE bindableId); Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName); /// If this workspace is currently active on its monitor. See also @@focused. @@ -47,7 +55,7 @@ class HyprlandWorkspace: public QObject { public: explicit HyprlandWorkspace(HyprlandIpc* ipc); - void updateInitial(qint32 id, const QString& name); + void updateInitial(const QString& address, const QString& name); void updateFromObject(QVariantMap object); /// Activate the workspace. @@ -58,6 +66,7 @@ public: /// > ``` Q_INVOKABLE void activate(); + [[nodiscard]] QBindable bindableAddress() { return &this->bAddress; } [[nodiscard]] QBindable bindableId() { return &this->bId; } [[nodiscard]] QBindable bindableName() { return &this->bName; } [[nodiscard]] QBindable bindableActive() { return &this->bActive; } @@ -75,7 +84,9 @@ public: void removeToplevel(HyprlandToplevel* toplevel); signals: + void addressChanged(); void idChanged(); + void typeChanged(); void nameChanged(); void activeChanged(); void focusedChanged(); @@ -97,6 +108,7 @@ private: ObjectModel mToplevels {this}; // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, QString, bAddress, &HyprlandWorkspace::addressChanged); Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(HyprlandWorkspace, qint32, bId, -1, &HyprlandWorkspace::idChanged); Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, QString, bName, &HyprlandWorkspace::nameChanged); Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, bool, bActive, &HyprlandWorkspace::activeChanged); diff --git a/src/wayland/hyprland/test/manual/workspaces.qml b/src/wayland/hyprland/test/manual/workspaces.qml index ef1bafe..2c4132b 100644 --- a/src/wayland/hyprland/test/manual/workspaces.qml +++ b/src/wayland/hyprland/test/manual/workspaces.qml @@ -16,7 +16,7 @@ FloatingWindow { color: "lightgray" ColumnLayout { - Text { text: `Workspace ${wsDelegate.modelData.id} on ${wsDelegate.modelData.monitor} | urgent: ${wsDelegate.modelData.urgent}`} + Text { text: `Workspace (addr ${wsDelegate.modelData.address}; id ${wsDelegate.modelData.id}; name: ${wsDelegate.modelData.name}) on ${wsDelegate.modelData.monitor} | urgent: ${wsDelegate.modelData.urgent}`} ColumnLayout { Repeater {