diff --git a/src/window/switcher/app-capture/AppCapture-1.0.gir b/src/window/switcher/app-capture/AppCapture-1.0.gir
index c7f6277..40bbd45 100644
--- a/src/window/switcher/app-capture/AppCapture-1.0.gir
+++ b/src/window/switcher/app-capture/AppCapture-1.0.gir
@@ -39,6 +39,15 @@
+
+
+
+
+
+
+
+
+
@@ -50,6 +59,9 @@
+
+
+
@@ -70,6 +82,9 @@
+
+
+
diff --git a/src/window/switcher/app-capture/README.md b/src/window/switcher/app-capture/README.md
index 8b13789..d146ffc 100644
--- a/src/window/switcher/app-capture/README.md
+++ b/src/window/switcher/app-capture/README.md
@@ -1 +1,69 @@
+# AppCapture
+`AppCapture` is a high-performance, C-based Wayland library with GObject Introspection (GIR) bindings. It allows you to efficiently capture real-time window previews (thumbnails) on Hyprland directly from Python.
+
+## How It Works
+
+This module is designed to interact directly with the Wayland compositor using several Wayland protocols:
+
+- `wlr-foreign-toplevel-management-unstable-v1`
+- `hyprland-toplevel-mapping-v1`
+- `hyprland-toplevel-export-v1`
+
+### Concurrency and Damage Tracking
+
+Instead of relying on inefficient Python polling loops, `AppCapture` is fully asynchronous and event-driven.
+
+- **Concurrent Captures**: It allocates an isolated `FrameContext` for every capture request. This means you can safely fire dozens of concurrent capture requests for different windows without memory corruption or blocking the main thread.
+- **Zero-CPU Damage Tracking**: When you request a frame, you can set `wait_for_damage = True`. `AppCapture` leverages Wayland's native damage tracking (`ignore_damage = 0`). The C-backend will pause the request and **only** wake up when the window actually repaints. This eliminates CPU polling overhead and allows 0% CPU consumption for static windows.
+- **Native Scaling**: The captured SHM buffer is scaled directly in C using `Cairo`. The library takes a bounding box (`max_width`, `max_height`) and intelligently applies a uniform "contain" scale to fit the box perfectly while preserving the window's aspect ratio.
+
+## How to Build
+
+The project uses the **Meson** build system and generates a shared library (`libappcapture.so`) along with a GObject Introspection Typelib (`AppCapture-1.0.typelib`).
+
+To build or recompile the module after making changes to the C code:
+
+```bash
+cd src/window/switcher/app-capture
+meson setup builddir # (Only needed the first time)
+meson compile -C builddir
+```
+
+## How to Use (Python)
+
+Once compiled, you can easily load the library into any Python script using PyGObject (`gi`). Ensure that the `builddir` is in your `GI_TYPELIB_PATH` and `LD_LIBRARY_PATH`.
+
+### Minimal Example
+
+```python
+import gi
+gi.require_version("AppCapture", "1.0")
+from gi.repository import AppCapture
+
+def on_frame_ready(capture, address, data, width, height, stride):
+ # 'data' is a GLib.Bytes object containing the raw ARGB32 pixel data
+ # Convert it to a Cairo Surface or GdkPixbuf
+ print(f"Frame received for {address}! Size: {width}x{height}")
+
+ # Example: Request the next frame, but wait for damage (0% CPU idle)
+ capture.capture_by_handle(address, 300, 168, True)
+
+def on_frame_failed(capture, address, reason):
+ print(f"Failed to capture {address}: {reason}")
+
+# 1. Initialize the capture module
+capture = AppCapture.Capture()
+capture.connect("frame-ready", on_frame_ready)
+capture.connect("frame-failed", on_frame_failed)
+
+# 2. Request a frame
+window_address = "0x12345678"
+max_width = 300
+max_height = 168
+
+# capture_by_handle(address, max_width, max_height, wait_for_damage)
+# False: Capture instantly (ideal for initial preview)
+# True: Suspend and wait for the window to visually update
+capture.capture_by_handle(window_address, max_width, max_height, False)
+```
diff --git a/src/window/switcher/app-capture/app-capture.c b/src/window/switcher/app-capture/app-capture.c
index 4f84e7f..d1a7bc0 100644
--- a/src/window/switcher/app-capture/app-capture.c
+++ b/src/window/switcher/app-capture/app-capture.c
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include "hyprland-toplevel-export-v1.h"
#include "wlr-foreign-toplevel-management-unstable-v1.h"
@@ -25,10 +26,6 @@ enum {
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
* =========================================================================
@@ -45,16 +42,6 @@ typedef struct {
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
* ========================================================================= */
@@ -73,9 +60,6 @@ struct _AppCapture {
/* 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;
@@ -84,6 +68,8 @@ struct _AppCapture {
uint32_t stride;
uint32_t format;
size_t shm_size;
+ gint target_width;
+ gint target_height;
};
G_DEFINE_TYPE(AppCapture, app_capture, G_TYPE_OBJECT)
@@ -92,16 +78,13 @@ 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 emit_frame_failed(AppCapture *self, const char *address, const char *reason);
static void do_capture(AppCapture *self,
- struct zwlr_foreign_toplevel_handle_v1 *wlr_handle);
+ struct zwlr_foreign_toplevel_handle_v1 *wlr_handle,
+ const char *address, gboolean wait_for_damage);
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
@@ -119,15 +102,11 @@ static void mapping_handle_window_address(void *data,
{
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);
}
@@ -137,7 +116,7 @@ static void mapping_handle_failed(void *data,
{
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. */
+ * Python will retry and eventually give up. */
hyprland_toplevel_window_mapping_handle_v1_destroy(handle);
g_free(ctx);
}
@@ -295,8 +274,37 @@ static const struct wl_registry_listener registry_listener = {
};
/* =========================================================================
- * Frame listener
- *
+ * Per-frame Context (enables concurrent/overlapping captures safely)
+ * ========================================================================= */
+typedef struct {
+ AppCapture *self;
+ char *address;
+ int shm_fd;
+ unsigned char *pixel_data;
+ size_t shm_size;
+ uint32_t width;
+ uint32_t height;
+ uint32_t stride;
+ uint32_t format;
+ gint target_width;
+ gint target_height;
+ gboolean wait_for_damage;
+} FrameContext;
+
+static void frame_context_free(FrameContext *ctx) {
+ if (ctx->pixel_data && ctx->pixel_data != MAP_FAILED) {
+ munmap(ctx->pixel_data, ctx->shm_size);
+ }
+ if (ctx->shm_fd >= 0) {
+ close(ctx->shm_fd);
+ }
+ g_free(ctx->address);
+ g_free(ctx);
+}
+
+/* =========================================================================
+ * Frame Listener Callbacks
+ * =========================================================================
* Event order: buffer → (linux_dmabuf) → buffer_done → [copy()] → flags → ready|failed
* ========================================================================= */
@@ -305,11 +313,11 @@ static void frame_handle_buffer(void *data,
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;
+ FrameContext *ctx = data;
+ ctx->format = format;
+ ctx->width = width;
+ ctx->height = height;
+ ctx->stride = stride;
}
static void frame_handle_linux_dmabuf(void *data,
@@ -320,53 +328,55 @@ static void frame_handle_linux_dmabuf(void *data,
static void frame_handle_buffer_done(void *data,
struct hyprland_toplevel_export_frame_v1 *frame)
{
- AppCapture *self = APP_CAPTURE(data);
+ FrameContext *ctx = data;
+ AppCapture *self = ctx->self;
- if (self->width == 0 || self->height == 0 || self->stride == 0) {
+ if (ctx->width == 0 || ctx->height == 0 || ctx->stride == 0) {
g_warning("AppCapture: buffer_done with zero dimensions");
hyprland_toplevel_export_frame_v1_destroy(frame);
- emit_frame_failed(self, "buffer_invalid");
+ emit_frame_failed(self, ctx->address, "buffer_invalid");
+ frame_context_free(ctx);
return;
}
- self->shm_size = (size_t)self->stride * self->height;
-
- self->shm_fd = memfd_create("app-capture-buffer", 0);
- if (self->shm_fd < 0) {
+ ctx->shm_size = (size_t)ctx->stride * ctx->height;
+ ctx->shm_fd = memfd_create("app-capture-buffer", 0);
+ if (ctx->shm_fd < 0) {
g_warning("AppCapture: memfd_create failed");
hyprland_toplevel_export_frame_v1_destroy(frame);
- emit_frame_failed(self, "alloc_failed");
+ emit_frame_failed(self, ctx->address, "alloc_failed");
+ frame_context_free(ctx);
return;
}
- if (ftruncate(self->shm_fd, (off_t)self->shm_size) < 0) {
+ if (ftruncate(ctx->shm_fd, (off_t)ctx->shm_size) < 0) {
g_warning("AppCapture: ftruncate failed");
- cleanup_shm(self);
hyprland_toplevel_export_frame_v1_destroy(frame);
- emit_frame_failed(self, "alloc_failed");
+ emit_frame_failed(self, ctx->address, "alloc_failed");
+ frame_context_free(ctx);
return;
}
- self->pixel_data = mmap(NULL, self->shm_size,
+ ctx->pixel_data = mmap(NULL, ctx->shm_size,
PROT_READ | PROT_WRITE, MAP_SHARED,
- self->shm_fd, 0);
- if (self->pixel_data == MAP_FAILED) {
+ ctx->shm_fd, 0);
+ if (ctx->pixel_data == MAP_FAILED) {
g_warning("AppCapture: mmap failed");
- self->pixel_data = NULL;
- cleanup_shm(self);
+ ctx->pixel_data = NULL;
hyprland_toplevel_export_frame_v1_destroy(frame);
- emit_frame_failed(self, "alloc_failed");
+ emit_frame_failed(self, ctx->address, "alloc_failed");
+ frame_context_free(ctx);
return;
}
- struct wl_shm_pool *pool = wl_shm_create_pool(self->shm, self->shm_fd,
- (int32_t)self->shm_size);
+ struct wl_shm_pool *pool = wl_shm_create_pool(self->shm, ctx->shm_fd,
+ (int32_t)ctx->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);
+ 0, (int32_t)ctx->width, (int32_t)ctx->height,
+ (int32_t)ctx->stride, ctx->format);
wl_shm_pool_destroy(pool);
- hyprland_toplevel_export_frame_v1_copy(frame, buffer, 1);
+ hyprland_toplevel_export_frame_v1_copy(frame, buffer, ctx->wait_for_damage ? 0 : 1);
wl_buffer_destroy(buffer);
}
@@ -379,32 +389,70 @@ static void frame_handle_ready(void *data,
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);
+ FrameContext *ctx = data;
+ AppCapture *self = ctx->self;
- if (!self->pixel_data) {
+ if (!ctx->pixel_data) {
g_warning("AppCapture: ready fired but pixel_data is NULL");
hyprland_toplevel_export_frame_v1_destroy(frame);
- emit_frame_failed(self, "internal");
+ emit_frame_failed(self, ctx->address, "internal");
+ frame_context_free(ctx);
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);
+ cairo_surface_t *src_surface = cairo_image_surface_create_for_data(
+ (unsigned char *)ctx->pixel_data, CAIRO_FORMAT_ARGB32, ctx->width, ctx->height, ctx->stride);
+
+ int req_width = ctx->target_width > 0 ? ctx->target_width : 300;
+ int req_height = ctx->target_height > 0 ? ctx->target_height : 168;
+
+ double scale_x = (double)req_width / (double)ctx->width;
+ double scale_y = (double)req_height / (double)ctx->height;
+ double scale = (scale_x < scale_y) ? scale_x : scale_y;
+
+ int target_width = (int)(ctx->width * scale);
+ int target_height = (int)(ctx->height * scale);
+
+ if (target_width <= 0) target_width = 1;
+ if (target_height <= 0) target_height = 1;
+
+ cairo_surface_t *dst_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, target_width, target_height);
+ cairo_t *cr = cairo_create(dst_surface);
+
+ cairo_scale(cr, (double)target_width / ctx->width, (double)target_height / ctx->height);
+ cairo_set_source_surface(cr, src_surface, 0, 0);
+ cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_GOOD);
+
+ cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
+ cairo_paint(cr);
+ cairo_destroy(cr);
+
+ cairo_surface_flush(dst_surface);
+ unsigned char *dst_data = cairo_image_surface_get_data(dst_surface);
+ int dst_stride = cairo_image_surface_get_stride(dst_surface);
+ int dst_size = dst_stride * target_height;
+
+ GBytes *bytes = g_bytes_new(dst_data, dst_size);
+ g_signal_emit(self, signals[SIGNAL_FRAME_READY], 0,
+ ctx->address, bytes, target_width, target_height, dst_stride);
+
+ g_bytes_unref(bytes);
+ cairo_surface_destroy(dst_surface);
+ cairo_surface_destroy(src_surface);
- cleanup_shm(self);
hyprland_toplevel_export_frame_v1_destroy(frame);
+ frame_context_free(ctx);
}
static void frame_handle_failed(void *data,
struct hyprland_toplevel_export_frame_v1 *frame)
{
- AppCapture *self = APP_CAPTURE(data);
+ FrameContext *ctx = data;
+ AppCapture *self = ctx->self;
g_warning("AppCapture: frame capture failed");
- cleanup_shm(self);
hyprland_toplevel_export_frame_v1_destroy(frame);
- emit_frame_failed(self, "frame_failed");
+ emit_frame_failed(self, ctx->address, "frame_failed");
+ frame_context_free(ctx);
}
static void frame_handle_damage(void *data,
@@ -426,9 +474,9 @@ static const struct hyprland_toplevel_export_frame_v1_listener frame_listener =
* Capture helpers
* ========================================================================= */
-static void emit_frame_failed(AppCapture *self, const char *reason)
+static void emit_frame_failed(AppCapture *self, const char *address, const char *reason)
{
- g_signal_emit(self, signals[SIGNAL_FRAME_FAILED], 0, reason);
+ g_signal_emit(self, signals[SIGNAL_FRAME_FAILED], 0, address, reason);
}
static struct zwlr_foreign_toplevel_handle_v1 *find_wlr_handle(AppCapture *self,
@@ -444,9 +492,16 @@ static struct zwlr_foreign_toplevel_handle_v1 *find_wlr_handle(AppCapture *self,
}
static void do_capture(AppCapture *self,
- struct zwlr_foreign_toplevel_handle_v1 *wlr_handle)
+ struct zwlr_foreign_toplevel_handle_v1 *wlr_handle,
+ const char *address, gboolean wait_for_damage)
{
- self->width = self->height = self->stride = self->format = 0;
+ FrameContext *ctx = g_new0(FrameContext, 1);
+ ctx->self = self;
+ ctx->address = g_strdup(address);
+ ctx->shm_fd = -1;
+ ctx->target_width = self->target_width;
+ ctx->target_height = self->target_height;
+ ctx->wait_for_damage = wait_for_damage;
struct hyprland_toplevel_export_frame_v1 *frame =
hyprland_toplevel_export_manager_v1_capture_toplevel_with_wlr_toplevel_handle(
@@ -455,89 +510,14 @@ static void do_capture(AppCapture *self,
wlr_handle
);
- hyprland_toplevel_export_frame_v1_add_listener(frame, &frame_listener, self);
+ hyprland_toplevel_export_frame_v1_add_listener(frame, &frame_listener, ctx);
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;
@@ -553,8 +533,6 @@ static void toplevel_entry_free(gpointer data)
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);
@@ -579,8 +557,8 @@ static void app_capture_class_init(AppCaptureClass *klass)
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
+ G_TYPE_NONE, 5,
+ G_TYPE_STRING, G_TYPE_BYTES, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT
);
signals[SIGNAL_FRAME_FAILED] = g_signal_new(
@@ -588,8 +566,8 @@ static void app_capture_class_init(AppCaptureClass *klass)
G_TYPE_FROM_CLASS(klass),
G_SIGNAL_RUN_LAST,
0, NULL, NULL, NULL,
- G_TYPE_NONE, 1,
- G_TYPE_STRING
+ G_TYPE_NONE, 2,
+ G_TYPE_STRING, G_TYPE_STRING
);
}
@@ -599,7 +577,6 @@ static void app_capture_init(AppCapture *self)
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);
@@ -636,17 +613,20 @@ AppCapture *app_capture_new(void)
return g_object_new(APP_TYPE_CAPTURE, NULL);
}
-void app_capture_capture_by_handle(AppCapture *self, const gchar *address)
+void app_capture_capture_by_handle(AppCapture *self, const gchar *address, gint target_width, gint target_height, gboolean wait_for_damage)
{
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");
+ emit_frame_failed(self, address, "no_export_manager");
return;
}
+ self->target_width = target_width;
+ self->target_height = target_height;
+
/* 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"))
@@ -656,17 +636,12 @@ void app_capture_capture_by_handle(AppCapture *self, const gchar *address)
struct zwlr_foreign_toplevel_handle_v1 *wlr_handle =
find_wlr_handle(self, addr);
if (wlr_handle) {
- do_capture(self, wlr_handle);
+ do_capture(self, wlr_handle, addr, wait_for_damage);
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);
+ /* Since Python has a retry loop (invoke_repeater fallback on failure),
+ we don't need a complex C-side pending queue. We just fail immediately
+ and let Python retry 1 second later. */
+ emit_frame_failed(self, addr, "not_mapped_yet");
}
diff --git a/src/window/switcher/app-capture/app-capture.h b/src/window/switcher/app-capture/app-capture.h
index f0991a6..4273949 100644
--- a/src/window/switcher/app-capture/app-capture.h
+++ b/src/window/switcher/app-capture/app-capture.h
@@ -38,6 +38,6 @@ AppCapture *app_capture_new(void);
* "alloc_failed" — memfd_create / ftruncate / mmap failed
* "internal" — defensive fallback (should never fire)
*/
-void app_capture_capture_by_handle(AppCapture *self, const gchar *address);
+void app_capture_capture_by_handle(AppCapture *self, const gchar *address, gint target_width, gint target_height, gboolean wait_for_damage);
G_END_DECLS
diff --git a/src/window/switcher/app-capture/meson.build b/src/window/switcher/app-capture/meson.build
index 19f9fd0..7702535 100644
--- a/src/window/switcher/app-capture/meson.build
+++ b/src/window/switcher/app-capture/meson.build
@@ -3,16 +3,17 @@ project('app-capture', 'c',
default_options: ['c_std=c11', 'warning_level=1']
)
-# ─── Dependencies ─────────────────────────────────────────────────────────────
+# 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)
+cairo_dep = dependency('cairo')
wayland_scanner = find_program('wayland-scanner')
-# ─── Protocol XML files ───────────────────────────────────────────────────────
+# Protocol XML files
# All must exist under protocols/ in the source tree:
#
# hyprland-toplevel-export-v1.xml (hyprwm/hyprland-protocols)
@@ -25,7 +26,7 @@ wlr_toplevel_xml = files('protocols/wlr-foreign-toplevel-management-unstable-v1.
hypr_mapping_xml = files('protocols/hyprland-toplevel-mapping-v1.xml')
ext_toplevel_xml = files('protocols/ext-foreign-toplevel-list-v1.xml')
-# ─── Generate C bindings ──────────────────────────────────────────────────────
+# Generate C bindings
# hyprland-toplevel-export: the capture protocol we call directly
hypr_export_client_h = custom_target('hypr-export-client-h',
@@ -68,7 +69,7 @@ ext_toplevel_private_c = custom_target('ext-toplevel-private-c',
command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'],
)
-# ─── Shared library ───────────────────────────────────────────────────────────
+# Shared library
lib = shared_library('appcapture',
sources: [
'app-capture.c',
@@ -87,12 +88,13 @@ lib = shared_library('appcapture',
wayland_client,
gobject,
gtk3,
+ cairo_dep,
],
gnu_symbol_visibility: 'default',
install: true,
)
-# ─── GObject Introspection ────────────────────────────────────────────────────
+# GObject Introspection
gir_file = files('AppCapture-1.0.gir')
install_data(gir_file,