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:
@@ -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)}")
|
||||
|
||||
Reference in New Issue
Block a user