feat: ajouter les Workspaces (enregistrer/restaurer l'etat des onglets)

Un workspace capture les onglets ouverts (sessions SSH, terminaux locaux,
splits avec leurs tailles) et les restaure plus tard. Ne reference les
sessions que par id : aucun mot de passe stocke, redemande a la restauration
exactement comme une ouverture normale.

- app/workspace.py : Workspace/WorkspaceTab/WorkspaceItem + WorkspaceStore
  (meme pattern que SessionStore/KeyManager : JSON atomique, schema_version).
- app/controllers/workspace_manager.py : capture() lit l'etat reel des onglets
  de ConnectionController (types de widgets, layout et tailles d'un
  SplitContainer) ; restore() rouvre chaque onglet via
  ConnectionController.build_session_tab()/build_sftp_tab()/new_local_terminal()
  (factorises hors de open_session()/open_sftp_session() pour etre reutilisables
  ici) puis reconstruit les splits avec split_tab(). Un element non restaurable
  (session supprimee depuis) est signale (compteur "ignores") plutot que de
  faire echouer toute la restauration.
- WorkspaceDialog : creer / enregistrer (ecraser) / ouvrir / renommer /
  supprimer, plus la case "Restaurer automatiquement le dernier workspace
  ouvert au demarrage" (desactivee par defaut).
- app/app_settings.py : preferences globales (theme, taille de police,
  dernier workspace) consolidees dans un seul module au lieu d'un mecanisme
  de lecture/ecriture duplique par prefernce dans theme.py.
- SplitContainer expose layout_kind et get_layout_sizes()/set_layout_sizes()
  (parcours recursif des QSplitter imbriques) pour la capture/restauration
  fidele d'une grille 2x2.

tests/test_workspace.py : capture (simple + split), sauvegarde/rechargement
JSON, restauration complete et partielle, renommer/supprimer/re-enregistrer,
restauration automatique - avec app_settings.SETTINGS_FILE redirige vers un
fichier temporaire pour ne jamais toucher les vrais reglages de l'utilisateur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Florian
2026-09-05 02:47:07 +02:00
parent 5917e1ca04
commit a3c72ade0b
13 changed files with 730 additions and 68 deletions

View File

@@ -18,10 +18,13 @@ 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.theme import THEME_LABELS, apply_theme, current_theme, save_theme
from app.workspace import WorkspaceStore
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)
@@ -36,6 +39,7 @@ class MainWindow(QMainWindow):
self.session_store = SessionStore()
self.key_manager = KeyManager()
self.workspace_store = WorkspaceStore()
self.broadcast = BroadcastManager(self._iter_tabs, self)
self.connection_controller = ConnectionController(
@@ -44,6 +48,9 @@ class MainWindow(QMainWindow):
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()
@@ -51,8 +58,10 @@ class MainWindow(QMainWindow):
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)
# 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
@@ -151,6 +160,12 @@ class MainWindow(QMainWindow):
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)
help_menu = self.menuBar().addMenu("&Aide")
about_action = QAction("A propos", self)
about_action.triggered.connect(self.show_about)
@@ -200,6 +215,10 @@ class MainWindow(QMainWindow):
dialog = KnownHostsDialog(parent=self)
dialog.exec()
def open_workspace_manager(self) -> None:
dialog = WorkspaceDialog(self.workspace_manager, parent=self)
dialog.exec()
def show_about(self) -> None:
QMessageBox.information(
self, "A propos",