roundabout,
created on Friday, 8 August 2025, 08:43:58 (1754642638),
received on Saturday, 9 August 2025, 12:22:37 (1754742157)
Author identity: Vlad <vlad.muntoiu@gmail.com>
6b8a247171f0ea5077f1343767fec19416aaaee5
applets/wf-window-list/__init__.py
@@ -21,6 +21,7 @@ import dataclasses
import os
import sys
import locale
import typing
from pathlib import Path
from pywayland.client import Display
from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor, WlOutput
@@ -160,6 +161,7 @@ class WindowButton(Gtk.ToggleButton):
super().__init__(**kwargs)
self.window_id: ZwlrForeignToplevelHandleV1 = window_id
self.wf_ipc_id: typing.Optional[int] = None
self.set_has_frame(False)
self.label = Gtk.Label()
self.icon = Gtk.Image.new_from_icon_name("application-x-executable")
@@ -208,8 +210,15 @@ class WindowButton(Gtk.ToggleButton):
self.label.set_text(value)
def set_icon_from_app_id(self, app_id):
# Try getting an icon from the correct theme
app_ids = app_id.split()
# If on Wayfire, find the IPC ID
for app_id in app_ids:
if app_id.startswith("wf-ipc-"):
self.wf_ipc_id = int(app_id.removeprefix("wf-ipc-"))
break
# Try getting an icon from the correct theme
icon_theme = Gtk.IconTheme.get_for_display(self.get_display())
for app_id in app_ids:
@@ -242,6 +251,7 @@ class WFWindowList(panorama_panel.Applet):
self.window_button_options = WindowButtonOptions(config.get("max_button_width", 256))
self.toplevel_buttons: dict[ZwlrForeignToplevelHandleV1, WindowButton] = {}
self.toplevel_buttons_by_wf_id: dict[int, WindowButton] = {}
# This button doesn't belong to any window but is used for the button group and to be
# selected when no window is focused
self.initial_button = Gtk.ToggleButton()
@@ -279,6 +289,42 @@ class WFWindowList(panorama_panel.Applet):
self.add_controller(self.drop_target)
# Make a Wayfire socket for workspace handling
try:
import wayfire
self.wf_socket = wayfire.WayfireSocket()
self.wf_socket.watch()
fd = self.wf_socket.client.fileno()
GLib.io_add_watch(fd, GLib.IO_IN, self.on_wf_event)
except:
# Wayfire raises Exception itself, so it cannot be narrowed down
self.wf_socket = None
def on_wf_event(self, source, condition):
if condition == GLib.IO_IN:
try:
message = self.wf_socket.read_next_event()
event = message.get("event")
match event:
case "view-workspace-changed":
view = message.get("view", {})
output = self.wf_socket.get_output(self.get_root().monitor_index + 1)
current_workspace = output["workspace"]["x"], output["workspace"]["y"]
if (message["to"]["x"], message["to"]["y"]) == current_workspace:
self.append(self.toplevel_buttons_by_wf_id[view["id"]])
else:
# Remove out-of-workspace window
self.remove(self.toplevel_buttons_by_wf_id[view["id"]])
case "wset-workspace-changed":
output_id = self.get_root().monitor_index + 1
if message["wset-data"]["output-id"] == output_id:
# It has changed on this monitor; refresh the window list
self.filter_to_wf_workspace()
except Exception as e:
print("Error reading Wayfire event:", e)
return True
def drop_button(self, drop_target: Gtk.DropTarget, value: int, x: float, y: float):
button: WindowButton = self.get_root().get_application().drags.pop(value)
if button.get_parent() is not self:
@@ -310,6 +356,23 @@ class WFWindowList(panorama_panel.Applet):
self.set_all_rectangles()
return True
def filter_to_wf_workspace(self):
output = self.wf_socket.get_output(self.get_root().monitor_index + 1)
for wf_id, button in self.toplevel_buttons_by_wf_id.items():
view = self.wf_socket.get_view(wf_id)
mid_x = view["geometry"]["x"] + view["geometry"]["width"] / 2
mid_y = view["geometry"]["y"] + view["geometry"]["height"] / 2
output_width = output["geometry"]["width"]
output_height = output["geometry"]["height"]
if 0 <= mid_x < output_width and 0 <= mid_y < output_height:
# It is in this workspace; keep it
if not button.get_realized():
self.append(button)
else:
# Remove it from this window list
if button.get_parent() is self:
self.remove(button)
def get_wl_resources(self):
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,)
@@ -343,6 +406,9 @@ class WFWindowList(panorama_panel.Applet):
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(block=True)
@@ -376,6 +442,7 @@ class WFWindowList(panorama_panel.Applet):
def on_output_entered(self, handle, output):
# TODO: make this configurable
# TODO: on wayfire, append/remove buttons when the workspace changes
if output != self.my_output:
return
if handle in self.toplevel_buttons:
@@ -442,11 +509,17 @@ class WFWindowList(panorama_panel.Applet):
if handle in self.toplevel_buttons:
button = self.toplevel_buttons[handle]
button.set_icon_from_app_id(app_id)
app_ids = app_id.split()
for app_id in app_ids:
if app_id.startswith("wf-ipc-"):
self.toplevel_buttons_by_wf_id[int(app_id.removeprefix("wf-ipc-"))] = button
def on_closed(self, handle):
if handle in self.toplevel_buttons:
self.remove(self.toplevel_buttons[handle])
self.toplevel_buttons.pop(handle)
if handle in self.toplevel_buttons_by_wf_id:
self.toplevel_buttons_by_wf_id.pop(handle)
self.set_all_rectangles()
config.yaml
@@ -1,12 +1,13 @@
panels:
- position: top
- position: bottom
monitor: 0
size: 40
autohide: false
hide_time: 300
applets:
left:
- WFWindowList: {}
- WFWindowList:
max_button_width: 256
centre: []
right:
- ClockApplet: