diff --git a/src/window/switcher/app-capture/AppCapture-1.0.gir b/src/window/switcher/app-capture/AppCapture-1.0.gir
new file mode 100644
index 0000000..c7f6277
--- /dev/null
+++ b/src/window/switcher/app-capture/AppCapture-1.0.gir
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/window/switcher/app-capture/README.md b/src/window/switcher/app-capture/README.md
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/window/switcher/app-capture/README.md
@@ -0,0 +1 @@
+
diff --git a/src/window/switcher/app-capture/app-capture.c b/src/window/switcher/app-capture/app-capture.c
new file mode 100644
index 0000000..4f84e7f
--- /dev/null
+++ b/src/window/switcher/app-capture/app-capture.c
@@ -0,0 +1,672 @@
+#define _GNU_SOURCE
+#include "app-capture.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "hyprland-toplevel-export-v1.h"
+#include "wlr-foreign-toplevel-management-unstable-v1.h"
+#include "hyprland-toplevel-mapping-v1.h"
+
+/* =========================================================================
+ * Signals
+ * ========================================================================= */
+
+enum {
+ SIGNAL_FRAME_READY,
+ SIGNAL_FRAME_FAILED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+/* How long capture_by_handle() will wait for an unknown address's wlr handle
+ * to arrive before giving up and emitting frame-failed. */
+#define PENDING_CAPTURE_TIMEOUT_MS 1500
+
+/* =========================================================================
+ * Toplevel entry — one per live wlr toplevel
+ * =========================================================================
+ * We build a table of these at init time by:
+ * 1. Binding zwlr_foreign_toplevel_manager_v1 and receiving toplevel events
+ * 2. For each handle, requesting a hyprland_toplevel_mapping to get the
+ * full 64-bit Hyprland address (split as hi + lo uint32s)
+ * This lets capture_by_handle() look up the wlr object for any address.
+ * ========================================================================= */
+
+typedef struct {
+ struct zwlr_foreign_toplevel_handle_v1 *wlr_handle;
+ char address[32]; /* hex string, e.g. "564f60266bd0", no "0x" prefix */
+ gboolean mapped; /* false until window_address event arrives */
+} ToplevelEntry;
+
+/* =========================================================================
+ * Pending capture — capture_by_handle() called before mapping arrived
+ * ========================================================================= */
+
+typedef struct {
+ AppCapture *self; /* unowned; pending queue is owned by self */
+ char *address; /* owned, stripped of "0x" */
+ guint timeout_id; /* 0 once consumed */
+} PendingCapture;
+
+/* =========================================================================
+ * Object struct
+ * ========================================================================= */
+
+struct _AppCapture {
+ GObject parent_instance;
+
+ /* Wayland globals */
+ struct wl_display *display;
+ struct wl_registry *registry;
+ struct wl_shm *shm;
+ struct hyprland_toplevel_export_manager_v1 *export_manager;
+ struct zwlr_foreign_toplevel_manager_v1 *wlr_manager;
+ struct hyprland_toplevel_mapping_manager_v1 *mapping_manager;
+
+ /* Address → wlr_handle table */
+ GPtrArray *toplevels; /* element-type: ToplevelEntry* */
+
+ /* Captures waiting on a not-yet-mapped address */
+ GPtrArray *pending_captures; /* element-type: PendingCapture* */
+
+ /* Per-frame state — reset before every capture */
+ int shm_fd;
+ unsigned char *pixel_data;
+ uint32_t width;
+ uint32_t height;
+ uint32_t stride;
+ uint32_t format;
+ size_t shm_size;
+};
+
+G_DEFINE_TYPE(AppCapture, app_capture, G_TYPE_OBJECT)
+
+/* =========================================================================
+ * Forward declarations
+ * ========================================================================= */
+
+static void cleanup_shm(AppCapture *self);
+static void request_mapping(AppCapture *self, ToplevelEntry *entry);
+static void emit_frame_failed(AppCapture *self, const char *reason);
+static void do_capture(AppCapture *self,
+ struct zwlr_foreign_toplevel_handle_v1 *wlr_handle);
+static struct zwlr_foreign_toplevel_handle_v1 *find_wlr_handle(AppCapture *self,
+ const char *addr);
+static void flush_pending_for_address(AppCapture *self, const char *address);
+static gboolean pending_capture_timeout_cb(gpointer data);
+static void pending_capture_free(gpointer data);
+
+/* =========================================================================
+ * Hyprland toplevel mapping listener
+ * Fires once per ToplevelEntry with the full 64-bit address.
+ * ========================================================================= */
+
+typedef struct {
+ AppCapture *self;
+ ToplevelEntry *entry;
+} MappingContext;
+
+static void mapping_handle_window_address(void *data,
+ struct hyprland_toplevel_window_mapping_handle_v1 *handle,
+ uint32_t address_hi, uint32_t address)
+{
+ MappingContext *ctx = data;
+ ToplevelEntry *entry = ctx->entry;
+ AppCapture *self = ctx->self;
+
+ uint64_t full = ((uint64_t)address_hi << 32) | (uint64_t)address;
+ snprintf(entry->address, sizeof(entry->address), "%" PRIx64, full);
+ entry->mapped = TRUE;
+
+ /* Dispatch any capture that was waiting for this address */
+ flush_pending_for_address(self, entry->address);
+
+ hyprland_toplevel_window_mapping_handle_v1_destroy(handle);
+ g_free(ctx);
+}
+
+static void mapping_handle_failed(void *data,
+ struct hyprland_toplevel_window_mapping_handle_v1 *handle)
+{
+ MappingContext *ctx = data;
+ /* Address stays empty — this entry won't be matchable, which is fine.
+ * Any pending capture for that address will time out via its own timer. */
+ hyprland_toplevel_window_mapping_handle_v1_destroy(handle);
+ g_free(ctx);
+}
+
+static const struct hyprland_toplevel_window_mapping_handle_v1_listener mapping_listener = {
+ .window_address = mapping_handle_window_address,
+ .failed = mapping_handle_failed,
+};
+
+/* =========================================================================
+ * wlr foreign toplevel listeners
+ * We only need enough to know when a window appears or disappears.
+ * ========================================================================= */
+
+static void wlr_handle_title(void *data,
+ struct zwlr_foreign_toplevel_handle_v1 *handle, const char *title)
+{ (void)data; (void)handle; (void)title; }
+
+static void wlr_handle_app_id(void *data,
+ struct zwlr_foreign_toplevel_handle_v1 *handle, const char *app_id)
+{ (void)data; (void)handle; (void)app_id; }
+
+static void wlr_handle_output_enter(void *data,
+ struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_output *output)
+{ (void)data; (void)handle; (void)output; }
+
+static void wlr_handle_output_leave(void *data,
+ struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_output *output)
+{ (void)data; (void)handle; (void)output; }
+
+static void wlr_handle_state(void *data,
+ struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_array *state)
+{ (void)data; (void)handle; (void)state; }
+
+static void wlr_handle_done(void *data,
+ struct zwlr_foreign_toplevel_handle_v1 *handle)
+{ (void)data; (void)handle; }
+
+static void wlr_handle_closed(void *data,
+ struct zwlr_foreign_toplevel_handle_v1 *handle)
+{
+ ToplevelEntry *entry = data;
+ /* Mark as unmapped so capture_by_handle skips it */
+ entry->mapped = FALSE;
+ zwlr_foreign_toplevel_handle_v1_destroy(handle);
+ entry->wlr_handle = NULL;
+}
+
+static void wlr_handle_parent(void *data,
+ struct zwlr_foreign_toplevel_handle_v1 *handle,
+ struct zwlr_foreign_toplevel_handle_v1 *parent)
+{ (void)data; (void)handle; (void)parent; }
+
+static const struct zwlr_foreign_toplevel_handle_v1_listener wlr_handle_listener = {
+ .title = wlr_handle_title,
+ .app_id = wlr_handle_app_id,
+ .output_enter = wlr_handle_output_enter,
+ .output_leave = wlr_handle_output_leave,
+ .state = wlr_handle_state,
+ .done = wlr_handle_done,
+ .closed = wlr_handle_closed,
+ .parent = wlr_handle_parent,
+};
+
+/* =========================================================================
+ * wlr foreign toplevel manager listener
+ * ========================================================================= */
+
+static void wlr_manager_handle_toplevel(void *data,
+ struct zwlr_foreign_toplevel_manager_v1 *manager,
+ struct zwlr_foreign_toplevel_handle_v1 *handle)
+{
+ (void)manager;
+ AppCapture *self = APP_CAPTURE(data);
+ ToplevelEntry *entry = g_new0(ToplevelEntry, 1);
+ entry->wlr_handle = handle;
+ entry->mapped = FALSE;
+
+ zwlr_foreign_toplevel_handle_v1_add_listener(handle,
+ &wlr_handle_listener, entry);
+
+ g_ptr_array_add(self->toplevels, entry);
+
+ /* Immediately request the Hyprland address mapping for this handle */
+ if (self->mapping_manager)
+ request_mapping(self, entry);
+}
+
+static void wlr_manager_handle_finished(void *data,
+ struct zwlr_foreign_toplevel_manager_v1 *manager)
+{ (void)data; (void)manager; }
+
+static const struct zwlr_foreign_toplevel_manager_v1_listener wlr_manager_listener = {
+ .toplevel = wlr_manager_handle_toplevel,
+ .finished = wlr_manager_handle_finished,
+};
+
+/* =========================================================================
+ * Mapping helper — ask Hyprland for the address of a wlr handle
+ * ========================================================================= */
+
+static void request_mapping(AppCapture *self, ToplevelEntry *entry)
+{
+ MappingContext *ctx = g_new0(MappingContext, 1);
+ ctx->self = self;
+ ctx->entry = entry;
+
+ struct hyprland_toplevel_window_mapping_handle_v1 *mh =
+ hyprland_toplevel_mapping_manager_v1_get_window_for_toplevel_wlr(
+ self->mapping_manager, entry->wlr_handle);
+
+ hyprland_toplevel_window_mapping_handle_v1_add_listener(mh,
+ &mapping_listener, ctx);
+}
+
+/* =========================================================================
+ * Registry listener
+ * ========================================================================= */
+
+static void registry_handle_global(void *data, struct wl_registry *registry,
+ uint32_t name, const char *interface, uint32_t version)
+{
+ (void)version;
+ AppCapture *self = APP_CAPTURE(data);
+
+ if (g_strcmp0(interface,
+ hyprland_toplevel_export_manager_v1_interface.name) == 0) {
+ self->export_manager = wl_registry_bind(registry, name,
+ &hyprland_toplevel_export_manager_v1_interface, 2);
+ }
+ else if (g_strcmp0(interface, wl_shm_interface.name) == 0) {
+ self->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1);
+ }
+ else if (g_strcmp0(interface,
+ zwlr_foreign_toplevel_manager_v1_interface.name) == 0) {
+ self->wlr_manager = wl_registry_bind(registry, name,
+ &zwlr_foreign_toplevel_manager_v1_interface, 3);
+ zwlr_foreign_toplevel_manager_v1_add_listener(self->wlr_manager,
+ &wlr_manager_listener, self);
+ }
+ else if (g_strcmp0(interface,
+ hyprland_toplevel_mapping_manager_v1_interface.name) == 0) {
+ self->mapping_manager = wl_registry_bind(registry, name,
+ &hyprland_toplevel_mapping_manager_v1_interface, 1);
+ }
+}
+
+static void registry_handle_global_remove(void *data,
+ struct wl_registry *registry, uint32_t name)
+{ (void)data; (void)registry; (void)name; }
+
+static const struct wl_registry_listener registry_listener = {
+ .global = registry_handle_global,
+ .global_remove = registry_handle_global_remove,
+};
+
+/* =========================================================================
+ * Frame listener
+ *
+ * Event order: buffer → (linux_dmabuf) → buffer_done → [copy()] → flags → ready|failed
+ * ========================================================================= */
+
+static void frame_handle_buffer(void *data,
+ struct hyprland_toplevel_export_frame_v1 *frame,
+ uint32_t format, uint32_t width, uint32_t height, uint32_t stride)
+{
+ (void)frame;
+ AppCapture *self = APP_CAPTURE(data);
+ self->format = format;
+ self->width = width;
+ self->height = height;
+ self->stride = stride;
+}
+
+static void frame_handle_linux_dmabuf(void *data,
+ struct hyprland_toplevel_export_frame_v1 *frame,
+ uint32_t format, uint32_t width, uint32_t height)
+{ (void)data; (void)frame; (void)format; (void)width; (void)height; }
+
+static void frame_handle_buffer_done(void *data,
+ struct hyprland_toplevel_export_frame_v1 *frame)
+{
+ AppCapture *self = APP_CAPTURE(data);
+
+ if (self->width == 0 || self->height == 0 || self->stride == 0) {
+ g_warning("AppCapture: buffer_done with zero dimensions");
+ hyprland_toplevel_export_frame_v1_destroy(frame);
+ emit_frame_failed(self, "buffer_invalid");
+ return;
+ }
+
+ self->shm_size = (size_t)self->stride * self->height;
+
+ self->shm_fd = memfd_create("app-capture-buffer", 0);
+ if (self->shm_fd < 0) {
+ g_warning("AppCapture: memfd_create failed");
+ hyprland_toplevel_export_frame_v1_destroy(frame);
+ emit_frame_failed(self, "alloc_failed");
+ return;
+ }
+
+ if (ftruncate(self->shm_fd, (off_t)self->shm_size) < 0) {
+ g_warning("AppCapture: ftruncate failed");
+ cleanup_shm(self);
+ hyprland_toplevel_export_frame_v1_destroy(frame);
+ emit_frame_failed(self, "alloc_failed");
+ return;
+ }
+
+ self->pixel_data = mmap(NULL, self->shm_size,
+ PROT_READ | PROT_WRITE, MAP_SHARED,
+ self->shm_fd, 0);
+ if (self->pixel_data == MAP_FAILED) {
+ g_warning("AppCapture: mmap failed");
+ self->pixel_data = NULL;
+ cleanup_shm(self);
+ hyprland_toplevel_export_frame_v1_destroy(frame);
+ emit_frame_failed(self, "alloc_failed");
+ return;
+ }
+
+ struct wl_shm_pool *pool = wl_shm_create_pool(self->shm, self->shm_fd,
+ (int32_t)self->shm_size);
+ struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool,
+ 0, (int32_t)self->width, (int32_t)self->height,
+ (int32_t)self->stride, self->format);
+ wl_shm_pool_destroy(pool);
+
+ hyprland_toplevel_export_frame_v1_copy(frame, buffer, 1);
+ wl_buffer_destroy(buffer);
+}
+
+static void frame_handle_flags(void *data,
+ struct hyprland_toplevel_export_frame_v1 *frame, uint32_t flags)
+{ (void)data; (void)frame; (void)flags; }
+
+static void frame_handle_ready(void *data,
+ struct hyprland_toplevel_export_frame_v1 *frame,
+ uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec)
+{
+ (void)tv_sec_hi; (void)tv_sec_lo; (void)tv_nsec;
+ AppCapture *self = APP_CAPTURE(data);
+
+ if (!self->pixel_data) {
+ g_warning("AppCapture: ready fired but pixel_data is NULL");
+ hyprland_toplevel_export_frame_v1_destroy(frame);
+ emit_frame_failed(self, "internal");
+ return;
+ }
+
+ GBytes *bytes = g_bytes_new(self->pixel_data, self->shm_size);
+ g_signal_emit(self, signals[SIGNAL_FRAME_READY], 0,
+ bytes, (gint)self->width, (gint)self->height, (gint)self->stride);
+ g_bytes_unref(bytes);
+
+ cleanup_shm(self);
+ hyprland_toplevel_export_frame_v1_destroy(frame);
+}
+
+static void frame_handle_failed(void *data,
+ struct hyprland_toplevel_export_frame_v1 *frame)
+{
+ AppCapture *self = APP_CAPTURE(data);
+ g_warning("AppCapture: frame capture failed");
+ cleanup_shm(self);
+ hyprland_toplevel_export_frame_v1_destroy(frame);
+ emit_frame_failed(self, "frame_failed");
+}
+
+static void frame_handle_damage(void *data,
+ struct hyprland_toplevel_export_frame_v1 *frame,
+ uint32_t x, uint32_t y, uint32_t width, uint32_t height)
+{ (void)data; (void)frame; (void)x; (void)y; (void)width; (void)height; }
+
+static const struct hyprland_toplevel_export_frame_v1_listener frame_listener = {
+ .buffer = frame_handle_buffer,
+ .linux_dmabuf = frame_handle_linux_dmabuf,
+ .buffer_done = frame_handle_buffer_done,
+ .flags = frame_handle_flags,
+ .ready = frame_handle_ready,
+ .failed = frame_handle_failed,
+ .damage = frame_handle_damage,
+};
+
+/* =========================================================================
+ * Capture helpers
+ * ========================================================================= */
+
+static void emit_frame_failed(AppCapture *self, const char *reason)
+{
+ g_signal_emit(self, signals[SIGNAL_FRAME_FAILED], 0, reason);
+}
+
+static struct zwlr_foreign_toplevel_handle_v1 *find_wlr_handle(AppCapture *self,
+ const char *addr)
+{
+ for (guint i = 0; i < self->toplevels->len; i++) {
+ ToplevelEntry *entry = g_ptr_array_index(self->toplevels, i);
+ if (entry->mapped && entry->wlr_handle &&
+ g_strcmp0(entry->address, addr) == 0)
+ return entry->wlr_handle;
+ }
+ return NULL;
+}
+
+static void do_capture(AppCapture *self,
+ struct zwlr_foreign_toplevel_handle_v1 *wlr_handle)
+{
+ self->width = self->height = self->stride = self->format = 0;
+
+ struct hyprland_toplevel_export_frame_v1 *frame =
+ hyprland_toplevel_export_manager_v1_capture_toplevel_with_wlr_toplevel_handle(
+ self->export_manager,
+ 0, /* overlay_cursor */
+ wlr_handle
+ );
+
+ hyprland_toplevel_export_frame_v1_add_listener(frame, &frame_listener, self);
+ wl_display_flush(self->display);
+}
+
+/* =========================================================================
+ * Pending capture queue
+ *
+ * When capture_by_handle() is called for an address whose wlr handle hasn't
+ * been mapped yet (race between Hyprland IPC and wlr foreign-toplevel), we
+ * park a PendingCapture and wait for either:
+ * - mapping_handle_window_address → flush_pending_for_address → do_capture
+ * - PENDING_CAPTURE_TIMEOUT_MS elapses → emit frame-failed
+ *
+ * In practice the JS layer is single-flight so this queue holds ≤ 1 entry.
+ * ========================================================================= */
+
+static void pending_capture_free(gpointer data)
+{
+ PendingCapture *pc = data;
+ if (pc->timeout_id != 0) {
+ g_source_remove(pc->timeout_id);
+ pc->timeout_id = 0;
+ }
+ g_free(pc->address);
+ g_free(pc);
+}
+
+static gboolean pending_capture_timeout_cb(gpointer data)
+{
+ PendingCapture *pc = data;
+ AppCapture *self = pc->self;
+
+ g_warning("AppCapture: timeout waiting for wlr handle '%s' "
+ "(toplevel table has %u entries)",
+ pc->address, self->toplevels->len);
+
+ /* Mark consumed so pending_capture_free doesn't double-remove */
+ pc->timeout_id = 0;
+
+ /* Remove first (frees pc), THEN emit — emit may re-enter capture_by_handle */
+ g_ptr_array_remove(self->pending_captures, pc);
+ emit_frame_failed(self, "no_handle_timeout");
+ return G_SOURCE_REMOVE;
+}
+
+static void flush_pending_for_address(AppCapture *self, const char *address)
+{
+ if (self->pending_captures->len == 0) return;
+
+ struct zwlr_foreign_toplevel_handle_v1 *wlr_handle =
+ find_wlr_handle(self, address);
+ if (!wlr_handle) return;
+
+ /* Iterate backwards so removals don't shift indices we're about to visit */
+ for (guint i = self->pending_captures->len; i > 0; i--) {
+ PendingCapture *pc = g_ptr_array_index(self->pending_captures, i - 1);
+ if (g_strcmp0(pc->address, address) != 0) continue;
+
+ do_capture(self, wlr_handle);
+ g_ptr_array_remove_index(self->pending_captures, i - 1);
+ /* JS side is single-flight — at most one match expected.
+ * Break to avoid issuing two captures against the same shm state. */
+ break;
+ }
+}
+
+/* =========================================================================
+ * SHM helpers
+ * ========================================================================= */
+
+static void cleanup_shm(AppCapture *self)
+{
+ if (self->pixel_data && self->pixel_data != MAP_FAILED) {
+ munmap(self->pixel_data, self->shm_size);
+ self->pixel_data = NULL;
+ }
+ if (self->shm_fd >= 0) {
+ close(self->shm_fd);
+ self->shm_fd = -1;
+ }
+ self->shm_size = 0;
+}
+
+static void toplevel_entry_free(gpointer data)
+{
+ ToplevelEntry *entry = data;
+ if (entry->wlr_handle)
+ zwlr_foreign_toplevel_handle_v1_destroy(entry->wlr_handle);
+ g_free(entry);
+}
+
+/* =========================================================================
+ * GObject lifecycle
+ * ========================================================================= */
+
+static void app_capture_finalize(GObject *object)
+{
+ AppCapture *self = APP_CAPTURE(object);
+ cleanup_shm(self);
+ g_ptr_array_unref(self->pending_captures);
+ g_ptr_array_unref(self->toplevels);
+ if (self->export_manager)
+ hyprland_toplevel_export_manager_v1_destroy(self->export_manager);
+ if (self->mapping_manager)
+ hyprland_toplevel_mapping_manager_v1_destroy(self->mapping_manager);
+ if (self->wlr_manager)
+ zwlr_foreign_toplevel_manager_v1_destroy(self->wlr_manager);
+ if (self->shm)
+ wl_shm_destroy(self->shm);
+ if (self->registry)
+ wl_registry_destroy(self->registry);
+ G_OBJECT_CLASS(app_capture_parent_class)->finalize(object);
+}
+
+static void app_capture_class_init(AppCaptureClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+ object_class->finalize = app_capture_finalize;
+
+ signals[SIGNAL_FRAME_READY] = g_signal_new(
+ "frame-ready",
+ G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 4,
+ G_TYPE_BYTES, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT
+ );
+
+ signals[SIGNAL_FRAME_FAILED] = g_signal_new(
+ "frame-failed",
+ G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 1,
+ G_TYPE_STRING
+ );
+}
+
+static void app_capture_init(AppCapture *self)
+{
+ self->shm_fd = -1;
+ self->pixel_data = NULL;
+ self->shm_size = 0;
+ self->toplevels = g_ptr_array_new_with_free_func(toplevel_entry_free);
+ self->pending_captures = g_ptr_array_new_with_free_func(pending_capture_free);
+
+ GdkDisplay *gdk_display = gdk_display_get_default();
+ self->display = gdk_wayland_display_get_wl_display(gdk_display);
+
+ self->registry = wl_display_get_registry(self->display);
+ wl_registry_add_listener(self->registry, ®istry_listener, self);
+
+ /* First roundtrip: compositor advertises globals, we bind them all */
+ wl_display_roundtrip(self->display);
+
+ /* Second roundtrip: wlr_manager emits toplevel events for existing
+ * windows, and mapping requests are sent for each one */
+ wl_display_roundtrip(self->display);
+
+ /* Third roundtrip: mapping responses (window_address events) arrive */
+ wl_display_roundtrip(self->display);
+
+ if (!self->export_manager)
+ g_warning("AppCapture: hyprland_toplevel_export_manager_v1 not found");
+ if (!self->wlr_manager)
+ g_warning("AppCapture: zwlr_foreign_toplevel_manager_v1 not found");
+ if (!self->mapping_manager)
+ g_warning("AppCapture: hyprland_toplevel_mapping_manager_v1 not found");
+ if (!self->shm)
+ g_warning("AppCapture: wl_shm not found");
+}
+
+/* =========================================================================
+ * Public API
+ * ========================================================================= */
+
+AppCapture *app_capture_new(void)
+{
+ return g_object_new(APP_TYPE_CAPTURE, NULL);
+}
+
+void app_capture_capture_by_handle(AppCapture *self, const gchar *address)
+{
+ g_return_if_fail(APP_IS_CAPTURE(self));
+ g_return_if_fail(address != NULL);
+
+ if (!self->export_manager) {
+ g_warning("AppCapture: export_manager not available");
+ emit_frame_failed(self, "no_export_manager");
+ return;
+ }
+
+ /* Strip optional "0x" prefix so matching works regardless of input */
+ const gchar *addr = address;
+ if (g_str_has_prefix(addr, "0x") || g_str_has_prefix(addr, "0X"))
+ addr += 2;
+
+ /* Fast path: handle already known and mapped */
+ struct zwlr_foreign_toplevel_handle_v1 *wlr_handle =
+ find_wlr_handle(self, addr);
+ if (wlr_handle) {
+ do_capture(self, wlr_handle);
+ return;
+ }
+
+ /* Slow path: park and wait up to PENDING_CAPTURE_TIMEOUT_MS for the
+ * wlr_manager toplevel/mapping events to arrive. Race window is
+ * typically <100 ms in practice but can spike under load. */
+ PendingCapture *pc = g_new0(PendingCapture, 1);
+ pc->self = self;
+ pc->address = g_strdup(addr);
+ pc->timeout_id = g_timeout_add(PENDING_CAPTURE_TIMEOUT_MS,
+ pending_capture_timeout_cb, pc);
+ g_ptr_array_add(self->pending_captures, pc);
+}
diff --git a/src/window/switcher/app-capture/app-capture.h b/src/window/switcher/app-capture/app-capture.h
new file mode 100644
index 0000000..f0991a6
--- /dev/null
+++ b/src/window/switcher/app-capture/app-capture.h
@@ -0,0 +1,43 @@
+#pragma once
+#include
+
+G_BEGIN_DECLS
+
+#define APP_TYPE_CAPTURE (app_capture_get_type())
+G_DECLARE_FINAL_TYPE(AppCapture, app_capture, APP, CAPTURE, GObject)
+
+/**
+ * app_capture_new:
+ *
+ * Returns: (transfer full): a new #AppCapture.
+ * Binds hyprland_toplevel_export_manager_v1, zwlr_foreign_toplevel_manager_v1,
+ * and hyprland_toplevel_mapping_manager_v1, then does three roundtrips to
+ * enumerate all live windows and map their addresses.
+ */
+AppCapture *app_capture_new(void);
+
+/**
+ * app_capture_capture_by_handle:
+ * @self: an #AppCapture
+ * @address: (transfer none): the Hyprland window address as a string.
+ * Accepts both "0x564f60266bd0" and "564f60266bd0" (with or without 0x).
+ *
+ * Captures one frame of the window. Every call eventually emits exactly one of:
+ * - #AppCapture::frame-ready (GBytes, gint, gint, gint) on success
+ * - #AppCapture::frame-failed (gchararray reason) on failure
+ *
+ * If the wlr handle for @address has not yet been announced/mapped (race
+ * between Hyprland IPC and the wlr foreign-toplevel protocol), the request
+ * is queued internally for up to ~1.5 s waiting for the mapping to arrive.
+ *
+ * Failure reasons emitted via frame-failed:
+ * "no_export_manager" — wayland init never bound the export protocol
+ * "no_handle_timeout" — wlr handle never appeared for this address
+ * "frame_failed" — compositor refused the export (unmapped, hidden, …)
+ * "buffer_invalid" — buffer event reported zero dimensions
+ * "alloc_failed" — memfd_create / ftruncate / mmap failed
+ * "internal" — defensive fallback (should never fire)
+ */
+void app_capture_capture_by_handle(AppCapture *self, const gchar *address);
+
+G_END_DECLS
diff --git a/src/window/switcher/app-capture/meson.build b/src/window/switcher/app-capture/meson.build
new file mode 100644
index 0000000..19f9fd0
--- /dev/null
+++ b/src/window/switcher/app-capture/meson.build
@@ -0,0 +1,110 @@
+project('app-capture', 'c',
+ version: '1.0',
+ default_options: ['c_std=c11', 'warning_level=1']
+)
+
+# ─── Dependencies ─────────────────────────────────────────────────────────────
+wayland_client = dependency('wayland-client')
+wayland_scanner_dep = dependency('wayland-scanner', native: true)
+gobject = dependency('gobject-2.0')
+gtk3 = dependency('gtk+-3.0')
+gir = dependency('gobject-introspection-1.0', required: true)
+
+wayland_scanner = find_program('wayland-scanner')
+
+# ─── Protocol XML files ───────────────────────────────────────────────────────
+# All must exist under protocols/ in the source tree:
+#
+# hyprland-toplevel-export-v1.xml (hyprwm/hyprland-protocols)
+# wlr-foreign-toplevel-management-unstable-v1.xml (swaywm/wlr-protocols)
+# hyprland-toplevel-mapping-v1.xml (hyprwm/hyprland-protocols)
+# ext-foreign-toplevel-list-v1.xml (wayland-protocols staging)
+#
+hypr_export_xml = files('protocols/hyprland-toplevel-export-v1.xml')
+wlr_toplevel_xml = files('protocols/wlr-foreign-toplevel-management-unstable-v1.xml')
+hypr_mapping_xml = files('protocols/hyprland-toplevel-mapping-v1.xml')
+ext_toplevel_xml = files('protocols/ext-foreign-toplevel-list-v1.xml')
+
+# ─── Generate C bindings ──────────────────────────────────────────────────────
+
+# hyprland-toplevel-export: the capture protocol we call directly
+hypr_export_client_h = custom_target('hypr-export-client-h',
+ input: hypr_export_xml, output: 'hyprland-toplevel-export-v1.h',
+ command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'],
+)
+hypr_export_private_c = custom_target('hypr-export-private-c',
+ input: hypr_export_xml, output: 'hyprland-toplevel-export-v1.c',
+ command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'],
+)
+
+# wlr-foreign-toplevel: used to enumerate live windows and pass handles to the
+# export protocol's v2 request; also provides zwlr_foreign_toplevel_handle_v1_interface
+# which hyprland-toplevel-export-v1.c references at link time.
+wlr_toplevel_client_h = custom_target('wlr-toplevel-client-h',
+ input: wlr_toplevel_xml, output: 'wlr-foreign-toplevel-management-unstable-v1.h',
+ command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'],
+)
+wlr_toplevel_private_c = custom_target('wlr-toplevel-private-c',
+ input: wlr_toplevel_xml, output: 'wlr-foreign-toplevel-management-unstable-v1.c',
+ command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'],
+)
+
+# hyprland-toplevel-mapping: maps wlr handles → full 64-bit Hyprland addresses.
+# Its generated .c references ext_foreign_toplevel_handle_v1_interface (below).
+hypr_mapping_client_h = custom_target('hypr-mapping-client-h',
+ input: hypr_mapping_xml, output: 'hyprland-toplevel-mapping-v1.h',
+ command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'],
+)
+hypr_mapping_private_c = custom_target('hypr-mapping-private-c',
+ input: hypr_mapping_xml, output: 'hyprland-toplevel-mapping-v1.c',
+ command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'],
+)
+
+# ext-foreign-toplevel-list: we never call it directly, but its private-code
+# provides ext_foreign_toplevel_handle_v1_interface which the mapping protocol's
+# generated .c references at link time.
+ext_toplevel_private_c = custom_target('ext-toplevel-private-c',
+ input: ext_toplevel_xml, output: 'ext-foreign-toplevel-list-v1.c',
+ command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'],
+)
+
+# ─── Shared library ───────────────────────────────────────────────────────────
+lib = shared_library('appcapture',
+ sources: [
+ 'app-capture.c',
+ hypr_export_private_c,
+ hypr_export_client_h,
+ wlr_toplevel_private_c,
+ wlr_toplevel_client_h,
+ hypr_mapping_private_c,
+ hypr_mapping_client_h,
+ ext_toplevel_private_c, # link-time symbol only, no header needed in our code
+ ],
+ include_directories: [
+ include_directories('.'),
+ ],
+ dependencies: [
+ wayland_client,
+ gobject,
+ gtk3,
+ ],
+ gnu_symbol_visibility: 'default',
+ install: true,
+)
+
+# ─── GObject Introspection ────────────────────────────────────────────────────
+gir_file = files('AppCapture-1.0.gir')
+
+install_data(gir_file,
+ install_dir: get_option('datadir') / 'gir-1.0',
+)
+
+gir_compiler = find_program('g-ir-compiler')
+
+custom_target('appcapture-typelib',
+ input: gir_file,
+ output: 'AppCapture-1.0.typelib',
+ command: [gir_compiler, '--output=@OUTPUT@', '@INPUT@'],
+ install: true,
+ install_dir: get_option('libdir') / 'girepository-1.0',
+)
diff --git a/src/window/switcher/app-capture/protocols/ext-foreign-toplevel-list-v1.xml b/src/window/switcher/app-capture/protocols/ext-foreign-toplevel-list-v1.xml
new file mode 100644
index 0000000..f1bbe0e
--- /dev/null
+++ b/src/window/switcher/app-capture/protocols/ext-foreign-toplevel-list-v1.xml
@@ -0,0 +1,219 @@
+
+
+
+ Copyright © 2018 Ilia Bozhinov
+ Copyright © 2020 Isaac Freund
+ Copyright © 2022 wb9688
+ Copyright © 2023 i509VCB
+
+ Permission to use, copy, modify, distribute, and sell this
+ software and its documentation for any purpose is hereby granted
+ without fee, provided that the above copyright notice appear in
+ all copies and that both that copyright notice and this permission
+ notice appear in supporting documentation, and that the name of
+ the copyright holders not be used in advertising or publicity
+ pertaining to distribution of the software without specific,
+ written prior permission. The copyright holders make no
+ representations about the suitability of this software for any
+ purpose. It is provided "as is" without express or implied
+ warranty.
+
+ THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ THIS SOFTWARE.
+
+
+
+ The purpose of this protocol is to provide protocol object handles for
+ toplevels, possibly originating from another client.
+
+ This protocol is intentionally minimalistic and expects additional
+ functionality (e.g. creating a screencopy source from a toplevel handle,
+ getting information about the state of the toplevel) to be implemented
+ in extension protocols.
+
+ The compositor may choose to restrict this protocol to a special client
+ launched by the compositor itself or expose it to all clients,
+ this is compositor policy.
+
+ The key words "must", "must not", "required", "shall", "shall not",
+ "should", "should not", "recommended", "may", and "optional" in this
+ document are to be interpreted as described in IETF RFC 2119.
+
+ Warning! The protocol described in this file is currently in the testing
+ phase. Backward compatible changes may be added together with the
+ corresponding interface version bump. Backward incompatible changes can
+ only be done by creating a new major version of the extension.
+
+
+
+
+ A toplevel is defined as a surface with a role similar to xdg_toplevel.
+ XWayland surfaces may be treated like toplevels in this protocol.
+
+ After a client binds the ext_foreign_toplevel_list_v1, each mapped
+ toplevel window will be sent using the ext_foreign_toplevel_list_v1.toplevel
+ event.
+
+ Clients which only care about the current state can perform a roundtrip after
+ binding this global.
+
+ For each instance of ext_foreign_toplevel_list_v1, the compositor must
+ create a new ext_foreign_toplevel_handle_v1 object for each mapped toplevel.
+
+ If a compositor implementation sends the ext_foreign_toplevel_list_v1.finished
+ event after the global is bound, the compositor must not send any
+ ext_foreign_toplevel_list_v1.toplevel events.
+
+
+
+
+ This event is emitted whenever a new toplevel window is created. It is
+ emitted for all toplevels, regardless of the app that has created them.
+
+ All initial properties of the toplevel (identifier, title, app_id) will be sent
+ immediately after this event using the corresponding events for
+ ext_foreign_toplevel_handle_v1. The compositor will use the
+ ext_foreign_toplevel_handle_v1.done event to indicate when all data has
+ been sent.
+
+
+
+
+
+
+ This event indicates that the compositor is done sending events
+ to this object. The client should destroy the object.
+ See ext_foreign_toplevel_list_v1.destroy for more information.
+
+ The compositor must not send any more toplevel events after this event.
+
+
+
+
+
+ This request indicates that the client no longer wishes to receive
+ events for new toplevels.
+
+ The Wayland protocol is asynchronous, meaning the compositor may send
+ further toplevel events until the stop request is processed.
+ The client should wait for a ext_foreign_toplevel_list_v1.finished
+ event before destroying this object.
+
+
+
+
+
+ This request should be called either when the client will no longer
+ use the ext_foreign_toplevel_list_v1 or after the finished event
+ has been received to allow destruction of the object.
+
+ If a client wishes to destroy this object it should send a
+ ext_foreign_toplevel_list_v1.stop request and wait for a ext_foreign_toplevel_list_v1.finished
+ event, then destroy the handles and then this object.
+
+
+
+
+
+
+ A ext_foreign_toplevel_handle_v1 object represents a mapped toplevel
+ window. A single app may have multiple mapped toplevels.
+
+
+
+
+ This request should be used when the client will no longer use the handle
+ or after the closed event has been received to allow destruction of the
+ object.
+
+ When a handle is destroyed, a new handle may not be created by the server
+ until the toplevel is unmapped and then remapped. Destroying a toplevel handle
+ is not recommended unless the client is cleaning up child objects
+ before destroying the ext_foreign_toplevel_list_v1 object, the toplevel
+ was closed or the toplevel handle will not be used in the future.
+
+ Other protocols which extend the ext_foreign_toplevel_handle_v1
+ interface should require destructors for extension interfaces be
+ called before allowing the toplevel handle to be destroyed.
+
+
+
+
+
+ The server will emit no further events on the ext_foreign_toplevel_handle_v1
+ after this event. Any requests received aside from the destroy request must
+ be ignored. Upon receiving this event, the client should destroy the handle.
+
+ Other protocols which extend the ext_foreign_toplevel_handle_v1
+ interface must also ignore requests other than destructors.
+
+
+
+
+
+ This event is sent after all changes in the toplevel state have
+ been sent.
+
+ This allows changes to the ext_foreign_toplevel_handle_v1 properties
+ to be atomically applied. Other protocols which extend the
+ ext_foreign_toplevel_handle_v1 interface may use this event to also
+ atomically apply any pending state.
+
+ This event must not be sent after the ext_foreign_toplevel_handle_v1.closed
+ event.
+
+
+
+
+
+ The title of the toplevel has changed.
+
+ The configured state must not be applied immediately. See
+ ext_foreign_toplevel_handle_v1.done for details.
+
+
+
+
+
+
+ The app id of the toplevel has changed.
+
+ The configured state must not be applied immediately. See
+ ext_foreign_toplevel_handle_v1.done for details.
+
+
+
+
+
+
+ This identifier is used to check if two or more toplevel handles belong
+ to the same toplevel.
+
+ The identifier is useful for command line tools or privileged clients
+ which may need to reference an exact toplevel across processes or
+ instances of the ext_foreign_toplevel_list_v1 global.
+
+ The compositor must only send this event when the handle is created.
+
+ The identifier must be unique per toplevel and its handles. Two different
+ toplevels must not have the same identifier. The identifier is only valid
+ as long as the toplevel is mapped. If the toplevel is unmapped the identifier
+ must not be reused. An identifier must not be reused by the compositor to
+ ensure there are no races when sharing identifiers between processes.
+
+ An identifier is a string that contains up to 32 printable ASCII bytes.
+ An identifier must not be an empty string. It is recommended that a
+ compositor includes an opaque generation value in identifiers. How the
+ generation value is used when generating the identifier is implementation
+ dependent.
+
+
+
+
+
diff --git a/src/window/switcher/app-capture/protocols/ext-image-capture-source-v1.xml b/src/window/switcher/app-capture/protocols/ext-image-capture-source-v1.xml
new file mode 100644
index 0000000..b31a5da
--- /dev/null
+++ b/src/window/switcher/app-capture/protocols/ext-image-capture-source-v1.xml
@@ -0,0 +1,109 @@
+
+
+
+ Copyright © 2022 Andri Yngvason
+ Copyright © 2024 Simon Ser
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice (including the next
+ paragraph) shall be included in all copies or substantial portions of the
+ Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+
+
+ This protocol serves as an intermediary between capturing protocols and
+ potential image capture sources such as outputs and toplevels.
+
+ This protocol may be extended to support more image capture sources in the
+ future, thereby adding those image capture sources to other protocols that
+ use the image capture source object without having to modify those
+ protocols.
+
+ Warning! The protocol described in this file is currently in the testing
+ phase. Backward compatible changes may be added together with the
+ corresponding interface version bump. Backward incompatible changes can
+ only be done by creating a new major version of the extension.
+
+
+
+
+ The image capture source object is an opaque descriptor for a capturable
+ resource. This resource may be any sort of entity from which an image
+ may be derived.
+
+ Note, because ext_image_capture_source_v1 objects are created from multiple
+ independent factory interfaces, the ext_image_capture_source_v1 interface is
+ frozen at version 1.
+
+
+
+
+ Destroys the image capture source. This request may be sent at any time
+ by the client.
+
+
+
+
+
+
+ A manager for creating image capture source objects for wl_output objects.
+
+
+
+
+ Creates a source object for an output. Images captured from this source
+ will show the same content as the output. Some elements may be omitted,
+ such as cursors and overlays that have been marked as transparent to
+ capturing.
+
+
+
+
+
+
+
+ Destroys the manager. This request may be sent at any time by the client
+ and objects created by the manager will remain valid after its
+ destruction.
+
+
+
+
+
+
+ A manager for creating image capture source objects for
+ ext_foreign_toplevel_handle_v1 objects.
+
+
+
+
+ Creates a source object for a foreign toplevel handle. Images captured
+ from this source will show the same content as the toplevel.
+
+
+
+
+
+
+
+ Destroys the manager. This request may be sent at any time by the client
+ and objects created by the manager will remain valid after its
+ destruction.
+
+
+
+
diff --git a/src/window/switcher/app-capture/protocols/ext-image-copy-capture-v1.xml b/src/window/switcher/app-capture/protocols/ext-image-copy-capture-v1.xml
new file mode 100644
index 0000000..1ac4369
--- /dev/null
+++ b/src/window/switcher/app-capture/protocols/ext-image-copy-capture-v1.xml
@@ -0,0 +1,456 @@
+
+
+
+ Copyright © 2021-2023 Andri Yngvason
+ Copyright © 2024 Simon Ser
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice (including the next
+ paragraph) shall be included in all copies or substantial portions of the
+ Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+
+
+ This protocol allows clients to ask the compositor to capture image sources
+ such as outputs and toplevels into user submitted buffers.
+
+ Warning! The protocol described in this file is currently in the testing
+ phase. Backward compatible changes may be added together with the
+ corresponding interface version bump. Backward incompatible changes can
+ only be done by creating a new major version of the extension.
+
+
+
+
+ This object is a manager which offers requests to start capturing from a
+ source.
+
+
+
+
+
+
+
+
+
+
+
+
+ Create a capturing session for an image capture source.
+
+ If the paint_cursors option is set, cursors shall be composited onto
+ the captured frame. The cursor must not be composited onto the frame
+ if this flag is not set.
+
+ If the options bitfield is invalid, the invalid_option protocol error
+ is sent.
+
+
+
+
+
+
+
+
+ Create a cursor capturing session for the pointer of an image capture
+ source.
+
+
+
+
+
+
+
+
+ Destroy the manager object.
+
+ Other objects created via this interface are unaffected.
+
+
+
+
+
+
+ This object represents an active image copy capture session.
+
+ After a capture session is created, buffer constraint events will be
+ emitted from the compositor to tell the client which buffer types and
+ formats are supported for reading from the session. The compositor may
+ re-send buffer constraint events whenever they change.
+
+ To advertise buffer constraints, the compositor must send in no
+ particular order: zero or more shm_format and dmabuf_format events, zero
+ or one dmabuf_device event, and exactly one buffer_size event. Then the
+ compositor must send a done event.
+
+ When the client has received all the buffer constraints, it can create a
+ buffer accordingly, attach it to the capture session using the
+ attach_buffer request, set the buffer damage using the damage_buffer
+ request and then send the capture request.
+
+
+
+
+
+
+
+
+ Provides the dimensions of the source image in buffer pixel coordinates.
+
+ The client must attach buffers that match this size.
+
+
+
+
+
+
+
+ Provides the format that must be used for shared-memory buffers.
+
+ This event may be emitted multiple times, in which case the client may
+ choose any given format.
+
+
+
+
+
+
+ This event advertises the device buffers must be allocated on for
+ dma-buf buffers.
+
+ In general the device is a DRM node. The DRM node type (primary vs.
+ render) is unspecified. Clients must not rely on the compositor sending
+ a particular node type. Clients cannot check two devices for equality
+ by comparing the dev_t value.
+
+
+
+
+
+
+ Provides the format that must be used for dma-buf buffers.
+
+ The client may choose any of the modifiers advertised in the array of
+ 64-bit unsigned integers.
+
+ This event may be emitted multiple times, in which case the client may
+ choose any given format.
+
+
+
+
+
+
+
+ This event is sent once when all buffer constraint events have been
+ sent.
+
+ The compositor must always end a batch of buffer constraint events with
+ this event, regardless of whether it sends the initial constraints or
+ an update.
+
+
+
+
+
+ This event indicates that the capture session has stopped and is no
+ longer available. This can happen in a number of cases, e.g. when the
+ underlying source is destroyed, if the user decides to end the image
+ capture, or if an unrecoverable runtime error has occurred.
+
+ The client should destroy the session after receiving this event.
+
+
+
+
+
+ Create a capture frame for this session.
+
+ At most one frame object can exist for a given session at any time. If
+ a client sends a create_frame request before a previous frame object
+ has been destroyed, the duplicate_frame protocol error is raised.
+
+
+
+
+
+
+ Destroys the session. This request can be sent at any time by the
+ client.
+
+ This request doesn't affect ext_image_copy_capture_frame_v1 objects created by
+ this object.
+
+
+
+
+
+
+ This object represents an image capture frame.
+
+ The client should attach a buffer, damage the buffer, and then send a
+ capture request.
+
+ If the capture is successful, the compositor must send the frame metadata
+ (transform, damage, presentation_time in any order) followed by the ready
+ event.
+
+ If the capture fails, the compositor must send the failed event.
+
+
+
+
+
+
+
+
+
+
+ Destroys the frame. This request can be sent at any time by the
+ client.
+
+
+
+
+
+ Attach a buffer to the session.
+
+ The wl_buffer.release request is unused.
+
+ The new buffer replaces any previously attached buffer.
+
+ This request must not be sent after capture, or else the
+ already_captured protocol error is raised.
+
+
+
+
+
+
+ Apply damage to the buffer which is to be captured next. This request
+ may be sent multiple times to describe a region.
+
+ The client indicates the accumulated damage since this wl_buffer was
+ last captured. During capture, the compositor will update the buffer
+ with at least the union of the region passed by the client and the
+ region advertised by ext_image_copy_capture_frame_v1.damage.
+
+ When a wl_buffer is captured for the first time, or when the client
+ doesn't track damage, the client must damage the whole buffer.
+
+ This is for optimisation purposes. The compositor may use this
+ information to reduce copying.
+
+ These coordinates originate from the upper left corner of the buffer.
+
+ If x or y are strictly negative, or if width or height are negative or
+ zero, the invalid_buffer_damage protocol error is raised.
+
+ This request must not be sent after capture, or else the
+ already_captured protocol error is raised.
+
+
+
+
+
+
+
+
+
+ Capture a frame.
+
+ Unless this is the first successful captured frame performed in this
+ session, the compositor may wait an indefinite amount of time for the
+ source content to change before performing the copy.
+
+ This request may only be sent once, or else the already_captured
+ protocol error is raised. A buffer must be attached before this request
+ is sent, or else the no_buffer protocol error is raised.
+
+
+
+
+
+ This event is sent before the ready event and holds the transform that
+ the compositor has applied to the buffer contents.
+
+
+
+
+
+
+ This event is sent before the ready event. It may be generated multiple
+ times to describe a region.
+
+ The first captured frame in a session will always carry full damage.
+ Subsequent frames' damaged regions describe which parts of the buffer
+ have changed since the last ready event.
+
+ These coordinates originate in the upper left corner of the buffer.
+
+
+
+
+
+
+
+
+
+ This event indicates the time at which the frame is presented to the
+ output in system monotonic time. This event is sent before the ready
+ event.
+
+ The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
+ each component being an unsigned 32-bit value. Whole seconds are in
+ tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
+ and the additional fractional part in tv_nsec as nanoseconds. Hence,
+ for valid timestamps tv_nsec must be in [0, 999999999].
+
+
+
+
+
+
+
+
+ Called as soon as the frame is copied, indicating it is available
+ for reading.
+
+ The buffer may be re-used by the client after this event.
+
+ After receiving this event, the client must destroy the object.
+
+
+
+
+
+
+ An unspecified runtime error has occurred. The client may retry.
+
+
+
+
+ The buffer submitted by the client doesn't match the latest session
+ constraints. The client should re-allocate its buffers and retry.
+
+
+
+
+ The session has stopped. See ext_image_copy_capture_session_v1.stopped.
+
+
+
+
+
+
+ This event indicates that the attempted frame copy has failed.
+
+ After receiving this event, the client must destroy the object.
+
+
+
+
+
+
+
+ This object represents a cursor capture session. It extends the base
+ capture session with cursor-specific metadata.
+
+
+
+
+
+
+
+
+ Destroys the session. This request can be sent at any time by the
+ client.
+
+ This request doesn't affect ext_image_copy_capture_frame_v1 objects created by
+ this object.
+
+
+
+
+
+ Gets the image copy capture session for this cursor session.
+
+ The session will produce frames of the cursor image. The compositor may
+ pause the session when the cursor leaves the captured area.
+
+ This request must not be sent more than once, or else the
+ duplicate_session protocol error is raised.
+
+
+
+
+
+
+ Sent when a cursor enters the captured area. It shall be generated
+ before the "position" and "hotspot" events when and only when a cursor
+ enters the area.
+
+ The cursor enters the captured area when the cursor image intersects
+ with the captured area. Note, this is different from e.g.
+ wl_pointer.enter.
+
+
+
+
+
+ Sent when a cursor leaves the captured area. No "position" or "hotspot"
+ event is generated for the cursor until the cursor enters the captured
+ area again.
+
+
+
+
+
+ Cursors outside the image capture source do not get captured and no
+ event will be generated for them.
+
+ The given position is the position of the cursor's hotspot and it is
+ relative to the main buffer's top left corner in transformed buffer
+ pixel coordinates. The coordinates may be negative or greater than the
+ main buffer size.
+
+
+
+
+
+
+
+ The hotspot describes the offset between the cursor image and the
+ position of the input device.
+
+ The given coordinates are the hotspot's offset from the origin in
+ buffer coordinates.
+
+ Clients should not apply the hotspot immediately: the hotspot becomes
+ effective when the next ext_image_copy_capture_frame_v1.ready event is received.
+
+ Compositors may delay this event until the client captures a new frame.
+
+
+
+
+
+
diff --git a/src/window/switcher/app-capture/protocols/hyprland-toplevel-export-v1.xml b/src/window/switcher/app-capture/protocols/hyprland-toplevel-export-v1.xml
new file mode 100644
index 0000000..b1185aa
--- /dev/null
+++ b/src/window/switcher/app-capture/protocols/hyprland-toplevel-export-v1.xml
@@ -0,0 +1,228 @@
+
+
+
+ Copyright © 2022 Vaxry
+ 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 ask for exporting another toplevel's
+ surface(s) to a buffer.
+
+ Particularly useful for sharing a single window.
+
+
+
+
+ This object is a manager which offers requests to start capturing from a
+ source.
+
+
+
+
+ Capture the next frame of a toplevel. (window)
+
+ The captured frame will not contain any server-side decorations and will
+ ignore the compositor-set geometry, like e.g. rounded corners.
+
+ It will contain all the subsurfaces and popups, however the latter will be clipped
+ to the geometry of the base surface.
+
+ The handle parameter refers to the address of the window as seen in `hyprctl clients`.
+ For example, for d161e7b0 it would be 3512854448.
+
+
+
+
+
+
+
+
+ All objects created by the manager will still remain valid, until their
+ appropriate destroy request has been called.
+
+
+
+
+
+
+ Same as capture_toplevel, but with a zwlr_foreign_toplevel_handle_v1 handle.
+
+
+
+
+
+
+
+
+
+
+ This object represents a single frame.
+
+ When created, a series of buffer events will be sent, each representing a
+ supported buffer type. The "buffer_done" event is sent afterwards to
+ indicate that all supported buffer types have been enumerated. The client
+ will then be able to send a "copy" request. If the capture is successful,
+ the compositor will send a "flags" followed by a "ready" event.
+
+ wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent.
+
+ If the capture failed, the "failed" event is sent. This can happen anytime
+ before the "ready" event.
+
+ Once either a "ready" or a "failed" event is received, the client should
+ destroy the frame.
+
+
+
+
+ Provides information about wl_shm buffer parameters that need to be
+ used for this frame. This event is sent once after the frame is created
+ if wl_shm buffers are supported.
+
+
+
+
+
+
+
+
+
+ Copy the frame to the supplied buffer. The buffer must have the
+ correct size, see hyprland_toplevel_export_frame_v1.buffer and
+ hyprland_toplevel_export_frame_v1.linux_dmabuf. The buffer needs to have a
+ supported format.
+
+ If the frame is successfully copied, a "flags" and a "ready" event is
+ sent. Otherwise, a "failed" event is sent.
+
+ This event will wait for appropriate damage to be copied, unless the ignore_damage
+ arg is set to a non-zero value.
+
+
+
+
+
+
+
+ This event is sent right before the ready event when ignore_damage was
+ not set. It may be generated multiple times for each copy
+ request.
+
+ The arguments describe a box around an area that has changed since the
+ last copy request that was derived from the current screencopy manager
+ instance.
+
+ The union of all regions received between the call to copy
+ and a ready event is the total damage since the prior ready event.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Provides flags about the frame. This event is sent once before the
+ "ready" event.
+
+
+
+
+
+
+ Called as soon as the frame is copied, indicating it is available
+ for reading. This event includes the time at which presentation happened
+ at.
+
+ The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
+ each component being an unsigned 32-bit value. Whole seconds are in
+ tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
+ and the additional fractional part in tv_nsec as nanoseconds. Hence,
+ for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
+ may have an arbitrary offset at start.
+
+ After receiving this event, the client should destroy the object.
+
+
+
+
+
+
+
+
+ This event indicates that the attempted frame copy has failed.
+
+ After receiving this event, the client should destroy the object.
+
+
+
+
+
+ Destroys the frame. This request can be sent at any time by the client.
+
+
+
+
+
+ Provides information about linux-dmabuf buffer parameters that need to
+ be used for this frame. This event is sent once after the frame is
+ created if linux-dmabuf buffers are supported.
+
+
+
+
+
+
+
+
+ This event is sent once after all buffer events have been sent.
+
+ The client should proceed to create a buffer of one of the supported
+ types, and send a "copy" request.
+
+
+
+
diff --git a/src/window/switcher/app-capture/protocols/hyprland-toplevel-mapping-v1.xml b/src/window/switcher/app-capture/protocols/hyprland-toplevel-mapping-v1.xml
new file mode 100644
index 0000000..a8da876
--- /dev/null
+++ b/src/window/switcher/app-capture/protocols/hyprland-toplevel-mapping-v1.xml
@@ -0,0 +1,124 @@
+
+
+
+ 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.
+
+
+
+
+ Get the window address for a toplevel.
+
+
+
+
+
+
+
+ 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.
+
+
+
+
\ No newline at end of file
diff --git a/src/window/switcher/app-capture/protocols/wlr-foreign-toplevel-management-unstable-v1.xml b/src/window/switcher/app-capture/protocols/wlr-foreign-toplevel-management-unstable-v1.xml
new file mode 100644
index 0000000..44505bb
--- /dev/null
+++ b/src/window/switcher/app-capture/protocols/wlr-foreign-toplevel-management-unstable-v1.xml
@@ -0,0 +1,270 @@
+
+
+
+ Copyright © 2018 Ilia Bozhinov
+
+ Permission to use, copy, modify, distribute, and sell this
+ software and its documentation for any purpose is hereby granted
+ without fee, provided that the above copyright notice appear in
+ all copies and that both that copyright notice and this permission
+ notice appear in supporting documentation, and that the name of
+ the copyright holders not be used in advertising or publicity
+ pertaining to distribution of the software without specific,
+ written prior permission. The copyright holders make no
+ representations about the suitability of this software for any
+ purpose. It is provided "as is" without express or implied
+ warranty.
+
+ THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ THIS SOFTWARE.
+
+
+
+
+ The purpose of this protocol is to enable the creation of taskbars
+ and docks by providing them with a list of opened applications and
+ letting them request certain actions on them, like maximizing, etc.
+
+ After a client binds the zwlr_foreign_toplevel_manager_v1, each opened
+ toplevel window will be sent via the toplevel event
+
+
+
+
+ This event is emitted whenever a new toplevel window is created. It
+ is emitted for all toplevels, regardless of the app that has created
+ them.
+
+ All initial details of the toplevel(title, app_id, states, etc.) will
+ be sent immediately after this event via the corresponding events in
+ zwlr_foreign_toplevel_handle_v1.
+
+
+
+
+
+
+ Indicates the client no longer wishes to receive events for new toplevels.
+ However the compositor may emit further toplevel_created events, until
+ the finished event is emitted.
+
+ The client must not send any more requests after this one.
+
+
+
+
+
+ This event indicates that the compositor is done sending events to the
+ zwlr_foreign_toplevel_manager_v1. The server will destroy the object
+ immediately after sending this request, so it will become invalid and
+ the client should free any resources associated with it.
+
+
+
+
+
+
+ A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel
+ window. Each app may have multiple opened toplevels.
+
+ Each toplevel has a list of outputs it is visible on, conveyed to the
+ client with the output_enter and output_leave events.
+
+
+
+
+ This event is emitted whenever the title of the toplevel changes.
+
+
+
+
+
+
+ This event is emitted whenever the app-id of the toplevel changes.
+
+
+
+
+
+
+ This event is emitted whenever the toplevel becomes visible on
+ the given output. A toplevel may be visible on multiple outputs.
+
+
+
+
+
+
+ This event is emitted whenever the toplevel stops being visible on
+ the given output. It is guaranteed that an entered-output event
+ with the same output has been emitted before this event.
+
+
+
+
+
+
+ Requests that the toplevel be maximized. If the maximized state actually
+ changes, this will be indicated by the state event.
+
+
+
+
+
+ Requests that the toplevel be unmaximized. If the maximized state actually
+ changes, this will be indicated by the state event.
+
+
+
+
+
+ Requests that the toplevel be minimized. If the minimized state actually
+ changes, this will be indicated by the state event.
+
+
+
+
+
+ Requests that the toplevel be unminimized. If the minimized state actually
+ changes, this will be indicated by the state event.
+
+
+
+
+
+ Request that this toplevel be activated on the given seat.
+ There is no guarantee the toplevel will be actually activated.
+
+
+
+
+
+
+ The different states that a toplevel can have. These have the same meaning
+ as the states with the same names defined in xdg-toplevel
+
+
+
+
+
+
+
+
+
+
+ This event is emitted immediately after the zlw_foreign_toplevel_handle_v1
+ is created and each time the toplevel state changes, either because of a
+ compositor action or because of a request in this protocol.
+
+
+
+
+
+
+
+ This event is sent after all changes in the toplevel state have been
+ sent.
+
+ This allows changes to the zwlr_foreign_toplevel_handle_v1 properties
+ to be seen as atomic, even if they happen via multiple events.
+
+
+
+
+
+ Send a request to the toplevel to close itself. The compositor would
+ typically use a shell-specific method to carry out this request, for
+ example by sending the xdg_toplevel.close event. However, this gives
+ no guarantees the toplevel will actually be destroyed. If and when
+ this happens, the zwlr_foreign_toplevel_handle_v1.closed event will
+ be emitted.
+
+
+
+
+
+ The rectangle of the surface specified in this request corresponds to
+ the place where the app using this protocol represents the given toplevel.
+ It can be used by the compositor as a hint for some operations, e.g
+ minimizing. The client is however not required to set this, in which
+ case the compositor is free to decide some default value.
+
+ If the client specifies more than one rectangle, only the last one is
+ considered.
+
+ The dimensions are given in surface-local coordinates.
+ Setting width=height=0 removes the already-set rectangle.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This event means the toplevel has been destroyed. It is guaranteed there
+ won't be any more events for this zwlr_foreign_toplevel_handle_v1. The
+ toplevel itself becomes inert so any requests will be ignored except the
+ destroy request.
+
+
+
+
+
+ Destroys the zwlr_foreign_toplevel_handle_v1 object.
+
+ This request should be called either when the client does not want to
+ use the toplevel anymore or after the closed event to finalize the
+ destruction of the object.
+
+
+
+
+
+
+
+ Requests that the toplevel be fullscreened on the given output. If the
+ fullscreen state and/or the outputs the toplevel is visible on actually
+ change, this will be indicated by the state and output_enter/leave
+ events.
+
+ The output parameter is only a hint to the compositor. Also, if output
+ is NULL, the compositor should decide which output the toplevel will be
+ fullscreened on, if at all.
+
+
+
+
+
+
+ Requests that the toplevel be unfullscreened. If the fullscreen state
+ actually changes, this will be indicated by the state event.
+
+
+
+
+
+
+
+ This event is emitted whenever the parent of the toplevel changes.
+
+ No event is emitted when the parent handle is destroyed by the client.
+
+
+
+
+