"""Palette de recherche globale (Ctrl+K) : sessions, hosts OpenSSH, workspaces, snippets et actions, avec recherche floue et navigation entierement au clavier.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Callable from PySide6.QtCore import Qt, QEvent from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QLineEdit, QListWidget, QListWidgetItem, QMessageBox, ) from app.openssh.config import load_openssh_hosts from app.services.fuzzy import fuzzy_score ICONS = { "session": "🖥", "openssh": "⚙", "workspace": "🗂", "snippet": "📋", "action": "▶", } MAX_RESULTS = 60 @dataclass class PaletteResult: kind: str label: str sublabel: str = "" activate: Callable[[], None] = field(default=lambda: None) @property def haystack(self) -> str: return f"{self.label} {self.sublabel}" class QuickConnectDialog(QDialog): def __init__( self, session_store, connection_controller, workspace_manager, snippet_store, session_controller, key_manager, parent=None, ): super().__init__(parent) self.session_store = session_store self.cc = connection_controller self.workspace_manager = workspace_manager self.snippet_store = snippet_store self.session_controller = session_controller self.key_manager = key_manager self.setWindowTitle("Recherche rapide") self.setMinimumSize(560, 420) # pas de bouton "?" dans le titre, sans interet ici self.setWindowFlag(Qt.WindowContextHelpButtonHint, False) layout = QVBoxLayout(self) self.search_edit = QLineEdit() self.search_edit.setPlaceholderText("Rechercher une session, un host, un workspace, un snippet...") layout.addWidget(self.search_edit) self.list = QListWidget() layout.addWidget(self.list, 1) self.search_edit.installEventFilter(self) self.search_edit.textChanged.connect(self._update_results) self.list.itemActivated.connect(lambda _item: self._activate_current()) self._all_results: list[PaletteResult] = self._collect_results() self._update_results() self.search_edit.setFocus() # ---------- collecte des resultats ---------- def _collect_results(self) -> list[PaletteResult]: results: list[PaletteResult] = [] for session in self.session_store.sessions: results.append(PaletteResult( kind="session", label=session.name, sublabel=f"{session.username}@{session.host}" + (f" ({session.group})" if session.group else ""), activate=lambda s=session: self.cc.open_session(s), )) try: hosts = load_openssh_hosts() except Exception: # noqa: BLE001 - ~/.ssh/config illisible : pas bloquant pour la palette hosts = [] for host in hosts: results.append(PaletteResult( kind="openssh", label=host.alias, sublabel=f"Host OpenSSH — {host.display_target}", activate=lambda h=host: self.cc.open_openssh_host(h), )) for workspace in self.workspace_manager.store.workspaces: results.append(PaletteResult( kind="workspace", label=workspace.name or "(sans nom)", sublabel=f"Workspace — {len(workspace.tabs)} onglet(s)", activate=lambda w=workspace: self.workspace_manager.restore(w), )) for snippet in self.snippet_store.snippets: results.append(PaletteResult( kind="snippet", label=snippet.name, sublabel=f"Snippet {snippet.category} — {snippet.command}".strip(), activate=lambda s=snippet: self._insert_snippet_in_current_tab(s), )) for label, action in self._quick_actions(): results.append(PaletteResult(kind="action", label=label, sublabel="Action", activate=action)) return results def _quick_actions(self) -> list[tuple[str, Callable[[], None]]]: from app.dialogs.workspace_dialog import WorkspaceDialog from app.dialogs.snippet_dialog import SnippetManagerDialog from app.dialogs.key_dialog import KeyManagerDialog return [ ("Nouvelle session...", self.session_controller.new_session), ("Connexion SSH rapide...", self.cc.quick_ssh), ("Connexion SFTP rapide...", self.cc.quick_sftp), ("Nouveau terminal local", self.cc.open_local_terminal), ("Gestionnaire de workspaces...", lambda: WorkspaceDialog(self.workspace_manager, parent=self.parent()).exec()), ("Gestionnaire de snippets...", lambda: SnippetManagerDialog(self.snippet_store, parent=self.parent()).exec()), ("Gestionnaire de cles SSH...", lambda: KeyManagerDialog(self.key_manager, parent=self.parent()).exec()), ] def _insert_snippet_in_current_tab(self, snippet) -> None: widget = self.cc.tabs.currentWidget() if hasattr(widget, "insert_snippet"): widget.insert_snippet(snippet) else: QMessageBox.information( self.parent(), "Snippets", "Ouvrez d'abord un terminal (ou un panneau) pour y inserer ce snippet.", ) # ---------- filtrage / affichage ---------- def _update_results(self) -> None: query = self.search_edit.text() self.list.clear() scored = [] for result in self._all_results: score = fuzzy_score(query, result.haystack) if score is not None: scored.append((score, result)) scored.sort(key=lambda pair: -pair[0]) for _score, result in scored[:MAX_RESULTS]: icon = ICONS.get(result.kind, "") item = QListWidgetItem(f"{icon} {result.label} — {result.sublabel}") item.setData(Qt.UserRole, result) self.list.addItem(item) if self.list.count(): self.list.setCurrentRow(0) # ---------- navigation clavier ---------- def eventFilter(self, obj, event) -> bool: if obj is self.search_edit and event.type() == QEvent.KeyPress: key = event.key() if key == Qt.Key_Down: self._move_selection(1) return True if key == Qt.Key_Up: self._move_selection(-1) return True if key in (Qt.Key_Return, Qt.Key_Enter): self._activate_current() return True if key == Qt.Key_Escape: self.reject() return True return super().eventFilter(obj, event) def _move_selection(self, delta: int) -> None: if not self.list.count(): return row = max(0, min(self.list.count() - 1, self.list.currentRow() + delta)) self.list.setCurrentRow(row) def _activate_current(self) -> None: item = self.list.currentItem() if not item: return result: PaletteResult = item.data(Qt.UserRole) self.accept() if result and result.activate: result.activate()