Files
pyssh-client/app/snippets.py
Florian 0a07ddcb62 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>
2026-09-05 02:55:10 +02:00

119 lines
4.4 KiB
Python

"""Snippets de commandes : bibliotheque organisee par categories, utilisable
depuis un terminal (inserer sans executer, executer, executer avec Multi-Send)."""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field, asdict
from pathlib import Path
from app.paths import DATA_DIR
from app.storage.json_store import load_json_document, save_json_document
SNIPPETS_FILE = DATA_DIR / "snippets.json"
SCHEMA_VERSION = 1
@dataclass
class Snippet:
id: str = field(default_factory=lambda: str(uuid.uuid4()))
name: str = ""
category: str = ""
description: str = ""
command: str = ""
def to_dict(self) -> dict:
return asdict(self)
@staticmethod
def from_dict(data: dict) -> "Snippet":
s = Snippet()
for k in Snippet.__dataclass_fields__:
if k in data:
setattr(s, k, data[k])
return s
# Quelques exemples fournis d'origine (premier lancement uniquement) : l'utilisateur
# peut les modifier ou les supprimer librement, ce ne sont que des points de depart.
DEFAULT_SNIPPETS: list[Snippet] = [
Snippet(name="Disk Usage", category="Linux",
description="Espace disque par systeme de fichiers", command="df -h"),
Snippet(name="Failed Services", category="Linux",
description="Services systemd en echec", command="systemctl --failed"),
Snippet(name="Listening Ports", category="Linux",
description="Ports en ecoute", command="ss -tulpn"),
Snippet(name="Containers", category="Docker",
description="Conteneurs (tous statuts)", command="docker ps -a"),
Snippet(name="Docker Cleanup", category="Docker",
description="Nettoyer images/conteneurs/volumes inutilises", command="docker system prune -a"),
Snippet(name="Logs", category="Docker",
description="Logs d'un conteneur (a completer avec son nom)", command="docker logs -f "),
Snippet(name="Cluster Status", category="Proxmox",
description="Etat du cluster Proxmox", command="pvecm status"),
Snippet(name="VM List", category="Proxmox",
description="Liste des machines virtuelles", command="qm list"),
]
class SnippetStore:
"""Charge/sauvegarde la liste des snippets depuis data/snippets.json."""
def __init__(self, path: Path = SNIPPETS_FILE):
self.path = path
self.snippets: list[Snippet] = []
self.load_warning: str | None = None
self.load()
def load(self) -> None:
was_missing = not self.path.exists()
document, warning = load_json_document(
self.path,
default={"schema_version": SCHEMA_VERSION, "snippets": [s.to_dict() for s in DEFAULT_SNIPPETS]},
current_version=SCHEMA_VERSION,
)
self.load_warning = warning
self.snippets = [Snippet.from_dict(s) for s in document.get("snippets", [])]
if was_missing:
self.save()
def save(self) -> None:
payload = {"schema_version": SCHEMA_VERSION, "snippets": [s.to_dict() for s in self.snippets]}
save_json_document(self.path, payload)
def get(self, snippet_id: str) -> Snippet | None:
return next((s for s in self.snippets if s.id == snippet_id), None)
def add(self, snippet: Snippet) -> None:
self.snippets.append(snippet)
self.save()
def update(self, snippet: Snippet) -> None:
for i, s in enumerate(self.snippets):
if s.id == snippet.id:
self.snippets[i] = snippet
break
self.save()
def delete(self, snippet_id: str) -> None:
self.snippets = [s for s in self.snippets if s.id != snippet_id]
self.save()
def categories(self) -> list[str]:
return sorted({s.category for s in self.snippets if s.category})
def by_category(self) -> dict[str, list[Snippet]]:
grouped: dict[str, list[Snippet]] = {}
for s in sorted(self.snippets, key=lambda s: s.name.lower()):
grouped.setdefault(s.category or "Sans categorie", []).append(s)
return dict(sorted(grouped.items(), key=lambda kv: kv[0].lower()))
def search(self, query: str) -> list[Snippet]:
q = query.lower().strip()
if not q:
return list(self.snippets)
return [
s for s in self.snippets
if q in s.name.lower() or q in s.category.lower()
or q in s.description.lower() or q in s.command.lower()
]