From 0ffd6205335c014140044f89ccbcdaa6d533bce2 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 12 Jul 2026 04:46:51 +0545 Subject: [PATCH] feat(src/window/switcher): add live preview toggle and refactor capture loop with invoke_repeater --- src/window/switcher/main.py | 91 +++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/src/window/switcher/main.py b/src/window/switcher/main.py index 1682509..c4aaeb7 100644 --- a/src/window/switcher/main.py +++ b/src/window/switcher/main.py @@ -1,23 +1,17 @@ import ctypes import os -import gi +from fabric.utils import get_relative_path -_so_path = os.path.join( - os.path.dirname(__file__), - "app-capture", - "builddir", - "libappcapture.so", -) -_so_path = os.path.normpath(_so_path) +_so_path = get_relative_path("app-capture/builddir/libappcapture.so") if os.path.isfile(_so_path): ctypes.CDLL(_so_path) -gi.require_version("AppCapture", "1.0") +# gi.require_version("AppCapture", "1.0") from collections import OrderedDict import cairo -from fabric.utils import Gdk, GdkPixbuf, GLib, logger +from fabric.utils import Gdk, GdkPixbuf, invoke_repeater, logger from fabric.widgets.box import Box from fabric.widgets.eventbox import EventBox from fabric.widgets.image import Image @@ -147,7 +141,7 @@ class ApplicationSwitcher(Window): self.show() self.ungrab_keyboard() self.grab_keyboard() - GLib.timeout_add(16, self._fade_in) + invoke_repeater(16, self._fade_in, initial_call=False) def _fade_in(self): current = self.get_opacity() @@ -163,7 +157,7 @@ class ApplicationSwitcher(Window): if self._pending_close is not None: close_window(self._pending_close) self._pending_close = None - GLib.timeout_add(150, self.hide) + invoke_repeater(150, self.hide, initial_call=False) self.ungrab_keyboard() def _get_pixbuf(self, class_name: str) -> GdkPixbuf.Pixbuf | None: @@ -193,7 +187,9 @@ class ApplicationSwitcher(Window): name="app-switcher-item-label", h_align="start", v_align="center", + ellipsize="end", ) + name_label.set_max_width_chars(25) header_box = Box( name="app-switcher-header-box", @@ -211,7 +207,6 @@ class ApplicationSwitcher(Window): h_align="center", v_align="center", ) - preview_box.set_size_request(300, 168) item_box = Box( name="app-switcher-item", @@ -221,6 +216,7 @@ class ApplicationSwitcher(Window): spacing=8, children=[header_box, preview_box], ) + item_box.set_size_request(300, 210) app_icon_ref = app_icon preview_image_ref = preview_image name_label_ref = name_label @@ -241,7 +237,9 @@ class ApplicationSwitcher(Window): label="", name="app-switcher-item-label", h_align="center", + ellipsize="end", ) + name_label.set_max_width_chars(25) item_box = Box( name="app-switcher-item", orientation="v", @@ -250,6 +248,7 @@ class ApplicationSwitcher(Window): spacing=2, children=[icon_box, name_label], ) + item_box.set_size_request(300, 210) app_icon_ref = image preview_image_ref = None name_label_ref = name_label @@ -558,10 +557,10 @@ class ApplicationSwitcher(Window): event_box.remove_style_class("hovered") return False - def _on_frame_ready(self, _capture, data, width, height, stride): + def _on_frame_ready(self, _capture, addr, data, width, height, stride): try: - addr = self._current_capture_addr - idx = self._addr_to_idx.get(addr) if addr else None + full_addr = addr if addr.startswith("0x") else f"0x{addr}" + idx = self._addr_to_idx.get(full_addr) if idx is not None and idx < len(self._items): raw = bytearray(data.get_data()) surface = cairo.ImageSurface.create_for_data( @@ -570,33 +569,55 @@ class ApplicationSwitcher(Window): pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0, width, height) item = self._items[idx] if item.preview_image is not None and pixbuf: - aspect = width / height - new_width = 300 - new_height = int(new_width / aspect) if aspect > 0 else 180 - scaled = pixbuf.scale_simple( - new_width, new_height, GdkPixbuf.InterpType.BILINEAR + item.preview_image.set_from_pixbuf(pixbuf) + + if self.get_visible() and config().get("switcher_live_preview", True): + fps_delay = config().get("switcher_live_preview_delay_ms", 200) + invoke_repeater( + fps_delay, + lambda: ( + self._capture.capture_by_handle(full_addr, 300, 168, True) + if self.get_visible() + else False + ), + initial_call=False, ) - item.preview_image.set_from_pixbuf(scaled) except Exception: logger.exception("_on_frame_ready error") - finally: - self._current_capture_addr = None - self._pump_capture_queue() - def _on_frame_failed(self, _capture, reason): - logger.debug(f"AppCapture frame-failed: {reason}") - self._current_capture_addr = None - self._pump_capture_queue() + def _on_frame_failed(self, _capture, addr, reason): + full_addr = addr if addr.startswith("0x") else f"0x{addr}" + logger.debug(f"AppCapture frame-failed for {full_addr}: {reason}") + + # Retry failed captures after a delay, in case the window was temporarily minimized or mapping + if self.get_visible() and config().get("switcher_live_preview", True): + invoke_repeater( + 1000, + lambda: ( + self._capture.capture_by_handle(full_addr, 300, 168, False) + if self.get_visible() + else False + ), + initial_call=False, + ) def _pump_capture_queue(self): - if self._current_capture_addr is not None: + use_preview = config().get("switcher_live_preview", True) + if not use_preview: return - while self._capture_queue: - addr = self._capture_queue.pop(0) + + # wait_for_damage = False forces an immediate capture so thumbnails populate instantly. + for w in self.windows: + addr = w["address"] if addr in self._addr_to_idx: - self._current_capture_addr = addr - self._capture.capture_by_handle(addr) - return + self._capture.capture_by_handle(addr, 300, 168, False) + + def _refill_and_pump(self): + # Called when the switcher becomes visible or windows change + if not self.get_visible(): + return False + self._pump_capture_queue() + return False def grab_keyboard(self): try: