roundabout,
created on Wednesday, 30 July 2025, 10:44:05 (1753872245),
received on Wednesday, 30 July 2025, 10:44:11 (1753872251)
Author identity: vlad <vlad.muntoiu@gmail.com>
fe2463cd6a125e404dd79bc0337e91a4c603b6d6
applets/wf-window-list/__init__.py
@@ -1,8 +1,9 @@
import dataclasses import os import sysfrom pathlib import Path from pywayland.client import Display from pywayland.protocol.wayland import WlRegistryfrom pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositorfrom pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1 import ( ZwlrForeignToplevelManagerV1, ZwlrForeignToplevelHandleV1
@@ -12,8 +13,19 @@ import panorama_panel
import gi gi.require_version("Gtk", "4.0") gi.require_version("GdkWayland", "4.0")from gi.repository import Gtk, GLib, Gio, Gdkfrom gi.repository import Gtk, GLib, Gtk4LayerShell, Gio, Gdk import ctypes from cffi import FFI ffi = FFI() ffi.cdef(""" void * gdk_wayland_display_get_wl_display (void * display); void * gdk_wayland_surface_get_wl_surface (void * surface); """) gtk = ffi.dlopen("libgtk-4.so.1")module_directory = Path(__file__).resolve().parent
@@ -25,11 +37,26 @@ def split_bytes_into_ints(array: bytes, size: int = 4) -> list[int]:
values: list[int] = [] for i in range(0, len(array), size): values.append(int.from_bytes(array[i : i+size], byteorder="little"))values.append(int.from_bytes(array[i : i+size], byteorder=sys.byteorder))return values def get_widget_rect(widget: Gtk.Widget) -> tuple[int, int, int, int]: width = int(widget.get_width()) height = int(widget.get_height()) toplevel = widget.get_root() if not toplevel: return None, None, width, height x, y = widget.translate_coordinates(toplevel, 0, 0) x = int(x) y = int(y) return x, y, width, height @dataclasses.dataclass class WindowState: minimised: bool
@@ -59,7 +86,7 @@ class WindowButton(Gtk.ToggleButton):
def __init__(self, window_id, window_title, **kwargs): super().__init__(**kwargs) self.window_id = window_idself.window_id: ZwlrForeignToplevelHandleV1 = window_idself.set_has_frame(False) self.label = Gtk.Label() self.icon = Gtk.Image.new_from_icon_name("application-x-executable")
@@ -69,8 +96,7 @@ class WindowButton(Gtk.ToggleButton):
self.set_child(box) self.window_title = window_title self.last_state = Falseself.window_state = WindowState(False, False, False, False)@property def window_title(self):
@@ -116,13 +142,11 @@ class WFWindowList(panorama_panel.Applet):
# selected when no window is focused self.initial_button = Gtk.ToggleButton() self.display = Display()self.display.connect()self.registry = self.display.get_registry()self.registry.dispatcher["global"] = self.on_globalself.display.roundtrip()fd = self.display.get_fd()GLib.io_add_watch(fd, GLib.IO_IN, self.on_display_event)self.display = None self.wl_surface_ptr = None self.registry = None self.compositor = None self.seat = Noneself.context_menu = self.make_context_menu() panorama_panel.track_popover(self.context_menu)
@@ -138,9 +162,27 @@ class WFWindowList(panorama_panel.Applet):
options_action.connect("activate", self.show_options) action_group.add_action(options_action) self.insert_action_group("applet", action_group) self.connect("realize", lambda *args: self.get_wl_resources())self.options_window = None def get_wl_resources(self): ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,) self.display = Display() wl_display_ptr = gtk.gdk_wayland_display_get_wl_display( ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(self.get_root().get_native().get_display().__gpointer__, None))) self.display._ptr = wl_display_ptr self.display.connect() self.registry = self.display.get_registry() self.registry.dispatcher["global"] = self.on_global self.display.roundtrip() fd = self.display.get_fd() GLib.io_add_watch(fd, GLib.IO_IN, self.on_display_event) def on_display_event(self, source, condition): if condition == GLib.IO_IN: self.display.dispatch(block=True)
@@ -154,6 +196,14 @@ class WFWindowList(panorama_panel.Applet):
self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished") self.display.roundtrip() self.display.flush() elif interface == "wl_seat": self.print_log("Seat found") self.seat = registry.bind(name, WlSeat, version) elif interface == "wl_compositor": self.compositor = registry.bind(name, WlCompositor, version) self.wl_surface_ptr = gtk.gdk_wayland_surface_get_wl_surface( ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer( self.get_root().get_native().get_surface().__gpointer__, None)))def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1, handle: ZwlrForeignToplevelHandleV1):
@@ -166,17 +216,31 @@ class WFWindowList(panorama_panel.Applet):
if handle not in self.toplevel_buttons: button = WindowButton(handle, title) button.set_group(self.initial_button) button.connect("clicked", lambda *a: self.on_button_click(handle))button.connect("clicked", self.on_button_click)self.toplevel_buttons[handle] = button self.append(button) else: button = self.toplevel_buttons[handle] button.window_title = title def on_button_click(self, button: WindowButton): if button.window_state.focused: # Already pressed in, so minimise the focused window # Set a rectangle for animation # TODO: provide surface here button.window_id.set_rectangle(self.surface, *get_widget_rect(button)) button.window_id.set_minimized() else: button.window_id.unset_minimized() button.window_id.activate(self.seat) self.display.flush() def on_state_changed(self, handle, states): if handle in self.toplevel_buttons: state_info = WindowState.from_state_array(states) button = self.toplevel_buttons[handle] button.window_state = state_infoif state_info.focused: button.set_active(True) else:
main.py
@@ -9,6 +9,8 @@ import typing
from itertools import accumulate, chain from pathlib import Path import ruamel.yaml as yaml from pywayland.client import Display from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositoros.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0"
@@ -554,7 +556,6 @@ class PanoramaPanel(Gtk.Application):
panel = Panel(self, monitor, position, size, monitor_index, autohide, hide_time) self.panels.append(panel) panel.show()print(f"{size}px panel on {position} edge of monitor {monitor_index}, autohide is {autohide} ({hide_time}ms)")
@@ -572,6 +573,9 @@ class PanoramaPanel(Gtk.Application):
area.append(applet_widget) panel.present() panel.realize() def do_activate(self): Gio.Application.do_activate(self)