roundabout,
created on Wednesday, 30 July 2025, 10:44:05 (1753872245),
received on Saturday, 9 August 2025, 12:22:37 (1754742157)
Author identity: vlad <vlad.muntoiu@gmail.com>
fe2463cd6a125e404dd79bc0337e91a4c603b6d6
applets/wf-window-list/__init__.py
@@ -1,8 +1,9 @@
import dataclasses
import os
import sys
from pathlib import Path
from pywayland.client import Display
from pywayland.protocol.wayland import WlRegistry
from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor
from 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, Gdk
from 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_id
self.window_id: ZwlrForeignToplevelHandleV1 = window_id
self.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 = False
self.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_global
self.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 = None
self.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_info
if 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, WlCompositor
os.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)