243 lines
8.3 KiB
Python
243 lines
8.3 KiB
Python
"""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."
|
|
)
|