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. + + + + +