soreau,
created on Tuesday, 2 September 2025, 17:31:54 (1756834314),
received on Saturday, 6 September 2025, 21:54:28 (1757195668)
Author identity: Scott Moreau <oreaus@gmail.com>
c140e138cdeb816d02a41245749463879c98237a
applets/wf-window-list/__init__.py
@@ -26,7 +26,7 @@ import typing
from pathlib import Path
from pywayland.utils import AnonymousFile
from pywayland.client import Display, EventQueue
from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor, WlOutput, WlBuffer, WlShm, WlShmPool
from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor, WlOutput, WlBuffer, WlShmPool
from pywayland.protocol.wayland.wl_output import WlOutputProxy
from pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1 import (
ZwlrForeignToplevelManagerV1,
@@ -36,7 +36,7 @@ from pywayland.protocol.wlr_screencopy_unstable_v1 import (
ZwlrScreencopyManagerV1,
ZwlrScreencopyFrameV1
)
from OpenGL import GL
import panorama_panel
import gi
@@ -421,12 +421,15 @@ class WFWindowList(panorama_panel.Applet):
# selected when no window is focused
self.initial_button = Gtk.ToggleButton()
self.display = None
self.my_output = None
self.wl_surface_ptr = None
self.registry = None
self.wl_surface = None
self.compositor = None
self.seat = None
self.output = None
self.wl_shm = None
#self.manager = None
self.screencopy_manager = None
self.live_preview_output_name = None
self.context_menu = self.make_context_menu()
panorama_panel.track_popover(self.context_menu)
@@ -563,62 +566,23 @@ class WFWindowList(panorama_panel.Applet):
self.remove(button)
def get_wl_resources(self, widget):
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.event_queue = EventQueue(self.display)
# Intentionally commented: the display is already connected by GTK
# self.display.connect()
my_monitor = Gtk4LayerShell.get_monitor(self.get_root())
# Iterate through monitors and get their Wayland output (wl_output)
# This is a hack to ensure output_enter/leave is called for toplevels
for monitor in self.get_root().get_application().monitors:
wl_output = gtk.gdk_wayland_monitor_get_wl_output(ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(monitor.__gpointer__, None)))
if wl_output and not monitor.output_proxy:
print("Create proxy")
output_proxy = WlOutputProxy(wl_output, self.display)
output_proxy.interface.registry[output_proxy._ptr] = output_proxy
monitor.output_proxy = output_proxy
if monitor == my_monitor:
self.my_output = monitor.output_proxy
# End hack
if self.wl_shm:
return
self.my_output = None
#self.wl_surface = self.get_root().get_application().wl_surface
self.compositor = self.get_root().get_application().compositor
self.seat = self.get_root().get_application().seat
self.output = None
self.wl_shm = None
self.manager = None
self.screencopy_manager = None
self.wl_shm = self.get_root().get_application().wl_shm
#self.manager = self.get_root().get_application().manager
self.screencopy_manager = self.get_root().get_application().screencopy_manager
self.live_preview_output_name = None
self.registry = self.display.get_registry()
self.registry.dispatcher["global"] = self.on_global
self.display.roundtrip()
if self.manager is None:
print("Could not load wf-window-list. Is foreign-toplevel protocol advertised by the compositor?")
print("(Wayfire requires enabling foreign-toplevel plugin)")
return
if self.screencopy_manager is None:
print("Protocol zwlr_screencopy_manager_v1 not found, live (minimized) window previews will not work!")
self.manager.dispatcher["toplevel"] = self.on_new_toplevel
self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished")
self.display.roundtrip()
fd = self.display.get_fd()
GLib.io_add_watch(fd, GLib.IO_IN, self.on_display_event)
if self.wf_socket is not None:
self.filter_to_wf_workspace()
def on_display_event(self, source, condition):
if condition & GLib.IO_IN:
self.display.dispatch(queue=self.event_queue)
return True
def set_live_preview_output(self, output):
self.output = output
@@ -631,48 +595,34 @@ class WFWindowList(panorama_panel.Applet):
def get_live_preview_output_name(self):
return self.live_preview_output_name
def wl_output_handle_name(self, output, name):
if name != self.get_live_preview_output_name():
output.release()
output.destroy()
return
self.set_live_preview_output(output)
def on_global(self, registry, name, interface, version):
if interface == "zwlr_foreign_toplevel_manager_v1":
self.print_log("Interface registered")
self.manager = registry.bind(name, ZwlrForeignToplevelManagerV1, version)
elif interface == "wl_output":
if self.live_preview_output_name is None:
return
wl_output = registry.bind(name, WlOutput, version)
wl_output.dispatcher["name"] = self.wl_output_handle_name
elif interface == "wl_seat":
self.print_log("Seat found")
self.seat = registry.bind(name, WlSeat, version)
elif interface == "wl_shm":
self.wl_shm = registry.bind(name, WlShm, version)
elif interface == "wl_compositor":
self.compositor = registry.bind(name, WlCompositor, version)
def wl_output_enter(self, output, name):
print("wl_output_enter")
if name == self.get_root().monitor_name and self.get_root().get_native().get_surface():
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)))
self.wl_surface = WlSurface()
self.wl_surface._ptr = wl_surface_ptr
elif interface == "zwlr_screencopy_manager_v1":
self.screencopy_manager = registry.bind(name, ZwlrScreencopyManagerV1, version)
def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1,
handle: ZwlrForeignToplevelHandleV1):
handle.dispatcher["title"] = self.on_title_changed
handle.dispatcher["app_id"] = self.on_app_id_changed
handle.dispatcher["output_enter"] = self.on_output_entered
handle.dispatcher["output_leave"] = self.on_output_left
handle.dispatcher["state"] = self.on_state_changed
handle.dispatcher["closed"] = self.on_closed
self.display.roundtrip()
def on_output_entered(self, handle, output):
self.my_output = output
toplevel_buttons = self.toplevel_buttons.copy()
for handle in toplevel_buttons:
self.foreign_toplevel_closed(handle)
if name != self.get_live_preview_output_name():
return
self.set_live_preview_output(output)
def wl_output_leave(self, output, name):
print("wl_output_leave")
if self.my_output == output:
self.my_output = None
toplevel_buttons = self.toplevel_buttons.copy()
for handle in toplevel_buttons:
self.foreign_toplevel_closed(handle)
if self.get_live_preview_output() == output:
self.set_live_preview_output(None)
def foreign_toplevel_output_enter(self, handle, output):
print("foreign_toplevel_output_enter")
button = WindowButton(handle, handle.title, self)
button.icon.set_pixel_size(self.icon_size)
button.output = None
@@ -690,7 +640,8 @@ class WFWindowList(panorama_panel.Applet):
self.append(button)
self.set_all_rectangles()
def on_output_left(self, handle, output):
def foreign_toplevel_output_leave(self, handle, output):
print("foreign_toplevel_output_leave")
if handle in self.toplevel_buttons:
button = self.toplevel_buttons[handle]
if button.get_parent() == self:
@@ -703,7 +654,7 @@ class WFWindowList(panorama_panel.Applet):
if not self.live_preview_tooltips:
button.set_tooltip_text(title)
def on_title_changed(self, handle, title):
def foreign_toplevel_title(self, handle, title):
handle.title = title
if handle in self.toplevel_buttons:
button = self.toplevel_buttons[handle]
@@ -728,7 +679,7 @@ class WFWindowList(panorama_panel.Applet):
else:
button.window_id.activate(self.seat)
def on_state_changed(self, handle, states):
def foreign_toplevel_state(self, handle, states):
if handle in self.toplevel_buttons:
state_info = WindowState.from_state_array(states)
button = self.toplevel_buttons[handle]
@@ -748,13 +699,13 @@ class WFWindowList(panorama_panel.Applet):
if app_id.startswith("wf-ipc-"):
self.toplevel_buttons_by_wf_id[int(app_id.removeprefix("wf-ipc-"))] = button
def on_app_id_changed(self, handle, app_id):
def foreign_toplevel_app_id(self, handle, app_id):
handle.app_id = app_id
if handle in self.toplevel_buttons:
button = self.toplevel_buttons[handle]
self.set_app_id(button, app_id)
def on_closed(self, handle):
def foreign_toplevel_closed(self, handle):
if handle not in self.toplevel_buttons:
return
button: WindowButton = self.toplevel_buttons[handle]
main.py
@@ -31,6 +31,16 @@ from itertools import accumulate, chain
from pathlib import Path
import ruamel.yaml as yaml
from pywayland.client import Display, EventQueue
from pywayland.protocol.wayland.wl_output import WlOutputProxy
from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor, WlOutput, WlShm
from pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1 import (
ZwlrForeignToplevelManagerV1,
ZwlrForeignToplevelHandleV1
)
from pywayland.protocol.wlr_screencopy_unstable_v1 import (
ZwlrScreencopyManagerV1,
ZwlrScreencopyFrameV1
)
faulthandler.enable()
@@ -64,7 +74,6 @@ locale.setlocale(locale.LC_ALL)
_ = locale.gettext
import panorama_panel
import wl4pgi
import asyncio
asyncio.set_event_loop_policy(GLibEventLoopPolicy())
@@ -353,7 +362,7 @@ POSITION_TO_LAYER_SHELL_EDGE = {
class Panel(Gtk.Window):
def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor, position: Gtk.PositionType = Gtk.PositionType.TOP, size: int = 40, autohide: bool = False, hide_time: int = 0, can_capture_keyboard: bool = False, monitor_index: int = 0):
def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor, position: Gtk.PositionType = Gtk.PositionType.TOP, size: int = 40, autohide: bool = False, hide_time: int = 0, can_capture_keyboard: bool = False):
super().__init__(application=application)
self.drop_motion_controller = None
self.motion_controller = None
@@ -543,6 +552,69 @@ class Panel(Gtk.Window):
self.motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out))
self.drop_motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out))
def wl_output_enter(self, output, name):
for area in (self.left_area, self.centre_area, self.right_area):
applet = area.get_first_child()
while applet:
applet.wl_output_enter(output, name)
applet = applet.get_next_sibling()
def wl_output_leave(self, output, name):
for area in (self.left_area, self.centre_area, self.right_area):
applet = area.get_first_child()
while applet:
applet.wl_output_leave(output, name)
applet = applet.get_next_sibling()
def foreign_toplevel_new(self, manager, toplevel):
for area in (self.left_area, self.centre_area, self.right_area):
applet = area.get_first_child()
while applet:
applet.foreign_toplevel_new(manager, toplevel)
applet = applet.get_next_sibling()
def foreign_toplevel_output_enter(self, toplevel, output):
for area in (self.left_area, self.centre_area, self.right_area):
applet = area.get_first_child()
while applet:
applet.foreign_toplevel_output_enter(toplevel, output)
applet = applet.get_next_sibling()
def foreign_toplevel_output_leave(self, toplevel, output):
for area in (self.left_area, self.centre_area, self.right_area):
applet = area.get_first_child()
while applet:
applet.foreign_toplevel_output_leave(toplevel, output)
applet = applet.get_next_sibling()
def foreign_toplevel_app_id(self, toplevel, app_id):
for area in (self.left_area, self.centre_area, self.right_area):
applet = area.get_first_child()
while applet:
applet.foreign_toplevel_app_id(toplevel, app_id)
applet = applet.get_next_sibling()
def foreign_toplevel_title(self, toplevel, title):
for area in (self.left_area, self.centre_area, self.right_area):
applet = area.get_first_child()
while applet:
applet.foreign_toplevel_title(toplevel, title)
applet = applet.get_next_sibling()
def foreign_toplevel_state(self, toplevel, state):
for area in (self.left_area, self.centre_area, self.right_area):
applet = area.get_first_child()
while applet:
applet.foreign_toplevel_state(toplevel, state)
applet = applet.get_next_sibling()
def foreign_toplevel_closed(self, toplevel):
for area in (self.left_area, self.centre_area, self.right_area):
applet = area.get_first_child()
while applet:
applet.foreign_toplevel_closed(toplevel)
applet = applet.get_next_sibling()
def get_all_subclasses(klass: type) -> list[type]:
subclasses = []
for subclass in klass.__subclasses__():
@@ -595,13 +667,20 @@ class PanoramaPanel(Gtk.Application):
application_id="com.roundabout_host.roundabout.PanoramaPanel",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
)
self.registry = None
self.display = Gdk.Display.get_default()
self.monitors = self.display.get_monitors()
for monitor in self.monitors:
monitor.output_proxy = None
self.applets_by_name: dict[str, panorama_panel.Applet] = {}
self.panels: list[Panel] = []
self.wl_globals = []
self.wl_display = None
self.compositor = None
self.seat = None
self.output = None
self.wl_shm = None
self.manager = None
self.screencopy_manager = None
self.manager_window = None
self.edit_mode = False
self.drags = {}
@@ -617,38 +696,117 @@ class PanoramaPanel(Gtk.Application):
_("NAME")
)
def receive_output_name(self, output, name: str):
def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1,
handle: ZwlrForeignToplevelHandleV1):
print("on_new_toplevel")
for panel in self.panels:
panel.foreign_toplevel_new(manager, handle)
handle.dispatcher["title"] = self.on_title_changed
handle.dispatcher["app_id"] = self.on_app_id_changed
handle.dispatcher["output_enter"] = self.on_output_enter
handle.dispatcher["output_leave"] = self.on_output_leave
handle.dispatcher["state"] = self.on_state_changed
handle.dispatcher["closed"] = self.on_closed
self.wl_display.roundtrip()
def on_output_leave(self, handle, output):
for panel in self.panels:
panel.foreign_toplevel_output_leave(handle, output)
def on_output_enter(self, handle, output):
for panel in self.panels:
panel.foreign_toplevel_output_enter(handle, output)
def on_title_changed(self, handle, title):
for panel in self.panels:
panel.foreign_toplevel_title(handle, title)
def on_app_id_changed(self, handle, app_id):
for panel in self.panels:
panel.foreign_toplevel_app_id(handle, app_id)
def on_state_changed(self, handle, state):
for panel in self.panels:
panel.foreign_toplevel_state(handle, state)
def on_closed(self, handle):
for panel in self.panels:
panel.foreign_toplevel_closed(handle)
def receive_output_name(self, output: WlOutput, name: str):
output.name = name
def on_global(self, registry, name, interface, version):
if interface == "wl_output":
output = registry.bind(name, wl4pgi.wayland.WlOutput.interface_type, version)
if interface == "zwlr_foreign_toplevel_manager_v1":
print("Interface registered")
self.foreign_toplevel_manager_id = name
self.foreign_toplevel_version = version
elif interface == "wl_output":
output = registry.bind(name, WlOutput, version)
output.id = name
self.wl_globals.append(output)
print(f"global {name} {interface} {version}")
output.global_name = name
output.connect("name", self.receive_output_name)
output.dispatcher["name"] = self.receive_output_name
output.name = None
while not output.name:
self.got_output_name = False
while not output.name or not self.foreign_toplevel_manager_id:
self.wl_display.dispatch(block=True)
if not output.name.startswith("live-preview"):
self.wl_output_ids.add(output.global_name)
print(output.name)
self.wl_output_ids.add(output.global_name)
if output.name == "DP-1":
self.generate_panels()
for panel in self.panels:
panel.wl_output_enter(output, output.name)
if output.name == panel.monitor_name:
if self.manager:
self.manager.destroy()
self.wl_globals.remove(self.manager)
self.manager = self.registry.bind(self.foreign_toplevel_manager_id, ZwlrForeignToplevelManagerV1, self.foreign_toplevel_version)
self.manager.id = self.foreign_toplevel_manager_id
self.wl_globals.append(self.manager)
self.manager.dispatcher["toplevel"] = self.on_new_toplevel
self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished")
self.wl_display.roundtrip()
self.got_output_name = True
self.panels_generated = True
print(f"Monitor {name} ({interface}) connected", file=sys.stderr)
elif interface == "wl_seat":
print("Seat found")
self.seat = registry.bind(name, WlSeat, version)
self.seat.id = name
self.wl_globals.append(self.seat)
elif interface == "wl_shm":
self.wl_shm = registry.bind(name, WlShm, version)
self.wl_shm.id = name
self.wl_globals.append(self.wl_shm)
elif interface == "wl_compositor":
self.compositor = registry.bind(name, WlCompositor, version)
self.compositor.id = name
self.wl_globals.append(self.compositor)
elif interface == "zwlr_screencopy_manager_v1":
self.screencopy_manager = registry.bind(name, ZwlrScreencopyManagerV1, version)
self.screencopy_manager.id = name
self.wl_globals.append(self.screencopy_manager)
def on_global_remove(self, registry, name):
print("global removed")
if name in self.wl_output_ids:
print(f"Monitor {name} disconnected", file=sys.stderr)
self.generate_panels()
#self.generate_panels()
self.panels_generated = False
for g in self.wl_globals:
if g.id == name:
for panel in self.panels:
panel.wl_output_leave(g, g.name)
break
self.wl_output_ids.discard(name)
for g in self.wl_globals:
if g.id == name:
g.destroy()
self.wl_globals.remove(g)
break
def generate_panels(self):
self.display = Gdk.Display.get_default()
self.monitors = self.display.get_monitors()
for monitor in self.monitors:
monitor.output_proxy = None
print("Generating panels...", file=sys.stderr)
self.exit_all_applets()
for panel in self.panels:
@@ -663,19 +821,21 @@ class PanoramaPanel(Gtk.Application):
yaml_file = yaml_loader.load(config_file)
for panel_data in yaml_file["panels"]:
position = PANEL_POSITIONS[panel_data["position"]]
monitor_index = panel_data["monitor"]
if monitor_index >= len(self.monitors):
my_monitor = None
for monitor in self.display.get_monitors():
if monitor.get_connector() == "DP-1":
my_monitor = monitor
if not my_monitor:
continue
monitor = self.monitors[monitor_index]
size = panel_data["size"]
autohide = panel_data["autohide"]
hide_time = panel_data["hide_time"]
can_capture_keyboard = panel_data["can_capture_keyboard"]
panel = Panel(self, monitor, position, size, autohide, hide_time, can_capture_keyboard, monitor_index)
panel = Panel(self, monitor, position, size, autohide, hide_time, can_capture_keyboard)
self.panels.append(panel)
print(f"{size}px panel on {position} edge of monitor {monitor_index}, autohide is {autohide} ({hide_time}ms)")
print(f"{size}px panel on {position}, autohide is {autohide} ({hide_time}ms)")
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
applet_list = panel_data["applets"].get(area_name)
@@ -692,8 +852,8 @@ class PanoramaPanel(Gtk.Application):
area.append(applet_widget)
panel.realize()
panel.present()
panel.realize()
def prepare(self):
self.release()
@@ -717,15 +877,32 @@ class PanoramaPanel(Gtk.Application):
self.applets_by_name[subclass.__name__] = subclass
self.wl_display = Display()
self.wl_display._ptr = gtk.gdk_wayland_display_get_wl_display(
wl_display_ptr = gtk.gdk_wayland_display_get_wl_display(
ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(self.display.__gpointer__, None)))
self.wl_display._ptr = wl_display_ptr
# Intentionally commented: the display is already connected by GTK
# self.display.connect()
self.registry = wl4pgi.wayland.WlRegistry(self.wl_display.get_registry())
self.registry.connect("global", self.on_global)
self.registry.connect("global_remove", self.on_global_remove)
# self.display.connect() # Iterate through monitors and get their Wayland output (wl_output)
# This is a hack to ensure output_enter/leave is called for toplevels
#if self.my_output:
# self.my_output.destroy()
#for monitor in self.monitors:
# wl_output = gtk.gdk_wayland_monitor_get_wl_output(ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(monitor.__gpointer__, None)))
# if wl_output:
# print("Create proxy")
# output_proxy = WlOutputProxy(wl_output, self.wl_display)
# output_proxy.interface.registry[output_proxy._ptr] = output_proxy
# End hack
self.panels_generated = False
self.got_output_name = False
self.foreign_toplevel_manager_id = None
self.foreign_toplevel_version = None
self.registry = self.wl_display.get_registry()
self.registry.dispatcher["global"] = self.on_global
self.registry.dispatcher["global_remove"] = self.on_global_remove
self.wl_display.roundtrip()
self.hold()
shared/panorama_panel.py
@@ -66,6 +66,33 @@ class Applet(Gtk.Box):
def print_log(cls, *args, **kwargs):
print(f"{cls.__name__}:", *args, **kwargs)
def wl_output_enter(self, output, name):
pass
def wl_output_leave(self, output, name):
pass
def foreign_toplevel_new(self, manager, toplevel):
pass
def foreign_toplevel_output_enter(self, toplevel, output):
pass
def foreign_toplevel_output_leave(self, toplevel, output):
pass
def foreign_toplevel_app_id(self, toplevel, app_id):
pass
def foreign_toplevel_title(self, toplevel, title):
pass
def foreign_toplevel_state(self, toplevel, state):
pass
def foreign_toplevel_closed(self, toplevel):
pass
def track_popover(popover: Gtk.Popover):
popover.connect("show", lambda *args: _popover_shown(popover))