""" Main Window GUI The primary application window with project grid """ import customtkinter as ctk from tkinter import filedialog, messagebox import os import threading import subprocess from typing import Optional from PIL import Image from gui.styles import COLORS, FONTS, WINDOW_CONFIG, TILE_SIZE, get_button_styles, get_colors from gui.project_tile import ProjectTile, AddProjectTile from gui.gitea_explorer import GiteaExplorer from gui.sidebar_view import SidebarView from gui.settings_dialog import SettingsDialog from project_manager import ProjectManager, Project from terminal_launcher import TerminalLauncher from readme_generator import ReadmeGenerator from vps_connection import VPSConnection from process_manager import ProcessManager from project_process_tracker import ProjectProcessTracker from src.gitea.repository_manager import RepositoryManager from pathlib import Path from utils.logger import logger class MainWindow: def __init__(self): # Load and set theme preference FIRST before anything else self.load_and_apply_theme() logger.info("Initializing Claude Project Manager") # Setup global exception handler self._setup_exception_handler() # Configure customtkinter ctk.set_default_color_theme("blue") # Initialize components logger.info("Initializing components") self.project_manager = ProjectManager() self.terminal_launcher = TerminalLauncher() self.readme_generator = ReadmeGenerator() self.vps_connection = VPSConnection() self.process_manager = ProcessManager() self.process_tracker = ProjectProcessTracker() self.repo_manager = RepositoryManager() # Set project manager reference self.process_tracker.set_project_manager(self.project_manager) # Store project tiles for updates self.project_tiles = {} self.selected_project_tile = None # Interaction tracking self.user_interacting = False self.pending_updates = [] # Create main window self.root = ctk.CTk() self.root.title(WINDOW_CONFIG['title']) self.root.geometry(f"{WINDOW_CONFIG['width']}x{WINDOW_CONFIG['height']}") self.root.minsize(WINDOW_CONFIG['min_width'], WINDOW_CONFIG['min_height']) # Make gitea_operation accessible from tiles self.root.gitea_operation = self.gitea_operation # Set window icon if available try: icon_path = os.path.join(os.path.dirname(__file__), "../icon.ico") if os.path.exists(icon_path): self.root.iconbitmap(icon_path) except: pass # Initialize handlers AFTER root is created (Phase 2 refactoring) self._init_handlers() self.setup_ui() self.refresh_projects() logger.info("Claude Project Manager initialized successfully") # Bind window resize event self.root.bind('', self.on_window_resize) self.resize_timer = None # Bind window close event self.root.protocol("WM_DELETE_WINDOW", self.on_closing) # Bind interaction events self.setup_interaction_tracking() # Start periodic status check after a delay self.root.after(30000, self.check_process_status) # Start after 30 seconds # Initialize activity sync self.init_activity_sync() def _init_handlers(self): """Initialize refactored handlers (Phase 2/3 refactoring)""" # Load feature flags from configuration from gui.config import refactoring_config self.refactoring_config = refactoring_config self.REFACTORING_FLAGS = refactoring_config.flags logger.info(f"Refactoring flags loaded: {self.REFACTORING_FLAGS}") # Initialize handlers try: from gui.handlers.gitea_operations import GiteaOperationsHandler from gui.handlers.process_manager import ProcessManagerHandler from gui.handlers.project_manager import ProjectManagerHandler from gui.handlers.ui_helpers import UIHelpersHandler self._gitea_handler = GiteaOperationsHandler(self) self._process_handler = ProcessManagerHandler(self) self._project_handler = ProjectManagerHandler(self) self._ui_handler = UIHelpersHandler(self) logger.info("Refactoring handlers initialized successfully") except Exception as e: logger.error(f"Failed to initialize handlers: {e}") # Disable all flags if initialization fails for key in self.REFACTORING_FLAGS: self.REFACTORING_FLAGS[key] = False def setup_ui(self): """Setup the main UI""" # Main container self.main_container = ctk.CTkFrame(self.root, fg_color=COLORS['bg_primary']) self.main_container.pack(fill="both", expand=True) # Header self.create_header() # Content area with scrollable frame self.create_content_area() # Status bar self.create_status_bar() def create_header(self): """Create header with title and toolbar - Facade method""" if hasattr(self, '_ui_handler') and self.REFACTORING_FLAGS.get('USE_UI_HELPERS', False): return self._ui_handler.create_header() else: return self._original_create_header() def _original_create_header(self): """Create header with title and toolbar""" header_frame = ctk.CTkFrame(self.main_container, fg_color=COLORS['bg_secondary'], height=80) header_frame.pack(fill="x", padx=0, pady=0) header_frame.pack_propagate(False) # Title self.title_label = ctk.CTkLabel( header_frame, text="IntelSight - Claude Project Manager", font=FONTS['heading'], text_color=COLORS['text_primary'] ) self.title_label.pack(side="left", padx=30, pady=20) # Toolbar buttons toolbar = ctk.CTkFrame(header_frame, fg_color="transparent") toolbar.pack(side="right", padx=30, pady=20) # WinSCP button self.winscp_btn = ctk.CTkButton( toolbar, text="📁 WinSCP", command=self.open_winscp, width=100, fg_color=COLORS['accent_primary'], hover_color=COLORS['accent_hover'], text_color="#FFFFFF", font=('Segoe UI', 12) ) self.winscp_btn.pack(side="left", padx=(0, 10)) # Log download button self.log_btn = ctk.CTkButton( toolbar, text="📥 Log", command=self.download_log, width=80, fg_color=COLORS['accent_primary'], hover_color=COLORS['accent_hover'], text_color="#FFFFFF", font=('Segoe UI', 12) ) self.log_btn.pack(side="left", padx=(0, 10)) # Refresh button self.refresh_btn = ctk.CTkButton( toolbar, text="↻ Refresh", command=self.refresh_projects, width=100, fg_color=COLORS['bg_tile'], hover_color=COLORS['bg_tile_hover'], text_color=COLORS['text_primary'] ) self.refresh_btn.pack(side="left", padx=(0, 10)) # Settings button self.settings_btn = ctk.CTkButton( toolbar, text="⚙️", command=self.open_settings, width=40, fg_color=COLORS['bg_tile'], hover_color=COLORS['bg_tile_hover'], text_color=COLORS['text_primary'], font=('Segoe UI', 16) ) self.settings_btn.pack(side="left") def create_content_area(self): """Create content area with Gitea sidebar and project tiles""" # Main content container content_frame = ctk.CTkFrame(self.main_container, fg_color="transparent") content_frame.pack(fill="both", expand=True) # Left sidebar for SidebarView self.sidebar_frame = ctk.CTkFrame(content_frame, fg_color=COLORS['bg_secondary'], width=300) self.sidebar_frame.pack(side="left", fill="y", padx=(20, 10), pady=20) self.sidebar_frame.pack_propagate(False) # SidebarView with tabs/tree modes self.sidebar_view = SidebarView( self.sidebar_frame, on_repo_select=self.on_gitea_repo_select ) self.sidebar_view.pack(fill="both", expand=True) self.sidebar_view.set_main_window(self) # Set reference for cross-selection # Keep gitea_explorer reference for compatibility self.gitea_explorer = self.sidebar_view.get_gitea_explorer() # Right side for project tiles tiles_container = ctk.CTkFrame(content_frame, fg_color="transparent") tiles_container.pack(side="right", fill="both", expand=True, padx=(10, 20), pady=20) # Scrollable frame container self.scroll_container = ctk.CTkScrollableFrame( tiles_container, fg_color=COLORS['bg_primary'] ) self.scroll_container.pack(fill="both", expand=True) # Flow frame for tiles (using pack instead of grid) self.flow_frame = ctk.CTkFrame(self.scroll_container, fg_color="transparent") self.flow_frame.pack(fill="both", expand=True) def create_status_bar(self): """Create status bar at bottom - Facade method""" if hasattr(self, '_ui_handler') and self.REFACTORING_FLAGS.get('USE_UI_HELPERS', False): return self._ui_handler.create_status_bar() else: return self._original_create_status_bar() def _original_create_status_bar(self): """Create status bar at bottom""" self.status_bar = ctk.CTkFrame(self.main_container, fg_color=COLORS['bg_secondary'], height=30) self.status_bar.pack(fill="x", side="bottom") self.status_bar.pack_propagate(False) self.status_label = ctk.CTkLabel( self.status_bar, text="Ready", font=FONTS['small'], text_color=COLORS['text_secondary'] ) self.status_label.pack(side="left", padx=20, pady=5) # Activity status (clickable) self.activity_frame = ctk.CTkFrame(self.status_bar, fg_color="transparent") self.activity_frame.pack(side="left", padx=(20, 0)) self.activity_label = ctk.CTkLabel( self.activity_frame, text="", font=FONTS['small'], text_color=COLORS['accent_primary'], cursor="hand2" ) self.activity_label.pack(side="left") self.activity_label.bind("", self.show_activity_details) # Project count self.count_label = ctk.CTkLabel( self.status_bar, text="0 projects", font=FONTS['small'], text_color=COLORS['text_secondary'] ) self.count_label.pack(side="right", padx=20, pady=5) def refresh_projects(self, differential=False): """Refresh the project grid""" # Skip if user is interacting if self.user_interacting: self.pending_updates.append(('refresh', None)) return logger.info(f"Refreshing projects (differential: {differential})") # Get all projects projects = self.project_manager.get_all_projects() # Update count project_count = len([p for p in projects if p.id not in ["vps-permanent", "admin-panel-permanent", "vps-docker-permanent"]]) self.count_label.configure(text=f"{project_count} project{'s' if project_count != 1 else ''}") if differential and self.project_tiles: # Differential update - only update changed tiles self._differential_update(projects) return # Full refresh - clear and rebuild for widget in self.flow_frame.winfo_children(): widget.destroy() self.project_tiles.clear() # Calculate how many tiles can fit in a row window_width = self.scroll_container.winfo_width() if self.scroll_container.winfo_width() > 1 else WINDOW_CONFIG['width'] - 60 tile_width = TILE_SIZE['width'] tile_margin = 10 tiles_per_row = max(1, window_width // (tile_width + tile_margin * 2)) # Create row frames current_row_frame = None tiles_in_current_row = 0 # Helper function to create a new row def create_new_row(): nonlocal current_row_frame, tiles_in_current_row current_row_frame = ctk.CTkFrame(self.flow_frame, fg_color="transparent") current_row_frame.pack(fill="x", pady=5) tiles_in_current_row = 0 # Start with first row create_new_row() # Add VPS tile first vps_project = next((p for p in projects if p.id == "vps-permanent"), None) if vps_project: self.create_project_tile_flow(vps_project, current_row_frame, is_vps=True) tiles_in_current_row += 1 # Add Admin Panel tile second admin_project = next((p for p in projects if p.id == "admin-panel-permanent"), None) if admin_project: if tiles_in_current_row >= tiles_per_row: create_new_row() self.create_project_tile_flow(admin_project, current_row_frame, is_vps=True) tiles_in_current_row += 1 # Add VPS Docker tile third vps_docker_project = next((p for p in projects if p.id == "vps-docker-permanent"), None) if vps_docker_project: if tiles_in_current_row >= tiles_per_row: create_new_row() self.create_project_tile_flow(vps_docker_project, current_row_frame, is_vps=True) tiles_in_current_row += 1 # Add project tiles for project in projects: if project.id not in ["vps-permanent", "admin-panel-permanent", "vps-docker-permanent"]: if tiles_in_current_row >= tiles_per_row: create_new_row() self.create_project_tile_flow(project, current_row_frame) tiles_in_current_row += 1 # Add "Add Project" tile if tiles_in_current_row >= tiles_per_row: create_new_row() self.create_add_tile_flow(current_row_frame) self.update_status("Projects refreshed") def create_project_tile(self, project: Project, row: int, col: int, is_vps: bool = False): """Create a project tile (legacy grid method)""" # This method is kept for compatibility but not used pass def create_add_tile(self, row: int, col: int): """Create add project tile (legacy grid method)""" # This method is kept for compatibility but not used pass def create_project_tile_flow(self, project: Project, parent_frame, is_vps: bool = False): """Create a project tile in flow layout""" is_running = self.process_tracker.is_running(project.id) tile = ProjectTile( parent_frame, project, on_open=self.open_project, on_readme=self.open_readme, on_delete=self.delete_project if not is_vps else None, on_stop=self.stop_project, is_vps=is_vps, is_running=is_running, on_rename=self.rename_project if not is_vps else None, on_select=self.on_project_select ) # Set main window reference for activity methods tile.main_window = self tile.pack(side="left", padx=10, pady=10) self.project_tiles[project.id] = tile def create_add_tile_flow(self, parent_frame): """Create add project tile in flow layout""" add_tile = AddProjectTile(parent_frame, on_add=self.add_new_project) add_tile.pack(side="left", padx=10, pady=10) def add_new_project(self): """Add a new project""" logger.info("Adding new project") # Open folder dialog folder_path = filedialog.askdirectory( title="Select Project Folder" ) if folder_path: # Get project name from folder project_name = os.path.basename(folder_path) logger.info(f"Selected project folder: {folder_path}") # Add to manager project = self.project_manager.add_project(project_name, folder_path) # Generate README in background threading.Thread( target=self.generate_readme_background, args=(project,), daemon=True ).start() # Don't launch Claude automatically - just add the project self.update_status(f"Added project: {project_name}") logger.info(f"Successfully added project: {project_name}") # Refresh grid with differential update self.refresh_projects(differential=True) def open_project(self, project: Project): """Open a project in Claude""" logger.info(f"Opening project: {project.name}") if project.id == "vps-permanent": # Handle VPS connection self.open_vps_connection() elif project.id == "admin-panel-permanent": # Handle Admin Panel self.open_admin_panel() elif project.id == "vps-docker-permanent": # Handle VPS Docker self.open_vps_docker() else: # Check if already running if self.process_tracker.is_running(project.id): self.update_status(f"Claude already running for: {project.name}", error=True) return # Normal project process = self.terminal_launcher.launch_claude_wsl(project.path, project.name) if process: # Track the process self.process_manager.processes[project.id] = process self.process_manager.save_process_data() self.process_tracker.set_running(project.id) # Update tile status immediately if project.id in self.project_tiles: self.project_tiles[project.id].update_status(True) self.update_status(f"Started: {project.name}") # Update last accessed project.update_last_accessed() self.project_manager.save_projects() # Update README in background threading.Thread( target=self.generate_readme_background, args=(project,), daemon=True ).start() # Start monitoring the process self.monitor_process(project.id, process) else: self.update_status(f"Failed to start: {project.name}", error=True) # Re-enable button if launch failed if project.id in self.project_tiles: self.project_tiles[project.id].update_status(False) def open_vps_connection(self): """Open VPS connection""" # Check if already running if self.process_tracker.is_running("vps-permanent"): self.update_status("VPS connection already active", error=True) return # Create VPS README if needed vps_readme_dir = os.path.join(os.path.dirname(__file__), "../data/vps_readme") os.makedirs(vps_readme_dir, exist_ok=True) vps_readme_path = os.path.join(vps_readme_dir, "VPS_README.md") # Generate VPS README self.vps_connection.generate_vps_readme(vps_readme_path) # Create connection script script = self.vps_connection.create_ssh_script() import tempfile with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f: f.write(script) script_path = f.name # Launch terminal import subprocess if os.path.exists(r"C:\Windows\System32\wt.exe"): process = subprocess.Popen([ "wt.exe", "-w", "0", "new-tab", "--title", "Claude VPS Server", "--", "cmd", "/c", script_path ]) else: process = subprocess.Popen(['cmd', '/c', 'start', 'Claude VPS', script_path]) # Track the process self.process_manager.processes["vps-permanent"] = process self.process_manager.save_process_data() self.process_tracker.set_running("vps-permanent") # Update tile status immediately if "vps-permanent" in self.project_tiles: self.project_tiles["vps-permanent"].update_status(True) self.update_status("Connected to VPS server") # Update VPS project vps_project = self.project_manager.get_project("vps-permanent") if vps_project: vps_project.update_last_accessed() self.project_manager.save_projects() # Monitor the process self.monitor_process("vps-permanent", process) def open_admin_panel(self): """Open Admin Panel via VPS connection with directory change""" # Check if already running if self.process_tracker.is_running("admin-panel-permanent"): self.update_status("Admin Panel already running", error=True) return # Create connection script for Admin Panel script = self.vps_connection.create_admin_panel_script() import tempfile with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f: f.write(script) script_path = f.name # Launch terminal import subprocess if os.path.exists(r"C:\Windows\System32\wt.exe"): process = subprocess.Popen([ "wt.exe", "-w", "0", "new-tab", "--title", "Claude Admin Panel", "--", "cmd", "/c", script_path ]) else: process = subprocess.Popen(['cmd', '/c', 'start', 'Claude Admin Panel', script_path]) # Track the process self.process_manager.processes["admin-panel-permanent"] = process self.process_manager.save_process_data() self.process_tracker.set_running("admin-panel-permanent") # Update tile status immediately if "admin-panel-permanent" in self.project_tiles: self.project_tiles["admin-panel-permanent"].update_status(True) self.update_status("Connected to Admin Panel") # Update Admin Panel project admin_project = self.project_manager.get_project("admin-panel-permanent") if admin_project: admin_project.update_last_accessed() self.project_manager.save_projects() # Monitor the process self.monitor_process("admin-panel-permanent", process) def open_vps_docker(self): """Open VPS Docker connection for Admin Panel restart""" # Check if already running if self.process_tracker.is_running("vps-docker-permanent"): self.update_status("VPS Docker restart already running", error=True) return # Create connection script for VPS Docker script = self.vps_connection.create_vps_docker_script() import tempfile with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f: f.write(script) script_path = f.name # Launch terminal import subprocess if os.path.exists(r"C:\Windows\System32\wt.exe"): process = subprocess.Popen([ "wt.exe", "-w", "0", "new-tab", "--title", "VPS Docker Restart", "--", "cmd", "/c", script_path ]) else: process = subprocess.Popen(['cmd', '/c', 'start', 'VPS Docker', script_path]) # Track the process self.process_manager.processes["vps-docker-permanent"] = process self.process_manager.save_process_data() self.process_tracker.set_running("vps-docker-permanent") # Update tile status immediately if "vps-docker-permanent" in self.project_tiles: self.project_tiles["vps-docker-permanent"].update_status(True) self.update_status("Starting VPS Docker Admin Panel restart") # Update VPS Docker project vps_docker_project = self.project_manager.get_project("vps-docker-permanent") if vps_docker_project: vps_docker_project.update_last_accessed() self.project_manager.save_projects() # Monitor the process self.monitor_process("vps-docker-permanent", process) def open_readme(self, project: Project): """Open project README""" if project.id == "vps-permanent": # VPS README readme_path = os.path.join(os.path.dirname(__file__), "../data/vps_readme/VPS_README.md") if os.path.exists(readme_path): os.startfile(readme_path) else: self.update_status("VPS README not found", error=True) elif project.id == "admin-panel-permanent": # Admin Panel doesn't have a README - just show a message self.update_status("Admin Panel has no README file") elif project.id == "vps-docker-permanent": # VPS Docker doesn't have a README - just show a message self.update_status("VPS Docker has no README file") else: # Project README readme_path = os.path.join(project.path, "CLAUDE_PROJECT_README.md") if not os.path.exists(readme_path): # Generate if doesn't exist self.update_status("Generating README...") self.readme_generator.generate_and_save_readme(project.path, project.name) # Open with default editor try: os.startfile(readme_path) self.update_status(f"Opened README: {project.name}") except: self.update_status(f"Could not open README", error=True) def delete_project(self, project: Project): """Delete a project from manager (not files) - Facade method""" if hasattr(self, '_project_handler') and self.REFACTORING_FLAGS.get('USE_PROJECT_HANDLER', False): return self._project_handler.delete_project(project) else: return self._original_delete_project(project) def _original_delete_project(self, project: Project): """Delete a project from manager (not files)""" logger.info(f"Attempting to delete project: {project.name}") if messagebox.askyesno("Projekt löschen", f"Möchten Sie das Projekt '{project.name}' wirklich aus dem Projekt-Manager entfernen?\n\n" "Hinweis: Die Dateien werden NICHT gelöscht."): self.project_manager.remove_project(project.id) self.refresh_projects() self.update_status(f"Removed: {project.name}") def rename_project(self, project: Project): """Rename a project's display name""" # Create simple dialog dialog = ctk.CTkToplevel(self.root) dialog.title("Anzeigename ändern") dialog.geometry("400x150") dialog.configure(fg_color=COLORS['bg_primary']) # Center the dialog dialog.transient(self.root) dialog.update_idletasks() x = (dialog.winfo_screenwidth() - 400) // 2 y = (dialog.winfo_screenheight() - 150) // 2 dialog.geometry(f"400x150+{x}+{y}") # Make dialog grab focus dialog.grab_set() dialog.lift() dialog.attributes('-topmost', True) # Label label = ctk.CTkLabel( dialog, text=f"Neuer Anzeigename für '{project.name}':", font=FONTS['tile_text'], text_color=COLORS['text_primary'] ) label.pack(pady=(20, 10)) # Entry entry = ctk.CTkEntry( dialog, width=350, font=FONTS['tile_text'], fg_color=COLORS['bg_secondary'], text_color=COLORS['text_primary'] ) entry.pack(pady=5) entry.insert(0, project.name) entry.select_range(0, 'end') # Force focus after a small delay dialog.after(100, lambda: entry.focus_force()) # Buttons button_frame = ctk.CTkFrame(dialog, fg_color="transparent") button_frame.pack(pady=15) def save_rename(): new_name = entry.get().strip() if new_name and new_name != project.name: # Update project name self.project_manager.update_project(project.id, name=new_name) # Update tile if project.id in self.project_tiles: self.project_tiles[project.id].update_project( self.project_manager.get_project(project.id) ) self.update_status(f"Renamed to: {new_name}") dialog.grab_release() dialog.destroy() # Save on Enter entry.bind("", lambda e: save_rename()) save_btn = ctk.CTkButton( button_frame, text="Speichern", command=save_rename, width=100, **get_button_styles()['primary'] ) save_btn.pack(side="left", padx=5) cancel_btn = ctk.CTkButton( button_frame, text="Abbrechen", command=lambda: [dialog.grab_release(), dialog.destroy()], width=100, **get_button_styles()['secondary'] ) cancel_btn.pack(side="left", padx=5) # Bind Escape key to cancel dialog.bind("", lambda e: [dialog.grab_release(), dialog.destroy()]) def generate_readme_background(self, project: Project): """Generate README in background thread""" try: self.readme_generator.generate_and_save_readme(project.path, project.name) except Exception as e: print(f"Error generating README: {e}") def update_status(self, message: str, error: bool = False): """Update status bar message - Facade method""" if hasattr(self, '_process_handler') and self.REFACTORING_FLAGS.get('USE_PROCESS_HANDLER', False): return self._process_handler.update_status(message, error) else: return self._original_update_status(message, error) def _original_update_status(self, message: str, error: bool = False): """Update status bar message""" self.status_label.configure( text=message, text_color=COLORS['accent_error'] if error else COLORS['text_secondary'] ) def download_log(self): """Download application log file""" logger.info("Log download button clicked") # Always use the original implementation for now return self._original_download_log() def _original_download_log(self): """Download application log file""" try: logger.info("Opening file dialog for log export") # Create default filename from datetime import datetime default_filename = f"CPM_Log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" # Open file dialog to choose save location file_path = filedialog.asksaveasfilename( defaultextension=".log", filetypes=[("Log files", "*.log"), ("Text files", "*.txt"), ("All files", "*.*")], initialfile=default_filename ) if file_path: logger.info(f"Exporting log to: {file_path}") # Export logs to chosen location logger.export_logs(file_path) self.update_status(f"Log saved to: {os.path.basename(file_path)}") logger.info(f"Log exported successfully to: {file_path}") messagebox.showinfo("Export erfolgreich", f"Log wurde gespeichert:\n{os.path.basename(file_path)}") else: logger.info("Log export cancelled by user") except Exception as e: error_msg = f"Error saving log: {str(e)}" self.update_status(error_msg, error=True) logger.error(error_msg, exc_info=True) messagebox.showerror("Export Error", error_msg) def open_settings(self): """Open settings dialog""" settings_dialog = SettingsDialog(self.root, self.sidebar_view) def open_winscp(self): """Open WinSCP with VPS connection""" try: import os import tempfile from pathlib import Path # VPS connection details vps_host = "91.99.192.14" vps_username = "claude-dev" vps_password = "z0E1Al}q2H?Yqd!O" # Check if WinSCP is in tools folder winscp_portable = Path(__file__).parent.parent / "tools" / "WinSCP" / "WinSCP.exe" # Alternative: Check if WinSCP is installed winscp_installed = r"C:\Program Files (x86)\WinSCP\WinSCP.exe" winscp_installed_64 = r"C:\Program Files\WinSCP\WinSCP.exe" winscp_path = None if winscp_portable.exists(): winscp_path = str(winscp_portable) logger.info(f"Using portable WinSCP: {winscp_path}") elif os.path.exists(winscp_installed): winscp_path = winscp_installed logger.info(f"Using installed WinSCP (x86): {winscp_path}") elif os.path.exists(winscp_installed_64): winscp_path = winscp_installed_64 logger.info(f"Using installed WinSCP (x64): {winscp_path}") else: # WinSCP not found from tkinter import messagebox messagebox.showwarning( "WinSCP nicht gefunden", "WinSCP wurde nicht gefunden.\n\n" "Bitte installieren Sie WinSCP von:\n" "https://winscp.net/download/WinSCP-Portable.zip\n\n" "Oder entpacken Sie WinSCP Portable in:\n" f"{winscp_portable.parent}" ) return # Create WinSCP session URL # Format: sftp://username:password@hostname/ session_url = f"sftp://{vps_username}:{vps_password}@{vps_host}/" # Start WinSCP with session URL cmd = f'"{winscp_path}" "{session_url}"' logger.info(f"Starting WinSCP with VPS connection") import subprocess subprocess.Popen(cmd, shell=True) self.update_status("WinSCP gestartet") except Exception as e: error_msg = f"Fehler beim Starten von WinSCP: {str(e)}" self.update_status(error_msg, error=True) logger.error(error_msg) messagebox.showerror("WinSCP Fehler", error_msg) def refresh_ui(self): """Refresh the entire UI""" # Import updated styles from gui.styles import COLORS, BUTTON_STYLES # Update main container self.main_container.configure(fg_color=COLORS['bg_primary']) # Update header for widget in self.main_container.winfo_children(): if isinstance(widget, ctk.CTkFrame): widget.configure(fg_color=COLORS['bg_secondary'] if widget.winfo_y() < 100 else COLORS['bg_primary']) # Update sidebar frame self.sidebar_frame.configure(fg_color=COLORS['bg_secondary']) # Update scroll container self.scroll_container.configure(fg_color=COLORS['bg_primary']) # Update status bar self.status_bar.configure(fg_color=COLORS['bg_secondary']) self.status_label.configure(text_color=COLORS['text_secondary']) self.count_label.configure(text_color=COLORS['text_secondary']) # Update title label self.title_label.configure(text_color=COLORS['text_primary']) # Update log button self.log_btn.configure( fg_color=COLORS['accent_primary'], hover_color=COLORS['accent_hover'], text_color="#FFFFFF" ) # Update refresh button self.refresh_btn.configure( fg_color=COLORS['bg_tile'], hover_color=COLORS['bg_tile_hover'], text_color=COLORS['text_primary'] ) # Update Gitea Explorer colors self.gitea_explorer.refresh_colors() # Update existing project tiles without full refresh self._update_project_tiles_colors() def load_and_apply_theme(self): """Apply dark mode - Facade method""" if hasattr(self, '_ui_handler') and self.REFACTORING_FLAGS.get('USE_UI_HELPERS', False): return self._ui_handler.load_and_apply_theme() else: return self._original_load_and_apply_theme() def _original_load_and_apply_theme(self): """Apply dark mode (only mode available)""" # Always use dark mode ctk.set_appearance_mode('dark') def on_window_resize(self, event): """Handle window resize events - Facade method""" if hasattr(self, '_ui_handler') and self.REFACTORING_FLAGS.get('USE_UI_HELPERS', False): return self._ui_handler.on_window_resize(event) else: return self._original_on_window_resize(event) def _original_on_window_resize(self, event): """Handle window resize events""" # Only process resize events from the main window if event.widget == self.root: # Cancel previous timer if self.resize_timer: self.root.after_cancel(self.resize_timer) # Set new timer to refresh after resize stops with differential update self.resize_timer = self.root.after(300, lambda: self.refresh_projects(differential=True)) def stop_project(self, project: Project): """Stop a running project""" logger.info(f"Stopping project: {project.name}") # Always try to stop, even if process manager doesn't have it tracked self.process_manager.stop_process(project.id, project.name) self.process_tracker.set_stopped(project.id) self.update_status(f"Stopped: {project.name}") # Update tile status if project.id in self.project_tiles: self.project_tiles[project.id].update_status(False) def _differential_update(self, projects): """Update only changed project tiles""" project_dict = {p.id: p for p in projects} existing_ids = set(self.project_tiles.keys()) new_ids = set(project_dict.keys()) # Remove tiles for deleted projects for removed_id in existing_ids - new_ids: if removed_id in self.project_tiles: self.project_tiles[removed_id].destroy() del self.project_tiles[removed_id] # Update existing tiles for project_id in existing_ids & new_ids: project = project_dict[project_id] tile = self.project_tiles[project_id] # Update project data tile.update_project(project) # Update running status is_running = self.process_tracker.is_running(project_id) if tile.is_running != is_running: tile.update_status(is_running) # Add new tiles (this is more complex due to flow layout) # For now, trigger full refresh if new projects are added if new_ids - existing_ids: self.refresh_projects(differential=False) def _update_project_tiles_colors(self): """Update colors of existing project tiles without recreating them""" from gui.styles import COLORS, BUTTON_STYLES # Update each project tile's colors for tile in self.project_tiles.values(): if hasattr(tile, 'is_vps') and not tile.is_vps: # Update regular project tile colors tile.configure(fg_color=COLORS['bg_tile']) # Update labels if hasattr(tile, 'title_label'): tile.title_label.configure(text_color=COLORS['text_primary']) if hasattr(tile, 'path_label'): tile.path_label.configure(text_color=COLORS['text_secondary']) if hasattr(tile, 'tags_label'): tile.tags_label.configure(text_color=COLORS['text_dim']) if hasattr(tile, 'time_label'): tile.time_label.configure(text_color=COLORS['text_dim']) if hasattr(tile, 'status_indicator'): tile.status_indicator.configure( text_color=COLORS['accent_success'] if tile.is_running else COLORS['accent_error'] ) if hasattr(tile, 'status_message'): tile.status_message.configure(text_color=COLORS['accent_warning']) # Update button styles if hasattr(tile, 'open_button'): btn_style = BUTTON_STYLES['danger'] if tile.is_running else BUTTON_STYLES['primary'] tile.open_button.configure( fg_color=btn_style['fg_color'], hover_color=btn_style['hover_color'], text_color=btn_style.get('text_color', '#FFFFFF') ) if hasattr(tile, 'gitea_button'): tile.gitea_button.configure( fg_color=BUTTON_STYLES['secondary']['fg_color'], hover_color=BUTTON_STYLES['secondary']['hover_color'], text_color=BUTTON_STYLES['secondary']['text_color'] ) if hasattr(tile, 'explorer_button'): tile.explorer_button.configure( fg_color=BUTTON_STYLES['secondary']['fg_color'], hover_color=BUTTON_STYLES['secondary']['hover_color'], text_color=BUTTON_STYLES['secondary']['text_color'] ) if hasattr(tile, 'delete_button'): tile.delete_button.configure( fg_color=COLORS['bg_secondary'], hover_color=COLORS['accent_error'], text_color=COLORS['text_secondary'] ) # Update Add Project Tile if it exists for widget in self.flow_frame.winfo_children(): if isinstance(widget, ctk.CTkFrame): for child in widget.winfo_children(): if child.__class__.__name__ == 'AddProjectTile': child.configure( fg_color=COLORS['bg_secondary'], border_color=COLORS['border_primary'] ) # Update nested labels in AddProjectTile for subwidget in child.winfo_children(): if isinstance(subwidget, ctk.CTkFrame): for label in subwidget.winfo_children(): if isinstance(label, ctk.CTkLabel): if label.cget("font")[1] == 48: # Folder icon label.configure(text_color=COLORS['text_dim']) elif "Add New Project" in label.cget("text"): label.configure(text_color=COLORS['text_secondary']) elif "Click to select" in label.cget("text"): label.configure(text_color=COLORS['text_dim']) def setup_interaction_tracking(self): """Setup tracking for user interactions""" # Track when dropdowns or menus are active self.root.bind_all("", self._on_click_start) self.root.bind_all("", self._on_click_end) self.root.bind_all("<>", self._on_dropdown_select) self.root.bind_all("", self._on_focus_in) self.root.bind_all("", self._on_focus_out) def _on_click_start(self, event): """Track start of user interaction""" self.user_interacting = True def _on_click_end(self, event): """Track end of user interaction""" # Delay to allow dropdown/menu operations to complete self.root.after(500, self._check_pending_updates) def _on_dropdown_select(self, event): """Handle dropdown selection""" self.user_interacting = True self.root.after(500, self._check_pending_updates) def _on_focus_in(self, event): """Track focus events""" if isinstance(event.widget, (ctk.CTkComboBox, ctk.CTkOptionMenu)): self.user_interacting = True def _on_focus_out(self, event): """Track focus loss""" self.root.after(200, self._check_pending_updates) def _check_pending_updates(self): """Process pending updates after interaction ends""" self.user_interacting = False # Process any pending updates if self.pending_updates: updates = self.pending_updates[:] self.pending_updates.clear() # Process unique update types update_types = set(u[0] for u in updates) if 'refresh' in update_types: self.refresh_projects(differential=True) elif 'status_check' in update_types: # Run status check immediately self.check_process_status() def monitor_process(self, project_id: str, process: subprocess.Popen): """Monitor a process and update status when it ends""" def check_process(): try: # Wait for process to complete process.wait() # Process has ended, update status self.root.after(0, self._handle_process_ended, project_id) except: pass # Start monitoring in background thread monitor_thread = threading.Thread(target=check_process, daemon=True) monitor_thread.start() def _handle_process_ended(self, project_id: str): """Handle when a process ends (called in main thread)""" # Update tracking self.process_tracker.set_stopped(project_id) if project_id in self.process_manager.processes: del self.process_manager.processes[project_id] self.process_manager.save_process_data() # Update tile status if project_id in self.project_tiles: self.project_tiles[project_id].update_status(False) # Update status bar project = self.project_manager.get_project(project_id) if project: self.update_status(f"Claude closed: {project.name}") def check_process_status(self): """Periodically check the status of all processes""" # Skip if user is interacting if self.user_interacting: self.pending_updates.append(('status_check', None)) # Schedule next check self.root.after(30000, self.check_process_status) return # Only check if we have any supposedly running projects if any(tile.is_running for tile in self.project_tiles.values()): # Check each running project tile for project_id, tile in self.project_tiles.items(): if tile.is_running: # Only check running projects actual_running = self.process_tracker.is_running(project_id) if not actual_running: # Only update the specific tile, not the entire UI tile.update_status(False) self.process_tracker.set_stopped(project_id) # Schedule next check in 30 seconds (less frequent) self.root.after(30000, self.check_process_status) def open_gitea_window(self): """Open Gitea integration window""" # Create Gitea window if not already open gitea_window = ctk.CTkToplevel(self.root) gitea_window.title("Gitea Integration") gitea_window.geometry("1200x700") gitea_window.configure(fg_color=COLORS['bg_primary']) # Center the window gitea_window.transient(self.root) gitea_window.update_idletasks() x = (gitea_window.winfo_screenwidth() - 1200) // 2 y = (gitea_window.winfo_screenheight() - 700) // 2 gitea_window.geometry(f"1200x700+{x}+{y}") # Create frame for Gitea UI gitea_frame = ctk.CTkFrame(gitea_window, fg_color=COLORS['bg_primary']) gitea_frame.pack(fill="both", expand=True, padx=10, pady=10) # Import and create Gitea UI try: from src.gitea.gitea_ui_ctk import GiteaIntegrationUI # Initialize Gitea UI with the window gitea_ui = GiteaIntegrationUI(gitea_window) self.update_status("Gitea Integration opened") except Exception as e: import traceback traceback.print_exc() messagebox.showerror("Error", f"Failed to open Gitea Integration: {e}") gitea_window.destroy() def on_gitea_repo_select(self, repo): """Handle Gitea repository selection""" logger.info(f"Gitea repo selected: {repo.get('name', 'Unknown')}") # Clear project tile selection when selecting a Gitea repo if hasattr(self, 'selected_project_tile') and self.selected_project_tile: self.selected_project_tile.set_selected(False) self.selected_project_tile = None self.selected_project = None # No toolbar needed anymore - using dropdown in tiles def clear_project_selection(self): """Clear the current project tile selection""" if self.selected_project_tile: self.selected_project_tile.set_selected(False) self.selected_project_tile = None self.selected_project = None # Show toolbar for Gitea repo status_text = f"Repository: {repo['name']}" if repo.get('private'): status_text += " (Privat)" self.gitea_toolbar.show_for_context('gitea_repo', repo, status_text) def on_project_select(self, project): """Handle project tile selection""" self.selected_project = project # Update visual selection if self.selected_project_tile and project.id in self.project_tiles: # Deselect previous tile self.selected_project_tile.set_selected(False) # Select new tile if project.id in self.project_tiles: self.selected_project_tile = self.project_tiles[project.id] self.selected_project_tile.set_selected(True) # Clear Gitea selection when project is selected if hasattr(self, 'gitea_explorer'): self.gitea_explorer.clear_selection() # No toolbar anymore - using dropdown buttons # Git operation callbacks def show_git_status(self, item): """Show git status for project or repo""" if isinstance(item, Project): # For local project try: # Hide console window on Windows startupinfo = None if os.name == 'nt': # Windows startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE result = subprocess.run( ["git", "status", "--porcelain"], cwd=item.path, capture_output=True, text=True, startupinfo=startupinfo ) if result.stdout: messagebox.showinfo("Git Status", f"Uncommitted changes:\n{result.stdout}") else: messagebox.showinfo("Git Status", "Working directory clean") except Exception as e: messagebox.showerror("Error", f"Failed to get git status: {e}") else: # For Gitea repo messagebox.showinfo("Repository Info", f"Name: {item['name']}\n" f"Private: {'Yes' if item.get('private') else 'No'}\n" f"Description: {item.get('description', 'No description')}") def commit_changes(self, project): """Commit changes for a project""" from tkinter import simpledialog # Check for changes git_ops = self.repo_manager.git_ops success, status = git_ops.status(Path(project.path)) if not status.strip(): messagebox.showinfo("Keine Änderungen", "Keine Änderungen zum Committen vorhanden.") return # Ask for commit message message = simpledialog.askstring( "Commit", "Commit-Nachricht eingeben:", parent=self.root ) if message: # Add all changes git_ops.add(Path(project.path)) # Commit success, result = git_ops.commit(Path(project.path), message) if success: messagebox.showinfo("Erfolg", "Änderungen erfolgreich committet!") else: messagebox.showerror("Fehler", f"Commit fehlgeschlagen: {result}") def push_to_gitea(self, project): """Push project to Gitea - Facade method""" if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False): return self._gitea_handler.push_to_gitea(project) else: return self._original_push_to_gitea(project) def _original_push_to_gitea(self, project): """Push project to Gitea""" import logging logger = logging.getLogger(__name__) project_path = Path(project.path) if hasattr(project, 'path') else Path(project['path']) project_name = project.name if hasattr(project, 'name') else project.get('name', 'Unknown') # Check current remote git_ops = self.repo_manager.git_ops success, remotes = git_ops.remote_list(project_path) if success: logger.info(f"Current remotes before push: {remotes}") # Check for large files before push large_files = git_ops.check_large_files(project_path, 50) # 50MB limit if large_files: msg = "⚠️ Große Dateien gefunden!\n\n" msg += "Folgende Dateien überschreiten 50MB:\n\n" total_size = 0 for file, size in large_files[:10]: # Show first 10 size_mb = size / (1024 * 1024) msg += f" • {file} ({size_mb:.1f} MB)\n" total_size += size if len(large_files) > 10: msg += f" ... und {len(large_files) - 10} weitere\n" total_mb = total_size / (1024 * 1024) msg += f"\nGesamtgröße: {total_mb:.1f} MB\n\n" # Simple solution: offer to exclude files msg += "Diese großen Dateien verhindern den Push zu Gitea.\n\n" msg += "EMPFEHLUNG: Große Dateien aus Git entfernen\n\n" msg += "Möchten Sie diese Dateien aus Git entfernen?\n" msg += "(Die Dateien bleiben auf Ihrer Festplatte erhalten)" if messagebox.askyesno("Große Dateien gefunden", msg, icon='warning'): # Remove large files from git but keep them locally try: # First create .gitignore gitignore_path = project_path / ".gitignore" existing_content = "" if gitignore_path.exists(): with open(gitignore_path, 'r', encoding='utf-8') as f: existing_content = f.read() # Add large files to .gitignore with open(gitignore_path, 'a', encoding='utf-8') as f: if existing_content and not existing_content.endswith('\n'): f.write('\n') f.write("\n# Große Dateien (automatisch hinzugefügt)\n") for file, _ in large_files: f.write(f"{file}\n") # Remove files from git index but keep local copies removed_count = 0 for file, _ in large_files: cmd = ["git", "rm", "--cached", file] success, _, _ = git_ops._run_git_command(cmd, cwd=project_path) if success: removed_count += 1 if removed_count > 0: # Commit the changes git_ops.add(Path(project_path), [".gitignore"]) success, _ = git_ops.commit(Path(project_path), f"Große Dateien aus Git entfernt ({removed_count} Dateien)") if success: messagebox.showinfo("Erfolg", f"{removed_count} große Dateien wurden aus Git entfernt.\n\n" "Die Dateien sind weiterhin lokal vorhanden.\n" "Sie können jetzt den Push erneut versuchen.") # Continue with push success, result = git_ops.push(project_path) if success: messagebox.showinfo("Push erfolgreich", "Änderungen wurden zu Gitea gepusht!") return else: messagebox.showerror("Push fehlgeschlagen", f"Push fehlgeschlagen: {result}") return else: messagebox.showerror("Fehler", "Konnte Änderungen nicht committen.") return except Exception as e: messagebox.showerror("Fehler", f"Fehler beim Entfernen der Dateien: {str(e)}") return else: # User wants alternative solution if messagebox.askyesno("Alternative", "Möchten Sie die großen Dateien manuell bearbeiten?\n\n" "Optionen:\n" "- Dateien komprimieren (z.B. ZIP)\n" "- Dateien auf externen Speicher verschieben\n" "- Dateien in kleinere Teile aufteilen\n\n" "Soll ich eine .gitignore-Datei erstellen?"): gitignore_path = project_path / ".gitignore" try: # Read existing .gitignore if exists existing_content = "" if gitignore_path.exists(): with open(gitignore_path, 'r', encoding='utf-8') as f: existing_content = f.read() # Add large files with open(gitignore_path, 'a', encoding='utf-8') as f: if existing_content and not existing_content.endswith('\n'): f.write('\n') f.write("\n# Große Dateien (automatisch hinzugefügt)\n") for file, _ in large_files: f.write(f"{file}\n") messagebox.showinfo("Erfolg", ".gitignore wurde aktualisiert.\n\n" "Bitte committen Sie die .gitignore-Datei und versuchen Sie es erneut.") except Exception as e: messagebox.showerror("Fehler", f"Konnte .gitignore nicht erstellen: {str(e)}") return # Perform push success, result = git_ops.push(project_path) if success: # Debug: Check where the repository actually is debug_info = "Push erfolgreich!\n\n" debug_info += f"Remote URLs:\n{remotes}\n\n" # Extract owner from remote URL import re match = re.search(r'gitea-undso\.intelsight\.de[:/]([^/]+)/([^/\.]+)', remotes) if match: remote_owner = match.group(1) remote_repo = match.group(2) debug_info += f"Push ging an: {remote_owner}/{remote_repo}\n\n" # Try to find the repository try: # Check if repo exists under detected owner repo = self.repo_manager.client.get_repository(remote_owner, remote_repo) debug_info += f"✅ Repository gefunden!\n" debug_info += f"URL: {repo.get('html_url', 'Unknown')}\n" debug_info += f"Größe: {repo.get('size', 0)} bytes\n" debug_info += f"Default Branch: {repo.get('default_branch', 'Keiner')}\n\n" except: debug_info += f"❌ Repository nicht unter {remote_owner}/{remote_repo} gefunden!\n\n" # Search in all accessible locations debug_info += "Suche Repository in allen Orten:\n" # Check user repos try: user_repos = self.repo_manager.list_all_repositories() found_in_user = any(r['name'] == remote_repo for r in user_repos) debug_info += f"- In Benutzer-Repos: {'Gefunden' if found_in_user else 'Nicht gefunden'}\n" except: debug_info += "- In Benutzer-Repos: Fehler beim Suchen\n" # Check org repos if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name: try: org_repos = self.repo_manager.list_organization_repositories(self.gitea_explorer.organization_name) found_in_org = any(r['name'] == remote_repo for r in org_repos) debug_info += f"- In Organisation {self.gitea_explorer.organization_name}: {'Gefunden' if found_in_org else 'Nicht gefunden'}\n" except: debug_info += f"- In Organisation: Fehler beim Suchen\n" debug_info += f"\nLog-Datei: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}" messagebox.showinfo("Push Debug Info", debug_info) # Refresh Gitea explorer if hasattr(self, 'gitea_explorer'): self.gitea_explorer.refresh_repositories() else: messagebox.showerror("Fehler", f"Push fehlgeschlagen: {result}") def pull_from_gitea(self, project): """Pull changes from Gitea""" git_ops = self.repo_manager.git_ops success, result = git_ops.pull(Path(project.path)) if success: messagebox.showinfo("Erfolg", "Änderungen erfolgreich gepullt!") else: messagebox.showerror("Fehler", f"Pull fehlgeschlagen: {result}") def fetch_from_gitea(self, item): """Fetch from Gitea""" if isinstance(item, Project): # For local project try: # Hide console window on Windows startupinfo = None if os.name == 'nt': # Windows startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE # Run git fetch result = subprocess.run( ["git", "fetch", "--all"], cwd=item.path, capture_output=True, text=True, startupinfo=startupinfo ) if result.returncode == 0: # Check if there are updates status_result = subprocess.run( ["git", "status", "-uno"], cwd=item.path, capture_output=True, text=True, startupinfo=startupinfo ) if "Your branch is behind" in status_result.stdout: messagebox.showinfo("Fetch Complete", "Updates available!\n\nUse Pull to get the latest changes.") else: messagebox.showinfo("Fetch Complete", "Already up to date.") else: messagebox.showerror("Fetch Error", f"Failed to fetch: {result.stderr}") except Exception as e: messagebox.showerror("Error", f"Failed to fetch: {e}") else: # For Gitea repo - just refresh the explorer if hasattr(self, 'gitea_explorer'): self.gitea_explorer.refresh_repositories() messagebox.showinfo("Refresh", "Repository list updated") def manage_branches(self, project): """Manage branches - Facade method (placeholder removed)""" if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False): return self._gitea_handler.manage_branches(project) else: # Skip the placeholder, use the real implementation return self._original_manage_branches_v2(project) # _original_manage_branches removed - was placeholder only def link_to_gitea(self, project): """Link to Gitea - Facade method (placeholder removed)""" if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False): return self._gitea_handler.link_to_gitea(project) else: # Skip the placeholder, use the real implementation at line 2455 return self._original_link_to_gitea_v2(project) # _original_link_to_gitea removed - was placeholder only def clone_repository(self, repo): """Clone a Gitea repository""" def do_clone(): try: # Update status in main thread self.root.after(0, lambda: self.update_status(f"Cloning {repo['name']}...")) # Clone repository - use owner from repo data if available repo_owner = repo.get('owner', {}).get('username') if repo.get('owner') else None logger.info(f"Attempting to clone repository: {repo['name']}") logger.info(f"Repository owner: {repo_owner}") logger.info(f"Current user: {self.repo_manager.current_user.get('username')}") if repo_owner and repo_owner != self.repo_manager.current_user.get('username'): # Check if it's an organization repository and user is a member try: user_orgs = self.repo_manager.client.list_user_organizations() org_names = [org.get('username', org.get('name', '')) for org in user_orgs] logger.info(f"User organizations: {org_names}") # Special handling for IntelSight organization if repo_owner == 'IntelSight': logger.info(f"Repository belongs to IntelSight organization - allowing clone") elif repo_owner not in org_names: # This is someone else's repo and user is not a member of the org logger.warning(f"User is not a member of organization: {repo_owner}") self.root.after(0, lambda: messagebox.showwarning( "Not your repository", f"This repository belongs to {repo_owner}.\nYou may need to fork it first." )) return else: logger.info(f"User is member of organization: {repo_owner}") except Exception as e: logger.error(f"Error checking organization membership: {e}") # If we can't check org membership, try to clone anyway logger.info("Proceeding with clone despite org check failure") # If we get here, the repo belongs to an org the user is a member of # Use the actual owner (user or organization) for cloning if repo_owner: success, path = self.repo_manager.git_ops.clone_repository(repo_owner, repo['name']) else: success, path = self.repo_manager.clone_repository(repo['name']) if success: # Create CPM project in main thread self.root.after(0, lambda: self._create_project_after_clone(repo['name'], str(path), repo_owner)) else: # Get more detailed error information error_msg = "Failed to clone repository.\n\n" error_msg += f"Repository: {repo['name']}\n" error_msg += f"Please check:\n" error_msg += "- Internet connection\n" error_msg += "- Git is installed\n" error_msg += "- Repository permissions" self.root.after(0, lambda: messagebox.showerror("Clone Error", error_msg)) except Exception as e: import traceback error_details = traceback.format_exc() print(f"Clone error details:\n{error_details}") error_msg = f"Failed to clone repository: {str(e)}\n\n" if "get_user_info" in str(e): error_msg += "Cannot connect to Gitea server. Please check your connection." elif "git" in str(e).lower(): error_msg += "Git might not be installed or accessible." self.root.after(0, lambda: messagebox.showerror("Clone Error", error_msg)) # Run in thread to avoid blocking UI threading.Thread(target=do_clone, daemon=True).start() def _create_project_after_clone(self, repo_name, path, repo_owner=None): """Create project after successful clone""" try: # Add project to CPM project = self.project_manager.add_project(repo_name, path) # Update project with Gitea repo reference if owner is provided if repo_owner: project.gitea_repo = f"{repo_owner}/{repo_name}" self.project_manager.update_project(project.id, gitea_repo=project.gitea_repo) # Refresh the project grid with differential update self.refresh_projects(differential=True) # Show success message self.update_status(f"Repository '{repo_name}' cloned and added to projects") messagebox.showinfo("Success", f"Repository cloned to:\n{path}\n\nProject added to CPM!") # Also refresh the Gitea explorer to update status if hasattr(self, 'gitea_explorer'): self.gitea_explorer.refresh_repositories() except Exception as e: messagebox.showerror("Error", f"Repository cloned but failed to create project: {e}") def create_project_from_repo(self, repo): """Create a CPM project from cloned repository""" local_path = Path.home() / "GiteaRepos" / repo['name'] if local_path.exists(): self.project_manager.create_project(repo['name'], str(local_path)) self.refresh_projects(differential=True) messagebox.showinfo("Success", f"Project created for {repo['name']}") else: messagebox.showwarning("Not Cloned", "Repository must be cloned first") def on_closing(self): """Handle window closing event""" # Stop all running processes self.process_manager.stop_all_processes() self.process_tracker.stop_all() # Destroy the window self.root.destroy() def gitea_operation(self, project, operation: str): """Handle Gitea operations from project tiles""" if operation == "status": self.show_git_status(project) elif operation == "commit": self.commit_changes(project) elif operation == "push": self.push_to_gitea(project) elif operation == "pull": self.pull_from_gitea(project) elif operation == "fetch": self.fetch_from_gitea(project) elif operation == "link": self.link_to_gitea(project) elif operation == "branch": self.manage_branches(project) elif operation == "init_push": self.init_and_push_to_gitea(project) elif operation == "init": self.init_git_repo(project) elif operation == "test": self.test_gitea_connection(project) elif operation == "verify": self.verify_repository_on_gitea(project) elif operation == "lfs": self.setup_git_lfs(project) elif operation == "manage_large": self.manage_large_files(project) elif operation == "fix_repo": self.fix_repository_issues(project) def init_and_push_to_gitea(self, project): """Initialize git repo and push to Gitea - Facade method""" if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False): return self._gitea_handler.init_and_push_to_gitea(project) else: return self._original_init_and_push_to_gitea(project) def _original_init_and_push_to_gitea(self, project): """Initialize git repo and push to Gitea""" from tkinter import simpledialog # Ask for repository name repo_name = simpledialog.askstring( "Neues Repository", f"Repository-Name für '{project.name}':", initialvalue=project.name ) while repo_name: try: # Log the creation attempt import logging logger = logging.getLogger(__name__) # Always try to create in organization first if available org_name = None if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name: org_name = self.gitea_explorer.organization_name # Create repository on Gitea if org_name and (not hasattr(self, 'gitea_explorer') or self.gitea_explorer.view_mode != "user"): # Create in organization logger.info(f"Creating repository '{repo_name}' in organization '{org_name}'") repo = self.repo_manager.create_repository(repo_name, auto_init=False, organization=org_name) else: # Create as user repository logger.info(f"Creating repository '{repo_name}' as user repository") repo = self.repo_manager.create_repository(repo_name, auto_init=False, organization=None) logger.info(f"Repository created: {repo}") # Verify repository was created repo_owner = repo.get('owner', {}).get('username', 'Unknown') repo_url = repo.get('html_url', 'Unknown') # Check if repo was created in the correct place expected_owner = org_name if org_name else self.repo_manager.client.config.username if repo_owner != expected_owner: messagebox.showwarning("Achtung", f"Repository wurde unter falschem Owner erstellt!\n\n" f"Erwartet: {expected_owner}\n" f"Erstellt unter: {repo_owner}\n\n" f"URL: {repo_url}") else: messagebox.showinfo("Repository erstellt", f"Repository '{repo_name}' wurde erstellt.\n\n" f"Owner: {repo_owner}\n" f"URL: {repo_url}") # Initialize local git repo git_ops = self.repo_manager.git_ops success, msg = git_ops.init_repository(Path(project.path)) if success: # Check for large files before adding large_files = [] import os for root, dirs, files in os.walk(project.path): # Skip .git directory if '.git' in root: continue for file in files: file_path = os.path.join(root, file) try: # Check if file is larger than 50MB (Gitea's typical limit) file_size = os.path.getsize(file_path) if file_size > 50 * 1024 * 1024: size_mb = file_size / (1024 * 1024) # Store relative path from project root rel_path = os.path.relpath(file_path, project.path) large_files.append((rel_path, size_mb)) except: pass if large_files: msg = (f"⚠️ WARNUNG: Große Dateien gefunden!\n\n" f"Gitea hat ein Upload-Limit. Folgende Dateien sind zu groß:\n\n") total_size = 0 for i, (file, size_mb) in enumerate(large_files[:5]): msg += f" • {file} ({size_mb:.1f} MB)\n" total_size += size_mb if len(large_files) > 5: msg += f" ... und {len(large_files) - 5} weitere\n" msg += f"\nGesamtgröße großer Dateien: {total_size:.1f} MB\n" msg += ("\n❌ DIESE DATEIEN KÖNNEN NICHT ZU GITEA GEPUSHT WERDEN!\n\n" "Optionen:\n" "1. Abbrechen und große Dateien entfernen\n" "2. Große Dateien zur .gitignore hinzufügen\n" "3. Git LFS einrichten (fortgeschritten)\n\n" "Trotzdem fortfahren? (Push wird fehlschlagen!)") if not messagebox.askyesno("Große Dateien gefunden", msg, icon='warning'): # Help user create .gitignore if messagebox.askyesno("Hilfe", "Soll ich eine .gitignore-Datei mit den großen Dateien erstellen?"): gitignore_path = Path(project.path) / ".gitignore" with open(gitignore_path, 'a', encoding='utf-8') as f: f.write("\n# Große Dateien automatisch hinzugefügt\n") for file, _ in large_files: f.write(f"{file}\n") messagebox.showinfo("Erfolg", ".gitignore wurde aktualisiert. Bitte erneut versuchen.") return # Add all files git_ops.add(Path(project.path)) # Initial commit git_ops.commit(Path(project.path), "Initial commit") # Add remote and push (use organization if repo was created there) # Get owner from the created repository owner = None if 'owner' in repo and repo['owner']: owner = repo['owner'].get('username', repo['owner'].get('login')) if not owner: # Fallback to determining owner based on mode if hasattr(self, 'gitea_explorer') and self.gitea_explorer.view_mode == "organization": if self.gitea_explorer.organization_name: owner = self.gitea_explorer.organization_name else: owner = self.repo_manager.client.config.username logger.info(f"Pushing to repository with owner: {owner}, repo: {repo_name}") logger.info(f"Repository details: {repo}") success, msg = git_ops.push_existing_repo_to_gitea( Path(project.path), owner, repo_name ) if success: # Show push details for debugging push_info = f"Push-Details:\n" push_info += f"Repository: {repo_name}\n" push_info += f"Owner: {owner}\n" push_info += f"Branch: {current_branch} -> main\n\n" # Get remote URL success_remote, remotes = git_ops.remote_list(Path(project.path)) if success_remote: push_info += f"Remote URLs:\n{remotes}\n\n" # Verify the repository exists on Gitea try: verify_repo = self.repo_manager.client.get_repository(owner, repo_name) repo_url = verify_repo.get('html_url', 'Unknown') clone_url = verify_repo.get('clone_url', 'Unknown') # Check if repository has content has_content = verify_repo.get('size', 0) > 0 or verify_repo.get('default_branch') push_info += f"Repository gefunden auf Gitea:\n" push_info += f"URL: {repo_url}\n" push_info += f"Clone URL: {clone_url}\n" push_info += f"Hat Inhalt: {'Ja' if has_content else 'Nein'}\n" push_info += f"Größe: {verify_repo.get('size', 0)} bytes\n" push_info += f"Default Branch: {verify_repo.get('default_branch', 'Keiner')}\n\n" if not has_content: push_info += "⚠️ WARNUNG: Repository existiert aber scheint leer zu sein!\n\n" push_info += "Bitte prüfen Sie:\n" push_info += f"1. Öffnen Sie: {repo_url}\n" push_info += f"2. Log-Datei: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}" # Update project with Gitea repo reference project.gitea_repo = f"{owner}/{repo_name}" self.project_manager.update_project(project.id, gitea_repo=project.gitea_repo) messagebox.showinfo("Push abgeschlossen", push_info) except Exception as e: push_info += f"\n⚠️ Konnte Repository nicht auf Gitea verifizieren:\n{str(e)}\n\n" push_info += "Mögliche Ursachen:\n" push_info += "- Repository wurde unter anderem Namen/Owner erstellt\n" push_info += "- Berechtigungsprobleme\n" push_info += "- Netzwerkprobleme\n\n" push_info += f"Log-Datei prüfen: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}" messagebox.showwarning("Push Status unklar", push_info) # Refresh Gitea explorer if exists if hasattr(self, 'gitea_explorer'): self.gitea_explorer.refresh_repositories() else: messagebox.showerror("Fehler", f"Push fehlgeschlagen: {msg}") else: messagebox.showerror("Fehler", f"Git-Initialisierung fehlgeschlagen: {msg}") break except Exception as e: error_msg = str(e) if "409" in error_msg: # Repository exists already action = messagebox.askyesno( "Repository existiert bereits", f"Ein Repository mit dem Namen '{repo_name}' existiert bereits.\n\n" "Möchten Sie einen anderen Namen wählen?" ) if action: repo_name = simpledialog.askstring( "Neues Repository", f"Bitte anderen Namen wählen ('{repo_name}' existiert bereits):", initialvalue=f"{repo_name}-2" ) else: break else: messagebox.showerror("Fehler", f"Fehler beim Erstellen des Repositories: {error_msg}") break def init_git_repo(self, project): """Initialize git repository for project""" git_ops = self.repo_manager.git_ops success, msg = git_ops.init_repository(Path(project.path)) if success: messagebox.showinfo("Erfolg", "Git-Repository initialisiert!") else: messagebox.showerror("Fehler", f"Initialisierung fehlgeschlagen: {msg}") def test_gitea_connection(self, project): """Test Gitea connection - Facade method (consolidates duplicates)""" if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False): return self._gitea_handler.test_gitea_connection(project) else: # Use the second version (enhanced) as default return self._original_test_gitea_connection_v2(project) def _original_test_gitea_connection(self, project): """Test Gitea connection and show permissions""" try: # Test API connection user_info = self.repo_manager.client.get_user_info() info = "Gitea-Verbindung erfolgreich!\n\n" info += f"Benutzer: {user_info.get('username', 'Unknown')}\n" info += f"E-Mail: {user_info.get('email', 'Unknown')}\n" info += f"Admin: {'Ja' if user_info.get('is_admin', False) else 'Nein'}\n\n" # Check organizations orgs = self.repo_manager.client.list_user_organizations() if orgs: info += "Organisationen:\n" for org in orgs: org_name = org.get('username', org.get('name', 'Unknown')) info += f" • {org_name}" # Check teams/permissions in org teams = self.repo_manager.client.get_user_teams_in_org(org_name) if teams: team_names = [t.get('name', 'Unknown') for t in teams] info += f" (Teams: {', '.join(team_names)})" info += "\n" else: info += "Keine Organisationen\n" info += f"\nServer: {self.repo_manager.client.config.base_url}" messagebox.showinfo("Verbindungstest", info) except Exception as e: messagebox.showerror("Verbindungsfehler", f"Konnte keine Verbindung zu Gitea herstellen:\n\n{str(e)}") def verify_repository_on_gitea(self, project): """Verify repository on Gitea - Facade method (consolidates duplicates)""" if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False): return self._gitea_handler.verify_repository_on_gitea(project) else: # Use the second version (with scrollable info) as default return self._original_verify_repository_on_gitea_v2(project) def _original_verify_repository_on_gitea(self, project): """Verify if repository exists on Gitea and show detailed info""" import re from pathlib import Path project_path = Path(project.path) # Check if it's a git repo if not (project_path / ".git").exists(): messagebox.showinfo("Info", "Dies ist kein Git-Repository.") return # Get remote info git_ops = self.repo_manager.git_ops success, remotes = git_ops.remote_list(project_path) if not success or not remotes: messagebox.showinfo("Info", "Kein Remote-Repository konfiguriert.") return # Extract owner and repo name from remote URL match = re.search(r'gitea-undso\.intelsight\.de[:/]([^/]+)/([^/\.]+)', remotes) if not match: messagebox.showwarning("Warnung", f"Konnte Repository-Info nicht aus Remote-URL extrahieren:\n{remotes}") return remote_owner = match.group(1) remote_repo = match.group(2) info = f"Suche Repository: {remote_owner}/{remote_repo}\n\n" # Check if repo exists under detected owner try: repo = self.repo_manager.client.get_repository(remote_owner, remote_repo) info += "✅ Repository gefunden!\n\n" info += f"Name: {repo.get('name', 'Unknown')}\n" info += f"Owner: {repo.get('owner', {}).get('username', 'Unknown')}\n" info += f"URL: {repo.get('html_url', 'Unknown')}\n" info += f"Clone URL: {repo.get('clone_url', 'Unknown')}\n" info += f"Privat: {'Ja' if repo.get('private', False) else 'Nein'}\n" info += f"Größe: {repo.get('size', 0) / 1024:.1f} KB\n" info += f"Default Branch: {repo.get('default_branch', 'Keiner')}\n" info += f"Erstellt: {repo.get('created_at', 'Unknown')}\n" info += f"Zuletzt aktualisiert: {repo.get('updated_at', 'Unknown')}\n" # Check for content has_content = repo.get('size', 0) > 0 or repo.get('default_branch') if not has_content: info += "\n⚠️ WARNUNG: Repository scheint leer zu sein!\n" except Exception as e: info += f"❌ Repository nicht unter {remote_owner}/{remote_repo} gefunden!\n" info += f"Fehler: {str(e)}\n\n" # Search in all locations info += "Suche in allen verfügbaren Orten:\n\n" # Check user repos try: user_repos = self.repo_manager.list_all_repositories() found_in_user = [r for r in user_repos if r['name'] == remote_repo] if found_in_user: info += f"✅ Gefunden in Benutzer-Repos:\n" for r in found_in_user: info += f" - {r.get('owner', {}).get('username', 'Unknown')}/{r['name']}\n" else: info += "❌ Nicht in Benutzer-Repos gefunden\n" except Exception as e: info += f"❌ Fehler beim Durchsuchen der Benutzer-Repos: {str(e)}\n" # Check org repos if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name: try: org_repos = self.repo_manager.list_organization_repositories(self.gitea_explorer.organization_name) found_in_org = [r for r in org_repos if r['name'] == remote_repo] if found_in_org: info += f"✅ Gefunden in Organisation {self.gitea_explorer.organization_name}:\n" for r in found_in_org: info += f" - {r.get('owner', {}).get('username', 'Unknown')}/{r['name']}\n" else: info += f"❌ Nicht in Organisation {self.gitea_explorer.organization_name} gefunden\n" except Exception as e: info += f"❌ Fehler beim Durchsuchen der Organisations-Repos: {str(e)}\n" # Check for debug file debug_file = project_path / "gitea_push_debug.txt" if debug_file.exists(): info += f"\n\nDebug-Datei gefunden: {debug_file}\n" try: with open(debug_file, 'r', encoding='utf-8') as f: debug_content = f.read() info += "Debug-Inhalt:\n" info += "-" * 40 + "\n" info += debug_content[:1000] # First 1000 chars if len(debug_content) > 1000: info += "\n... (gekürzt)" except: info += "Konnte Debug-Datei nicht lesen.\n" info += f"\n\nLog-Datei: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}" # Show in scrollable dialog self._show_scrollable_info("Repository-Verifizierung", info) def fix_repository_issues(self, project): """Fix repository issues - Facade method""" if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False): return self._gitea_handler.fix_repository_issues(project) else: return self._original_fix_repository_issues(project) def _original_fix_repository_issues(self, project): """Fix common repository issues""" project_path = Path(project.path) git_ops = self.repo_manager.git_ops # Check current status issues = [] # Check if it's a git repo if not (project_path / ".git").exists(): issues.append("❌ Kein Git-Repository") else: # Check remote success, remotes = git_ops.remote_list(project_path) if success and remotes: issues.append(f"✅ Remote vorhanden:\n{remotes}") # Check for authentication issues if "Authentication failed" in remotes or "21cbba8d" in remotes: issues.append("⚠️ Alter/falscher Token im Remote gefunden") else: issues.append("❌ Kein Remote konfiguriert") # Check for LFS gitattributes = project_path / ".gitattributes" if gitattributes.exists(): with open(gitattributes, 'r') as f: if 'filter=lfs' in f.read(): issues.append("⚠️ Git LFS ist konfiguriert (kann Probleme verursachen)") msg = "Repository-Status:\n\n" + "\n".join(issues) + "\n\nWas möchten Sie tun?" # Create dialog dialog = ctk.CTkToplevel(self.root) dialog.title("Repository reparieren") dialog.geometry("600x400") dialog.transient(self.root) # Center dialog dialog.update_idletasks() x = (dialog.winfo_screenwidth() - 600) // 2 y = (dialog.winfo_screenheight() - 400) // 2 dialog.geometry(f"600x400+{x}+{y}") # Message text_widget = ctk.CTkTextbox(dialog, width=580, height=250) text_widget.pack(padx=10, pady=10) text_widget.insert("1.0", msg) text_widget.configure(state="disabled") # Buttons button_frame = ctk.CTkFrame(dialog) button_frame.pack(fill="x", padx=10, pady=(0, 10)) def fix_remote(): """Fix remote URL with correct credentials""" dialog.destroy() # Get current organization org_name = "IntelSight" if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name: org_name = self.gitea_explorer.organization_name # Remove old remote git_ops.remote_remove(project_path, "origin") # Add correct remote success, msg = git_ops.add_remote_to_existing_repo( project_path, org_name, project.name ) if success: messagebox.showinfo("Erfolg", f"Remote wurde korrigiert!\n\n" f"Repository: {org_name}/{project.name}\n" f"Sie können jetzt pushen.") else: messagebox.showerror("Fehler", f"Fehler beim Korrigieren: {msg}") def disable_lfs(): """Disable LFS temporarily""" dialog.destroy() success, msg = git_ops.disable_lfs_for_push(project_path) if success: messagebox.showinfo("Erfolg", "Git LFS wurde deaktiviert.\n\nVersuchen Sie den Push erneut.") else: messagebox.showerror("Fehler", f"Fehler: {msg}") def check_on_gitea(): """Check if repo exists on Gitea""" dialog.destroy() self.verify_repository_on_gitea(project) # Add buttons based on issues if any("falscher Token" in issue for issue in issues): fix_btn = ctk.CTkButton(button_frame, text="🔧 Remote korrigieren", command=fix_remote, width=180, height=40) fix_btn.pack(side="left", padx=5) if any("LFS" in issue for issue in issues): lfs_btn = ctk.CTkButton(button_frame, text="🚫 LFS deaktivieren", command=disable_lfs, width=180, height=40) lfs_btn.pack(side="left", padx=5) check_btn = ctk.CTkButton(button_frame, text="🔍 Auf Gitea prüfen", command=check_on_gitea, width=180, height=40) check_btn.pack(side="left", padx=5) close_btn = ctk.CTkButton(dialog, text="Schließen", command=dialog.destroy, width=100) close_btn.pack(pady=10) dialog.grab_set() def manage_large_files(self, project): """Manage large files - Facade method""" if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False): return self._gitea_handler.manage_large_files(project) else: return self._original_manage_large_files(project) def _original_manage_large_files(self, project): """Manage large files in the repository""" project_path = Path(project.path) git_ops = self.repo_manager.git_ops # Check for large files large_files = git_ops.check_large_files(project_path, 50) # Files > 50MB if not large_files: messagebox.showinfo("Info", "Keine großen Dateien gefunden (>50MB).\n\nIhr Repository kann problemlos gepusht werden.") return # Show large files msg = f"Gefundene große Dateien ({len(large_files)} Dateien):\n\n" total_size = 0 for i, (file, size) in enumerate(large_files[:15]): size_mb = size / (1024 * 1024) msg += f" • {file} ({size_mb:.1f} MB)\n" total_size += size if i >= 14 and len(large_files) > 15: msg += f" ... und {len(large_files) - 15} weitere\n" break total_mb = total_size / (1024 * 1024) msg += f"\nGesamtgröße: {total_mb:.1f} MB\n\n" msg += "Was möchten Sie tun?" # Create dialog with options dialog = ctk.CTkToplevel(self.root) dialog.title("Große Dateien verwalten") dialog.geometry("600x500") dialog.transient(self.root) # Center dialog dialog.update_idletasks() x = (dialog.winfo_screenwidth() - 600) // 2 y = (dialog.winfo_screenheight() - 500) // 2 dialog.geometry(f"600x500+{x}+{y}") # Message text_widget = ctk.CTkTextbox(dialog, width=580, height=300) text_widget.pack(padx=10, pady=10) text_widget.insert("1.0", msg) text_widget.configure(state="disabled") # Buttons button_frame = ctk.CTkFrame(dialog) button_frame.pack(fill="x", padx=10, pady=(0, 10)) def remove_from_git(): """Remove large files from git but keep locally""" dialog.destroy() try: # Create .gitignore gitignore_path = project_path / ".gitignore" existing_content = "" if gitignore_path.exists(): with open(gitignore_path, 'r', encoding='utf-8') as f: existing_content = f.read() # Add large files to .gitignore with open(gitignore_path, 'a', encoding='utf-8') as f: if existing_content and not existing_content.endswith('\n'): f.write('\n') f.write("\n# Große Dateien (automatisch hinzugefügt)\n") for file, _ in large_files: f.write(f"{file}\n") # Remove files from git index removed_count = 0 failed_files = [] for file, _ in large_files: cmd = ["git", "rm", "--cached", file] success, _, stderr = git_ops._run_git_command(cmd, cwd=project_path) if success: removed_count += 1 else: failed_files.append((file, stderr)) if removed_count > 0: # Commit the changes git_ops.add(Path(project_path), [".gitignore"]) success, _ = git_ops.commit(Path(project_path), f"Große Dateien aus Git entfernt ({removed_count} Dateien)") result_msg = f"✅ {removed_count} Dateien wurden aus Git entfernt.\n\n" result_msg += "Die Dateien sind weiterhin lokal vorhanden.\n" if failed_files: result_msg += f"\n⚠️ {len(failed_files)} Dateien konnten nicht entfernt werden.\n" result_msg += "\nSie können jetzt pushen!" messagebox.showinfo("Erfolg", result_msg) else: messagebox.showwarning("Warnung", "Keine Dateien konnten entfernt werden.") except Exception as e: messagebox.showerror("Fehler", f"Fehler beim Entfernen: {str(e)}") def show_gitignore(): """Show .gitignore content""" dialog.destroy() gitignore_content = "# Große Dateien\n" for file, _ in large_files: gitignore_content += f"{file}\n" # Create dialog to show content info_dialog = ctk.CTkToplevel(self.root) info_dialog.title(".gitignore Inhalt") info_dialog.geometry("500x400") label = ctk.CTkLabel(info_dialog, text="Fügen Sie diese Zeilen zu .gitignore hinzu:", font=("Arial", 14)) label.pack(pady=10) text = ctk.CTkTextbox(info_dialog, width=480, height=300) text.pack(padx=10, pady=10) text.insert("1.0", gitignore_content) def copy_to_clipboard(): self.root.clipboard_clear() self.root.clipboard_append(gitignore_content) messagebox.showinfo("Kopiert", "Inhalt wurde in die Zwischenablage kopiert!") copy_btn = ctk.CTkButton(info_dialog, text="In Zwischenablage kopieren", command=copy_to_clipboard) copy_btn.pack(pady=10) info_dialog.grab_set() def check_status(): """Check git status""" dialog.destroy() success, status = git_ops.status(project_path) if success: self._show_scrollable_info("Git Status", f"Git Status für {project.name}:\n\n{status}") else: messagebox.showerror("Fehler", "Konnte Git Status nicht abrufen.") # Add buttons remove_btn = ctk.CTkButton(button_frame, text="🗑️ Aus Git entfernen\n(Dateien bleiben lokal)", command=remove_from_git, width=180, height=60) remove_btn.pack(side="left", padx=5) gitignore_btn = ctk.CTkButton(button_frame, text="📝 .gitignore anzeigen", command=show_gitignore, width=180, height=60) gitignore_btn.pack(side="left", padx=5) status_btn = ctk.CTkButton(button_frame, text="📊 Git Status", command=check_status, width=180, height=60) status_btn.pack(side="left", padx=5) # Info text info_label = ctk.CTkLabel(dialog, text="Tipp: 'Aus Git entfernen' ist die einfachste Lösung.\nDie Dateien bleiben auf Ihrer Festplatte erhalten!", font=("Arial", 12), text_color="yellow") info_label.pack(pady=10) close_btn = ctk.CTkButton(dialog, text="Schließen", command=dialog.destroy, width=100) close_btn.pack(pady=10) dialog.grab_set() def setup_git_lfs(self, project): """Setup Git LFS - Facade method""" if hasattr(self, '_gitea_handler') and self.REFACTORING_FLAGS.get('USE_GITEA_HANDLER', False): return self._gitea_handler.setup_git_lfs(project) else: return self._original_setup_git_lfs(project) def _original_setup_git_lfs(self, project): """Setup Git LFS for the project""" project_path = Path(project.path) # Check for large files git_ops = self.repo_manager.git_ops large_files = git_ops.check_large_files(project_path, 50) # Files > 50MB if not large_files: messagebox.showinfo("Info", "Keine großen Dateien gefunden (>50MB).\n\nGit LFS ist möglicherweise nicht erforderlich.") return # Show large files and ask what to do msg = f"Gefundene große Dateien ({len(large_files)} Dateien):\n\n" total_size = 0 for i, (file, size) in enumerate(large_files[:10]): size_mb = size / (1024 * 1024) msg += f" • {file} ({size_mb:.1f} MB)\n" total_size += size if i >= 9 and len(large_files) > 10: msg += f" ... und {len(large_files) - 10} weitere\n" break total_mb = total_size / (1024 * 1024) msg += f"\nGesamtgröße: {total_mb:.1f} MB\n\n" msg += "Möchten Sie Git LFS für diese Dateien einrichten?" if messagebox.askyesno("Git LFS einrichten", msg): # Check if Git LFS is installed success, result = git_ops.setup_lfs(project_path) if not success: messagebox.showerror("Fehler", result) return # Ask which files to track choices = { "all": "Alle großen Dateien (>50MB)", "types": "Nach Dateityp (z.B. *.mp4, *.zip)", "specific": "Spezifische Dateien auswählen" } dialog = ctk.CTkToplevel(self.root) dialog.title("LFS Tracking-Optionen") dialog.geometry("400x300") dialog.transient(self.root) # Center dialog dialog.update_idletasks() x = (dialog.winfo_screenwidth() - 400) // 2 y = (dialog.winfo_screenheight() - 300) // 2 dialog.geometry(f"400x300+{x}+{y}") selected_option = tk.StringVar(value="all") label = ctk.CTkLabel(dialog, text="Wie möchten Sie die Dateien tracken?", font=("Arial", 14)) label.pack(pady=20) for key, text in choices.items(): radio = ctk.CTkRadioButton(dialog, text=text, variable=selected_option, value=key) radio.pack(pady=5, padx=20, anchor="w") result = {"ok": False} def on_ok(): result["ok"] = True result["choice"] = selected_option.get() dialog.destroy() ok_btn = ctk.CTkButton(dialog, text="OK", command=on_ok) ok_btn.pack(pady=20) dialog.grab_set() dialog.wait_window() if not result.get("ok"): return # Process based on choice patterns = [] if result["choice"] == "all": # Track all large files patterns = [file for file, _ in large_files] elif result["choice"] == "types": # Get unique extensions extensions = set() for file, _ in large_files: ext = Path(file).suffix if ext: extensions.add(ext) # Ask which extensions to track ext_msg = "Folgende Dateitypen wurden gefunden:\n" for ext in extensions: ext_msg += f" • *{ext}\n" ext_msg += "\nWelche möchten Sie mit LFS tracken?\n(Komma-getrennt, z.B. .mp4,.zip)" from tkinter import simpledialog selected = simpledialog.askstring("Dateitypen auswählen", ext_msg) if selected: for ext in selected.split(","): ext = ext.strip() if not ext.startswith("."): ext = "." + ext patterns.append(f"*{ext}") else: # Specific files - show a selection dialog # For simplicity, track all for now patterns = [file for file, _ in large_files] if patterns: # Track with LFS success, result = git_ops.track_with_lfs(project_path, patterns) if success: messagebox.showinfo("Erfolg", f"{result}\n\n" "Nächste Schritte:\n" "1. Committen Sie die .gitattributes Datei\n" "2. Große Dateien werden nun über LFS verwaltet\n" "3. Der Push sollte jetzt funktionieren") # Offer to migrate existing files if messagebox.askyesno("Migration", "Möchten Sie existierende große Dateien zu LFS migrieren?\n\n" "Dies wird die Dateien aus dem Git-History entfernen und neu hinzufügen."): success, result = git_ops.migrate_to_lfs(project_path, [f for f, _ in large_files]) if success: messagebox.showinfo("Erfolg", result) else: messagebox.showerror("Fehler", result) else: messagebox.showerror("Fehler", result) def _show_scrollable_info(self, title, content): """Show information in a scrollable dialog - Facade method""" if hasattr(self, '_ui_handler') and self.REFACTORING_FLAGS.get('USE_UI_HELPERS', False): return self._ui_handler._show_scrollable_info(title, content) else: return self._original_show_scrollable_info(title, content) def _original_show_scrollable_info(self, title, content): """Show information in a scrollable dialog""" dialog = ctk.CTkToplevel(self.root) dialog.title(title) dialog.geometry("600x500") # Center the dialog dialog.transient(self.root) dialog.update_idletasks() x = (dialog.winfo_screenwidth() - 600) // 2 y = (dialog.winfo_screenheight() - 500) // 2 dialog.geometry(f"600x500+{x}+{y}") # Text widget with scrollbar text_frame = ctk.CTkFrame(dialog) text_frame.pack(fill="both", expand=True, padx=10, pady=10) text_widget = ctk.CTkTextbox(text_frame, width=580, height=450) text_widget.pack(fill="both", expand=True) text_widget.insert("1.0", content) text_widget.configure(state="disabled") # Close button close_btn = ctk.CTkButton( dialog, text="Schließen", command=dialog.destroy, width=100 ) close_btn.pack(pady=(0, 10)) dialog.grab_set() dialog.focus_set() def _original_manage_branches_v2(self, project): """Manage branches for a project - Full implementation""" from tkinter import simpledialog git_ops = self.repo_manager.git_ops # Get current branches success, branches = git_ops.branch(Path(project.path), list_all=True) if success: current_branch = None branch_list = [] for line in branches.strip().split('\n'): if line.startswith('*'): current_branch = line[2:].strip() branch_list.append(f"{line.strip()} (aktuell)") else: branch_list.append(line.strip()) # Show options action = messagebox.askyesnocancel( "Branch-Verwaltung", f"Aktuelle Branches:\n{chr(10).join(branch_list)}\n\n" "Ja = Neuen Branch erstellen\n" "Nein = Zu anderem Branch wechseln\n" "Abbrechen = Schließen" ) if action is True: # Create new branch branch_name = simpledialog.askstring( "Neuer Branch", "Name des neuen Branches:", parent=self.root ) if branch_name: success, result = git_ops.checkout(Path(project.path), branch_name, create=True) if success: messagebox.showinfo("Erfolg", f"Branch '{branch_name}' erstellt und gewechselt") else: messagebox.showerror("Fehler", f"Branch-Erstellung fehlgeschlagen: {result}") elif action is False: # Switch branch branch_name = simpledialog.askstring( "Branch wechseln", "Zu welchem Branch wechseln?", parent=self.root ) if branch_name: success, result = git_ops.checkout(Path(project.path), branch_name) if success: messagebox.showinfo("Erfolg", f"Zu Branch '{branch_name}' gewechselt") else: messagebox.showerror("Fehler", f"Branch-Wechsel fehlgeschlagen: {result}") else: messagebox.showerror("Fehler", f"Konnte Branches nicht abrufen: {branches}") def _original_link_to_gitea_v2(self, project): """Link local project to Gitea repository - Full implementation""" from tkinter import simpledialog # Ask for repository name repo_name = simpledialog.askstring( "Mit Gitea verknüpfen", "Name des Gitea-Repositories:", initialvalue=project.name, parent=self.root ) if repo_name: git_ops = self.repo_manager.git_ops # Check if repo exists on Gitea try: repo = self.repo_manager.client.get_repository( self.repo_manager.client.config.username, repo_name ) # Add remote success, msg = git_ops.add_remote_to_existing_repo( Path(project.path), self.repo_manager.client.config.username, repo_name ) if success: # Update project with Gitea repo reference project.gitea_repo = f"{self.repo_manager.client.config.username}/{repo_name}" self.project_manager.update_project(project.id, gitea_repo=project.gitea_repo) messagebox.showinfo("Erfolg", f"Erfolgreich mit Repository '{repo_name}' verknüpft!") else: messagebox.showerror("Fehler", f"Verknüpfung fehlgeschlagen: {msg}") except Exception as e: # Repository doesn't exist if messagebox.askyesno("Repository erstellen?", f"Repository '{repo_name}' existiert nicht.\nSoll es erstellt werden?"): try: # Create repository repo = self.repo_manager.create_repository(repo_name, auto_init=False) # Add remote success, msg = git_ops.add_remote_to_existing_repo( Path(project.path), self.repo_manager.client.config.username, repo_name ) if success: # Update project with Gitea repo reference project.gitea_repo = f"{self.repo_manager.client.config.username}/{repo_name}" self.project_manager.update_project(project.id, gitea_repo=project.gitea_repo) messagebox.showinfo("Erfolg", f"Repository '{repo_name}' erstellt und verknüpft!") else: messagebox.showerror("Fehler", f"Verknüpfung fehlgeschlagen: {msg}") except Exception as create_error: messagebox.showerror("Fehler", f"Repository-Erstellung fehlgeschlagen: {str(create_error)}") def _original_test_gitea_connection_v2(self, project=None): """Test Gitea connection and show detailed information - Enhanced version""" try: # Test basic connection user_info = self.repo_manager.client.get_user_info() username = user_info.get('username', 'Unknown') # Get organizations orgs = self.repo_manager.client.list_user_organizations() org_names = [org['username'] for org in orgs] # Build info message info = f"✅ Gitea Verbindung erfolgreich!\n\n" info += f"Benutzer: {username}\n" info += f"Server: {self.repo_manager.client.config.base_url}\n" info += f"Organisationen: {', '.join(org_names) if org_names else 'Keine'}\n\n" # Check organization permissions if in org mode if hasattr(self, 'gitea_explorer') and self.gitea_explorer.view_mode == "organization": org_name = self.gitea_explorer.organization_name if org_name: teams = self.repo_manager.client.get_user_teams_in_org(org_name) if teams: info += f"Teams in {org_name}:\n" for team in teams: info += f" - {team.get('name', 'Unknown')} " perms = [] if team.get('can_create_org_repo'): perms.append("kann Repos erstellen") if team.get('permission') == 'admin': perms.append("Admin") elif team.get('permission') == 'write': perms.append("Schreiben") elif team.get('permission') == 'read': perms.append("Lesen") info += f"({', '.join(perms) if perms else 'keine Rechte'})\n" else: info += f"⚠️ Keine Teams in Organisation {org_name} gefunden!\n" info += "Dies könnte der Grund sein, warum Repositories nicht erstellt werden können.\n" info += "\n" # If we have a project, show its remote info if project: project_path = Path(project.path) if hasattr(project, 'path') else Path(project['path']) if project_path.exists(): success, remotes = self.repo_manager.git_ops.remote_list(project_path) if success and remotes: info += f"Git Remote URLs:\n{remotes}\n\n" # Check if .git exists if (project_path / '.git').exists(): # Get current branch success, branch_out = self.repo_manager.git_ops._run_git_command( ["git", "branch", "--show-current"], cwd=project_path ) if success: info += f"Aktueller Branch: {branch_out.strip()}\n" # List repositories in current mode info += "Repositories:\n" try: if hasattr(self, 'gitea_explorer'): if self.gitea_explorer.view_mode == "organization" and self.gitea_explorer.organization_name: # List org repos org_repos = self.repo_manager.list_organization_repositories(self.gitea_explorer.organization_name) info += f"In Organisation {self.gitea_explorer.organization_name}: {len(org_repos)} Repositories\n" for repo in org_repos[:5]: # Show first 5 info += f" - {repo['name']}\n" if len(org_repos) > 5: info += f" ... und {len(org_repos) - 5} weitere\n" else: # List user repos user_repos = self.repo_manager.list_all_repositories() info += f"Benutzer Repositories: {len(user_repos)}\n" for repo in user_repos[:5]: # Show first 5 info += f" - {repo['name']} (Owner: {repo.get('owner', {}).get('username', 'Unknown')})\n" if len(user_repos) > 5: info += f" ... und {len(user_repos) - 5} weitere\n" except Exception as e: info += f"Fehler beim Abrufen der Repositories: {str(e)}\n" # Show log file location log_file = Path.home() / ".claude_project_manager" / "gitea_operations.log" info += f"\nLog-Datei: {log_file}" messagebox.showinfo("Gitea Verbindungstest", info) except Exception as e: error_msg = f"❌ Gitea Verbindung fehlgeschlagen!\n\n" error_msg += f"Fehler: {str(e)}\n\n" error_msg += f"Server: {self.repo_manager.client.config.base_url}\n" error_msg += "\nBitte prüfen Sie:\n" error_msg += "- Netzwerkverbindung\n" error_msg += "- API Token Gültigkeit\n" error_msg += "- Server Erreichbarkeit" messagebox.showerror("Gitea Verbindungstest", error_msg) def _original_verify_repository_on_gitea_v2(self, project): """Verify if repository exists on Gitea and show detailed info - Enhanced version""" project_name = project.name if hasattr(project, 'name') else project.get('name', 'Unknown') project_path = Path(project.path) if hasattr(project, 'path') else Path(project['path']) info = f"Repository Verifizierung für: {project_name}\n" info += "=" * 50 + "\n\n" # Check git remotes git_ops = self.repo_manager.git_ops success, remotes = git_ops.remote_list(project_path) if success and remotes: info += f"Git Remote URLs:\n{remotes}\n\n" # Extract repo name and owner from remote import re match = re.search(r'gitea-undso\.intelsight\.de[:/]([^/]+)/([^/\.]+)', remotes) if match: remote_owner = match.group(1) remote_repo = match.group(2) info += f"Remote zeigt auf: {remote_owner}/{remote_repo}\n\n" # Search for repository info += "Suche Repository auf Gitea:\n" # 1. Check exact location try: repo = self.repo_manager.client.get_repository(remote_owner, remote_repo) info += f"✅ Gefunden unter {remote_owner}/{remote_repo}\n" info += f" URL: {repo.get('html_url', 'Unknown')}\n" info += f" Größe: {repo.get('size', 0)} bytes\n" info += f" Erstellt: {repo.get('created_at', 'Unknown')}\n" info += f" Aktualisiert: {repo.get('updated_at', 'Unknown')}\n" info += f" Default Branch: {repo.get('default_branch', 'Unknown')}\n" info += f" Privat: {'Ja' if repo.get('private') else 'Nein'}\n" except Exception as e: info += f"❌ Nicht gefunden unter {remote_owner}/{remote_repo}\n" info += f" Fehler: {str(e)}\n" # 2. Search in all user repos info += "\nSuche in allen Benutzer-Repositories:\n" try: user_repos = self.repo_manager.list_all_repositories() matching_repos = [r for r in user_repos if r['name'] == remote_repo or r['name'] == project_name] if matching_repos: for repo in matching_repos: info += f"✅ Gefunden: {repo['owner']['username']}/{repo['name']}\n" info += f" URL: {repo.get('html_url', 'Unknown')}\n" else: info += "❌ Nicht in Benutzer-Repositories gefunden\n" except Exception as e: info += f"❌ Fehler beim Durchsuchen: {str(e)}\n" # 3. Search in organization if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name: info += f"\nSuche in Organisation {self.gitea_explorer.organization_name}:\n" try: org_repos = self.repo_manager.list_organization_repositories(self.gitea_explorer.organization_name) matching_repos = [r for r in org_repos if r['name'] == remote_repo or r['name'] == project_name] if matching_repos: for repo in matching_repos: info += f"✅ Gefunden: {repo['name']}\n" info += f" URL: {repo.get('html_url', 'Unknown')}\n" else: info += "❌ Nicht in Organisation gefunden\n" except Exception as e: info += f"❌ Fehler beim Durchsuchen: {str(e)}\n" else: info += "❌ Keine Git Remote gefunden!\n" # Check for debug file debug_file = project_path / "gitea_push_debug.txt" if debug_file.exists(): info += f"\n\n📄 Debug-Datei gefunden: {debug_file}\n" try: with open(debug_file, 'r', encoding='utf-8') as f: debug_content = f.read() info += "Debug-Inhalt:\n" + "-" * 30 + "\n" info += debug_content[:1000] # First 1000 chars if len(debug_content) > 1000: info += "\n... (gekürzt)" except: info += "Konnte Debug-Datei nicht lesen\n" # Show in scrollable dialog dialog = ctk.CTkToplevel(self.root) dialog.title("Repository Verifizierung") dialog.geometry("800x600") text_widget = ctk.CTkTextbox(dialog, wrap="word") text_widget.pack(fill="both", expand=True, padx=10, pady=10) text_widget.insert("1.0", info) text_widget.configure(state="disabled") close_btn = ctk.CTkButton( dialog, text="Schließen", command=dialog.destroy ) close_btn.pack(pady=10) def init_activity_sync(self): """Initialize activity sync service""" from services.activity_sync import activity_service # Set callback for activity updates activity_service.on_activities_update = self.on_activities_update # Connect if configured if activity_service.is_configured(): threading.Thread(target=activity_service.connect, daemon=True).start() # Start periodic update self.update_activity_status() def on_activities_update(self, activities): """Handle activity updates from server""" # Update UI in main thread self.root.after(0, lambda: self.update_activity_display(activities)) def update_activity_display(self, activities): """Update activity display in status bar""" count = len(activities) if count > 0: self.activity_label.configure(text=f"🟢 {count} Teammitglieder aktiv") else: self.activity_label.configure(text="") # Update project tiles for project_id, tile in self.project_tiles.items(): if hasattr(tile, 'check_activity'): tile.check_activity() def update_activity_status(self): """Periodic update of activity status""" from services.activity_sync import activity_service if activity_service.connected: # Update display with current activities self.update_activity_display(activity_service.activities) # Schedule next update self.root.after(5000, self.update_activity_status) # Update every 5 seconds def show_activity_details(self, event=None): """Show detailed activity information""" from services.activity_sync import activity_service if not activity_service.activities: return # Create dialog dialog = ctk.CTkToplevel(self.root) dialog.title("Team-Aktivitäten") dialog.geometry("400x300") dialog.transient(self.root) dialog.grab_set() # Header header = ctk.CTkLabel( dialog, text="👥 Aktive Teammitglieder", font=FONTS['subtitle'], text_color=COLORS['text_primary'] ) header.pack(pady=(20, 10)) # Activity list list_frame = ctk.CTkScrollableFrame( dialog, fg_color=COLORS['bg_secondary'] ) list_frame.pack(fill="both", expand=True, padx=20, pady=(0, 20)) # Display each activity for activity in activity_service.activities: if activity.get('isActive'): activity_frame = ctk.CTkFrame( list_frame, fg_color=COLORS['bg_tile'] ) activity_frame.pack(fill="x", pady=5, padx=10) # User and project info info_text = f"{activity['userName']}\n📁 {activity['projectName']}" info_label = ctk.CTkLabel( activity_frame, text=info_text, font=FONTS['body'], text_color=COLORS['text_primary'], justify="left" ) info_label.pack(anchor="w", padx=10, pady=5) # Close button close_btn = ctk.CTkButton( dialog, text="Schließen", command=dialog.destroy, width=100 ) close_btn.pack(pady=(0, 20)) # Center dialog dialog.update_idletasks() x = (dialog.winfo_screenwidth() - dialog.winfo_width()) // 2 y = (dialog.winfo_screenheight() - dialog.winfo_height()) // 2 dialog.geometry(f"+{x}+{y}") def start_activity(self, project): """Start activity for a project""" from services.activity_sync import activity_service if not activity_service.is_configured(): messagebox.showinfo( "Activity Server", "Bitte konfigurieren Sie den Activity Server in den Einstellungen." ) return if not activity_service.connected: messagebox.showwarning( "Nicht verbunden", "Keine Verbindung zum Activity Server." ) return # Start activity success = activity_service.start_activity( project_name=project.name, project_path=project.path ) if success: self.update_status(f"Aktivität gestartet: {project.name}") # Update tile to show activity if project.id in self.project_tiles: self.project_tiles[project.id].update_activity_status(True) else: messagebox.showerror( "Fehler", "Konnte Aktivität nicht starten." ) def stop_activity(self): """Stop current activity""" from services.activity_sync import activity_service success = activity_service.stop_activity() if success: self.update_status("Aktivität beendet") # Update all tiles for tile in self.project_tiles.values(): if hasattr(tile, 'update_activity_status'): tile.update_activity_status(False) def _setup_exception_handler(self): """Setup global exception handler to log all errors""" import sys import tkinter def handle_exception(exc_type, exc_value, exc_traceback): """Handle uncaught exceptions""" if issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback) return logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) # Show error dialog error_msg = f"Ein unerwarteter Fehler ist aufgetreten:\n\n{exc_type.__name__}: {exc_value}\n\nDetails wurden im Log gespeichert." if hasattr(self, 'root'): messagebox.showerror("Kritischer Fehler", error_msg) # Set the exception handler sys.excepthook = handle_exception # Also handle Tkinter exceptions def handle_tk_error(self, exc, val, tb): logger.critical(f"Tkinter exception: {exc}: {val}", exc_info=(exc, val, tb)) return True # Prevent default error dialog tkinter.Tk.report_callback_exception = handle_tk_error def run(self): """Run the application""" self.root.mainloop()