Skip to content

Commit 362c8e1

Browse files
Maeeenoutfoxxed
authored andcommitted
hyprland/ipc: expose Hyprland toplevels
1 parent c115df8 commit 362c8e1

File tree

11 files changed

+680
-38
lines changed

11 files changed

+680
-38
lines changed

src/wayland/hyprland/ipc/connection.cpp

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,19 @@
1414
#include <qlogging.h>
1515
#include <qloggingcategory.h>
1616
#include <qobject.h>
17+
#include <qproperty.h>
18+
#include <qqml.h>
1719
#include <qtenvironmentvariables.h>
1820
#include <qtmetamacros.h>
1921
#include <qtypes.h>
2022
#include <qvariant.h>
2123

2224
#include "../../../core/model.hpp"
2325
#include "../../../core/qmlscreen.hpp"
26+
#include "../../toplevel_management/handle.hpp"
27+
#include "hyprland_toplevel.hpp"
2428
#include "monitor.hpp"
29+
#include "toplevel_mapping.hpp"
2530
#include "workspace.hpp"
2631

2732
namespace qs::hyprland::ipc {
@@ -62,11 +67,16 @@ HyprlandIpc::HyprlandIpc() {
6267
QObject::connect(&this->eventSocket, &QLocalSocket::errorOccurred, this, &HyprlandIpc::eventSocketError);
6368
QObject::connect(&this->eventSocket, &QLocalSocket::stateChanged, this, &HyprlandIpc::eventSocketStateChanged);
6469
QObject::connect(&this->eventSocket, &QLocalSocket::readyRead, this, &HyprlandIpc::eventSocketReady);
70+
71+
auto *instance = HyprlandToplevelMappingManager::instance();
72+
QObject::connect(instance, &HyprlandToplevelMappingManager::toplevelAddressed, this, &HyprlandIpc::toplevelAddressed);
73+
6574
// clang-format on
6675

6776
this->eventSocket.connectToServer(this->mEventSocketPath, QLocalSocket::ReadOnly);
6877
this->refreshMonitors(true);
6978
this->refreshWorkspaces(true);
79+
this->refreshToplevels();
7080
}
7181

7282
QString HyprlandIpc::requestSocketPath() const { return this->mRequestSocketPath; }
@@ -113,6 +123,36 @@ void HyprlandIpc::eventSocketReady() {
113123
}
114124
}
115125

126+
void HyprlandIpc::toplevelAddressed(
127+
wayland::toplevel_management::impl::ToplevelHandle* handle,
128+
quint64 address
129+
) {
130+
auto* waylandToplevel =
131+
wayland::toplevel_management::ToplevelManager::instance()->forImpl(handle);
132+
133+
if (!waylandToplevel) return;
134+
135+
auto* attached = qobject_cast<HyprlandToplevel*>(
136+
qmlAttachedPropertiesObject<HyprlandToplevel>(waylandToplevel, false)
137+
);
138+
139+
auto* hyprToplevel = this->findToplevelByAddress(address, true);
140+
141+
if (attached) {
142+
if (attached->address()) {
143+
qCDebug(logHyprlandIpc) << "Toplevel" << attached->addressStr() << "already has address"
144+
<< address;
145+
146+
return;
147+
}
148+
149+
attached->setAddress(address);
150+
attached->setHyprlandHandle(hyprToplevel);
151+
}
152+
153+
hyprToplevel->setWaylandHandle(waylandToplevel->implHandle());
154+
}
155+
116156
void HyprlandIpc::makeRequest(
117157
const QByteArray& request,
118158
const std::function<void(bool, QByteArray)>& callback
@@ -166,6 +206,8 @@ ObjectModel<HyprlandMonitor>* HyprlandIpc::monitors() { return &this->mMonitors;
166206

167207
ObjectModel<HyprlandWorkspace>* HyprlandIpc::workspaces() { return &this->mWorkspaces; }
168208

209+
ObjectModel<HyprlandToplevel>* HyprlandIpc::toplevels() { return &this->mToplevels; }
210+
169211
QVector<QByteArrayView> HyprlandIpc::parseEventArgs(QByteArrayView event, quint16 count) {
170212
auto args = QVector<QByteArrayView>();
171213

@@ -218,6 +260,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
218260
if (event->name == "configreloaded") {
219261
this->refreshMonitors(true);
220262
this->refreshWorkspaces(true);
263+
this->refreshToplevels();
221264
} else if (event->name == "monitoraddedv2") {
222265
auto args = event->parseView(3);
223266

@@ -390,6 +433,133 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
390433
// the fullscreen state changed, but this falls apart if you move a fullscreen
391434
// window between workspaces.
392435
this->refreshWorkspaces(false);
436+
} else if (event->name == "openwindow") {
437+
auto args = event->parseView(4);
438+
auto ok = false;
439+
auto windowAddress = args.at(0).toULongLong(&ok, 16);
440+
441+
if (!ok) return;
442+
443+
auto workspaceName = QString::fromUtf8(args.at(1));
444+
auto windowTitle = QString::fromUtf8(args.at(2));
445+
auto windowClass = QString::fromUtf8(args.at(3));
446+
447+
auto* workspace = this->findWorkspaceByName(workspaceName, false);
448+
if (!workspace) {
449+
qCWarning(logHyprlandIpc) << "Got openwindow for workspace" << workspaceName
450+
<< "which was not previously tracked.";
451+
return;
452+
}
453+
454+
auto* toplevel = this->findToplevelByAddress(windowAddress, false);
455+
const bool existed = toplevel != nullptr;
456+
457+
if (!toplevel) toplevel = new HyprlandToplevel(this);
458+
toplevel->updateInitial(windowAddress, windowTitle, workspaceName);
459+
460+
workspace->insertToplevel(toplevel);
461+
462+
if (!existed) {
463+
this->mToplevels.insertObject(toplevel);
464+
qCDebug(logHyprlandIpc) << "New toplevel created with address" << windowAddress << ", title"
465+
<< windowTitle << ", workspace" << workspaceName;
466+
}
467+
} else if (event->name == "closewindow") {
468+
auto args = event->parseView(1);
469+
auto ok = false;
470+
auto windowAddress = args.at(0).toULongLong(&ok, 16);
471+
472+
if (!ok) return;
473+
474+
const auto& mList = this->mToplevels.valueList();
475+
auto toplevelIter = std::ranges::find_if(mList, [windowAddress](HyprlandToplevel* m) {
476+
return m->address() == windowAddress;
477+
});
478+
479+
if (toplevelIter == mList.end()) {
480+
qCWarning(logHyprlandIpc) << "Got closewindow for address" << windowAddress
481+
<< "which was not previously tracked.";
482+
return;
483+
}
484+
485+
auto* toplevel = *toplevelIter;
486+
auto index = toplevelIter - mList.begin();
487+
this->mToplevels.removeAt(index);
488+
489+
// Remove from workspace
490+
auto* workspace = toplevel->bindableWorkspace().value();
491+
if (workspace) {
492+
workspace->toplevels()->removeObject(toplevel);
493+
}
494+
495+
delete toplevel;
496+
} else if (event->name == "movewindowv2") {
497+
auto args = event->parseView(3);
498+
auto ok = false;
499+
auto windowAddress = args.at(0).toULongLong(&ok, 16);
500+
auto workspaceName = QString::fromUtf8(args.at(2));
501+
502+
auto* toplevel = this->findToplevelByAddress(windowAddress, false);
503+
if (!toplevel) {
504+
qCWarning(logHyprlandIpc) << "Got movewindowv2 event for client with address" << windowAddress
505+
<< "which was not previously tracked.";
506+
return;
507+
}
508+
509+
HyprlandWorkspace* workspace = this->findWorkspaceByName(workspaceName, false);
510+
if (!workspace) {
511+
qCWarning(logHyprlandIpc) << "Got movewindowv2 event for workspace" << args.at(2)
512+
<< "which was not previously tracked.";
513+
return;
514+
}
515+
516+
auto* oldWorkspace = toplevel->bindableWorkspace().value();
517+
toplevel->setWorkspace(workspace);
518+
519+
if (oldWorkspace) {
520+
oldWorkspace->removeToplevel(toplevel);
521+
}
522+
523+
workspace->insertToplevel(toplevel);
524+
} else if (event->name == "windowtitlev2") {
525+
auto args = event->parseView(2);
526+
auto ok = false;
527+
auto windowAddress = args.at(0).toULongLong(&ok, 16);
528+
auto windowTitle = QString::fromUtf8(args.at(1));
529+
530+
if (!ok) return;
531+
532+
// It happens that Hyprland sends windowtitlev2 events before event
533+
// "openwindow" is emitted, so let's preemptively create it
534+
auto* toplevel = this->findToplevelByAddress(windowAddress, true);
535+
if (!toplevel) {
536+
qCWarning(logHyprlandIpc) << "Got windowtitlev2 event for client with address"
537+
<< windowAddress << "which was not previously tracked.";
538+
return;
539+
}
540+
541+
toplevel->bindableTitle().setValue(windowTitle);
542+
} else if (event->name == "activewindowv2") {
543+
auto args = event->parseView(1);
544+
auto ok = false;
545+
auto windowAddress = args.at(0).toULongLong(&ok, 16);
546+
547+
if (!ok) return;
548+
549+
// Did not observe "activewindowv2" event before "openwindow",
550+
// but better safe than sorry, so create if missing.
551+
auto* toplevel = this->findToplevelByAddress(windowAddress, true);
552+
this->bActiveToplevel = toplevel;
553+
} else if (event->name == "urgent") {
554+
auto args = event->parseView(1);
555+
auto ok = false;
556+
auto windowAddress = args.at(0).toULongLong(&ok, 16);
557+
558+
if (!ok) return;
559+
560+
// It happens that Hyprland sends urgent before "openwindow"
561+
auto* toplevel = this->findToplevelByAddress(windowAddress, true);
562+
toplevel->bindableUrgent().setValue(true);
393563
}
394564
}
395565

@@ -496,6 +666,71 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) {
496666
});
497667
}
498668

669+
HyprlandToplevel* HyprlandIpc::findToplevelByAddress(quint64 address, bool createIfMissing) {
670+
const auto& mList = this->mToplevels.valueList();
671+
HyprlandToplevel* toplevel = nullptr;
672+
673+
auto toplevelIter =
674+
std::ranges::find_if(mList, [&](HyprlandToplevel* m) { return m->address() == address; });
675+
676+
toplevel = toplevelIter == mList.end() ? nullptr : *toplevelIter;
677+
678+
if (!toplevel && createIfMissing) {
679+
qCDebug(logHyprlandIpc) << "Toplevel with address" << address
680+
<< "requested before creation, performing early init";
681+
682+
toplevel = new HyprlandToplevel(this);
683+
toplevel->updateInitial(address, "", "");
684+
this->mToplevels.insertObject(toplevel);
685+
}
686+
687+
return toplevel;
688+
}
689+
690+
void HyprlandIpc::refreshToplevels() {
691+
if (this->requestingToplevels) return;
692+
this->requestingToplevels = true;
693+
694+
this->makeRequest("j/clients", [this](bool success, const QByteArray& resp) {
695+
this->requestingToplevels = false;
696+
if (!success) return;
697+
698+
qCDebug(logHyprlandIpc) << "Parsing j/clients response";
699+
auto json = QJsonDocument::fromJson(resp).array();
700+
701+
const auto& mList = this->mToplevels.valueList();
702+
703+
for (auto entry: json) {
704+
auto object = entry.toObject().toVariantMap();
705+
706+
bool ok = false;
707+
auto address = object.value("address").toString().toULongLong(&ok, 16);
708+
709+
if (!ok) {
710+
qCWarning(logHyprlandIpc) << "Invalid address in j/clients entry:" << object;
711+
continue;
712+
}
713+
714+
auto toplevelsIter =
715+
std::ranges::find_if(mList, [&](HyprlandToplevel* m) { return m->address() == address; });
716+
717+
auto* toplevel = toplevelsIter == mList.end() ? nullptr : *toplevelsIter;
718+
auto exists = toplevel != nullptr;
719+
720+
if (!exists) toplevel = new HyprlandToplevel(this);
721+
toplevel->updateFromObject(object);
722+
723+
if (!exists) {
724+
qCDebug(logHyprlandIpc) << "New toplevel created with address" << address;
725+
this->mToplevels.insertObject(toplevel);
726+
}
727+
728+
auto* workspace = toplevel->bindableWorkspace().value();
729+
workspace->insertToplevel(toplevel);
730+
}
731+
});
732+
}
733+
499734
HyprlandMonitor*
500735
HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) {
501736
const auto& mList = this->mMonitors.valueList();

src/wayland/hyprland/ipc/connection.hpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@
1414

1515
#include "../../../core/model.hpp"
1616
#include "../../../core/qmlscreen.hpp"
17+
#include "../../../wayland/toplevel_management/handle.hpp"
1718

1819
namespace qs::hyprland::ipc {
1920

2021
class HyprlandMonitor;
2122
class HyprlandWorkspace;
23+
class HyprlandToplevel;
2224

2325
} // namespace qs::hyprland::ipc
2426

2527
Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandWorkspace*);
2628
Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandMonitor*);
29+
Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandToplevel*);
2730

2831
namespace qs::hyprland::ipc {
2932

@@ -85,18 +88,25 @@ class HyprlandIpc: public QObject {
8588
return &this->bFocusedWorkspace;
8689
}
8790

91+
[[nodiscard]] QBindable<HyprlandToplevel*> bindableActiveToplevel() const {
92+
return &this->bActiveToplevel;
93+
}
94+
8895
void setFocusedMonitor(HyprlandMonitor* monitor);
8996

9097
[[nodiscard]] ObjectModel<HyprlandMonitor>* monitors();
9198
[[nodiscard]] ObjectModel<HyprlandWorkspace>* workspaces();
99+
[[nodiscard]] ObjectModel<HyprlandToplevel>* toplevels();
92100

93101
// No byId because these preemptively create objects. The given id is set if created.
94102
HyprlandWorkspace* findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id = -1);
95103
HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1);
104+
HyprlandToplevel* findToplevelByAddress(quint64 address, bool createIfMissing);
96105

97106
// canCreate avoids making ghost workspaces when the connection races
98107
void refreshWorkspaces(bool canCreate);
99108
void refreshMonitors(bool canCreate);
109+
void refreshToplevels();
100110

101111
// The last argument may contain commas, so the count is required.
102112
[[nodiscard]] static QVector<QByteArrayView> parseEventArgs(QByteArrayView event, quint16 count);
@@ -107,12 +117,18 @@ class HyprlandIpc: public QObject {
107117

108118
void focusedMonitorChanged();
109119
void focusedWorkspaceChanged();
120+
void activeToplevelChanged();
110121

111122
private slots:
112123
void eventSocketError(QLocalSocket::LocalSocketError error) const;
113124
void eventSocketStateChanged(QLocalSocket::LocalSocketState state);
114125
void eventSocketReady();
115126

127+
void toplevelAddressed(
128+
qs::wayland::toplevel_management::impl::ToplevelHandle* handle,
129+
quint64 address
130+
);
131+
116132
void onFocusedMonitorDestroyed();
117133

118134
private:
@@ -128,10 +144,12 @@ private slots:
128144
bool valid = false;
129145
bool requestingMonitors = false;
130146
bool requestingWorkspaces = false;
147+
bool requestingToplevels = false;
131148
bool monitorsRequested = false;
132149

133150
ObjectModel<HyprlandMonitor> mMonitors {this};
134151
ObjectModel<HyprlandWorkspace> mWorkspaces {this};
152+
ObjectModel<HyprlandToplevel> mToplevels {this};
135153

136154
HyprlandIpcEvent event {this};
137155

@@ -148,6 +166,13 @@ private slots:
148166
bFocusedWorkspace,
149167
&HyprlandIpc::focusedWorkspaceChanged
150168
);
169+
170+
Q_OBJECT_BINDABLE_PROPERTY(
171+
HyprlandIpc,
172+
HyprlandToplevel*,
173+
bActiveToplevel,
174+
&HyprlandIpc::activeToplevelChanged
175+
);
151176
};
152177

153178
} // namespace qs::hyprland::ipc

0 commit comments

Comments
 (0)