"""Registry page - browse and install tools from registry.""" from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QLabel, QHeaderView, QGroupBox, QTextBrowser, QSplitter, QMessageBox, QComboBox, QFrame ) from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QColor from ...registry_client import RegistryClient, RegistryError from ...config import load_config from ...tool import list_tools, load_tool class SearchWorker(QThread): """Background worker for registry search.""" finished = Signal(object) # PaginatedResponse error = Signal(str) def __init__(self, query: str = "", category: str = None, sort: str = "downloads", tags: list = None, page: int = 1, per_page: int = 20): super().__init__() self.query = query self.category = category self.sort = sort self.tags = tags self.page = page self.per_page = per_page def run(self): try: client = RegistryClient() category = self.category if self.category and self.category != "All" else None # Use list_tools for browsing (no query/tags), search_tools for searching if not self.query and not self.tags: result = client.list_tools( category=category, page=self.page, per_page=self.per_page, sort=self.sort ) else: result = client.search_tools( self.query, category=category, tags=self.tags, page=self.page, per_page=self.per_page, sort=self.sort, include_facets=True ) self.finished.emit(result) except Exception as e: self.error.emit(str(e)) class InstallWorker(QThread): """Background worker for tool installation.""" finished = Signal(str) error = Signal(str) def __init__(self, owner: str, name: str): super().__init__() self.owner = owner self.name = name def run(self): try: client = RegistryClient() client.install_tool(self.owner, self.name) self.finished.emit(f"{self.owner}/{self.name}") except Exception as e: self.error.emit(str(e)) class RegistryPage(QWidget): """Registry browser page.""" def __init__(self, main_window): super().__init__() self.main_window = main_window self._search_worker = None self._install_worker = None self._selected_tool = None self._current_page = 1 self._total_pages = 1 self._current_tags = [] self._installed_tools = {} # name -> version self._setup_ui() def _setup_ui(self): """Set up the UI.""" layout = QVBoxLayout(self) layout.setContentsMargins(24, 24, 24, 24) layout.setSpacing(16) # Header header = QHBoxLayout() title = QLabel("Tool Registry") title.setObjectName("heading") header.addWidget(title) header.addStretch() # Browse all button self.btn_browse = QPushButton("Browse All") self.btn_browse.clicked.connect(self._browse_all) header.addWidget(self.btn_browse) layout.addLayout(header) # Search and filters row filters_box = QWidget() filters_layout = QHBoxLayout(filters_box) filters_layout.setContentsMargins(0, 0, 0, 0) filters_layout.setSpacing(12) # Search input self.search_input = QLineEdit() self.search_input.setPlaceholderText("Search tools...") self.search_input.returnPressed.connect(self._do_search) filters_layout.addWidget(self.search_input, 2) # Category filter cat_label = QLabel("Category:") filters_layout.addWidget(cat_label) self.category_combo = QComboBox() self.category_combo.addItems(["All", "Text", "Developer", "Data", "Other"]) self.category_combo.setMinimumWidth(100) self.category_combo.currentTextChanged.connect(self._on_filter_changed) filters_layout.addWidget(self.category_combo) # Sort dropdown sort_label = QLabel("Sort:") filters_layout.addWidget(sort_label) self.sort_combo = QComboBox() self.sort_combo.addItem("Most Popular", "downloads") self.sort_combo.addItem("Newest", "published_at") self.sort_combo.addItem("Name (A-Z)", "name") self.sort_combo.setMinimumWidth(120) self.sort_combo.currentIndexChanged.connect(self._on_filter_changed) filters_layout.addWidget(self.sort_combo) # Search button self.btn_search = QPushButton("Search") self.btn_search.clicked.connect(self._do_search) filters_layout.addWidget(self.btn_search) layout.addWidget(filters_box) # Active tags display self.tags_widget = QWidget() self.tags_layout = QHBoxLayout(self.tags_widget) self.tags_layout.setContentsMargins(0, 0, 0, 0) self.tags_layout.setSpacing(8) self.tags_label = QLabel("Filtering by tags:") self.tags_label.setStyleSheet("color: #718096;") self.tags_layout.addWidget(self.tags_label) self.tags_layout.addStretch() self.tags_widget.hide() layout.addWidget(self.tags_widget) # Content splitter splitter = QSplitter(Qt.Horizontal) # Left: Results table left = QWidget() left_layout = QVBoxLayout(left) left_layout.setContentsMargins(0, 0, 0, 0) self.results_table = QTableWidget() self.results_table.setColumnCount(6) self.results_table.setHorizontalHeaderLabels(["", "Name", "Owner", "Rating", "Downloads", "Version"]) self.results_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Fixed) self.results_table.setColumnWidth(0, 30) # Installed indicator self.results_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.results_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.results_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents) self.results_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents) self.results_table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeToContents) self.results_table.setSelectionBehavior(QTableWidget.SelectRows) self.results_table.setSelectionMode(QTableWidget.SingleSelection) self.results_table.verticalHeader().setVisible(False) self.results_table.itemSelectionChanged.connect(self._on_selection_changed) left_layout.addWidget(self.results_table) # Pagination controls pagination = QWidget() pag_layout = QHBoxLayout(pagination) pag_layout.setContentsMargins(0, 8, 0, 0) self.btn_prev = QPushButton("← Previous") self.btn_prev.clicked.connect(self._prev_page) self.btn_prev.setEnabled(False) pag_layout.addWidget(self.btn_prev) pag_layout.addStretch() self.page_label = QLabel("Page 1 of 1") self.page_label.setStyleSheet("color: #718096;") pag_layout.addWidget(self.page_label) pag_layout.addStretch() self.btn_next = QPushButton("Next →") self.btn_next.clicked.connect(self._next_page) self.btn_next.setEnabled(False) pag_layout.addWidget(self.btn_next) left_layout.addWidget(pagination) splitter.addWidget(left) # Right: Tool details right = QWidget() right_layout = QVBoxLayout(right) right_layout.setContentsMargins(0, 0, 0, 0) details_box = QGroupBox("Tool Details") details_layout = QVBoxLayout(details_box) self.details_text = QTextBrowser() self.details_text.setOpenExternalLinks(False) self.details_text.anchorClicked.connect(self._on_tag_clicked) details_layout.addWidget(self.details_text) right_layout.addWidget(details_box, 1) # Action buttons actions = QHBoxLayout() self.btn_install = QPushButton("Install") self.btn_install.clicked.connect(self._install_tool) self.btn_install.setEnabled(False) actions.addWidget(self.btn_install) self.btn_update = QPushButton("Update Available") self.btn_update.clicked.connect(self._install_tool) self.btn_update.setEnabled(False) self.btn_update.setStyleSheet("background-color: #48bb78; color: white;") self.btn_update.hide() actions.addWidget(self.btn_update) right_layout.addLayout(actions) splitter.addWidget(right) splitter.setSizes([550, 450]) layout.addWidget(splitter, 1) # Status label self.status_label = QLabel("") self.status_label.setStyleSheet("color: #718096;") layout.addWidget(self.status_label) def refresh(self): """Refresh on page enter - load popular tools.""" self._load_installed_tools() self._browse_all() def _load_installed_tools(self): """Load list of installed tools and their versions.""" self._installed_tools = {} for name in list_tools(): tool = load_tool(name) if tool: self._installed_tools[name] = tool.version or "1.0.0" def _browse_all(self): """Browse all tools (no search query).""" self.search_input.clear() self._current_page = 1 self._do_search() def _on_filter_changed(self): """Handle filter/sort change.""" self._current_page = 1 self._do_search() def _do_search(self): """Perform search with current filters.""" query = self.search_input.text().strip() category = self.category_combo.currentText() sort = self.sort_combo.currentData() self.btn_search.setEnabled(False) self.btn_browse.setEnabled(False) self.status_label.setText("Searching...") self.results_table.setRowCount(0) self._search_worker = SearchWorker( query=query, category=category, sort=sort, tags=self._current_tags if self._current_tags else None, page=self._current_page, per_page=20 ) self._search_worker.finished.connect(self._on_search_complete) self._search_worker.error.connect(self._on_search_error) self._search_worker.start() def _on_search_complete(self, result): """Handle search results.""" self.btn_search.setEnabled(True) self.btn_browse.setEnabled(True) tools = result.data self._total_pages = result.total_pages or 1 total = result.total or len(tools) self.status_label.setText(f"Found {total} tools") self.page_label.setText(f"Page {self._current_page} of {self._total_pages}") # Update pagination buttons self.btn_prev.setEnabled(self._current_page > 1) self.btn_next.setEnabled(self._current_page < self._total_pages) self.results_table.setRowCount(len(tools)) for row, tool in enumerate(tools): tool_name = tool.get("name", "") # Installed indicator installed_item = QTableWidgetItem() if tool_name in self._installed_tools: installed_version = self._installed_tools[tool_name] registry_version = tool.get("version", "1.0.0") if installed_version != registry_version: installed_item.setText("↑") # Update available installed_item.setToolTip(f"Update available: {installed_version} → {registry_version}") installed_item.setForeground(QColor("#48bb78")) else: installed_item.setText("✓") installed_item.setToolTip("Installed") installed_item.setForeground(QColor("#4299e1")) installed_item.setTextAlignment(Qt.AlignCenter) self.results_table.setItem(row, 0, installed_item) # Name name_item = QTableWidgetItem(tool_name) name_item.setData(Qt.UserRole, tool) self.results_table.setItem(row, 1, name_item) # Owner self.results_table.setItem(row, 2, QTableWidgetItem(tool.get("owner", ""))) # Rating (stars) rating = tool.get("average_rating", 0) or 0 rating_count = tool.get("rating_count", 0) or 0 stars = self._rating_to_stars(rating) rating_item = QTableWidgetItem(f"{stars} ({rating_count})") rating_item.setToolTip(f"{rating:.1f}/5 from {rating_count} ratings") self.results_table.setItem(row, 3, rating_item) # Downloads downloads = tool.get("downloads", 0) downloads_str = self._format_downloads(downloads) self.results_table.setItem(row, 4, QTableWidgetItem(downloads_str)) # Version self.results_table.setItem(row, 5, QTableWidgetItem(tool.get("version", "1.0.0"))) def _rating_to_stars(self, rating: float) -> str: """Convert rating to star display.""" if rating <= 0: return "☆☆☆☆☆" full_stars = int(rating) half_star = rating - full_stars >= 0.5 empty_stars = 5 - full_stars - (1 if half_star else 0) return "★" * full_stars + ("½" if half_star else "") + "☆" * empty_stars def _format_downloads(self, downloads: int) -> str: """Format download count.""" if downloads >= 1000000: return f"{downloads/1000000:.1f}M" elif downloads >= 1000: return f"{downloads/1000:.1f}K" return str(downloads) def _on_search_error(self, error: str): """Handle search error.""" self.btn_search.setEnabled(True) self.btn_browse.setEnabled(True) self.status_label.setText(f"Error: {error}") def _prev_page(self): """Go to previous page.""" if self._current_page > 1: self._current_page -= 1 self._do_search() def _next_page(self): """Go to next page.""" if self._current_page < self._total_pages: self._current_page += 1 self._do_search() def _on_selection_changed(self): """Handle selection change.""" items = self.results_table.selectedItems() if not items: self._selected_tool = None self.details_text.clear() self.btn_install.setEnabled(False) self.btn_update.hide() return row = items[0].row() name_item = self.results_table.item(row, 1) tool = name_item.data(Qt.UserRole) self._selected_tool = tool self._show_tool_details(tool) # Check if installed / update available tool_name = tool.get("name", "") if tool_name in self._installed_tools: installed_version = self._installed_tools[tool_name] registry_version = tool.get("version", "1.0.0") if installed_version != registry_version: self.btn_install.hide() self.btn_update.show() self.btn_update.setEnabled(True) self.btn_update.setText(f"Update ({installed_version} → {registry_version})") else: self.btn_install.setText("Reinstall") self.btn_install.setEnabled(True) self.btn_install.show() self.btn_update.hide() else: self.btn_install.setText("Install") self.btn_install.setEnabled(True) self.btn_install.show() self.btn_update.hide() def _show_tool_details(self, tool: dict): """Show tool details.""" lines = [] owner = tool.get('owner', '') name = tool.get('name', '') lines.append(f"
{tool.get('description')}
") # Rating display rating = tool.get("average_rating", 0) or 0 rating_count = tool.get("rating_count", 0) or 0 stars = self._rating_to_stars(rating) lines.append(f"Rating: {stars} ({rating:.1f}/5 from {rating_count} reviews)
") lines.append(f"Version: {tool.get('version', '1.0.0')}
") lines.append(f"Downloads: {tool.get('downloads', 0):,}
") if tool.get("category"): lines.append(f"Category: {tool.get('category')}
") # Clickable tags if tool.get("tags"): tags_html = [] for tag in tool.get("tags", []): tags_html.append( f'{tag}' ) lines.append(f"Tags: {''.join(tags_html)}
") # Publisher info if tool.get("publisher_reputation"): rep = tool.get("publisher_reputation", {}) lines.append(f"" f"Publisher: @{owner} · {rep.get('total_tools', 0)} tools · " f"{rep.get('total_downloads', 0):,} total downloads
") # Installed status tool_name = tool.get("name", "") if tool_name in self._installed_tools: installed_version = self._installed_tools[tool_name] registry_version = tool.get("version", "1.0.0") if installed_version != registry_version: lines.append(f"" f"✓ Installed (v{installed_version}) - Update available!
") else: lines.append(f"✓ Installed (v{installed_version})
") self.details_text.setHtml("\n".join(lines)) def _on_tag_clicked(self, url): """Handle tag click to filter by tag.""" tag = url.toString().replace("tag:", "") if tag not in self._current_tags: self._current_tags.append(tag) self._update_tags_display() self._current_page = 1 self._do_search() def _update_tags_display(self): """Update the active tags display.""" # Clear existing tag buttons while self.tags_layout.count() > 2: item = self.tags_layout.takeAt(2) if item.widget(): item.widget().deleteLater() if self._current_tags: self.tags_widget.show() for tag in self._current_tags: tag_btn = QPushButton(f"{tag} ×") tag_btn.setStyleSheet( "background: #4299e1; color: white; border: none; " "padding: 4px 8px; border-radius: 4px;" ) tag_btn.clicked.connect(lambda checked, t=tag: self._remove_tag(t)) self.tags_layout.insertWidget(self.tags_layout.count() - 1, tag_btn) # Add clear all button clear_btn = QPushButton("Clear all") clear_btn.setStyleSheet("color: #e53e3e;") clear_btn.setFlat(True) clear_btn.clicked.connect(self._clear_tags) self.tags_layout.insertWidget(self.tags_layout.count() - 1, clear_btn) else: self.tags_widget.hide() def _remove_tag(self, tag: str): """Remove a tag from the filter.""" if tag in self._current_tags: self._current_tags.remove(tag) self._update_tags_display() self._current_page = 1 self._do_search() def _clear_tags(self): """Clear all tag filters.""" self._current_tags = [] self._update_tags_display() self._current_page = 1 self._do_search() def _install_tool(self): """Install selected tool.""" if not self._selected_tool: return owner = self._selected_tool.get("owner", "") name = self._selected_tool.get("name", "") self.btn_install.setEnabled(False) self.btn_update.setEnabled(False) self.status_label.setText(f"Installing {owner}/{name}...") self._install_worker = InstallWorker(owner, name) self._install_worker.finished.connect(self._on_install_complete) self._install_worker.error.connect(self._on_install_error) self._install_worker.start() def _on_install_complete(self, tool_id: str): """Handle install complete.""" self.btn_install.setEnabled(True) self.status_label.setText(f"Installed {tool_id}") self.main_window.show_status(f"Installed {tool_id}") # Refresh installed tools list self._load_installed_tools() # Update the display if self._selected_tool: self._show_tool_details(self._selected_tool) self._on_selection_changed() # Refresh the table to update indicators self._do_search() QMessageBox.information(self, "Success", f"Successfully installed {tool_id}") def _on_install_error(self, error: str): """Handle install error.""" self.btn_install.setEnabled(True) self.btn_update.setEnabled(True) self.status_label.setText(f"Error: {error}") QMessageBox.critical(self, "Install Error", f"Failed to install tool:\n{error}")