from __future__ import annotations import re import tkinter as tk from dataclasses import dataclass @dataclass(frozen=True, slots=True) class MonitorBounds: x: int y: int width: int height: int primary: bool = False @property def center(self) -> tuple[int, int]: return self.x + self.width // 2, self.y + self.height // 2 def as_tuple(self) -> tuple[int, int, int, int]: return self.x, self.y, self.width, self.height def available_monitors(root: tk.Misc | None = None) -> list[MonitorBounds]: try: from screeninfo import get_monitors monitors = [ MonitorBounds( int(monitor.x), int(monitor.y), int(monitor.width), int(monitor.height), bool(getattr(monitor, "is_primary", False)), ) for monitor in get_monitors() ] if monitors: return monitors except Exception: pass if root is None: return [] return [ MonitorBounds( int(root.winfo_vrootx()), int(root.winfo_vrooty()), int(root.winfo_vrootwidth()), int(root.winfo_vrootheight()), True, ) ] def preferred_monitor( root: tk.Misc, saved_bounds: tuple[int, int, int, int] | None = None, ) -> MonitorBounds: monitors = available_monitors(root) if saved_bounds: for monitor in monitors: if monitor.as_tuple() == saved_bounds: return monitor return next((monitor for monitor in monitors if monitor.primary), monitors[0]) def monitor_for_geometry(root: tk.Misc, geometry: str) -> MonitorBounds: monitors = available_monitors(root) parsed = parse_geometry(geometry) if not parsed: return next((monitor for monitor in monitors if monitor.primary), monitors[0]) width, height, x, y = parsed center_x, center_y = x + width // 2, y + height // 2 containing = [ monitor for monitor in monitors if monitor.x <= center_x < monitor.x + monitor.width and monitor.y <= center_y < monitor.y + monitor.height ] if containing: return containing[0] return min(monitors, key=lambda monitor: _distance_squared(monitor.center, (center_x, center_y))) def centered_geometry(width: int, height: int, monitor: MonitorBounds) -> str: fitted_width = min(width, max(640, monitor.width - 80)) fitted_height = min(height, max(480, monitor.height - 80)) x = monitor.x + (monitor.width - fitted_width) // 2 y = monitor.y + (monitor.height - fitted_height) // 2 return format_geometry(fitted_width, fitted_height, x, y) def ensure_visible_geometry(geometry: str, monitor: MonitorBounds) -> str: parsed = parse_geometry(geometry) if not parsed: return centered_geometry(1500, 860, monitor) width, height, x, y = parsed width = min(width, max(640, monitor.width - 40)) height = min(height, max(480, monitor.height - 40)) x = min(max(x, monitor.x), monitor.x + monitor.width - width) y = min(max(y, monitor.y), monitor.y + monitor.height - height) return format_geometry(width, height, x, y) def parse_geometry(geometry: str) -> tuple[int, int, int, int] | None: match = re.fullmatch(r"(\d+)x(\d+)([+-]\d+)([+-]\d+)", geometry.strip()) if not match: return None return tuple(int(value) for value in match.groups()) # type: ignore[return-value] def format_geometry(width: int, height: int, x: int, y: int) -> str: return f"{width}x{height}{x:+d}{y:+d}" def _distance_squared(left: tuple[int, int], right: tuple[int, int]) -> int: return (left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2