main_window.py concentrait la fenetre principale, l'arbre des sessions, les 4 classes d'onglets, la detection de commandes ssh et le multi-send : 983 lignes et une seule classe qui pilotait toute l'application. - app/services/ssh_target.py : detection d'une cible ssh dans un terminal (pur, sans dependance Qt) - app/ui/tabs.py : BaseTerminalTab / SessionTabWidget / SFTPTabWidget / LocalTerminalTabWidget (deplaces tels quels) - app/controllers/session_controller.py : panneau lateral (arbre sessions + terminaux locaux) et CRUD des sessions - app/controllers/connection_controller.py : cycle de vie des onglets (ouverture, connexion rapide, renommage, fermeture) - app/controllers/broadcast_manager.py : bouton et logique du mode Multi-Send main_window.py ne fait plus que construire ces composants et les relier par signaux (983 -> ~200 lignes). Comportement inchange ; MainWindow.tabs et detect_ssh_target restent importables depuis app.main_window pour la compatibilite avec les tests existants. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
202 lines
8.5 KiB
Python
202 lines
8.5 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.theme import THEME_LABELS, apply_theme, current_theme, save_theme
|
|
from app.controllers.broadcast_manager import BroadcastManager
|
|
from app.controllers.session_controller import SessionController
|
|
from app.controllers.connection_controller import ConnectionController
|
|
|
|
# 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.broadcast = BroadcastManager(self._iter_tabs, self)
|
|
self.connection_controller = ConnectionController(
|
|
self.session_store, self.key_manager, self.broadcast, parent_widget=self,
|
|
)
|
|
self.session_controller = SessionController(
|
|
self.session_store, self.key_manager, self.broadcast.button, 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
|
|
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):
|
|
tabs = self.connection_controller.tabs
|
|
return [tabs.widget(i) for i in range(tabs.count())]
|
|
|
|
# ---------- 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.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")
|
|
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)
|
|
|
|
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 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)
|