"""Project list widget showing all discovered projects.""" import subprocess import webbrowser from pathlib import Path from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QAction from PyQt6.QtWidgets import ( QLabel, QListWidget, QListWidgetItem, QMenu, QVBoxLayout, QWidget, ) from development_hub.project_discovery import Project, discover_projects class ProjectListWidget(QWidget): """Widget displaying the list of projects with context menu actions.""" project_selected = pyqtSignal(Project) open_terminal_requested = pyqtSignal(Project) def __init__(self, parent=None): super().__init__(parent) self._projects: list[Project] = [] self._setup_ui() self.refresh() def _setup_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Header header = QLabel("PROJECTS") header.setObjectName("header") layout.addWidget(header) # Project list self.list_widget = QListWidget() self.list_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.list_widget.customContextMenuRequested.connect(self._show_context_menu) self.list_widget.itemClicked.connect(self._on_item_clicked) self.list_widget.itemDoubleClicked.connect(self._on_item_double_clicked) layout.addWidget(self.list_widget) def refresh(self): """Refresh the project list from the build configuration.""" self.list_widget.clear() self._projects = discover_projects() for project in self._projects: item = QListWidgetItem(project.title) item.setData(Qt.ItemDataRole.UserRole, project) # Add tooltip with project info tooltip = f"{project.tagline}\n\nPath: {project.path}" if not project.exists: tooltip += "\n\n(Directory not found)" item.setForeground(Qt.GlobalColor.darkGray) item.setToolTip(tooltip) self.list_widget.addItem(item) def _get_selected_project(self) -> Project | None: """Get the currently selected project.""" item = self.list_widget.currentItem() if item: return item.data(Qt.ItemDataRole.UserRole) return None def _on_item_clicked(self, item: QListWidgetItem): """Handle single click on project.""" project = item.data(Qt.ItemDataRole.UserRole) self.project_selected.emit(project) def _on_item_double_clicked(self, item: QListWidgetItem): """Handle double-click - open terminal at project.""" project = item.data(Qt.ItemDataRole.UserRole) if project.exists: self.open_terminal_requested.emit(project) def _show_context_menu(self, position): """Show context menu for project actions.""" item = self.list_widget.itemAt(position) if not item: return project = item.data(Qt.ItemDataRole.UserRole) menu = QMenu(self) # Open Terminal open_terminal = QAction("Open Terminal", self) open_terminal.triggered.connect(lambda: self.open_terminal_requested.emit(project)) open_terminal.setEnabled(project.exists) menu.addAction(open_terminal) # Open in Editor open_editor = QAction("Open in Editor", self) open_editor.triggered.connect(lambda: self._open_in_editor(project)) open_editor.setEnabled(project.exists) menu.addAction(open_editor) menu.addSeparator() # View on Gitea view_gitea = QAction("View on Gitea", self) view_gitea.triggered.connect(lambda: webbrowser.open(project.gitea_url)) menu.addAction(view_gitea) # View Documentation view_docs = QAction("View Documentation", self) view_docs.triggered.connect(lambda: webbrowser.open(project.docs_url)) menu.addAction(view_docs) menu.addSeparator() # Update Documentation (AI-powered) update_docs = QAction("Update Documentation...", self) update_docs.triggered.connect(lambda: self._update_docs(project)) menu.addAction(update_docs) # Deploy Docs deploy_docs = QAction("Deploy Docs", self) deploy_docs.triggered.connect(lambda: self._deploy_docs(project)) menu.addAction(deploy_docs) menu.exec(self.list_widget.mapToGlobal(position)) def _open_in_editor(self, project: Project): """Open project in the default editor.""" import os import shutil # Try common editors in order of preference editors = [ os.environ.get("EDITOR"), "pycharm", "code", # VS Code "subl", # Sublime Text "gedit", "xdg-open", ] for editor in editors: if editor and shutil.which(editor): subprocess.Popen([editor, str(project.path)]) return def _deploy_docs(self, project: Project): """Deploy documentation for project.""" from PyQt6.QtWidgets import QMessageBox build_script = Path.home() / "PycharmProjects/project-docs/scripts/build-public-docs.sh" if build_script.exists(): # Run in background, suppress output subprocess.Popen( [str(build_script), project.key, "--deploy"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # Show notification QMessageBox.information( self, "Deploy Started", f"Documentation deployment started for {project.title}.\n\n" f"This runs in the background. Check Gitea Pages\n" f"in a minute to verify the deployment." ) def _update_docs(self, project: Project): """Update documentation using CmdForge update-docs tool.""" from PyQt6.QtWidgets import QMessageBox # Ask if user wants to deploy after updating reply = QMessageBox.question( self, "Update Documentation", f"This will use AI to analyze {project.title} and update its documentation.\n\n" f"The update-docs tool will:\n" f"1. Read the project's code and current docs\n" f"2. Generate updated overview.md\n" f"3. Optionally deploy to Gitea Pages\n\n" f"Deploy after updating?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Yes ) if reply == QMessageBox.StandardButton.Cancel: return deploy_flag = "true" if reply == QMessageBox.StandardButton.Yes else "false" # Run the update-docs CmdForge tool # The tool expects input on stdin (can be empty) and --project argument cmd = [ "python3", "-m", "cmdforge.runner", "update-docs", "--project", project.key, "--deploy", deploy_flag ] try: # Run with empty stdin, capture output result = subprocess.run( cmd, input="", capture_output=True, text=True, timeout=120 # 2 minute timeout for AI processing ) if result.returncode == 0: QMessageBox.information( self, "Documentation Updated", f"Documentation for {project.title} has been updated.\n\n" f"{result.stdout}" ) else: QMessageBox.warning( self, "Update Failed", f"Failed to update documentation:\n\n{result.stderr or result.stdout}" ) except subprocess.TimeoutExpired: QMessageBox.warning( self, "Update Timeout", "Documentation update timed out. Try running manually:\n\n" f"update-docs --project {project.key}" ) except FileNotFoundError: QMessageBox.warning( self, "Tool Not Found", "CmdForge update-docs tool not found.\n\n" "Make sure CmdForge is installed and the update-docs tool exists." )