development-hub/src/development_hub/project_list.py

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."
)