""" EU-Utility - Main Entry Point Launch the overlay, floating icon, dashboard, and plugin system. """ import sys import os import time from pathlib import Path # Add project root to path project_root = Path(__file__).parent.parent if str(project_root) not in sys.path: sys.path.insert(0, str(project_root)) # Import debug logger FIRST (before anything else) from core.debug_logger import debug_logger, timed, log_call debug_logger.info("MAIN", "Starting EU-Utility...") start_time = time.perf_counter() try: from PyQt6.QtWidgets import QApplication from PyQt6.QtCore import Qt, QObject, pyqtSignal PYQT_AVAILABLE = True debug_logger.info("MAIN", "PyQt6 imported successfully") except ImportError as e: PYQT_AVAILABLE = False debug_logger.error("MAIN", f"PyQt6 import failed: {e}") print("Error: PyQt6 is required.") print("Install with: pip install PyQt6") sys.exit(1) try: import keyboard KEYBOARD_AVAILABLE = True debug_logger.info("MAIN", "keyboard library imported") except ImportError: KEYBOARD_AVAILABLE = False debug_logger.warn("MAIN", "keyboard library not available") print("Warning: 'keyboard' library not installed.") print("Global hotkeys won't work. Install with: pip install keyboard") debug_logger.start_timer("imports") from core.plugin_manager import PluginManager from core.perfect_ux import create_perfect_window from core.tray_icon import TrayIcon from core.settings import get_settings from core.overlay_widgets import OverlayManager from core.api import get_api, get_widget_api, get_external_api from core.log_reader import get_log_reader from core.ocr_service import get_ocr_service from core.screenshot import get_screenshot_service from core.notifications import get_notification_manager, NotificationManager from core.nexus_api import get_nexus_api from core.http_client import get_http_client from core.window_manager import get_window_manager from core.event_bus import get_event_bus from core.tasks import get_task_manager from core.audio import get_audio_manager from core.clipboard import get_clipboard_manager debug_logger.end_timer("imports") from core.data_store import get_data_store class HotkeyHandler(QObject): """Signal bridge for thread-safe hotkey handling.""" toggle_signal = pyqtSignal() hide_overlays_signal = pyqtSignal() class EUUtilityApp: """Main application controller.""" def __init__(self): self.app = None self.overlay = None self.floating_icon = None self.plugin_manager = None self.hotkey_handler = None self.settings = None self.overlay_manager = None self.api = None self.notification_manager = None def run(self): """Start the application.""" debug_logger.start_timer("MAIN_run_total") # Create Qt Application debug_logger.start_timer("MAIN_create_app") self.app = QApplication(sys.argv) self.app.setQuitOnLastWindowClosed(False) debug_logger.end_timer("MAIN_create_app") # Enable high DPI scaling (Qt6 has this enabled by default) if hasattr(Qt, 'AA_EnableHighDpiScaling'): try: self.app.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling) except (AttributeError, TypeError): pass # Initialize Plugin API debug_logger.start_timer("MAIN_init_api") print("Initializing Plugin API...") self.api = get_api() self._setup_api_services() debug_logger.end_timer("MAIN_init_api") # Initialize Event Bus debug_logger.start_timer("MAIN_init_eventbus") print("Initializing Event Bus...") self.event_bus = get_event_bus() self._print_event_bus_stats() debug_logger.end_timer("MAIN_init_eventbus") # Initialize Notification Manager debug_logger.start_timer("MAIN_init_notifications") print("Initializing Notification Manager...") self.notification_manager = get_notification_manager() self.notification_manager.initialize(self.app) self.api.register_notification_service(self.notification_manager) print("[Core] Notification service registered") debug_logger.end_timer("MAIN_init_notifications") # Load settings debug_logger.start_timer("MAIN_load_settings") self.settings = get_settings() debug_logger.end_timer("MAIN_load_settings") # Create hotkey handler (must be in main thread) debug_logger.start_timer("MAIN_init_hotkeys") self.hotkey_handler = HotkeyHandler() debug_logger.end_timer("MAIN_init_hotkeys") # Initialize plugin manager debug_logger.start_timer("MAIN_load_plugins") print("Loading plugins...") self.plugin_manager = PluginManager(None) self.plugin_manager.load_all_plugins() debug_logger.end_timer("MAIN_load_plugins") # Create overlay manager debug_logger.start_timer("MAIN_init_overlay_mgr") self.overlay_manager = OverlayManager(self.app) debug_logger.end_timer("MAIN_init_overlay_mgr") # Create perfect UX main window debug_logger.start_timer("MAIN_create_window") print("Creating main window with perfect UX...") self.dashboard = create_perfect_window(self.plugin_manager) self.plugin_manager.overlay = self.dashboard self.dashboard.show() debug_logger.end_timer("MAIN_create_window") # Create system tray icon (replaces floating icon) debug_logger.start_timer("MAIN_create_tray") print("Creating system tray icon...") self.tray_icon = TrayIcon(self.app) self.tray_icon.show_dashboard.connect(self._toggle_overlay) self.tray_icon.toggle_activity_bar.connect(self._toggle_activity_bar) self.tray_icon.quit_app.connect(self.quit) debug_logger.end_timer("MAIN_create_tray") # Create Activity Bar (in-game overlay) - controlled by overlay controller debug_logger.start_timer("MAIN_create_activitybar") print("Creating Activity Bar...") try: from core.activity_bar import get_activity_bar from core.overlay_controller import get_overlay_controller, OverlayMode self.activity_bar = get_activity_bar(self.plugin_manager) if self.activity_bar: # Connect widget request signal self.activity_bar.widget_requested.connect(self._on_activity_bar_widget) # Get overlay controller with window manager self.overlay_controller = get_overlay_controller( self.activity_bar, getattr(self, 'window_manager', None), parent=self.app # Pass QApplication as parent ) # Set mode from settings (default: game focused - Blish HUD style) settings = get_settings() mode_str = settings.get('activity_bar.overlay_mode', 'overlay_game') try: mode = OverlayMode(mode_str) except ValueError: mode = OverlayMode.OVERLAY_GAME_FOCUSED # Default to game focused self.overlay_controller.set_mode(mode) self.overlay_controller.start() print(f"[Core] Activity Bar created (mode: {mode.value})") if mode.value == 'overlay_game': print("[Core] Overlay will ONLY show when EU game window is focused (Blish HUD style)") print("[Core] Overlay is hidden on desktop - switch to EU to see it") else: print("[Core] Activity Bar not available") self.activity_bar = None self.overlay_controller = None except Exception as e: print(f"[Core] Failed to create Activity Bar: {e}") debug_logger.error("MAIN", f"ActivityBar creation failed: {e}") self.activity_bar = None self.overlay_controller = None debug_logger.end_timer("MAIN_create_activitybar") # Connect hotkey signals self.hotkey_handler.toggle_signal.connect(self._on_toggle_signal) # Setup global hotkeys debug_logger.start_timer("MAIN_setup_hotkeys") self._setup_hotkeys() debug_logger.end_timer("MAIN_setup_hotkeys") # Load saved overlay widgets debug_logger.start_timer("MAIN_load_widgets") self._load_overlay_widgets() debug_logger.end_timer("MAIN_load_widgets") # Log total startup time total_startup = debug_logger.end_timer("MAIN_run_total") debug_logger.info("MAIN", f"=== TOTAL STARTUP TIME: {total_startup:.2f}ms ===") print("EU-Utility started!") print("Dashboard window is open (Desktop App)") print("Use system tray icon to access dashboard") print("Press Ctrl+Shift+U to toggle in-game overlay (Activity Bar)") print("Press Ctrl+Shift+H to hide all overlays") print(f"Loaded {len(self.plugin_manager.get_all_plugins())} plugins") # Show overlay mode info if hasattr(self, 'overlay_controller') and self.overlay_controller: mode = self.overlay_controller._mode print(f"\nActivity Bar Mode: {mode.value}") if mode.value == 'overlay_game': print(" - Overlay auto-shows when EU game is focused") print(" - Press Ctrl+Shift+U to manually toggle overlay") elif mode.value == 'overlay_always': print(" - Overlay always visible") print(" - Press Ctrl+Shift+U to hide/show overlay") elif mode.value == 'overlay_toggle': print(" - Overlay starts hidden") print(" - Press Ctrl+Shift+U to toggle overlay") elif mode.value == 'overlay_temp': print(" - Press Ctrl+Shift+U to show for 8 seconds") elif mode.value == 'desktop_app': print(" - Activity bar only in desktop app") # Show Event Bus stats self._print_event_bus_stats() # Run debug_logger.info("MAIN", "Starting Qt event loop...") return self.app.exec() def _setup_api_services(self): """Setup shared API services - Window, OCR and Log are core services.""" # Initialize and start Log Reader print("[Core] Initializing Log Reader...") self.log_reader = get_log_reader() if self.log_reader.start(): print(f"[Core] Log Reader started - watching: {self.log_reader.log_path}") else: print("[Core] Log Reader not available - log file not found") # Register Log service with API self.api.register_log_service(self.log_reader.read_lines) # Initialize Window Manager (Windows-only, gracefully handles Linux) print("[Core] Initializing Window Manager...") self.window_manager = get_window_manager() if self.window_manager.is_available(): # Try to find EU window on startup eu_window = self.window_manager.find_eu_window() if eu_window: print(f"[Core] Found EU window: {eu_window.title} ({eu_window.width}x{eu_window.height})") else: print("[Core] EU window not found - will retry when needed") # Register Window service with API self.api.register_window_service(self.window_manager) else: print("[Core] Window Manager not available (Windows only)") # Screenshot Service - Initialize on startup (lightweight) print("[Core] Initializing Screenshot Service...") self.screenshot_service = get_screenshot_service() if self.screenshot_service.is_available(): self.api.register_screenshot_service(self.screenshot_service) backends = self.screenshot_service.get_available_backends() print(f"[Core] Screenshot Service ready (backends: {', '.join(backends)})") else: print("[Core] Screenshot Service not available - install pillow or pyautogui") # OCR Service - LAZY INITIALIZATION (don't init on startup) # It will initialize on first use print("[Core] OCR Service configured (lazy init)") self.ocr_service = get_ocr_service() # Register OCR service with API (lazy - will init on first call) self.api.register_ocr_service(self._lazy_ocr_handler) # Initialize Nexus API Service print("[Core] Initializing Nexus API Service...") self.nexus_api = get_nexus_api() self.api.register_nexus_service(self.nexus_api) # HTTP Client - Initialize on startup print("[Core] Initializing HTTP Client...") try: self.http_client = get_http_client( cache_dir="cache/http", default_cache_ttl=3600, rate_limit_delay=0.1, # Small delay between requests max_retries=3, backoff_factor=0.5, respect_cache_control=True ) self.api.register_http_service(self.http_client) print("[Core] HTTP Client initialized with caching") except Exception as e: print(f"[Core] HTTP Client initialization failed: {e}") # Initialize Audio Service print("[Core] Initializing Audio Service...") self.audio_manager = get_audio_manager() if self.audio_manager.is_available(): self.api.register_audio_service(self.audio_manager) backend = self.audio_manager.get_backend() volume = int(self.audio_manager.get_volume() * 100) print(f"[Core] Audio Service ready (backend: {backend}, volume: {volume}%)") else: print("[Core] Audio Service not available - no audio backend found") # Initialize Task Manager print("[Core] Initializing Task Manager...") try: self.task_manager = get_task_manager(max_workers=4) self.task_manager.initialize() self.api.register_task_service(self.task_manager) print("[Core] Task Manager initialized with 4 workers") except Exception as e: print(f"[Core] Task Manager initialization failed: {e}") # Initialize Clipboard Manager print("[Core] Initializing Clipboard Manager...") self.clipboard_manager = get_clipboard_manager() if self.clipboard_manager.is_available(): self.api.register_clipboard_service(self.clipboard_manager) print("[Core] Clipboard Service ready") else: print("[Core] Clipboard Service not available - install pyperclip") # Initialize Data Store print("[Core] Initializing Data Store...") self.data_store = get_data_store() self.api.register_data_service(self.data_store) print("[Core] Data Store ready") print("[Core] API services registered: Window, Screenshot, OCR (lazy), Log, Nexus, HTTP, Audio, Tasks, Clipboard, Data") def _lazy_ocr_handler(self, region=None): """Lazy OCR handler - triggers init on first use.""" return self.ocr_service.recognize(region=region) def _print_event_bus_stats(self): """Print Event Bus statistics on startup.""" if not hasattr(self, 'event_bus') or not self.event_bus: return stats = self.event_bus.get_stats() print("\n" + "=" * 50) print("📊 Event Bus Statistics") print("=" * 50) print(f" Total Events Published: {stats.get('total_published', 0)}") print(f" Total Events Delivered: {stats.get('total_delivered', 0)}") print(f" Active Subscriptions: {stats.get('active_subscriptions', 0)}") print(f" Events Per Minute: {stats.get('events_per_minute', 0)}") print(f" Avg Delivery Time: {stats.get('avg_delivery_ms', 0)} ms") print(f" Errors: {stats.get('errors', 0)}") top_types = stats.get('top_event_types', {}) if top_types: print(f"\n Top Event Types:") for event_type, count in list(top_types.items())[:5]: print(f" • {event_type}: {count}") print("=" * 50 + "\n") def _setup_hotkeys(self): """Setup global hotkeys.""" if KEYBOARD_AVAILABLE: try: # Ctrl+Shift+U: Toggle in-game overlay (Activity Bar) keyboard.add_hotkey('ctrl+shift+u', self._on_activity_bar_hotkey) # Hide all overlays keyboard.add_hotkey('ctrl+shift+h', self._on_hide_overlays_pressed) # Alternative: Toggle activity bar (kept for compatibility) keyboard.add_hotkey('ctrl+shift+b', self._on_activity_bar_hotkey) except Exception as e: print(f"Failed to register hotkey: {e}") def _on_hotkey_pressed(self): """Called when toggle hotkey is pressed.""" if self.hotkey_handler: self.hotkey_handler.toggle_signal.emit() def _on_hide_overlays_pressed(self): """Called when hide hotkey is pressed.""" if self.overlay_manager: self.overlay_manager.hide_all() def _on_activity_bar_hotkey(self): """Called when activity bar hotkey is pressed.""" self._toggle_activity_bar() def _on_toggle_signal(self): """Handle toggle signal in main thread.""" self._toggle_overlay() def _on_activity_bar_widget(self, plugin_id: str): """Handle activity bar widget request.""" print(f"[Main] Activity bar requested plugin: {plugin_id}") # Get plugin class all_plugins = self.plugin_manager.get_all_discovered_plugins() if plugin_id not in all_plugins: print(f"[Main] Plugin not found: {plugin_id}") return plugin_class = all_plugins[plugin_id] # Check if plugin has a mini widget if hasattr(plugin_class, 'get_mini_widget'): try: # Create mini widget widget = plugin_class.get_mini_widget() if widget: widget.show() print(f"[Main] Created mini widget for {plugin_class.name}") except Exception as e: print(f"[Main] Error creating mini widget: {e}") else: # No mini widget, try to show main dashboard self._toggle_overlay() # Switch to this plugin if self.dashboard: self.dashboard.show_plugin(plugin_id) def _toggle_overlay(self): """Toggle dashboard visibility.""" debug_logger.start_timer("MAIN_toggle_overlay") debug_logger.debug("MAIN", f"_toggle_overlay called, dashboard exists: {self.dashboard is not None}") if self.dashboard: if self.dashboard.isVisible(): debug_logger.debug("MAIN", "Hiding dashboard...") self.dashboard.hide() debug_logger.debug("MAIN", "Dashboard hidden") else: debug_logger.debug("MAIN", "Showing dashboard...") self.dashboard.show() debug_logger.debug("MAIN", "Dashboard shown, calling raise_...") self.dashboard.raise_() debug_logger.debug("MAIN", "Calling activateWindow...") self.dashboard.activateWindow() debug_logger.debug("MAIN", "Dashboard activation complete") else: debug_logger.warn("MAIN", "Dashboard is None!") debug_logger.end_timer("MAIN_toggle_overlay") def _toggle_activity_bar(self): """Toggle activity bar visibility via overlay controller.""" debug_logger.start_timer("MAIN_toggle_activity_bar") debug_logger.debug("MAIN", f"_toggle_activity_bar called, controller exists: {hasattr(self, 'overlay_controller') and self.overlay_controller is not None}") if hasattr(self, 'overlay_controller') and self.overlay_controller: try: self.overlay_controller.toggle() # Update tray icon checkbox is_visible = self.activity_bar.isVisible() if self.activity_bar else False if hasattr(self, 'tray_icon') and self.tray_icon: self.tray_icon.set_activity_bar_checked(is_visible) debug_logger.debug("MAIN", f"Activity bar toggled, now visible: {is_visible}") except Exception as e: debug_logger.error("MAIN", f"Error toggling activity bar: {e}") else: debug_logger.warn("MAIN", "Overlay Controller not available") debug_logger.end_timer("MAIN_toggle_activity_bar") def _load_overlay_widgets(self): """Load saved overlay widgets.""" widget_settings = self.settings.get('overlay_widgets', {}) for widget_name, config in widget_settings.items(): if config.get('enabled', False): try: self.overlay_manager.create_widget( widget_name.replace('_overlay', ''), widget_name ) except Exception as e: print(f"Failed to load overlay widget {widget_name}: {e}") def create_overlay_widget(self, widget_type, name): """Create an overlay widget.""" if self.overlay_manager: return self.overlay_manager.create_widget(widget_type, name) return None def quit(self): """Quit the application.""" print("[Core] Shutting down...") # Stop overlay controller if hasattr(self, 'overlay_controller') and self.overlay_controller: print("[Core] Stopping Overlay Controller...") self.overlay_controller.stop() # Stop log reader if hasattr(self, 'log_reader'): self.log_reader.stop() # Close all notifications if self.notification_manager: self.notification_manager.close_all() # Shutdown Event Bus if hasattr(self, 'event_bus') and self.event_bus: print("[Core] Shutting down Event Bus...") self.event_bus.shutdown() # Shutdown Audio if hasattr(self, 'audio_manager') and self.audio_manager: print("[Core] Shutting down Audio...") self.audio_manager.shutdown() # Shutdown Task Manager if hasattr(self, 'task_manager') and self.task_manager: print("[Core] Shutting down Task Manager...") self.task_manager.shutdown(wait=True, timeout=30.0) # Window manager has no persistent resources to clean up if self.overlay_manager: self.overlay_manager.hide_all() if self.plugin_manager: self.plugin_manager.shutdown_all() self.app.quit() def main(): """Entry point.""" app = EUUtilityApp() sys.exit(app.run()) if __name__ == "__main__": main()