2025-05-19 09:32:17 +02:00

234 lines
7.4 KiB
Python

#!/usr/bin/env python3
import sys
from typing import Dict, List, Optional
import pywayland
from pywayland.client import Display
from pywayland.protocol.wayland import WlOutput, WlSeat
# Import the protocol interfaces from your files
from wlr_foreign_toplevel_management_unstable_v1.zwlr_foreign_toplevel_manager_v1 import (
ZwlrForeignToplevelManagerV1,
)
from wlr_foreign_toplevel_management_unstable_v1.zwlr_foreign_toplevel_handle_v1 import (
ZwlrForeignToplevelHandleV1,
)
class Window:
"""Represents a toplevel window in the compositor."""
def __init__(self, handle: ZwlrForeignToplevelHandleV1):
self.handle = handle
self.title: str = "Unknown"
self.app_id: str = "Unknown"
self.states: List[str] = []
self.outputs: List[WlOutput] = []
self.parent: Optional["Window"] = None
self.closed = False
def __str__(self) -> str:
state_str = (
", ".join([ZwlrForeignToplevelHandleV1.state(s).name for s in self.states])
if self.states
else "normal"
)
return (
f"Window(title='{self.title}', app_id='{self.app_id}', state={state_str})"
)
class WaylandWindowManager:
"""Manages Wayland windows using the foreign toplevel protocol."""
def __init__(self):
self.display = Display()
self.windows: Dict[ZwlrForeignToplevelHandleV1, Window] = {}
self.manager = None
self.running = False
def connect(self) -> bool:
"""Connect to the Wayland display and bind to the toplevel manager."""
try:
self.display.connect()
print("Connected to Wayland display")
# Get the registry to find the foreign toplevel manager
registry = self.display.get_registry()
registry.dispatcher["global"] = self._registry_global_handler
# Roundtrip to process registry events
self.display.roundtrip()
if not self.manager:
print(
"Foreign toplevel manager not found. Is wlr-foreign-toplevel-management protocol supported?"
)
return False
return True
except Exception as e:
print(f"Failed to connect: {e}")
return False
def _registry_global_handler(self, registry, id, interface, version):
"""Handle registry global objects."""
if interface == ZwlrForeignToplevelManagerV1.name:
print(f"Found foreign toplevel manager (id={id}, version={version})")
self.manager = registry.bind(
id, ZwlrForeignToplevelManagerV1, min(version, 3)
)
self.manager.dispatcher["toplevel"] = self._handle_toplevel
self.manager.dispatcher["finished"] = self._handle_manager_finished
def _handle_toplevel(self, manager, toplevel):
"""Handle a new toplevel window."""
window = Window(toplevel)
self.windows[toplevel] = window
print(window)
# Setup event dispatchers for the toplevel
toplevel.dispatcher["title"] = self._handle_title
toplevel.dispatcher["app_id"] = self._handle_app_id
toplevel.dispatcher["state"] = self._handle_state
toplevel.dispatcher["done"] = self._handle_done
toplevel.dispatcher["closed"] = self._handle_closed
toplevel.dispatcher["output_enter"] = self._handle_output_enter
toplevel.dispatcher["output_leave"] = self._handle_output_leave
def _handle_title(self, toplevel, title):
"""Handle toplevel title changes."""
window = self.windows.get(toplevel)
if window:
window.title = title
def _handle_app_id(self, toplevel, app_id):
"""Handle toplevel app_id changes."""
window = self.windows.get(toplevel)
if window:
window.app_id = app_id
def _handle_state(self, toplevel, states):
"""Handle toplevel state changes."""
window = self.windows.get(toplevel)
if window:
window.states = states
def _handle_done(self, toplevel):
"""Handle toplevel done event."""
window = self.windows.get(toplevel)
if window and not window.closed:
print(f"Window updated: {window}")
def _handle_closed(self, toplevel):
"""Handle toplevel closed event."""
window = self.windows.get(toplevel)
if window:
window.closed = True
print(f"Window closed: {window}")
# Clean up the toplevel object
toplevel.destroy()
# Remove from our dictionary
del self.windows[toplevel]
def _handle_output_enter(self, toplevel, output):
"""Handle toplevel entering an output."""
window = self.windows.get(toplevel)
if window and output not in window.outputs:
window.outputs.append(output)
def _handle_output_leave(self, toplevel, output):
"""Handle toplevel leaving an output."""
window = self.windows.get(toplevel)
if window and output in window.outputs:
window.outputs.remove(output)
def _handle_parent(self, toplevel, parent):
"""Handle toplevel parent changes."""
window = self.windows.get(toplevel)
if window:
if parent is None:
window.parent = None
else:
parent_window = self.windows.get(parent)
if parent_window:
window.parent = parent_window
def _handle_manager_finished(self, manager):
"""Handle manager finished event."""
print("Foreign toplevel manager finished")
self.running = False
def get_windows(self) -> List[Window]:
"""Get all currently active windows."""
# Filter out closed windows
active_windows = [
window for window in self.windows.values() if not window.closed
]
return active_windows
def run(self):
"""Run the event loop to receive window updates."""
self.running = True
print("Listening for window events (press Ctrl+C to exit)...")
try:
while self.running:
self.display.dispatch(block=True)
except KeyboardInterrupt:
print("\nExiting...")
finally:
self.cleanup()
def cleanup(self):
"""Clean up resources."""
print("cleanup")
if self.manager:
self.manager.stop()
# Destroy all toplevel handles
for toplevel, window in list(self.windows.items()):
if not window.closed:
toplevel.destroy()
# Disconnect from display
if self.display:
self.display.disconnect()
self.running = False
def main():
"""Main entry point."""
manager = WaylandWindowManager()
if not manager.connect():
return 1
# # Run for a short time to collect initial windows
for _ in range(1):
manager.display.dispatch(block=True)
# Print all windows
windows = manager.get_windows()
print("\nActive windows:")
if windows:
for i, window in enumerate(windows, 1):
print(f"{i}. {window}")
else:
print("No windows found")
# # Option to keep monitoring window events
# if len(sys.argv) > 1 and sys.argv[1] == "--monitor":
# manager.run()
# else:
manager.cleanup()
return 0
if __name__ == "__main__":
sys.exit(main())