Palette globale recherchant a travers sessions, hosts OpenSSH, workspaces, snippets et actions courantes, avec recherche floue et navigation entierement au clavier (fleches, Entree, Echap). - app/services/fuzzy.py : correspondance par sous-sequence (comme fzf, sans dependance externe), score favorisant les caracteres consecutifs et les debuts de mot. - QuickConnectDialog : collecte les resultats depuis les stores/controleurs deja existants (aucune duplication de logique - activer une session appelle ConnectionController.open_session(), un workspace WorkspaceManager.restore(), un snippet insert_snippet() dans l'onglet courant, sans jamais l'executer automatiquement). Les actions (Nouvelle session, Gestionnaires...) reutilisent les methodes des controleurs/dialogues existants. - Ctrl+K (menu Fichier > Recherche rapide...) ouvre la palette, reconstruite a chaque ouverture pour refleter l'etat courant. - insert_snippet()/execute_snippet() rendus publics sur BaseTerminalTab (utilises depuis un autre module par la palette). tests/test_fuzzy.py (scoring, tri par pertinence) et test_quick_connect.py (collecte des 4 types de resultats + actions, filtrage, navigation clavier, activation reelle d'une session et d'un snippet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
261 lines
11 KiB
Python
261 lines
11 KiB
Python
"""Fenetre principale : assemble le panneau des sessions, les onglets, les menus.
|
|
|
|
La logique metier vit dans des controleurs dedies (app.controllers) et les widgets
|
|
d'onglet dans app.ui.tabs ; cette classe se contente de les construire et de les
|
|
relier.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtGui import QAction, QActionGroup, QKeySequence
|
|
from PySide6.QtWidgets import (
|
|
QMainWindow, QDockWidget, QMessageBox, QStatusBar, QApplication,
|
|
)
|
|
|
|
from app.session_manager import SessionStore
|
|
from app.key_manager import KeyManager
|
|
from app.local_terminal import SHELL_LABELS, DEFAULT_SHELL
|
|
from app.terminal_widget import TerminalWidget
|
|
from app.dialogs.key_dialog import KeyManagerDialog
|
|
from app.dialogs.known_hosts_dialog import KnownHostsDialog
|
|
from app.dialogs.workspace_dialog import WorkspaceDialog
|
|
from app.dialogs.snippet_dialog import SnippetManagerDialog
|
|
from app.dialogs.quick_connect_dialog import QuickConnectDialog
|
|
from app.theme import THEME_LABELS, apply_theme, current_theme, save_theme
|
|
from app.workspace import WorkspaceStore
|
|
from app.snippets import SnippetStore
|
|
from app.controllers.broadcast_manager import BroadcastManager
|
|
from app.controllers.session_controller import SessionController
|
|
from app.controllers.connection_controller import ConnectionController
|
|
from app.controllers.workspace_manager import WorkspaceManager
|
|
from app.ui.split_pane import SplitContainer
|
|
|
|
# re-exporte pour compatibilite (utilise par tests/test_detect.py et du code existant)
|
|
from app.services.ssh_target import detect_ssh_target # noqa: F401
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setWindowTitle("PySSH Client")
|
|
self.resize(1280, 820)
|
|
|
|
self.session_store = SessionStore()
|
|
self.key_manager = KeyManager()
|
|
self.workspace_store = WorkspaceStore()
|
|
self.snippet_store = SnippetStore()
|
|
|
|
self.broadcast = BroadcastManager(self._iter_tabs, self)
|
|
self.connection_controller = ConnectionController(
|
|
self.session_store, self.key_manager, self.broadcast,
|
|
snippet_store=self.snippet_store, parent_widget=self,
|
|
)
|
|
self.session_controller = SessionController(
|
|
self.session_store, self.key_manager, self.broadcast.button, parent_widget=self,
|
|
)
|
|
self.workspace_manager = WorkspaceManager(
|
|
self.workspace_store, self.session_store, self.connection_controller, parent_widget=self,
|
|
)
|
|
self._wire_controllers()
|
|
|
|
self._build_menu()
|
|
self._build_sidebar()
|
|
self.setCentralWidget(self.connection_controller.tabs)
|
|
self.setStatusBar(QStatusBar())
|
|
|
|
# demarre directement dans un terminal utilisable, sans avoir a en choisir un -
|
|
# sauf si un workspace vient d'etre restaure automatiquement (il a deja le sien)
|
|
if not self.workspace_manager.try_restore_last_on_startup():
|
|
self.connection_controller.open_local_terminal(DEFAULT_SHELL)
|
|
|
|
# ---------- acces retro-compatible (tests, code externe) ----------
|
|
@property
|
|
def tabs(self):
|
|
return self.connection_controller.tabs
|
|
|
|
def _iter_tabs(self):
|
|
"""Widgets de terminal joignables par le Multi-Send : les onglets de
|
|
premier niveau, et les panneaux a l'interieur des onglets divises."""
|
|
tabs = self.connection_controller.tabs
|
|
result = []
|
|
for i in range(tabs.count()):
|
|
widget = tabs.widget(i)
|
|
if isinstance(widget, SplitContainer):
|
|
result.extend(pane.content for pane in widget.panes)
|
|
else:
|
|
result.append(widget)
|
|
return result
|
|
|
|
# ---------- cablage entre controleurs ----------
|
|
def _wire_controllers(self) -> None:
|
|
sc, cc = self.session_controller, self.connection_controller
|
|
sc.open_session_requested.connect(cc.open_session)
|
|
sc.open_sftp_requested.connect(cc.open_sftp_session)
|
|
sc.open_local_shell_requested.connect(cc.open_local_terminal)
|
|
sc.open_openssh_host_requested.connect(cc.open_openssh_host)
|
|
sc.quick_sftp_requested.connect(cc.quick_sftp)
|
|
sc.manage_keys_requested.connect(self.open_key_manager)
|
|
sc.manage_known_hosts_requested.connect(self.open_known_hosts_manager)
|
|
cc.session_saved.connect(sc.reload)
|
|
|
|
# ---------- construction UI ----------
|
|
def _build_menu(self) -> None:
|
|
file_menu = self.menuBar().addMenu("&Fichier")
|
|
|
|
quick_connect_action = QAction("Recherche rapide...", self)
|
|
quick_connect_action.setShortcut(QKeySequence("Ctrl+K"))
|
|
quick_connect_action.triggered.connect(self.open_quick_connect)
|
|
file_menu.addAction(quick_connect_action)
|
|
|
|
file_menu.addSeparator()
|
|
|
|
new_session_action = QAction("Nouvelle session...", self)
|
|
new_session_action.setShortcut(QKeySequence("Ctrl+N"))
|
|
new_session_action.triggered.connect(self.session_controller.new_session)
|
|
file_menu.addAction(new_session_action)
|
|
|
|
file_menu.addSeparator()
|
|
|
|
quick_ssh_action = QAction("Connexion SSH rapide...", self)
|
|
quick_ssh_action.setShortcut(QKeySequence("Ctrl+Shift+S"))
|
|
quick_ssh_action.triggered.connect(self.connection_controller.quick_ssh)
|
|
file_menu.addAction(quick_ssh_action)
|
|
|
|
quick_sftp_action = QAction("Connexion SFTP (gestionnaire de fichiers)...", self)
|
|
quick_sftp_action.setShortcut(QKeySequence("Ctrl+Shift+F"))
|
|
quick_sftp_action.triggered.connect(self.connection_controller.quick_sftp)
|
|
file_menu.addAction(quick_sftp_action)
|
|
|
|
file_menu.addSeparator()
|
|
|
|
default_action = QAction(
|
|
f"Nouveau terminal local ({SHELL_LABELS.get(DEFAULT_SHELL, DEFAULT_SHELL)})", self
|
|
)
|
|
default_action.setShortcut(QKeySequence("Ctrl+Shift+T"))
|
|
default_action.triggered.connect(lambda: self.connection_controller.open_local_terminal(DEFAULT_SHELL))
|
|
file_menu.addAction(default_action)
|
|
|
|
others_menu = file_menu.addMenu("Autre terminal local")
|
|
for shell_id, label in SHELL_LABELS.items():
|
|
others_menu.addAction(label, lambda s=shell_id: self.connection_controller.open_local_terminal(s))
|
|
|
|
file_menu.addSeparator()
|
|
quit_action = QAction("Quitter", self)
|
|
quit_action.triggered.connect(self.close)
|
|
file_menu.addAction(quit_action)
|
|
|
|
keys_menu = self.menuBar().addMenu("&Cles SSH")
|
|
manage_keys_action = QAction("Gestionnaire de cles...", self)
|
|
manage_keys_action.triggered.connect(self.open_key_manager)
|
|
keys_menu.addAction(manage_keys_action)
|
|
|
|
view_menu = self.menuBar().addMenu("&Affichage")
|
|
|
|
self.toggle_sidebar_action = QAction("Panneau des sessions", self, checkable=True)
|
|
self.toggle_sidebar_action.setChecked(True)
|
|
self.toggle_sidebar_action.setShortcut(QKeySequence("F9"))
|
|
self.toggle_sidebar_action.triggered.connect(self.toggle_sidebar)
|
|
view_menu.addAction(self.toggle_sidebar_action)
|
|
view_menu.addSeparator()
|
|
|
|
theme_menu = view_menu.addMenu("Theme")
|
|
self.theme_group = QActionGroup(self)
|
|
self.theme_group.setExclusive(True)
|
|
for theme_id, label in THEME_LABELS.items():
|
|
action = QAction(label, self, checkable=True)
|
|
action.setChecked(theme_id == current_theme())
|
|
action.triggered.connect(lambda _checked=False, t=theme_id: self.change_theme(t))
|
|
self.theme_group.addAction(action)
|
|
theme_menu.addAction(action)
|
|
|
|
hosts_menu = self.menuBar().addMenu("&Hotes connus")
|
|
manage_hosts_action = QAction("Gestionnaire des hotes connus...", self)
|
|
manage_hosts_action.triggered.connect(self.open_known_hosts_manager)
|
|
hosts_menu.addAction(manage_hosts_action)
|
|
|
|
workspaces_menu = self.menuBar().addMenu("&Workspaces")
|
|
manage_workspaces_action = QAction("Gestionnaire de workspaces...", self)
|
|
manage_workspaces_action.setShortcut(QKeySequence("Ctrl+Shift+W"))
|
|
manage_workspaces_action.triggered.connect(self.open_workspace_manager)
|
|
workspaces_menu.addAction(manage_workspaces_action)
|
|
|
|
snippets_menu = self.menuBar().addMenu("&Snippets")
|
|
manage_snippets_action = QAction("Gestionnaire de snippets...", self)
|
|
manage_snippets_action.triggered.connect(self.open_snippet_manager)
|
|
snippets_menu.addAction(manage_snippets_action)
|
|
|
|
help_menu = self.menuBar().addMenu("&Aide")
|
|
about_action = QAction("A propos", self)
|
|
about_action.triggered.connect(self.show_about)
|
|
help_menu.addAction(about_action)
|
|
|
|
def _build_sidebar(self) -> None:
|
|
dock = QDockWidget("Sessions", self)
|
|
# Closable : sans cette option, le panneau n'a pas de bouton de fermeture
|
|
# et ne peut pas etre masque.
|
|
dock.setFeatures(
|
|
QDockWidget.DockWidgetMovable
|
|
| QDockWidget.DockWidgetFloatable
|
|
| QDockWidget.DockWidgetClosable
|
|
)
|
|
self.sessions_dock = dock
|
|
|
|
dock.setWidget(self.session_controller.widget)
|
|
self.addDockWidget(Qt.LeftDockWidgetArea, dock)
|
|
self.resizeDocks([dock], [300], Qt.Horizontal)
|
|
# garde la case du menu Affichage synchronisee si on ferme le panneau par sa croix
|
|
dock.visibilityChanged.connect(self.toggle_sidebar_action.setChecked)
|
|
|
|
# ---------- cles / affichage / a propos ----------
|
|
def toggle_sidebar(self, visible: bool | None = None) -> None:
|
|
"""Affiche ou masque le panneau des sessions (F9)."""
|
|
if visible is None:
|
|
visible = not self.sessions_dock.isVisible()
|
|
self.sessions_dock.setVisible(visible)
|
|
self.toggle_sidebar_action.setChecked(visible)
|
|
|
|
def change_theme(self, theme: str) -> None:
|
|
"""Applique un theme a chaud : feuille de style de l'interface + couleurs des
|
|
terminaux deja ouverts (qui ont leur propre palette, hors QSS)."""
|
|
app = QApplication.instance()
|
|
if app is None:
|
|
return
|
|
apply_theme(app, theme)
|
|
save_theme(theme)
|
|
for widget in self.findChildren(TerminalWidget):
|
|
widget.refresh_theme()
|
|
|
|
def open_key_manager(self) -> None:
|
|
dialog = KeyManagerDialog(self.key_manager, parent=self)
|
|
dialog.exec()
|
|
|
|
def open_known_hosts_manager(self) -> None:
|
|
dialog = KnownHostsDialog(parent=self)
|
|
dialog.exec()
|
|
|
|
def open_workspace_manager(self) -> None:
|
|
dialog = WorkspaceDialog(self.workspace_manager, parent=self)
|
|
dialog.exec()
|
|
|
|
def open_snippet_manager(self) -> None:
|
|
dialog = SnippetManagerDialog(self.snippet_store, parent=self)
|
|
dialog.exec()
|
|
|
|
def open_quick_connect(self) -> None:
|
|
dialog = QuickConnectDialog(
|
|
self.session_store, self.connection_controller, self.workspace_manager,
|
|
self.snippet_store, self.session_controller, self.key_manager, parent=self,
|
|
)
|
|
dialog.exec()
|
|
|
|
def show_about(self) -> None:
|
|
QMessageBox.information(
|
|
self, "A propos",
|
|
"PySSH Client\n\nClient SSH/SFTP local avec gestion de sessions, de cles, "
|
|
"et terminaux locaux integres.\nBase sur PySide6, paramiko, pywinpty et pyte.",
|
|
)
|
|
|
|
def closeEvent(self, event) -> None:
|
|
self.connection_controller.close_all()
|
|
super().closeEvent(event)
|