234 lines
7.4 KiB
Python
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())
|