diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt
index fd1da674..fd014635 100644
--- a/src/wayland/hyprland/ipc/CMakeLists.txt
+++ b/src/wayland/hyprland/ipc/CMakeLists.txt
@@ -17,6 +17,21 @@ install_qml_module(quickshell-hyprland-ipc)
target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick)
+if (WAYLAND_TOPLEVEL_MANAGEMENT)
+ target_sources(quickshell-hyprland-ipc PRIVATE
+ toplevel_mapping.cpp
+ hyprland_toplevel.cpp
+ )
+
+ wl_proto(wlp-hyprland-toplevel-mapping hyprland-toplevel-mapping-v1 "${CMAKE_CURRENT_SOURCE_DIR}")
+
+ target_link_libraries(quickshell-hyprland-ipc PRIVATE
+ Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
+ wlp-hyprland-toplevel-mapping
+ wlp-foreign-toplevel
+ )
+endif()
+
qs_module_pch(quickshell-hyprland-ipc SET large)
target_link_libraries(quickshell PRIVATE quickshell-hyprland-ipcplugin)
diff --git a/src/wayland/hyprland/ipc/hyprland-toplevel-mapping-v1.xml b/src/wayland/hyprland/ipc/hyprland-toplevel-mapping-v1.xml
new file mode 100644
index 00000000..ec7b1cea
--- /dev/null
+++ b/src/wayland/hyprland/ipc/hyprland-toplevel-mapping-v1.xml
@@ -0,0 +1,113 @@
+
+
+
+ Copyright © 2025 WhySoBad
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ 3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+ This protocol allows clients to retrieve the mapping of toplevels to hyprland window addresses.
+
+
+
+
+ This object is a manager which offers requests to retrieve a window address
+ for a toplevel.
+
+
+
+
+ This request has been edited to remove a compile dep.
+
+
+
+
+
+ Get the window address for a wlr toplevel.
+
+
+
+
+
+
+
+ All objects created by the manager will still remain valid, until their appropriate destroy
+ request has been called.
+
+
+
+
+
+
+ This object represents a mapping of a (wlr) toplevel to a window address.
+
+ Once created, the `window_address` event will be sent containing the address of the window
+ associated with the toplevel.
+ Should the mapping fail, the `failed` event will be sent.
+
+
+
+
+ The full 64bit window address. The `address` field contains the lower 32 bits whilst the
+ `address_hi` contains the upper 32 bits
+
+
+
+
+
+
+
+ The mapping of the toplevel to a window address failed. Most likely the window does not
+ exist (anymore).
+
+
+
+
+
+ Destroy the handle. This request can be sent at any time by the client.
+
+
+
+
diff --git a/src/wayland/hyprland/ipc/hyprland_toplevel.cpp b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp
new file mode 100644
index 00000000..93c924c6
--- /dev/null
+++ b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp
@@ -0,0 +1,53 @@
+#include "hyprland_toplevel.hpp"
+
+#include
+#include
+#include
+#include
+
+#include "toplevel_mapping.hpp"
+#include "../../toplevel_management/handle.hpp"
+#include "../../toplevel_management/qml.hpp"
+
+using namespace qs::wayland::toplevel_management;
+using namespace qs::wayland::toplevel_management::impl;
+
+namespace qs::hyprland::ipc {
+
+HyprlandToplevel::HyprlandToplevel(Toplevel* toplevel)
+ : QObject(toplevel)
+ , handle(toplevel->implHandle()) {
+ auto* instance = HyprlandToplevelMappingManager::instance();
+ auto addr = instance->getToplevelAddress(handle);
+
+ if (addr != 0) this->setAddress(addr);
+ else {
+ QObject::connect(
+ instance,
+ &HyprlandToplevelMappingManager::toplevelAddressed,
+ this,
+ &HyprlandToplevel::onToplevelAddressed
+ );
+ }
+}
+
+void HyprlandToplevel::onToplevelAddressed(ToplevelHandle* handle, quint64 address) {
+ if (handle == this->handle) {
+ this->setAddress(address);
+ QObject::disconnect(HyprlandToplevelMappingManager::instance(), nullptr, this, nullptr);
+ }
+}
+
+void HyprlandToplevel::setAddress(quint64 address) {
+ this->mAddress = QString::number(address, 16);
+ emit this->addressChanged();
+}
+
+HyprlandToplevel* HyprlandToplevel::qmlAttachedProperties(QObject* object) {
+ if (auto* toplevel = qobject_cast(object)) {
+ return new HyprlandToplevel(toplevel);
+ } else {
+ return nullptr;
+ }
+}
+} // namespace qs::hyprland::ipc
diff --git a/src/wayland/hyprland/ipc/hyprland_toplevel.hpp b/src/wayland/hyprland/ipc/hyprland_toplevel.hpp
new file mode 100644
index 00000000..2cc70a5a
--- /dev/null
+++ b/src/wayland/hyprland/ipc/hyprland_toplevel.hpp
@@ -0,0 +1,50 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "../../toplevel_management/handle.hpp"
+#include "../../toplevel_management/qml.hpp"
+
+namespace qs::hyprland::ipc {
+
+//! Exposes Hyprland window address for a Toplevel
+/// Attached object of @@Quickshell.Wayland.Toplevel which exposes
+/// a Hyprland window address for the window.
+class HyprlandToplevel: public QObject {
+ Q_OBJECT;
+ QML_ELEMENT;
+ QML_UNCREATABLE("");
+ QML_ATTACHED(HyprlandToplevel);
+ /// Hexadecimal Hyprland window address. Will be an empty string until
+ /// the address is reported.
+ Q_PROPERTY(QString address READ address NOTIFY addressChanged);
+
+public:
+ explicit HyprlandToplevel(qs::wayland::toplevel_management::Toplevel* toplevel);
+
+ [[nodiscard]] QString address() { return this->mAddress; }
+
+ static HyprlandToplevel* qmlAttachedProperties(QObject* object);
+
+signals:
+ void addressChanged();
+
+private slots:
+ void onToplevelAddressed(
+ qs::wayland::toplevel_management::impl::ToplevelHandle* handle,
+ quint64 address
+ );
+
+private:
+ void setAddress(quint64 address);
+
+ QString mAddress;
+ // doesn't have to be nulled on destroy, only used for comparison
+ qs::wayland::toplevel_management::impl::ToplevelHandle* handle;
+};
+
+} // namespace qs::hyprland::ipc
diff --git a/src/wayland/hyprland/ipc/toplevel_mapping.cpp b/src/wayland/hyprland/ipc/toplevel_mapping.cpp
new file mode 100644
index 00000000..dadd7d89
--- /dev/null
+++ b/src/wayland/hyprland/ipc/toplevel_mapping.cpp
@@ -0,0 +1,83 @@
+#include "toplevel_mapping.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "../../toplevel_management/manager.hpp"
+
+using namespace qs::wayland::toplevel_management::impl;
+
+namespace qs::hyprland::ipc {
+
+HyprlandToplevelMappingHandle::~HyprlandToplevelMappingHandle() {
+ if (this->isInitialized()) this->destroy();
+}
+
+void HyprlandToplevelMappingHandle::hyprland_toplevel_window_mapping_handle_v1_window_address(
+ quint32 addressHi,
+ quint32 addressLo
+) {
+ auto address = static_cast(addressHi) << 32 | addressLo;
+ HyprlandToplevelMappingManager::instance()->assignAddress(this->handle, address);
+ delete this;
+}
+
+void HyprlandToplevelMappingHandle::hyprland_toplevel_window_mapping_handle_v1_failed() {
+ delete this;
+}
+
+HyprlandToplevelMappingManager::HyprlandToplevelMappingManager()
+ : QWaylandClientExtensionTemplate(1) {
+ this->initialize();
+
+ if (!this->isInitialized()) {
+ qWarning() << "Compositor does not support hyprland-toplevel-mapping-v1."
+ "It will not be possible to derive hyprland addresses from toplevels.";
+ return;
+ }
+
+ QObject::connect(
+ ToplevelManager::instance(),
+ &ToplevelManager::toplevelReady,
+ this,
+ &HyprlandToplevelMappingManager::onToplevelReady
+ );
+
+ for (auto* toplevel: ToplevelManager::instance()->readyToplevels()) {
+ this->onToplevelReady(toplevel);
+ }
+}
+
+void HyprlandToplevelMappingManager::onToplevelReady(ToplevelHandle* handle) {
+ QObject::connect(
+ handle,
+ &QObject::destroyed,
+ this,
+ &HyprlandToplevelMappingManager::onToplevelDestroyed
+ );
+
+ new HyprlandToplevelMappingHandle(handle, this->get_window_for_toplevel_wlr(handle->object()));
+}
+
+void HyprlandToplevelMappingManager::assignAddress(ToplevelHandle* handle, quint64 address) {
+ this->addresses.insert(handle, address);
+ emit this->toplevelAddressed(handle, address);
+}
+
+void HyprlandToplevelMappingManager::onToplevelDestroyed(QObject* object) {
+ this->addresses.remove(static_cast(object)); // NOLINT
+}
+
+quint64 HyprlandToplevelMappingManager::getToplevelAddress(ToplevelHandle* handle) const {
+ return this->addresses.value(handle);
+}
+
+HyprlandToplevelMappingManager* HyprlandToplevelMappingManager::instance() {
+ static auto* instance = new HyprlandToplevelMappingManager();
+ return instance;
+}
+
+} // namespace qs::hyprland::ipc
diff --git a/src/wayland/hyprland/ipc/toplevel_mapping.hpp b/src/wayland/hyprland/ipc/toplevel_mapping.hpp
new file mode 100644
index 00000000..31eeade9
--- /dev/null
+++ b/src/wayland/hyprland/ipc/toplevel_mapping.hpp
@@ -0,0 +1,70 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "../../toplevel_management/handle.hpp"
+#include "wayland-hyprland-toplevel-mapping-v1-client-protocol.h"
+
+namespace qs::hyprland::ipc {
+
+class HyprlandToplevelMappingHandle: QtWayland::hyprland_toplevel_window_mapping_handle_v1 {
+public:
+ explicit HyprlandToplevelMappingHandle(
+ qs::wayland::toplevel_management::impl::ToplevelHandle* handle,
+ ::hyprland_toplevel_window_mapping_handle_v1* mapping
+ )
+ : QtWayland::hyprland_toplevel_window_mapping_handle_v1(mapping)
+ , handle(handle) {}
+
+ ~HyprlandToplevelMappingHandle() override;
+ Q_DISABLE_COPY_MOVE(HyprlandToplevelMappingHandle);
+
+protected:
+ void hyprland_toplevel_window_mapping_handle_v1_window_address(
+ quint32 addressHi,
+ quint32 addressLo
+ ) override;
+
+ void hyprland_toplevel_window_mapping_handle_v1_failed() override;
+
+private:
+ qs::wayland::toplevel_management::impl::ToplevelHandle* handle;
+};
+
+class HyprlandToplevelMappingManager
+ : public QWaylandClientExtensionTemplate
+ , public QtWayland::hyprland_toplevel_mapping_manager_v1 {
+ Q_OBJECT;
+
+public:
+ explicit HyprlandToplevelMappingManager();
+
+ static HyprlandToplevelMappingManager* instance();
+
+ [[nodiscard]] quint64
+ getToplevelAddress(qs::wayland::toplevel_management::impl::ToplevelHandle* handle) const;
+
+signals:
+ void toplevelAddressed(
+ qs::wayland::toplevel_management::impl::ToplevelHandle* handle,
+ quint64 address
+ );
+
+private slots:
+ void onToplevelReady(qs::wayland::toplevel_management::impl::ToplevelHandle* handle);
+ void onToplevelDestroyed(QObject* object);
+
+private:
+ void
+ assignAddress(qs::wayland::toplevel_management::impl::ToplevelHandle* handle, quint64 address);
+ QHash addresses;
+
+ friend class HyprlandToplevelMappingHandle;
+};
+} // namespace qs::hyprland::ipc