feat: ajouter les Snippets de commandes
Bibliotheque de commandes organisee par categories (Linux/Docker/Proxmox fournis en exemples au premier lancement, librement modifiables), avec recherche. - app/snippets.py : Snippet/SnippetStore, meme pattern que les autres stores (JSON atomique, schema_version). - SnippetManagerDialog : creer/modifier/supprimer/rechercher, regroupe par categorie. - Integration terminal : le dernier commit du depot a delibrement fait du clic droit un raccourci de collage (style terminal Unix), sans menu contextuel. Plutot que d'ecraser ce choix recent, les snippets passent par un bouton dedie "📋 Snippets" dans la barre de l'onglet (SessionTabWidget et LocalTerminalTabWidget), avec un menu Categorie -> Snippet -> Action (Inserer dans le terminal / Executer / Executer avec Multi-Send). "Inserer" n'envoie jamais Entree (le texte est ecrit, a l'utilisateur de valider) ; seul "Executer" l'ajoute. - BroadcastManager.broadcast_now() : diffusion explicite et ponctuelle vers tous les terminaux, independante de l'etat du bouton Multi (utilisee par "Executer avec Multi-Send", qui est une action volontaire et pas une frappe clavier a relayer). - La construction du menu (_build_snippets_menu) est separee de son affichage (_show_snippets_menu) : testable sans declencher l'appel bloquant menu.exec(). tests/test_snippets.py : stockage (CRUD, recherche, categories, persistance) et integration terminal (inserer/executer/multi-send, construction et declenchement du menu), avec LocalTerminalConnection.start() neutralisee. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ from PySide6.QtCore import Qt, Signal, QTimer
|
||||
from PySide6.QtGui import QKeySequence, QShortcut
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QToolButton, QLabel, QMessageBox, QSplitter,
|
||||
QFrame, QLineEdit, QFileDialog,
|
||||
QFrame, QLineEdit, QFileDialog, QMenu,
|
||||
)
|
||||
|
||||
from app.session_manager import Session, AUTH_KEY, AUTH_PASSWORD
|
||||
@@ -24,10 +24,12 @@ from app.local_terminal import LocalTerminalConnection, SHELL_POWERSHELL, SHELL_
|
||||
from app.terminal_widget import TerminalWidget
|
||||
from app.sftp_panel import SFTPPanel
|
||||
from app.dialogs.tunnel_dialog import TunnelDialog
|
||||
from app.dialogs.snippet_dialog import SnippetManagerDialog
|
||||
from app.tunnels import TunnelManager
|
||||
from app.dialogs.connect_prompt import ask_secret, confirm_host_key, confirm_host_key_change
|
||||
from app.services.ssh_target import detect_ssh_target
|
||||
from app.services.reconnect import compute_backoff_delay
|
||||
from app.snippets import Snippet, SnippetStore
|
||||
|
||||
# sequences d'echappement ANSI, retirees des journaux pour qu'ils restent lisibles
|
||||
ANSI_ESCAPE_RE = re.compile(
|
||||
@@ -44,8 +46,14 @@ class BaseTerminalTab(QWidget):
|
||||
connexion (SSH ou locale) qui expose la meme interface de signaux
|
||||
(connected / data_received / disconnected / error / send / resize)."""
|
||||
|
||||
def __init__(self, initial_status: str, parent=None):
|
||||
def __init__(
|
||||
self, initial_status: str, snippet_store: SnippetStore | None = None,
|
||||
broadcast=None, parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.snippet_store = snippet_store
|
||||
self.broadcast = broadcast
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
@@ -65,6 +73,15 @@ class BaseTerminalTab(QWidget):
|
||||
self.reconnect_btn.clicked.connect(self.reconnect)
|
||||
self.bar.addWidget(self.reconnect_btn)
|
||||
|
||||
# Clic droit = coller (comme un terminal Unix) : les snippets passent donc
|
||||
# par ce bouton plutot que par un menu contextuel, qui entrerait en conflit.
|
||||
self.snippets_btn = QToolButton()
|
||||
self.snippets_btn.setText("📋 Snippets")
|
||||
self.snippets_btn.setToolTip("Inserer ou executer une commande enregistree")
|
||||
self.snippets_btn.setPopupMode(QToolButton.InstantPopup)
|
||||
self.snippets_btn.clicked.connect(self._show_snippets_menu)
|
||||
self.bar.addWidget(self.snippets_btn)
|
||||
|
||||
self.log_btn = QToolButton()
|
||||
self.log_btn.setText("⏺ Journal")
|
||||
self.log_btn.setToolTip("Enregistrer la session dans un fichier")
|
||||
@@ -204,6 +221,54 @@ class BaseTerminalTab(QWidget):
|
||||
self.reconnect_btn.show()
|
||||
QMessageBox.critical(self, "Erreur", message)
|
||||
|
||||
# ---------- snippets ----------
|
||||
def _show_snippets_menu(self) -> None:
|
||||
menu = self._build_snippets_menu()
|
||||
menu.exec(self.snippets_btn.mapToGlobal(self.snippets_btn.rect().bottomLeft()))
|
||||
|
||||
def _build_snippets_menu(self) -> QMenu:
|
||||
menu = QMenu(self)
|
||||
if self.snippet_store is None or not self.snippet_store.snippets:
|
||||
hint = menu.addAction("(aucun snippet)")
|
||||
hint.setEnabled(False)
|
||||
else:
|
||||
for category, snippets in self.snippet_store.by_category().items():
|
||||
cat_menu = menu.addMenu(category)
|
||||
for snippet in snippets:
|
||||
snippet_menu = cat_menu.addMenu(snippet.name)
|
||||
if snippet.description:
|
||||
snippet_menu.setToolTip(snippet.description)
|
||||
insert_action = snippet_menu.addAction("Inserer dans le terminal")
|
||||
insert_action.triggered.connect(lambda checked=False, s=snippet: self._insert_snippet(s))
|
||||
exec_action = snippet_menu.addAction("Executer")
|
||||
exec_action.triggered.connect(lambda checked=False, s=snippet: self._execute_snippet(s))
|
||||
if self.broadcast is not None:
|
||||
multi_action = snippet_menu.addAction("Executer avec Multi-Send")
|
||||
multi_action.triggered.connect(
|
||||
lambda checked=False, s=snippet: self._execute_snippet(s, multi=True)
|
||||
)
|
||||
menu.addSeparator()
|
||||
manage_action = menu.addAction("Gerer les snippets...")
|
||||
manage_action.triggered.connect(self._manage_snippets)
|
||||
return menu
|
||||
|
||||
def _manage_snippets(self) -> None:
|
||||
if self.snippet_store is None:
|
||||
return
|
||||
SnippetManagerDialog(self.snippet_store, parent=self).exec()
|
||||
|
||||
def _insert_snippet(self, snippet: Snippet) -> None:
|
||||
"""N'envoie jamais Entree : le texte est ecrit, a l'utilisateur de valider."""
|
||||
if self.connection:
|
||||
self.connection.send(snippet.command.encode("utf-8"))
|
||||
|
||||
def _execute_snippet(self, snippet: Snippet, multi: bool = False) -> None:
|
||||
data = snippet.command.encode("utf-8") + b"\r"
|
||||
if multi and self.broadcast is not None:
|
||||
self.broadcast.broadcast_now(data)
|
||||
elif self.connection:
|
||||
self.connection.send(data)
|
||||
|
||||
# ---------- recherche dans l'historique ----------
|
||||
def open_search(self) -> None:
|
||||
self.search_bar.show()
|
||||
@@ -235,9 +300,12 @@ class SessionTabWidget(BaseTerminalTab):
|
||||
|
||||
def __init__(
|
||||
self, session: Session, secret: str, bastion_target=None, bastion_password: str = "",
|
||||
parent=None,
|
||||
snippet_store=None, broadcast=None, parent=None,
|
||||
):
|
||||
super().__init__(f"○ Connexion a {session.username}@{session.host}...", parent)
|
||||
super().__init__(
|
||||
f"○ Connexion a {session.username}@{session.host}...",
|
||||
snippet_store=snippet_store, broadcast=broadcast, parent=parent,
|
||||
)
|
||||
self.session = session
|
||||
self._bastion_target = bastion_target
|
||||
self._bastion_password = bastion_password
|
||||
@@ -526,8 +594,11 @@ class LocalTerminalTabWidget(BaseTerminalTab):
|
||||
|
||||
sftp_requested = Signal(object) # cible ssh detectee (ou None)
|
||||
|
||||
def __init__(self, shell: str = SHELL_POWERSHELL, parent=None):
|
||||
super().__init__(f"○ Ouverture de {SHELL_LABELS.get(shell, shell)}...", parent)
|
||||
def __init__(self, shell: str = SHELL_POWERSHELL, snippet_store=None, broadcast=None, parent=None):
|
||||
super().__init__(
|
||||
f"○ Ouverture de {SHELL_LABELS.get(shell, shell)}...",
|
||||
snippet_store=snippet_store, broadcast=broadcast, parent=parent,
|
||||
)
|
||||
self.shell = shell
|
||||
|
||||
# Meme dans un terminal local, on tape souvent "ssh user@ip" a la main :
|
||||
|
||||
Reference in New Issue
Block a user