#!/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())