"""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.groups import GroupSettingsStore 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.group_store = GroupSettingsStore() 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, group_store=self.group_store, ) 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)