""" GUI Utilities for Jackify Frontend """ import re from typing import Tuple, Optional from PySide6.QtWidgets import QApplication, QWidget from PySide6.QtCore import QSize, QPoint ANSI_COLOR_MAP = { '30': 'black', '31': 'red', '32': 'green', '33': 'yellow', '34': 'blue', '35': 'magenta', '36': 'cyan', '37': 'white', '90': 'gray', '91': 'lightcoral', '92': 'lightgreen', '93': 'khaki', '94': 'lightblue', '95': 'violet', '96': 'lightcyan', '97': 'white' } ANSI_RE = re.compile(r'\x1b\[(\d+)(;\d+)?m') # Pattern to match terminal control codes (cursor movement, line clearing, etc.) ANSI_CONTROL_RE = re.compile( r'\x1b\[' # CSI sequence start r'[0-9;]*' # Parameters r'[A-Za-z]' # Command letter ) def strip_ansi_control_codes(text): """Remove ALL ANSI escape sequences including control codes. This is useful for Hoolamike output which uses terminal control codes for progress bars that don't render well in QTextEdit. """ return ANSI_CONTROL_RE.sub('', text) def ansi_to_html(text): """Convert ANSI color codes to HTML""" result = '' last_end = 0 color = None for match in ANSI_RE.finditer(text): start, end = match.span() code = match.group(1) if start > last_end: chunk = text[last_end:start] if color: result += f'{chunk}' else: result += chunk if code == '0': color = None elif code in ANSI_COLOR_MAP: color = ANSI_COLOR_MAP[code] last_end = end if last_end < len(text): chunk = text[last_end:] if color: result += f'{chunk}' else: result += chunk result = result.replace('\n', '
') return result def get_screen_geometry(widget: Optional[QWidget] = None) -> Tuple[int, int, int, int]: """ Get available screen geometry for a widget. Args: widget: Widget to get screen for (uses primary screen if None) Returns: Tuple of (x, y, width, height) for available screen geometry """ app = QApplication.instance() if not app: return (0, 0, 1920, 1080) # Fallback if widget: screen = None window_handle = widget.windowHandle() if window_handle and window_handle.screen(): screen = window_handle.screen() else: try: global_pos = widget.mapToGlobal(widget.rect().center()) except Exception: global_pos = QPoint(0, 0) if app: screen = app.screenAt(global_pos) if not screen and app: screen = app.primaryScreen() else: screen = app.primaryScreen() if screen: geometry = screen.availableGeometry() return (geometry.x(), geometry.y(), geometry.width(), geometry.height()) return (0, 0, 1920, 1080) # Fallback def calculate_window_size( widget: Optional[QWidget] = None, width_ratio: float = 0.7, height_ratio: float = 0.6, min_width: int = 900, min_height: int = 500, max_width: Optional[int] = None, max_height: Optional[int] = None ) -> Tuple[int, int]: """ Calculate appropriate window size based on screen geometry. Args: widget: Widget to calculate size for (uses primary screen if None) width_ratio: Fraction of screen width to use (0.0-1.0) height_ratio: Fraction of screen height to use (0.0-1.0) min_width: Minimum window width min_height: Minimum window height max_width: Maximum window width (None = no limit) max_height: Maximum window height (None = no limit) Returns: Tuple of (width, height) """ _, _, screen_width, screen_height = get_screen_geometry(widget) # Calculate size based on ratios width = int(screen_width * width_ratio) height = int(screen_height * height_ratio) # Apply minimums width = max(width, min_width) height = max(height, min_height) # Apply maximums if max_width: width = min(width, max_width) if max_height: height = min(height, max_height) # Ensure we don't exceed screen bounds width = min(width, screen_width) height = min(height, screen_height) return (width, height) def calculate_window_position( widget: QWidget, window_width: int, window_height: int, parent: Optional[QWidget] = None ) -> QPoint: """ Calculate appropriate window position (centered on parent or screen). Args: widget: Widget to position window_width: Width of window to position window_height: Height of window to position parent: Parent widget to center on (centers on screen if None) Returns: QPoint with x, y coordinates """ _, _, screen_width, screen_height = get_screen_geometry(widget) if parent: parent_geometry = parent.geometry() x = parent_geometry.x() + (parent_geometry.width() - window_width) // 2 y = parent_geometry.y() + (parent_geometry.height() - window_height) // 2 else: # Center on screen x = (screen_width - window_width) // 2 y = (screen_height - window_height) // 2 # Ensure window stays on screen x = max(0, min(x, screen_width - window_width)) y = max(0, min(y, screen_height - window_height)) return QPoint(x, y) def set_responsive_minimum(window: Optional[QWidget], min_width: int = 960, min_height: int = 520, margin: int = 32): """ Apply minimum size constraints that respect the current screen bounds. Args: window: Target window min_width: Desired minimum width min_height: Desired minimum height margin: Pixels to subtract from available size to avoid full-screen overlap """ if window is None: return _, _, screen_width, screen_height = get_screen_geometry(window) width_cap = min_width height_cap = min_height if screen_width: available_width = max(640, screen_width - margin) available_width = min(available_width, screen_width) width_cap = min(min_width, available_width) if screen_height: available_height = max(520, screen_height - margin) available_height = min(available_height, screen_height) height_cap = min(min_height, available_height) window.setMinimumSize(QSize(width_cap, height_cap)) def load_saved_window_size(window: QWidget) -> Optional[Tuple[int, int]]: """ Load saved window size from config if available. Only returns sizes that are reasonable (compact menu size, not expanded). Args: window: Window widget (used to validate size against screen) Returns: Tuple of (width, height) if saved size exists and is valid, None otherwise """ try: from ...backend.handlers.config_handler import ConfigHandler config_handler = ConfigHandler() saved_width = config_handler.get('window_width') saved_height = config_handler.get('window_height') if saved_width and saved_height: # Validate saved size is reasonable (not too small, fits on screen) _, _, screen_width, screen_height = get_screen_geometry(window) min_width = 1200 min_height = 500 max_height = int(screen_height * 0.6) # Reject sizes larger than 60% of screen (expanded state) # Ensure saved size is within reasonable bounds (compact menu size) # Reject expanded sizes that are too tall if (min_width <= saved_width <= screen_width and min_height <= saved_height <= max_height): return (saved_width, saved_height) except Exception: pass return None def save_window_size(window: QWidget): """ Save current window size to config. Args: window: Window widget to save size for """ try: from ...backend.handlers.config_handler import ConfigHandler config_handler = ConfigHandler() size = window.size() config_handler.set('window_width', size.width()) config_handler.set('window_height', size.height()) config_handler.save_config() except Exception: pass def apply_window_size_and_position( window: QWidget, width_ratio: float = 0.7, height_ratio: float = 0.6, min_width: int = 900, min_height: int = 500, max_width: Optional[int] = None, max_height: Optional[int] = None, parent: Optional[QWidget] = None, preserve_position: bool = False, use_saved_size: bool = True ): """ Apply dynamic window sizing and positioning based on screen geometry. Optionally uses saved window size if user has manually resized before. Args: window: Window widget to size/position width_ratio: Fraction of screen width to use (if no saved size) height_ratio: Fraction of screen height to use (if no saved size) min_width: Minimum window width min_height: Minimum window height max_width: Maximum window width (None = no limit) max_height: Maximum window height (None = no limit) parent: Parent widget to center on (centers on screen if None) preserve_position: If True, preserve current size and position (only set minimums) use_saved_size: If True, check for saved window size first """ # Set minimum size first window.setMinimumSize(QSize(min_width, min_height)) # If preserve_position is True, don't resize - just ensure minimums are set if preserve_position: # Only ensure current size meets minimums, don't change size current_size = window.size() if current_size.width() < min_width: window.resize(min_width, current_size.height()) if current_size.height() < min_height: window.resize(window.size().width(), min_height) return # Check for saved window size first width = None height = None if use_saved_size: saved_size = load_saved_window_size(window) if saved_size: width, height = saved_size # If no saved size, calculate dynamically if width is None or height is None: width, height = calculate_window_size( window, width_ratio, height_ratio, min_width, min_height, max_width, max_height ) # Calculate and set position pos = calculate_window_position(window, width, height, parent) window.resize(width, height) window.move(pos)