Confort du terminal, reconnexion, et hotes deja connus de ssh

Zoom de police : Ctrl+molette et Ctrl+ +/-, Ctrl+0 pour revenir a la taille
par defaut. La taille choisie est memorisee entre deux lancements.

Barre de defilement de l'historique. pyte ne sait defiler que par demi-pages :
la barre est donc graduee en pages plutot qu'en lignes, ce qui donne un repere
visuel de position et permet de faire glisser. Maj+PagePrec/Suiv au clavier,
Ctrl+Fin pour revenir a la vue en direct.

Recherche dans l'historique (Ctrl+F) : la barre apparait en bas de l'onglet et
la recherche remonte l'historique page par page jusqu'a trouver, en
selectionnant la correspondance — donc visible et directement copiable.

Reconnexion en un clic : un bouton apparait dans la barre de l'onglet des que
la connexion tombe. La fabrication de la connexion a ete extraite dans
_create_connection(), ce qui permet de la recreer a l'identique sans ressaisir
quoi que ce soit. Utile avec des serveurs de test qui redemarrent souvent.

Les hotes deja approuves par la commande ssh (~/.ssh/known_hosts) sont
desormais reconnus, ce qui evite de reconfirmer une empreinte que votre ssh
connait deja. paramiko ne reecrit jamais ce fichier : les nouvelles cles
continuent d'aller dans celui de l'application.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
snoopy
2026-09-04 11:31:50 +02:00
parent f82288b2e4
commit f537868da3
6 changed files with 431 additions and 32 deletions

View File

@@ -6,11 +6,11 @@ import re
import uuid
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QAction, QActionGroup, QKeySequence
from PySide6.QtGui import QAction, QActionGroup, QKeySequence, QShortcut
from PySide6.QtWidgets import (
QMainWindow, QDockWidget, QTreeWidget, QTreeWidgetItem, QTabWidget, QWidget,
QVBoxLayout, QHBoxLayout, QToolButton, QLabel, QMenu, QMessageBox, QSplitter,
QStatusBar, QFrame, QApplication,
QStatusBar, QFrame, QApplication, QLineEdit,
)
from app.session_manager import SessionStore, Session, AUTH_KEY, AUTH_PASSWORD
@@ -93,6 +93,15 @@ class BaseTerminalTab(QWidget):
self.status_label = QLabel(initial_status)
self.status_label.setObjectName("statusLabel")
self.bar.addWidget(self.status_label, 1)
# Apparait quand la connexion tombe : les serveurs de test redemarrent souvent,
# inutile de fermer l'onglet et de tout ressaisir.
self.reconnect_btn = QToolButton()
self.reconnect_btn.setObjectName("primary")
self.reconnect_btn.setText("⟳ Reconnecter")
self.reconnect_btn.hide()
self.reconnect_btn.clicked.connect(self.reconnect)
self.bar.addWidget(self.reconnect_btn)
bar_frame = QFrame()
bar_frame.setObjectName("tabBar")
bar_frame.setLayout(self.bar)
@@ -103,6 +112,28 @@ class BaseTerminalTab(QWidget):
self.splitter.addWidget(self.terminal)
layout.addWidget(self.splitter, 1)
# barre de recherche, masquee tant qu'on n'a pas fait Ctrl+F
self.search_bar = QFrame()
self.search_bar.setObjectName("tabBar")
search_layout = QHBoxLayout(self.search_bar)
search_layout.setContentsMargins(10, 4, 10, 4)
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Rechercher dans l'historique... (Entree = precedent, Echap = fermer)")
self.search_status = QLabel("")
self.search_status.setObjectName("statusLabel")
close_search = QToolButton()
close_search.setText("✕")
search_layout.addWidget(self.search_input, 1)
search_layout.addWidget(self.search_status)
search_layout.addWidget(close_search)
self.search_bar.hide()
layout.addWidget(self.search_bar)
self.terminal.search_requested.connect(self.open_search)
self.search_input.returnPressed.connect(self._run_search)
close_search.clicked.connect(self.close_search)
QShortcut(QKeySequence(Qt.Key_Escape), self.search_input, activated=self.close_search)
self.connection = None # assigne par les sous-classes
def _wire_connection(self) -> None:
@@ -113,16 +144,56 @@ class BaseTerminalTab(QWidget):
self.terminal.data_to_send.connect(self.connection.send)
self.terminal.resized.connect(self.connection.resize)
def _create_connection(self):
"""Fabrique la connexion. Redefinie par chaque sous-classe : c'est ce qui
permet de la recreer a l'identique lors d'une reconnexion."""
raise NotImplementedError
def start_connection(self) -> None:
self.connection = self._create_connection()
self._wire_connection()
self.connection.start()
def reconnect(self) -> None:
if self.connection:
self.connection.close_connection()
self.connection.wait(1500)
self.terminal.reset_screen()
self.reconnect_btn.hide()
self.status_label.setText("○ Reconnexion...")
self.start_connection()
def _on_connected(self) -> None:
self.reconnect_btn.hide()
self.terminal.setFocus()
def _on_disconnected(self, reason: str) -> None:
self.status_label.setText(f"● Deconnecte ({reason})")
self.reconnect_btn.show()
def _on_error(self, message: str) -> None:
self.status_label.setText(f"● Erreur : {message}")
self.reconnect_btn.show()
QMessageBox.critical(self, "Erreur", message)
# ---------- recherche dans l'historique ----------
def open_search(self) -> None:
self.search_bar.show()
self.search_input.setFocus()
self.search_input.selectAll()
def close_search(self) -> None:
self.search_bar.hide()
self.search_status.clear()
self.terminal.setFocus()
def _run_search(self) -> None:
needle = self.search_input.text()
if not needle:
return
found = self.terminal.find_text(needle)
self.search_status.setText("" if found else "aucun resultat")
def close_connection(self) -> None:
if self.connection:
self.connection.close_connection()
@@ -145,27 +216,30 @@ class SessionTabWidget(BaseTerminalTab):
self.sftp_toggle.setEnabled(False)
self.bar.addWidget(self.sftp_toggle)
self.connection = SSHConnection(
self._secret = secret
self.sftp_toggle.toggled.connect(self._toggle_sftp)
self.start_connection()
def _create_connection(self):
session = self.session
connection = SSHConnection(
host=session.host,
port=session.port,
username=session.username,
auth_type=session.auth_type,
key_path=session.key_path,
password=secret if session.auth_type == AUTH_PASSWORD else "",
passphrase=secret if session.auth_type == AUTH_KEY else "",
password=self._secret if session.auth_type == AUTH_PASSWORD else "",
passphrase=self._secret if session.auth_type == AUTH_KEY else "",
# comme MobaXterm : le navigateur de fichiers est la des l'ouverture,
# pas cache derriere une action supplementaire
open_sftp_on_connect=True,
)
self._wire_connection()
self.connection.sftp_ready.connect(self._on_sftp_ready)
self.connection.password_prompt.connect(self._on_password_prompt)
self.connection.host_key_prompt.connect(self._on_host_key_prompt)
self.connection.host_key_changed_prompt.connect(self._on_host_key_changed_prompt)
self.connection.passphrase_prompt.connect(self._on_passphrase_prompt)
self.sftp_toggle.toggled.connect(self._toggle_sftp)
self.connection.start()
connection.sftp_ready.connect(self._on_sftp_ready)
connection.password_prompt.connect(self._on_password_prompt)
connection.host_key_prompt.connect(self._on_host_key_prompt)
connection.host_key_changed_prompt.connect(self._on_host_key_changed_prompt)
connection.passphrase_prompt.connect(self._on_passphrase_prompt)
return connection
def _on_connected(self) -> None:
self.status_label.setText(f"● Connecte : {self.session.username}@{self.session.host}:{self.session.port}")
@@ -313,9 +387,10 @@ class LocalTerminalTabWidget(BaseTerminalTab):
)
self.bar.addWidget(self.sftp_btn)
self.connection = LocalTerminalConnection(shell=shell)
self._wire_connection()
self.connection.start()
self.start_connection()
def _create_connection(self):
return LocalTerminalConnection(shell=self.shell)
def _on_connected(self) -> None:
self.status_label.setText(f"● Terminal local — {SHELL_LABELS.get(self.shell, self.shell)}")