373 lines
12 KiB
Python
373 lines
12 KiB
Python
"""Dialogs for Development Hub."""
|
|
|
|
import json
|
|
import subprocess
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QProcess
|
|
from PyQt6.QtWidgets import (
|
|
QDialog,
|
|
QVBoxLayout,
|
|
QHBoxLayout,
|
|
QFormLayout,
|
|
QLineEdit,
|
|
QTextEdit,
|
|
QPushButton,
|
|
QLabel,
|
|
QProgressBar,
|
|
QMessageBox,
|
|
QGroupBox,
|
|
)
|
|
|
|
from development_hub.settings import Settings
|
|
|
|
|
|
class RambleThread(QThread):
|
|
"""Thread to run Ramble subprocess."""
|
|
|
|
finished = pyqtSignal(dict) # Emits parsed result
|
|
error = pyqtSignal(str) # Emits error message
|
|
|
|
def __init__(self, prompt: str, fields: list[str], criteria: dict[str, str] = None):
|
|
super().__init__()
|
|
self.prompt = prompt
|
|
self.fields = fields
|
|
self.criteria = criteria or {}
|
|
|
|
def run(self):
|
|
"""Run Ramble and parse output."""
|
|
# Check if ramble is available
|
|
ramble_path = shutil.which("ramble")
|
|
if not ramble_path:
|
|
self.error.emit("Ramble is not installed or not in PATH.\nInstall with: pip install ramble")
|
|
return
|
|
|
|
# Build command
|
|
cmd = [
|
|
ramble_path,
|
|
"--prompt", self.prompt,
|
|
"--fields", *self.fields,
|
|
]
|
|
|
|
if self.criteria:
|
|
cmd.extend(["--criteria", json.dumps(self.criteria)])
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300, # 5 minute timeout
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
# User cancelled or error
|
|
if "cancelled" in result.stderr.lower():
|
|
self.finished.emit({})
|
|
else:
|
|
self.error.emit(f"Ramble error: {result.stderr}")
|
|
return
|
|
|
|
# Parse JSON output
|
|
try:
|
|
data = json.loads(result.stdout)
|
|
self.finished.emit(data)
|
|
except json.JSONDecodeError:
|
|
# Ramble might output the dialog result differently
|
|
# Try to find JSON in the output
|
|
self.finished.emit({})
|
|
|
|
except subprocess.TimeoutExpired:
|
|
self.error.emit("Ramble timed out after 5 minutes")
|
|
except Exception as e:
|
|
self.error.emit(f"Error running Ramble: {e}")
|
|
|
|
|
|
class NewProjectThread(QThread):
|
|
"""Thread to run new-project script."""
|
|
|
|
output = pyqtSignal(str)
|
|
finished = pyqtSignal(bool, str) # success, message
|
|
|
|
def __init__(self, name: str, title: str, tagline: str, deploy: bool):
|
|
super().__init__()
|
|
self.name = name
|
|
self.title = title
|
|
self.tagline = tagline
|
|
self.deploy = deploy
|
|
|
|
def run(self):
|
|
"""Run the new-project script."""
|
|
script = Path.home() / "PycharmProjects" / "development-hub" / "bin" / "new-project"
|
|
|
|
if not script.exists():
|
|
self.finished.emit(False, f"Script not found: {script}")
|
|
return
|
|
|
|
cmd = [
|
|
str(script),
|
|
self.name,
|
|
"--title", self.title,
|
|
"--tagline", self.tagline,
|
|
]
|
|
|
|
if self.deploy:
|
|
cmd.append("--deploy")
|
|
|
|
try:
|
|
process = subprocess.Popen(
|
|
cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
bufsize=1,
|
|
)
|
|
|
|
# Stream output
|
|
for line in process.stdout:
|
|
self.output.emit(line.rstrip())
|
|
|
|
process.wait()
|
|
|
|
if process.returncode == 0:
|
|
self.finished.emit(True, f"Project '{self.name}' created successfully!")
|
|
else:
|
|
self.finished.emit(False, f"Project creation failed with code {process.returncode}")
|
|
|
|
except Exception as e:
|
|
self.finished.emit(False, f"Error: {e}")
|
|
|
|
|
|
class NewProjectDialog(QDialog):
|
|
"""Dialog for creating a new project."""
|
|
|
|
project_created = pyqtSignal(str) # Emits project name
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("New Project")
|
|
self.setMinimumWidth(500)
|
|
self.settings = Settings()
|
|
|
|
self._ramble_thread = None
|
|
self._create_thread = None
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
"""Set up the dialog UI."""
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Form fields
|
|
form_group = QGroupBox("Project Details")
|
|
form_layout = QFormLayout(form_group)
|
|
|
|
self.name_input = QLineEdit()
|
|
self.name_input.setPlaceholderText("my-project (lowercase, hyphens)")
|
|
self.name_input.textChanged.connect(self._validate)
|
|
form_layout.addRow("Name:", self.name_input)
|
|
|
|
self.title_input = QLineEdit()
|
|
self.title_input.setPlaceholderText("My Project")
|
|
form_layout.addRow("Title:", self.title_input)
|
|
|
|
self.tagline_input = QLineEdit()
|
|
self.tagline_input.setPlaceholderText("A short description of the project")
|
|
form_layout.addRow("Tagline:", self.tagline_input)
|
|
|
|
layout.addWidget(form_group)
|
|
|
|
# Ramble button
|
|
ramble_layout = QHBoxLayout()
|
|
ramble_layout.addStretch()
|
|
|
|
self.ramble_btn = QPushButton("🎤 Ramble...")
|
|
self.ramble_btn.setToolTip("Describe your project idea and let AI fill in the fields")
|
|
self.ramble_btn.clicked.connect(self._start_ramble)
|
|
ramble_layout.addWidget(self.ramble_btn)
|
|
|
|
layout.addLayout(ramble_layout)
|
|
|
|
# Output area (hidden initially)
|
|
self.output_group = QGroupBox("Output")
|
|
output_layout = QVBoxLayout(self.output_group)
|
|
|
|
self.output_text = QTextEdit()
|
|
self.output_text.setReadOnly(True)
|
|
self.output_text.setMaximumHeight(150)
|
|
output_layout.addWidget(self.output_text)
|
|
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setRange(0, 0) # Indeterminate
|
|
self.progress_bar.hide()
|
|
output_layout.addWidget(self.progress_bar)
|
|
|
|
self.output_group.hide()
|
|
layout.addWidget(self.output_group)
|
|
|
|
# Buttons
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addStretch()
|
|
|
|
self.cancel_btn = QPushButton("Cancel")
|
|
self.cancel_btn.clicked.connect(self.reject)
|
|
button_layout.addWidget(self.cancel_btn)
|
|
|
|
self.create_btn = QPushButton("Create Project")
|
|
self.create_btn.setDefault(True)
|
|
self.create_btn.setEnabled(False)
|
|
self.create_btn.clicked.connect(self._create_project)
|
|
button_layout.addWidget(self.create_btn)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
def _validate(self):
|
|
"""Validate form and enable/disable create button."""
|
|
name = self.name_input.text().strip()
|
|
# Basic validation: name must be non-empty and valid
|
|
valid = bool(name) and name.replace("-", "").replace("_", "").isalnum()
|
|
self.create_btn.setEnabled(valid)
|
|
|
|
def _start_ramble(self):
|
|
"""Launch Ramble to fill in fields."""
|
|
self.ramble_btn.setEnabled(False)
|
|
self.ramble_btn.setText("🎤 Running Ramble...")
|
|
|
|
self._ramble_thread = RambleThread(
|
|
prompt="Describe the project you want to create. What does it do? What problem does it solve?",
|
|
fields=["Name", "Title", "Tagline"],
|
|
criteria={
|
|
"Name": "lowercase, use hyphens instead of spaces, no special characters",
|
|
"Title": "Title case, human readable",
|
|
"Tagline": "One sentence description",
|
|
}
|
|
)
|
|
self._ramble_thread.finished.connect(self._on_ramble_finished)
|
|
self._ramble_thread.error.connect(self._on_ramble_error)
|
|
self._ramble_thread.start()
|
|
|
|
def _on_ramble_finished(self, result: dict):
|
|
"""Handle Ramble completion."""
|
|
self.ramble_btn.setEnabled(True)
|
|
self.ramble_btn.setText("🎤 Ramble...")
|
|
|
|
if result and "fields" in result:
|
|
fields = result["fields"]
|
|
if "Name" in fields:
|
|
self.name_input.setText(fields["Name"])
|
|
if "Title" in fields:
|
|
self.title_input.setText(fields["Title"])
|
|
if "Tagline" in fields:
|
|
self.tagline_input.setText(fields["Tagline"])
|
|
|
|
def _on_ramble_error(self, error: str):
|
|
"""Handle Ramble error."""
|
|
self.ramble_btn.setEnabled(True)
|
|
self.ramble_btn.setText("🎤 Ramble...")
|
|
QMessageBox.warning(self, "Ramble Error", error)
|
|
|
|
def _create_project(self):
|
|
"""Start project creation."""
|
|
name = self.name_input.text().strip()
|
|
title = self.title_input.text().strip() or name.replace("-", " ").title()
|
|
tagline = self.tagline_input.text().strip() or f"The {title} project"
|
|
|
|
# Show output area
|
|
self.output_group.show()
|
|
self.output_text.clear()
|
|
self.progress_bar.show()
|
|
|
|
# Disable inputs
|
|
self.name_input.setEnabled(False)
|
|
self.title_input.setEnabled(False)
|
|
self.tagline_input.setEnabled(False)
|
|
self.ramble_btn.setEnabled(False)
|
|
self.create_btn.setEnabled(False)
|
|
|
|
# Start creation thread
|
|
self._create_thread = NewProjectThread(
|
|
name=name,
|
|
title=title,
|
|
tagline=tagline,
|
|
deploy=self.settings.deploy_docs_after_creation,
|
|
)
|
|
self._create_thread.output.connect(self._on_create_output)
|
|
self._create_thread.finished.connect(self._on_create_finished)
|
|
self._create_thread.start()
|
|
|
|
def _on_create_output(self, line: str):
|
|
"""Handle output from creation script."""
|
|
self.output_text.append(line)
|
|
# Auto-scroll to bottom
|
|
scrollbar = self.output_text.verticalScrollBar()
|
|
scrollbar.setValue(scrollbar.maximum())
|
|
|
|
def _on_create_finished(self, success: bool, message: str):
|
|
"""Handle project creation completion."""
|
|
self.progress_bar.hide()
|
|
|
|
if success:
|
|
self.output_text.append(f"\n✓ {message}")
|
|
self.project_created.emit(self.name_input.text().strip())
|
|
|
|
# Change cancel to close
|
|
self.cancel_btn.setText("Close")
|
|
self.create_btn.hide()
|
|
else:
|
|
self.output_text.append(f"\n✗ {message}")
|
|
|
|
# Re-enable inputs for retry
|
|
self.name_input.setEnabled(True)
|
|
self.title_input.setEnabled(True)
|
|
self.tagline_input.setEnabled(True)
|
|
self.ramble_btn.setEnabled(True)
|
|
self.create_btn.setEnabled(True)
|
|
|
|
|
|
class SettingsDialog(QDialog):
|
|
"""Dialog for application settings."""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Settings")
|
|
self.setMinimumWidth(400)
|
|
self.settings = Settings()
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
"""Set up the dialog UI."""
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Project creation settings
|
|
project_group = QGroupBox("Project Creation")
|
|
project_layout = QVBoxLayout(project_group)
|
|
|
|
from PyQt6.QtWidgets import QCheckBox
|
|
self.deploy_checkbox = QCheckBox("Deploy docs after creating new project")
|
|
self.deploy_checkbox.setChecked(self.settings.deploy_docs_after_creation)
|
|
project_layout.addWidget(self.deploy_checkbox)
|
|
|
|
layout.addWidget(project_group)
|
|
|
|
# Buttons
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addStretch()
|
|
|
|
cancel_btn = QPushButton("Cancel")
|
|
cancel_btn.clicked.connect(self.reject)
|
|
button_layout.addWidget(cancel_btn)
|
|
|
|
save_btn = QPushButton("Save")
|
|
save_btn.setDefault(True)
|
|
save_btn.clicked.connect(self._save)
|
|
button_layout.addWidget(save_btn)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
def _save(self):
|
|
"""Save settings and close."""
|
|
self.settings.deploy_docs_after_creation = self.deploy_checkbox.isChecked()
|
|
self.accept()
|