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