From d14ca709849ba0a0e3de5126b77cfc7819c9b100 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 5 Jun 2024 19:26:20 -0700 Subject: [PATCH] hyprland/ipc: add hyprland ipc Only monitors and workspaces are fully tracked for now. --- CMakeLists.txt | 2 + src/wayland/hyprland/CMakeLists.txt | 5 + src/wayland/hyprland/ipc/CMakeLists.txt | 18 + src/wayland/hyprland/ipc/connection.cpp | 542 ++++++++++++++++++++++++ src/wayland/hyprland/ipc/connection.hpp | 123 ++++++ src/wayland/hyprland/ipc/monitor.cpp | 136 ++++++ src/wayland/hyprland/ipc/monitor.hpp | 85 ++++ src/wayland/hyprland/ipc/qml.cpp | 52 +++ src/wayland/hyprland/ipc/qml.hpp | 66 +++ src/wayland/hyprland/ipc/workspace.cpp | 79 ++++ src/wayland/hyprland/ipc/workspace.hpp | 59 +++ src/wayland/hyprland/module.md | 4 + 12 files changed, 1171 insertions(+) create mode 100644 src/wayland/hyprland/ipc/CMakeLists.txt create mode 100644 src/wayland/hyprland/ipc/connection.cpp create mode 100644 src/wayland/hyprland/ipc/connection.hpp create mode 100644 src/wayland/hyprland/ipc/monitor.cpp create mode 100644 src/wayland/hyprland/ipc/monitor.hpp create mode 100644 src/wayland/hyprland/ipc/qml.cpp create mode 100644 src/wayland/hyprland/ipc/qml.hpp create mode 100644 src/wayland/hyprland/ipc/workspace.cpp create mode 100644 src/wayland/hyprland/ipc/workspace.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a386f5a8..246428ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) option(X11 "Enable X11 support" ON) option(HYPRLAND "Support hyprland specific features" ON) +option(HYPRLAND_IPC "Hyprland IPC" ON) option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) @@ -38,6 +39,7 @@ message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) + message(STATUS " IPC: ${HYPRLAND_IPC}") message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}") endif() diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index be6bf49c..be2f0c59 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -4,6 +4,11 @@ target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) set(HYPRLAND_MODULES) +if (HYPRLAND_IPC) + add_subdirectory(ipc) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._Ipc) +endif() + if (HYPRLAND_FOCUS_GRAB) add_subdirectory(focus_grab) list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._FocusGrab) diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt new file mode 100644 index 00000000..59200462 --- /dev/null +++ b/src/wayland/hyprland/ipc/CMakeLists.txt @@ -0,0 +1,18 @@ +qt_add_library(quickshell-hyprland-ipc STATIC + connection.cpp + monitor.cpp + workspace.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-hyprland-ipc + URI Quickshell.Hyprland._Ipc + VERSION 0.1 +) + +target_link_libraries(quickshell-hyprland-ipc PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland-ipc) +qs_pch(quickshell-hyprland-ipcplugin) + +target_link_libraries(quickshell PRIVATE quickshell-hyprland-ipcplugin) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp new file mode 100644 index 00000000..e7265c70 --- /dev/null +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -0,0 +1,542 @@ +#include "connection.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "monitor.hpp" +#include "workspace.hpp" + +namespace qs::hyprland::ipc { + +Q_LOGGING_CATEGORY(logHyprlandIpc, "quickshell.hyprland.ipc", QtWarningMsg); +Q_LOGGING_CATEGORY(logHyprlandIpcEvents, "quickshell.hyprland.ipc.events", QtWarningMsg); + +HyprlandIpc::HyprlandIpc() { + auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); + if (his.isEmpty()) { + qWarning() << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Cannot connect to hyprland."; + return; + } + + auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); + auto hyprlandDir = runtimeDir + "/hypr/" + his; + + if (!QFileInfo(hyprlandDir).isDir()) { + hyprlandDir = "/tmp/hypr/" + his; + } + + if (!QFileInfo(hyprlandDir).isDir()) { + qWarning() << "Unable to find hyprland socket. Cannot connect to hyprland."; + return; + } + + this->mRequestSocketPath = hyprlandDir + "/.socket.sock"; + this->mEventSocketPath = hyprlandDir + "/.socket2.sock"; + + // clang-format off + QObject::connect(&this->eventSocket, &QLocalSocket::errorOccurred, this, &HyprlandIpc::eventSocketError); + QObject::connect(&this->eventSocket, &QLocalSocket::stateChanged, this, &HyprlandIpc::eventSocketStateChanged); + QObject::connect(&this->eventSocket, &QLocalSocket::readyRead, this, &HyprlandIpc::eventSocketReady); + // clang-format on + + // Sockets don't appear to be able to send data in the first event loop + // cycle of the program, so delay it by one. No idea why this is the case. + QTimer::singleShot(0, [this]() { + this->eventSocket.connectToServer(this->mEventSocketPath, QLocalSocket::ReadOnly); + this->refreshMonitors(true); + this->refreshWorkspaces(true); + }); +} + +QString HyprlandIpc::requestSocketPath() const { return this->mRequestSocketPath; } +QString HyprlandIpc::eventSocketPath() const { return this->mEventSocketPath; } + +void HyprlandIpc::eventSocketError(QLocalSocket::LocalSocketError error) const { + if (!this->valid) { + qWarning() << "Unable to connect to hyprland event socket:" << error; + } else { + qWarning() << "Hyprland event socket error:" << error; + } +} + +void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { + if (state == QLocalSocket::ConnectedState) { + qCInfo(logHyprlandIpc) << "Hyprland event socket connected."; + emit this->connected(); + } else if (state == QLocalSocket::UnconnectedState && this->valid) { + qCWarning(logHyprlandIpc) << "Hyprland event socket disconnected."; + } + + this->valid = state == QLocalSocket::ConnectedState; +} + +void HyprlandIpc::eventSocketReady() { + while (true) { + auto rawEvent = this->eventSocket.readLine(); + if (rawEvent.isEmpty()) break; + + // remove trailing \n + rawEvent.truncate(rawEvent.length() - 1); + auto splitIdx = rawEvent.indexOf(">>"); + auto event = QByteArrayView(rawEvent.data(), splitIdx); + auto data = QByteArrayView( + rawEvent.data() + splitIdx + 2, // NOLINT + rawEvent.data() + rawEvent.length() // NOLINT + ); + qCDebug(logHyprlandIpcEvents) << "Received event:" << rawEvent << "parsed as" << event << data; + + this->event.name = event; + this->event.data = data; + this->onEvent(&this->event); + emit this->rawEvent(&this->event); + } +} + +void HyprlandIpc::makeRequest( + const QByteArray& request, + const std::function& callback +) { + auto* requestSocket = new QLocalSocket(this); + qCDebug(logHyprlandIpc) << "Making request:" << request; + + auto connectedCallback = [this, request, requestSocket, callback]() { + auto responseCallback = [requestSocket, callback]() { + auto response = requestSocket->readAll(); + callback(true, std::move(response)); + delete requestSocket; + }; + + QObject::connect(requestSocket, &QLocalSocket::readyRead, this, responseCallback); + + requestSocket->write(request); + }; + + auto errorCallback = [=](QLocalSocket::LocalSocketError error) { + qCWarning(logHyprlandIpc) << "Error making request:" << error << "request:" << request; + requestSocket->deleteLater(); + callback(false, {}); + }; + + QObject::connect(requestSocket, &QLocalSocket::connected, this, connectedCallback); + QObject::connect(requestSocket, &QLocalSocket::errorOccurred, this, errorCallback); + + requestSocket->connectToServer(this->mRequestSocketPath); +} + +void HyprlandIpc::dispatch(const QString& request) { + this->makeRequest( + ("dispatch " + request).toUtf8(), + [request](bool success, const QByteArray& response) { + if (!success) { + qCWarning(logHyprlandIpc) << "Failed to request dispatch of" << request; + return; + } + + if (response != "ok") { + qCWarning(logHyprlandIpc) + << "Dispatch request" << request << "failed with error" << response; + } + } + ); +} + +ObjectModel* HyprlandIpc::monitors() { return &this->mMonitors; } + +ObjectModel* HyprlandIpc::workspaces() { return &this->mWorkspaces; } + +QVector HyprlandIpc::parseEventArgs(QByteArrayView event, quint16 count) { + auto args = QVector(); + + for (auto i = 0; i < count - 1; i++) { + auto splitIdx = event.indexOf(','); + if (splitIdx == -1) break; + args.push_back(event.sliced(0, splitIdx)); + event = event.sliced(splitIdx + 1); + } + + if (!event.isEmpty()) { + args.push_back(event); + } + + return args; +} + +QVector HyprlandIpcEvent::parse(qint32 argumentCount) const { + auto args = QVector(); + + for (auto arg: this->parseView(argumentCount)) { + args.push_back(QString::fromUtf8(arg)); + } + + return args; +} + +QVector HyprlandIpcEvent::parseView(qint32 argumentCount) const { + return HyprlandIpc::parseEventArgs(this->data, argumentCount); +} + +QString HyprlandIpcEvent::nameStr() const { return QString::fromUtf8(this->name); } +QString HyprlandIpcEvent::dataStr() const { return QString::fromUtf8(this->data); } + +HyprlandIpc* HyprlandIpc::instance() { + static HyprlandIpc* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new HyprlandIpc(); + } + + return instance; +} + +void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { + if (event->name == "configreloaded") { + this->refreshMonitors(true); + this->refreshWorkspaces(true); + } else if (event->name == "monitoraddedv2") { + auto args = event->parseView(3); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + // hyprland will often reference the monitor before creation, in which case + // it will already exist. + auto* monitor = this->findMonitorByName(name, false); + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + monitor = new HyprlandMonitor(this); + } + + qCDebug(logHyprlandIpc) << "Monitor added with id" << id << "name" << name + << "preemptively created:" << existed; + + monitor->updateInitial(id, name, QString::fromUtf8(args.at(2))); + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + // refresh even if it already existed because workspace focus might have changed. + this->refreshMonitors(false); + } else if (event->name == "monitorremoved") { + const auto& mList = this->mMonitors.valueList(); + auto name = QString::fromUtf8(event->data); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; + }); + + if (monitorIter == mList.end()) { + qCWarning(logHyprlandIpc) << "Got removal for monitor" << name + << "which was not previously tracked."; + return; + } + + auto index = monitorIter - mList.begin(); + auto* monitor = *monitorIter; + + qCDebug(logHyprlandIpc) << "Monitor removed with id" << monitor->id() << "name" + << monitor->name(); + this->mMonitors.removeAt(index); + + // delete the monitor object in the next event loop cycle so it's likely to + // still exist when future events reference it after destruction. + // If we get to the next cycle and things still reference it (unlikely), nulls + // can make it to the frontend. + monitor->deleteLater(); + } else if (event->name == "createworkspacev2") { + auto args = event->parseView(2); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + qCDebug(logHyprlandIpc) << "Workspace created with id" << id << "name" << name; + + auto* workspace = this->findWorkspaceByName(name, false); + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + workspace = new HyprlandWorkspace(this); + } + + workspace->updateInitial(id, name); + + if (!existed) { + this->refreshWorkspaces(false); + this->mWorkspaces.insertObject(workspace); + } + } else if (event->name == "destroyworkspacev2") { + auto args = event->parseView(2); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + const auto& mList = this->mWorkspaces.valueList(); + + auto workspaceIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandWorkspace* m) { + return m->id() == id; + }); + + if (workspaceIter == mList.end()) { + qCWarning(logHyprlandIpc) << "Got removal for workspace id" << id << "name" << name + << "which was not previously tracked."; + return; + } + + auto index = workspaceIter - mList.begin(); + auto* workspace = *workspaceIter; + + qCDebug(logHyprlandIpc) << "Workspace removed with id" << id << "name" << name; + this->mWorkspaces.removeAt(index); + + // workspaces have not been observed to be referenced after deletion + delete workspace; + + for (auto* monitor: this->mMonitors.valueList()) { + if (monitor->activeWorkspace() == nullptr) { + // removing a monitor will cause a new workspace to be created and destroyed after removal, + // but it won't go back to a real workspace afterwards and just leaves a null, so we + // re-query monitors if this appears to be the case. + this->refreshMonitors(false); + break; + } + } + } else if (event->name == "focusedmon") { + auto args = event->parseView(2); + auto name = QString::fromUtf8(args.at(0)); + auto workspaceName = QString::fromUtf8(args.at(1)); + + HyprlandWorkspace* workspace = nullptr; + if (workspaceName != "?") { // what the fuck + workspace = this->findWorkspaceByName(workspaceName, false); + } + + auto* monitor = this->findMonitorByName(name, true); + this->setFocusedMonitor(monitor); + monitor->setActiveWorkspace(workspace); + } else if (event->name == "workspacev2") { + auto args = event->parseView(2); + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + if (this->mFocusedMonitor != nullptr) { + auto* workspace = this->findWorkspaceByName(name, true, id); + this->mFocusedMonitor->setActiveWorkspace(workspace); + } + } else if (event->name == "moveworkspacev2") { + auto args = event->parseView(3); + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + auto monitorName = QString::fromUtf8(args.at(2)); + + auto* workspace = this->findWorkspaceByName(name, true, id); + auto* monitor = this->findMonitorByName(monitorName, true); + + workspace->setMonitor(monitor); + } +} + +HyprlandWorkspace* +HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id) { + const auto& mList = this->mWorkspaces.valueList(); + + auto workspaceIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); + + if (workspaceIter != mList.end()) { + return *workspaceIter; + } else if (createIfMissing) { + qCDebug(logHyprlandIpc) << "Workspace" << name + << "requested before creation, performing early init"; + auto* workspace = new HyprlandWorkspace(this); + workspace->updateInitial(id, name); + this->mWorkspaces.insertObject(workspace); + return workspace; + } else { + return nullptr; + } +} + +void HyprlandIpc::refreshWorkspaces(bool canCreate) { + if (this->requestingWorkspaces) return; + this->requestingWorkspaces = true; + + this->makeRequest("j/workspaces", [this, canCreate](bool success, const QByteArray& resp) { + this->requestingWorkspaces = false; + if (!success) return; + + qCDebug(logHyprlandIpc) << "parsing workspaces response"; + auto json = QJsonDocument::fromJson(resp).array(); + + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); + + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto name = object.value("name").toString(); + + auto workspaceIter = + std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); + + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + if (!canCreate) continue; + workspace = new HyprlandWorkspace(this); + } + + workspace->updateFromObject(object); + + if (!existed) { + this->mWorkspaces.insertObject(workspace); + } + + names.push_back(name); + } + + auto removedWorkspaces = QVector(); + + for (auto* workspace: mList) { + if (!names.contains(workspace->name())) { + removedWorkspaces.push_back(workspace); + } + } + + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } + }); +} + +HyprlandMonitor* +HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) { + const auto& mList = this->mMonitors.valueList(); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; + }); + + if (monitorIter != mList.end()) { + return *monitorIter; + } else if (createIfMissing) { + qCDebug(logHyprlandIpc) << "Monitor" << name + << "requested before creation, performing early init"; + auto* monitor = new HyprlandMonitor(this); + monitor->updateInitial(id, name, ""); + this->mMonitors.insertObject(monitor); + return monitor; + } else { + return nullptr; + } +} + +HyprlandMonitor* HyprlandIpc::focusedMonitor() const { return this->mFocusedMonitor; } + +HyprlandMonitor* HyprlandIpc::monitorFor(QuickshellScreenInfo* screen) { + // Wayland monitors appear after hyprland ones are created and disappear after destruction + // so simply not doing any preemptive creation is enough. + + if (screen == nullptr) return nullptr; + return this->findMonitorByName(screen->name(), false); +} + +void HyprlandIpc::setFocusedMonitor(HyprlandMonitor* monitor) { + if (monitor == this->mFocusedMonitor) return; + + if (this->mFocusedMonitor != nullptr) { + QObject::disconnect(this->mFocusedMonitor, nullptr, this, nullptr); + } + + this->mFocusedMonitor = monitor; + + if (monitor != nullptr) { + QObject::connect(monitor, &QObject::destroyed, this, &HyprlandIpc::onFocusedMonitorDestroyed); + } + emit this->focusedMonitorChanged(); +} + +void HyprlandIpc::onFocusedMonitorDestroyed() { + this->mFocusedMonitor = nullptr; + emit this->focusedMonitorChanged(); +} + +void HyprlandIpc::refreshMonitors(bool canCreate) { + if (this->requestingMonitors) return; + this->requestingMonitors = true; + + this->makeRequest("j/monitors", [this, canCreate](bool success, const QByteArray& resp) { + this->requestingMonitors = false; + if (!success) return; + + qCDebug(logHyprlandIpc) << "parsing monitors response"; + auto json = QJsonDocument::fromJson(resp).array(); + + const auto& mList = this->mMonitors.valueList(); + auto ids = QVector(); + + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto id = object.value("id").toInt(); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandMonitor* m) { + return m->id() == id; + }); + + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + if (!canCreate) continue; + monitor = new HyprlandMonitor(this); + } + + monitor->updateFromObject(object); + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + ids.push_back(id); + } + + auto removedMonitors = QVector(); + + for (auto* monitor: mList) { + if (!ids.contains(monitor->id())) { + removedMonitors.push_back(monitor); + } + } + + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + // see comment in onEvent + monitor->deleteLater(); + } + }); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp new file mode 100644 index 00000000..d566a866 --- /dev/null +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandMonitor; +class HyprlandWorkspace; + +} // namespace qs::hyprland::ipc + +Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandWorkspace*); +Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandMonitor*); + +namespace qs::hyprland::ipc { + +///! Live Hyprland IPC event. +/// Live Hyprland IPC event. Holding this object after the +/// signal handler exits is undefined as the event instance +/// is reused. +class HyprlandIpcEvent: public QObject { + Q_OBJECT; + /// The name of the event. + Q_PROPERTY(QString name READ nameStr CONSTANT); + /// The unparsed data of the event. + Q_PROPERTY(QString data READ dataStr CONSTANT); + QML_NAMED_ELEMENT(HyprlandEvent); + QML_UNCREATABLE("HyprlandIpcEvents cannot be created."); + +public: + HyprlandIpcEvent(QObject* parent): QObject(parent) {} + + /// Parse this event with a known number of arguments. + /// + /// Argument count is required as some events can contain commas + /// in the last argument, which can be ignored as long as the count is known. + Q_INVOKABLE [[nodiscard]] QVector parse(qint32 argumentCount) const; + [[nodiscard]] QVector parseView(qint32 argumentCount) const; + + [[nodiscard]] QString nameStr() const; + [[nodiscard]] QString dataStr() const; + + void reset(); + QByteArrayView name; + QByteArrayView data; +}; + +class HyprlandIpc: public QObject { + Q_OBJECT; + +public: + static HyprlandIpc* instance(); + + [[nodiscard]] QString requestSocketPath() const; + [[nodiscard]] QString eventSocketPath() const; + + void + makeRequest(const QByteArray& request, const std::function& callback); + void dispatch(const QString& request); + + [[nodiscard]] HyprlandMonitor* monitorFor(QuickshellScreenInfo* screen); + [[nodiscard]] HyprlandMonitor* focusedMonitor() const; + void setFocusedMonitor(HyprlandMonitor* monitor); + + [[nodiscard]] ObjectModel* monitors(); + [[nodiscard]] ObjectModel* workspaces(); + + // No byId because these preemptively create objects. The given id is set if created. + HyprlandWorkspace* findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id = 0); + HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1); + + // canCreate avoids making ghost workspaces when the connection races + void refreshWorkspaces(bool canCreate); + void refreshMonitors(bool canCreate); + + // The last argument may contain commas, so the count is required. + [[nodiscard]] static QVector parseEventArgs(QByteArrayView event, quint16 count); + +signals: + void connected(); + void rawEvent(HyprlandIpcEvent* event); + + void focusedMonitorChanged(); + +private slots: + void eventSocketError(QLocalSocket::LocalSocketError error) const; + void eventSocketStateChanged(QLocalSocket::LocalSocketState state); + void eventSocketReady(); + + void onFocusedMonitorDestroyed(); + +private: + explicit HyprlandIpc(); + + void onEvent(HyprlandIpcEvent* event); + + QLocalSocket eventSocket; + QString mRequestSocketPath; + QString mEventSocketPath; + bool valid = false; + bool requestingMonitors = false; + bool requestingWorkspaces = false; + + ObjectModel mMonitors {this}; + ObjectModel mWorkspaces {this}; + HyprlandMonitor* mFocusedMonitor = nullptr; + //HyprlandWorkspace* activeWorkspace = nullptr; + + HyprlandIpcEvent event {this}; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/monitor.cpp b/src/wayland/hyprland/ipc/monitor.cpp new file mode 100644 index 00000000..8ee5e207 --- /dev/null +++ b/src/wayland/hyprland/ipc/monitor.cpp @@ -0,0 +1,136 @@ +#include "monitor.hpp" +#include + +#include +#include +#include +#include + +#include "workspace.hpp" + +namespace qs::hyprland::ipc { + +qint32 HyprlandMonitor::id() const { return this->mId; } +QString HyprlandMonitor::name() const { return this->mName; } +QString HyprlandMonitor::description() const { return this->mDescription; } +qint32 HyprlandMonitor::x() const { return this->mX; } +qint32 HyprlandMonitor::y() const { return this->mY; } +qint32 HyprlandMonitor::width() const { return this->mWidth; } +qint32 HyprlandMonitor::height() const { return this->mHeight; } +qreal HyprlandMonitor::scale() const { return this->mScale; } +QVariantMap HyprlandMonitor::lastIpcObject() const { return this->mLastIpcObject; } + +void HyprlandMonitor::updateInitial(qint32 id, QString name, QString description) { + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (description != this->mDescription) { + this->mDescription = std::move(description); + emit this->descriptionChanged(); + } +} + +void HyprlandMonitor::updateFromObject(QVariantMap object) { + auto id = object.value("id").value(); + auto name = object.value("name").value(); + auto description = object.value("description").value(); + auto x = object.value("x").value(); + auto y = object.value("y").value(); + auto width = object.value("width").value(); + auto height = object.value("height").value(); + auto scale = object.value("height").value(); + auto activeWorkspaceObj = object.value("activeWorkspace").value(); + auto activeWorkspaceId = activeWorkspaceObj.value("id").value(); + auto activeWorkspaceName = activeWorkspaceObj.value("name").value(); + auto focused = object.value("focused").value(); + + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (description != this->mDescription) { + this->mDescription = std::move(description); + emit this->descriptionChanged(); + } + + if (x != this->mX) { + this->mX = x; + emit this->xChanged(); + } + + if (y != this->mY) { + this->mY = y; + emit this->yChanged(); + } + + if (width != this->mWidth) { + this->mWidth = width; + emit this->widthChanged(); + } + + if (height != this->mHeight) { + this->mHeight = height; + emit this->heightChanged(); + } + + if (scale != this->mScale) { + this->mScale = scale; + emit this->scaleChanged(); + } + + if (this->mActiveWorkspace == nullptr || this->mActiveWorkspace->name() != activeWorkspaceName) { + auto* workspace = this->ipc->findWorkspaceByName(activeWorkspaceName, true, activeWorkspaceId); + workspace->setMonitor(this); + this->setActiveWorkspace(workspace); + } + + this->mLastIpcObject = std::move(object); + emit this->lastIpcObjectChanged(); + + if (focused) { + this->ipc->setFocusedMonitor(this); + } +} + +HyprlandWorkspace* HyprlandMonitor::activeWorkspace() const { return this->mActiveWorkspace; } + +void HyprlandMonitor::setActiveWorkspace(HyprlandWorkspace* workspace) { + if (workspace == this->mActiveWorkspace) return; + + if (this->mActiveWorkspace != nullptr) { + QObject::disconnect(this->mActiveWorkspace, nullptr, this, nullptr); + } + + this->mActiveWorkspace = workspace; + + if (workspace != nullptr) { + QObject::connect( + workspace, + &QObject::destroyed, + this, + &HyprlandMonitor::onActiveWorkspaceDestroyed + ); + } + + emit this->activeWorkspaceChanged(); +} + +void HyprlandMonitor::onActiveWorkspaceDestroyed() { + this->mActiveWorkspace = nullptr; + emit this->activeWorkspaceChanged(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/monitor.hpp b/src/wayland/hyprland/ipc/monitor.hpp new file mode 100644 index 00000000..6b5d2ecc --- /dev/null +++ b/src/wayland/hyprland/ipc/monitor.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandMonitor: public QObject { + Q_OBJECT; + Q_PROPERTY(qint32 id READ id NOTIFY idChanged); + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + Q_PROPERTY(QString description READ description NOTIFY descriptionChanged); + Q_PROPERTY(qint32 x READ x NOTIFY xChanged); + Q_PROPERTY(qint32 y READ y NOTIFY yChanged); + Q_PROPERTY(qint32 width READ width NOTIFY widthChanged); + Q_PROPERTY(qint32 height READ height NOTIFY heightChanged); + Q_PROPERTY(qreal scale READ scale NOTIFY scaleChanged); + /// Last json returned for this monitor, as a javascript object. + /// + /// > [!WARNING] This is *not* updated unless the monitor object is fetched again from + /// > Hyprland. If you need a value that is subject to change and does not have a dedicated + /// > property, run `HyprlandIpc.refreshMonitors()` and wait for this property to update. + Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); + /// The currently active workspace on this monitor. May be null. + Q_PROPERTY(HyprlandWorkspace* activeWorkspace READ activeWorkspace NOTIFY activeWorkspaceChanged); + QML_ELEMENT; + QML_UNCREATABLE("HyprlandMonitors must be retrieved from the HyprlandIpc object."); + +public: + explicit HyprlandMonitor(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) {} + + void updateInitial(qint32 id, QString name, QString description); + void updateFromObject(QVariantMap object); + + [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString description() const; + [[nodiscard]] qint32 x() const; + [[nodiscard]] qint32 y() const; + [[nodiscard]] qint32 width() const; + [[nodiscard]] qint32 height() const; + [[nodiscard]] qreal scale() const; + [[nodiscard]] QVariantMap lastIpcObject() const; + + void setActiveWorkspace(HyprlandWorkspace* workspace); + [[nodiscard]] HyprlandWorkspace* activeWorkspace() const; + +signals: + void idChanged(); + void nameChanged(); + void descriptionChanged(); + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + void scaleChanged(); + void lastIpcObjectChanged(); + void activeWorkspaceChanged(); + +private slots: + void onActiveWorkspaceDestroyed(); + +private: + HyprlandIpc* ipc; + + qint32 mId = -1; + QString mName; + QString mDescription; + qint32 mX = 0; + qint32 mY = 0; + qint32 mWidth = 0; + qint32 mHeight = 0; + qreal mScale = 0; + QVariantMap mLastIpcObject; + + HyprlandWorkspace* mActiveWorkspace = nullptr; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/qml.cpp b/src/wayland/hyprland/ipc/qml.cpp new file mode 100644 index 00000000..1e75ee9c --- /dev/null +++ b/src/wayland/hyprland/ipc/qml.cpp @@ -0,0 +1,52 @@ +#include "qml.hpp" + +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +HyprlandIpcQml::HyprlandIpcQml() { + auto* instance = HyprlandIpc::instance(); + + QObject::connect(instance, &HyprlandIpc::rawEvent, this, &HyprlandIpcQml::rawEvent); + QObject::connect( + instance, + &HyprlandIpc::focusedMonitorChanged, + this, + &HyprlandIpcQml::focusedMonitorChanged + ); +} + +void HyprlandIpcQml::dispatch(const QString& request) { + HyprlandIpc::instance()->dispatch(request); +} + +HyprlandMonitor* HyprlandIpcQml::monitorFor(QuickshellScreenInfo* screen) { + return HyprlandIpc::instance()->monitorFor(screen); +} + +void HyprlandIpcQml::refreshMonitors() { HyprlandIpc::instance()->refreshMonitors(false); } + +void HyprlandIpcQml::refreshWorkspaces() { HyprlandIpc::instance()->refreshWorkspaces(false); } + +QString HyprlandIpcQml::requestSocketPath() { return HyprlandIpc::instance()->requestSocketPath(); } + +QString HyprlandIpcQml::eventSocketPath() { return HyprlandIpc::instance()->eventSocketPath(); } + +HyprlandMonitor* HyprlandIpcQml::focusedMonitor() { + return HyprlandIpc::instance()->focusedMonitor(); +} + +ObjectModel* HyprlandIpcQml::monitors() { + return HyprlandIpc::instance()->monitors(); +} + +ObjectModel* HyprlandIpcQml::workspaces() { + return HyprlandIpc::instance()->workspaces(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/qml.hpp b/src/wayland/hyprland/ipc/qml.hpp new file mode 100644 index 00000000..2d39623f --- /dev/null +++ b/src/wayland/hyprland/ipc/qml.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandIpcQml: public QObject { + Q_OBJECT; + /// Path to the request socket (.socket.sock) + Q_PROPERTY(QString requestSocketPath READ requestSocketPath CONSTANT); + /// Path to the event socket (.socket2.sock) + Q_PROPERTY(QString eventSocketPath READ eventSocketPath CONSTANT); + /// The currently focused hyprland monitor. May be null. + Q_PROPERTY(HyprlandMonitor* focusedMonitor READ focusedMonitor NOTIFY focusedMonitorChanged); + /// All hyprland monitors. + Q_PROPERTY(ObjectModel* monitors READ monitors CONSTANT); + /// All hyprland workspaces. + Q_PROPERTY(ObjectModel* workspaces READ workspaces CONSTANT); + QML_NAMED_ELEMENT(Hyprland); + QML_SINGLETON; + +public: + explicit HyprlandIpcQml(); + + /// Execute a hyprland [dispatcher](https://wiki.hyprland.org/Configuring/Dispatchers). + Q_INVOKABLE static void dispatch(const QString& request); + + /// Get the HyprlandMonitor object that corrosponds to a quickshell screen. + Q_INVOKABLE static HyprlandMonitor* monitorFor(QuickshellScreenInfo* screen); + + /// Refresh monitor information. + /// + /// Many actions that will invalidate monitor state don't send events, + /// so this function is available if required. + Q_INVOKABLE static void refreshMonitors(); + + /// Refresh workspace information. + /// + /// Many actions that will invalidate workspace state don't send events, + /// so this function is available if required. + Q_INVOKABLE static void refreshWorkspaces(); + + [[nodiscard]] static QString requestSocketPath(); + [[nodiscard]] static QString eventSocketPath(); + [[nodiscard]] static HyprlandMonitor* focusedMonitor(); + [[nodiscard]] static ObjectModel* monitors(); + [[nodiscard]] static ObjectModel* workspaces(); + +signals: + /// Emitted for every event that comes in through the hyprland event socket (socket2). + /// + /// See [Hyprland Wiki: IPC](https://wiki.hyprland.org/IPC/) for a list of events. + void rawEvent(HyprlandIpcEvent* event); + + void focusedMonitorChanged(); +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.cpp b/src/wayland/hyprland/ipc/workspace.cpp new file mode 100644 index 00000000..fbf8477f --- /dev/null +++ b/src/wayland/hyprland/ipc/workspace.cpp @@ -0,0 +1,79 @@ +#include "workspace.hpp" +#include + +#include +#include +#include +#include + +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +qint32 HyprlandWorkspace::id() const { return this->mId; } +QString HyprlandWorkspace::name() const { return this->mName; } +QVariantMap HyprlandWorkspace::lastIpcObject() const { return this->mLastIpcObject; } + +void HyprlandWorkspace::updateInitial(qint32 id, QString name) { + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } +} + +void HyprlandWorkspace::updateFromObject(QVariantMap object) { + auto id = object.value("id").value(); + auto name = object.value("name").value(); + auto monitorId = object.value("monitorID").value(); + auto monitorName = object.value("monitor").value(); + + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (!monitorName.isEmpty() + && (this->mMonitor == nullptr || this->mMonitor->name() != monitorName)) + { + auto* monitor = this->ipc->findMonitorByName(monitorName, true, monitorId); + this->setMonitor(monitor); + } + + this->mLastIpcObject = std::move(object); + emit this->lastIpcObjectChanged(); +} + +HyprlandMonitor* HyprlandWorkspace::monitor() const { return this->mMonitor; } + +void HyprlandWorkspace::setMonitor(HyprlandMonitor* monitor) { + if (monitor == this->mMonitor) return; + + if (this->mMonitor != nullptr) { + QObject::disconnect(this->mMonitor, nullptr, this, nullptr); + } + + this->mMonitor = monitor; + + if (monitor != nullptr) { + QObject::connect(monitor, &QObject::destroyed, this, &HyprlandWorkspace::onMonitorDestroyed); + } + + emit this->monitorChanged(); +} + +void HyprlandWorkspace::onMonitorDestroyed() { + this->mMonitor = nullptr; + emit this->monitorChanged(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.hpp b/src/wayland/hyprland/ipc/workspace.hpp new file mode 100644 index 00000000..a63901e6 --- /dev/null +++ b/src/wayland/hyprland/ipc/workspace.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandWorkspace: public QObject { + Q_OBJECT; + Q_PROPERTY(qint32 id READ id NOTIFY idChanged); + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + /// Last json returned for this workspace, as a javascript object. + /// + /// > [!WARNING] This is *not* updated unless the workspace object is fetched again from + /// > Hyprland. If you need a value that is subject to change and does not have a dedicated + /// > property, run `HyprlandIpc.refreshWorkspaces()` and wait for this property to update. + Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); + Q_PROPERTY(HyprlandMonitor* monitor READ monitor NOTIFY monitorChanged); + QML_ELEMENT; + QML_UNCREATABLE("HyprlandWorkspaces must be retrieved from the HyprlandIpc object."); + +public: + explicit HyprlandWorkspace(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) {} + + void updateInitial(qint32 id, QString name); + void updateFromObject(QVariantMap object); + + [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QVariantMap lastIpcObject() const; + + void setMonitor(HyprlandMonitor* monitor); + [[nodiscard]] HyprlandMonitor* monitor() const; + +signals: + void idChanged(); + void nameChanged(); + void lastIpcObjectChanged(); + void monitorChanged(); + +private slots: + void onMonitorDestroyed(); + +private: + HyprlandIpc* ipc; + + qint32 mId = -1; + QString mName; + QVariantMap mLastIpcObject; + HyprlandMonitor* mMonitor = nullptr; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md index 1b3e2fbf..6c2de249 100644 --- a/src/wayland/hyprland/module.md +++ b/src/wayland/hyprland/module.md @@ -1,6 +1,10 @@ name = "Quickshell.Hyprland" description = "Hyprland specific Quickshell types" headers = [ + "ipc/connection.hpp", + "ipc/monitor.hpp", + "ipc/workspace.hpp", + "ipc/qml.hpp", "focus_grab/qml.hpp", "global_shortcuts/qml.hpp", ]