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:
118
app/snippets.py
Normal file
118
app/snippets.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""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()
|
||||
]
|
||||
Reference in New Issue
Block a user