Convert TUI to PySide6 desktop GUI
Major UI overhaul replacing the urwid-based Terminal UI with a modern PySide6 desktop application. New GUI features: - Sidebar navigation (My Tools, Registry, Providers) - Tool Builder with visual form for creating/editing tools - Registry browser with search and one-click install - Provider management page - Connect dialog for account pairing - Publish dialog for sharing tools - Keyboard shortcuts (Ctrl+N, Ctrl+S, Ctrl+R, Ctrl+1/2/3, Escape, Ctrl+Q) - Window geometry persistence (remembers size/position) - Modern clean stylesheet Removed: - ui.py, ui_snack.py, ui_registry.py, ui_urwid.py - ui_urwid/ directory (urwid TUI implementation) Updated: - pyproject.toml: PySide6 now required, removed urwid - CLI entry points to launch GUI - All documentation (README, CLAUDE.md, AGENTS.md, wiki) Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
"""Registry page - browse and install tools from registry."""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLineEdit,
|
||||
QPushButton, QTableWidget, QTableWidgetItem, QLabel,
|
||||
QHeaderView, QGroupBox, QTextEdit, QSplitter, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
|
||||
from ...registry_client import RegistryClient, RegistryError
|
||||
from ...config import load_config
|
||||
|
||||
|
||||
class SearchWorker(QThread):
|
||||
"""Background worker for registry search."""
|
||||
finished = Signal(list)
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, query: str, page: int = 1):
|
||||
super().__init__()
|
||||
self.query = query
|
||||
self.page = page
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
client = RegistryClient()
|
||||
result = client.search_tools(self.query, page=self.page, per_page=20)
|
||||
# result is a PaginatedResponse with data attribute
|
||||
self.finished.emit(result.data)
|
||||
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._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(24, 24, 24, 24)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Header
|
||||
title = QLabel("Tool Registry")
|
||||
title.setObjectName("heading")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Search bar
|
||||
search_box = QWidget()
|
||||
search_layout = QHBoxLayout(search_box)
|
||||
search_layout.setContentsMargins(0, 0, 0, 0)
|
||||
search_layout.setSpacing(8)
|
||||
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.setPlaceholderText("Search tools...")
|
||||
self.search_input.returnPressed.connect(self._do_search)
|
||||
search_layout.addWidget(self.search_input, 1)
|
||||
|
||||
self.btn_search = QPushButton("Search")
|
||||
self.btn_search.clicked.connect(self._do_search)
|
||||
search_layout.addWidget(self.btn_search)
|
||||
|
||||
layout.addWidget(search_box)
|
||||
|
||||
# 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(4)
|
||||
self.results_table.setHorizontalHeaderLabels(["Name", "Owner", "Downloads", "Version"])
|
||||
self.results_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
self.results_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
||||
self.results_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
||||
self.results_table.horizontalHeader().setSectionResizeMode(3, 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)
|
||||
|
||||
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 = QTextEdit()
|
||||
self.details_text.setReadOnly(True)
|
||||
self.details_text.setPlaceholderText("Select a tool to view details")
|
||||
details_layout.addWidget(self.details_text)
|
||||
|
||||
right_layout.addWidget(details_box, 1)
|
||||
|
||||
# Install button
|
||||
self.btn_install = QPushButton("Install")
|
||||
self.btn_install.clicked.connect(self._install_tool)
|
||||
self.btn_install.setEnabled(False)
|
||||
right_layout.addWidget(self.btn_install)
|
||||
|
||||
splitter.addWidget(right)
|
||||
splitter.setSizes([500, 500])
|
||||
|
||||
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."""
|
||||
pass # Could auto-search popular tools
|
||||
|
||||
def _do_search(self):
|
||||
"""Perform search."""
|
||||
query = self.search_input.text().strip()
|
||||
if not query:
|
||||
return
|
||||
|
||||
self.btn_search.setEnabled(False)
|
||||
self.status_label.setText("Searching...")
|
||||
self.results_table.setRowCount(0)
|
||||
|
||||
self._search_worker = SearchWorker(query)
|
||||
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, tools: list):
|
||||
"""Handle search results."""
|
||||
self.btn_search.setEnabled(True)
|
||||
self.status_label.setText(f"Found {len(tools)} tools")
|
||||
|
||||
self.results_table.setRowCount(len(tools))
|
||||
for row, tool in enumerate(tools):
|
||||
name_item = QTableWidgetItem(tool.get("name", ""))
|
||||
name_item.setData(Qt.UserRole, tool)
|
||||
self.results_table.setItem(row, 0, name_item)
|
||||
self.results_table.setItem(row, 1, QTableWidgetItem(tool.get("owner", "")))
|
||||
self.results_table.setItem(row, 2, QTableWidgetItem(str(tool.get("downloads", 0))))
|
||||
self.results_table.setItem(row, 3, QTableWidgetItem(tool.get("version", "1.0.0")))
|
||||
|
||||
def _on_search_error(self, error: str):
|
||||
"""Handle search error."""
|
||||
self.btn_search.setEnabled(True)
|
||||
self.status_label.setText(f"Error: {error}")
|
||||
|
||||
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)
|
||||
return
|
||||
|
||||
row = items[0].row()
|
||||
name_item = self.results_table.item(row, 0)
|
||||
tool = name_item.data(Qt.UserRole)
|
||||
self._selected_tool = tool
|
||||
self._show_tool_details(tool)
|
||||
self.btn_install.setEnabled(True)
|
||||
|
||||
def _show_tool_details(self, tool: dict):
|
||||
"""Show tool details."""
|
||||
lines = []
|
||||
lines.append(f"<h2>{tool.get('owner', '')}/{tool.get('name', '')}</h2>")
|
||||
|
||||
if tool.get("description"):
|
||||
lines.append(f"<p>{tool.get('description')}</p>")
|
||||
|
||||
lines.append(f"<p><strong>Version:</strong> {tool.get('version', '1.0.0')}</p>")
|
||||
lines.append(f"<p><strong>Downloads:</strong> {tool.get('downloads', 0)}</p>")
|
||||
|
||||
if tool.get("category"):
|
||||
lines.append(f"<p><strong>Category:</strong> {tool.get('category')}</p>")
|
||||
|
||||
if tool.get("tags"):
|
||||
tags = ", ".join(tool.get("tags", []))
|
||||
lines.append(f"<p><strong>Tags:</strong> {tags}</p>")
|
||||
|
||||
self.details_text.setHtml("\n".join(lines))
|
||||
|
||||
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.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}")
|
||||
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.status_label.setText(f"Error: {error}")
|
||||
QMessageBox.critical(self, "Install Error", f"Failed to install tool:\n{error}")
|
||||
Reference in New Issue
Block a user