#!/usr/bin/env python # -*- coding: utf-8 -*- """ NexusLauncher - Main Program A modern application launcher """ import sys import ctypes import customtkinter as ctk from config import ConfigManager from config.constants import ( BG_COLOR_DARK, BG_COLOR_BUTTON, BG_COLOR_BUTTON_HOVER, BORDER_COLOR, SEGMENTED_BUTTON_SELECTED_COLOR, SEGMENTED_BUTTON_SELECTED_HOVER_COLOR, SEGMENTED_BUTTON_UNSELECTED_COLOR, SEGMENTED_BUTTON_UNSELECTED_HOVER_COLOR, DROPDOWN_FG_COLOR, DROPDOWN_HOVER_COLOR, TEXT_COLOR_PRIMARY, COLOR_TRANSPARENT ) from ui import SettingsWindow, get_icons_dir, IconManager from ui.task import TaskPanel from ui.project import ProjectPanel from ui.utilities import WindowManager, UIHelpers class NexusLauncher(ctk.CTk): """Main application class""" def __init__(self): super().__init__() # Debug mode control self.debug_mode = False # Create a splash screen self.splash = None try: from ui import SplashScreen self.splash = SplashScreen(self) except Exception as e: self._log(f"Failed to create splash screen: {e}", "WARNING") # Initialization Manager self.config_manager = ConfigManager() self.window_manager = WindowManager(self, self.config_manager) # Set Windows AppUserModelID self.window_manager.setup_window_appid() # Icon Management self.icons_dir = get_icons_dir() self.icon_size = self.config_manager.get_icon_size() self.icon_manager = IconManager(self.icons_dir, self.icon_size) # Window Configuration self._setup_window() # Create Interface self._create_widgets() # Binding events self._bind_events() # Initialize UI self._initialize_ui() # Set tray and window close events self.protocol("WM_DELETE_WINDOW", self.window_manager.hide_window) self.window_manager.setup_tray_icon() def _log(self, message: str, level: str = "INFO"): """Unified logging method Args: message: Log messages level: Log levels (DEBUG, INFO, WARNING, ERROR) """ # DEBUG level logs are only output in debug mode. if level == "DEBUG" and not self.debug_mode: return prefix = f"[{level}]" full_message = f"{prefix} {message}" print(full_message) def _setup_window(self): """Set window properties""" self.title("NexusLauncher") width, height = self.config_manager.get_window_size() self.minsize(200, 200) # Position the window to the bottom right corner self.window_manager.position_window_bottom_right(width, height) # Set icon and theme self.window_manager.set_window_icon() ctk.set_appearance_mode("dark") self.configure(fg_color=BG_COLOR_DARK) def _create_widgets(self): """Creating UI Components""" # Configuring Grid Layout self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(1, weight=1) # Create each part self._create_header() self._create_project_area() def _create_header(self): """Create a top menu bar""" self.menu_frame = ctk.CTkFrame(self, height=40, corner_radius=0) self.menu_frame.grid(row=0, column=0, sticky="ew", padx=0, pady=0) self.menu_frame.grid_columnconfigure(0, weight=1) # Title ctk.CTkLabel( self.menu_frame, text="NexusLauncher", font=ctk.CTkFont(size=16, weight="bold") ).grid(row=0, column=0, padx=20, pady=8, sticky="w") # Set button ctk.CTkButton( self.menu_frame, text="⚙ Setting", command=self._open_settings, width=90, height=30, font=ctk.CTkFont(size=12) ).grid(row=0, column=1, padx=20, pady=8, sticky="e") def _create_project_area(self): """Create project area""" self.project_frame = ctk.CTkFrame(self, corner_radius=0, fg_color=COLOR_TRANSPARENT) self.project_frame.grid(row=1, column=0, sticky="nsew", padx=0, pady=0) self.project_frame.grid_columnconfigure(0, weight=1) self.project_frame.grid_rowconfigure(1, weight=1) self._create_project_selector() self._create_tabview() def _create_project_selector(self): """Project creation selection dropdown""" project_select_frame = ctk.CTkFrame(self.project_frame, fg_color=COLOR_TRANSPARENT) project_select_frame.grid(row=0, column=0, sticky="ew", padx=20, pady=(3, 3)) project_select_frame.grid_columnconfigure(1, weight=1) ctk.CTkLabel( project_select_frame, text="Current Project:", font=ctk.CTkFont(size=13, weight="bold") ).grid(row=0, column=0, padx=(0, 10), sticky="w") self.project_combo = ctk.CTkComboBox( project_select_frame, command=self._on_project_changed, height=32, font=ctk.CTkFont(size=12), dropdown_font=ctk.CTkFont(size=12), button_color=BG_COLOR_BUTTON, button_hover_color=BG_COLOR_BUTTON_HOVER, border_color=BORDER_COLOR, border_width=1, state="readonly", corner_radius=8, dropdown_fg_color=DROPDOWN_FG_COLOR, dropdown_hover_color=DROPDOWN_HOVER_COLOR, dropdown_text_color=TEXT_COLOR_PRIMARY, justify="left" ) self.project_combo.grid(row=0, column=1, sticky="ew") def _create_tabview(self): """Create a tab""" self.tabview = ctk.CTkTabview( self.project_frame, height=40, corner_radius=10, segmented_button_fg_color=SEGMENTED_BUTTON_UNSELECTED_COLOR, segmented_button_selected_color=SEGMENTED_BUTTON_SELECTED_COLOR, segmented_button_selected_hover_color=SEGMENTED_BUTTON_SELECTED_HOVER_COLOR, segmented_button_unselected_color=SEGMENTED_BUTTON_UNSELECTED_COLOR, segmented_button_unselected_hover_color=SEGMENTED_BUTTON_UNSELECTED_HOVER_COLOR, anchor="w" ) self.tabview.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5)) self._create_project_tab() self._create_task_tab() self.tabview.configure(command=self._on_tab_changed) self.tabview.set("Project") def _create_project_tab(self): """Create Project tab""" self.project_tab = self.tabview.add("Project") self.project_tab.grid_columnconfigure(0, weight=1) self.project_tab.grid_rowconfigure(0, weight=1) # Use ProjectPanel self.project_panel = ProjectPanel( self.project_tab, self.config_manager, self.icon_manager, log_callback=self.window_manager.log_with_timestamp, fg_color=COLOR_TRANSPARENT ) self.project_panel.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) # Set icon size self.project_panel.set_icon_size(self.icon_size) # Initialize project background color self._update_project_background() def _create_task_tab(self): """Create Task tab""" self.task_tab = self.tabview.add("Task") self.task_tab.grid_columnconfigure(0, weight=1) self.task_tab.grid_rowconfigure(0, weight=1) # Using TaskPanel self.task_panel = TaskPanel(self.task_tab, self.config_manager, fg_color=COLOR_TRANSPARENT) self.task_panel.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) # Initialize project colors self._update_task_colors() def _bind_events(self): """Binding events""" self.project_combo.bind("", self._on_combo_click, add="+") self.bind_all("", self._on_zoom, add="+") self.bind("", self._on_window_resize) def _initialize_ui(self): """Initialize UI""" self.after(100, lambda: UIHelpers.adjust_tab_button_width(self.tabview)) self.after(150, self._configure_tab_style) self.after(200, lambda: UIHelpers.fix_dropdown_width(self.project_combo)) self.after(250, self._update_tab_appearance) # Delay updating the tab appearance to ensure the UI is fully initialized. self._update_project_list() # Initialize Project tab content self.after(300, self.project_panel.refresh) # Close the startup screen if self.splash: self.after(350, self._close_splash) def _close_splash(self): """Close the startup screen""" if self.splash: try: self.splash.close() self.splash = None except: pass def _configure_tab_style(self): """Configure tab styles""" UIHelpers.configure_tab_transparency(self.project_tab, self.task_tab) def _on_combo_click(self, event): """Drop-down click event""" self.after(1, lambda: UIHelpers.fix_dropdown_width(self.project_combo)) def _on_tab_changed(self): """Tab switching event""" current_tab = self.tabview.get() self._log(f"Switched to tab: {current_tab}", "DEBUG") if current_tab == "Task": self.task_panel.refresh() elif current_tab == "Project": self.project_panel.refresh() def _on_zoom(self, event): """Handling zoom events""" # Check if the event originated from the main window. widget = event.widget if hasattr(widget, 'winfo_toplevel'): if widget.winfo_toplevel() != self: return # Delegate to project_panel return self.project_panel.handle_zoom(event) def _on_window_resize(self, event): """Window resize event""" if event.widget == self: if hasattr(self, '_resize_timer'): self.after_cancel(self._resize_timer) self._resize_timer = self.after(150, self._on_resize_complete) def _on_resize_complete(self): """Processing after window adjustment""" try: self.project_panel.on_window_resize() UIHelpers.adjust_tab_button_width(self.tabview) except Exception as e: self._log(f"Window adjustment handling failed: {e}", "WARNING") def _update_project_list(self): """Update project list""" projects = self.config_manager.get_projects() if projects: self.project_combo.configure(values=projects) current_project = self.config_manager.get_current_project() if current_project in projects: self.project_combo.set(current_project) else: self.project_combo.set(projects[0]) self.config_manager.set_current_project(projects[0]) else: self.project_combo.configure(values=["No project"]) self.project_combo.set("No project") def _on_project_changed(self, choice): """Project Switching Event""" self.config_manager.set_current_project(choice) self._update_tab_appearance() self.project_panel.refresh() self.task_panel.refresh() def _update_tab_appearance(self): """Update tab appearance""" current_project = self.config_manager.get_current_project() if not current_project: return # Update background color self._update_project_background() self._update_task_colors() # Update tab icons (including height settings). self.project_panel.update_tab_icon(self.tabview, current_project) def _update_project_background(self): """Update project panel background color""" current_project = self.config_manager.get_current_project() if current_project: project_color = self.config_manager.get_project_color(current_project) if project_color: self.project_panel.configure(fg_color=project_color) if hasattr(self.project_panel, 'update_background_color'): self.project_panel.update_background_color(project_color) def _update_task_colors(self): """Update task panel colors""" current_project = self.config_manager.get_current_project() if current_project: project_color = self.config_manager.get_project_color(current_project) if project_color and hasattr(self.task_panel, 'update_colors'): self.task_panel.update_colors(project_color) def _open_settings(self): """Open settings window""" self.window_manager.log_with_timestamp("🔧 Open settings window") settings_window = SettingsWindow(self, self.config_manager, self._on_settings_updated) def _on_settings_updated(self): """Set the callback after the update""" self.window_manager.log_with_timestamp("🔄 Settings have been updated. Please reload the application.") self._update_project_list() self._update_tab_appearance() self.project_panel.refresh() def log(self, message: str): """Log recording (delegated to window_manager)""" self.window_manager.log(message) def main(): """Main function""" # Create a mutex lock to ensure that only one instance runs. mutex_name = "Global\\NexusLauncher_SingleInstance_Mutex" try: # Try to create a mutex lock kernel32 = ctypes.windll.kernel32 mutex = kernel32.CreateMutexW(None, False, mutex_name) last_error = kernel32.GetLastError() # ERROR_ALREADY_EXISTS = 183 if last_error == 183: print("[INFO] NexusLauncher is already running and attempting to display the main window....") # Try to find and activate an existing window. try: user32 = ctypes.windll.user32 # First try finding the window. hwnd = user32.FindWindowW(None, "NexusLauncher") if hwnd: # Show the window (whether it's hidden or minimized). # SW_SHOW = 5, SW_RESTORE = 9 user32.ShowWindow(hwnd, 9) # First restore user32.ShowWindow(hwnd, 5) # Re-display # Bring the window to the foreground and activate it. user32.SetForegroundWindow(hwnd) user32.SetActiveWindow(hwnd) print("[INFO] The existing NexusLauncher window is now displayed.") else: print("[WARNING] NexusLauncher window handle not found") print("[INFO] The program may run in the system tray; please open it from the tray menu.") except Exception as e: print(f"[WARNING] Unable to activate an existing window: {e}") print("[INFO] Please open NexusLauncher from the system tray.") sys.exit(0) # Launch application app = NexusLauncher() app.mainloop() # Release the mutex if mutex: kernel32.CloseHandle(mutex) except Exception as e: print(f"[ERROR] Startup failed: {e}") sys.exit(1) if __name__ == "__main__": main()