Edition de fichier distant en place, et journalisation de session

Edition SFTP facon WinSCP : clic droit sur un fichier distant > "Editer".
Le fichier est telecharge dans un temporaire, ouvert avec l'application
associee du systeme, et renvoye automatiquement a chaque enregistrement.
Beaucoup d'editeurs enregistrent en remplacant le fichier plutot qu'en le
reecrivant : le chemin surveille disparait alors du QFileSystemWatcher, il est
donc reinscrit apres chaque renvoi pour que les enregistrements suivants
fonctionnent aussi.

Journalisation : un bouton dans la barre de l'onglet enregistre toute la
session dans un fichier. Les sequences d'echappement ANSI sont retirees pour
que le journal reste lisible dans un editeur de texte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
snoopy
2026-09-04 11:35:39 +02:00
parent f537868da3
commit 6de0aad79c
2 changed files with 917 additions and 797 deletions

View File

@@ -5,12 +5,13 @@ from __future__ import annotations
import json
import os
import posixpath
import tempfile
import stat
from datetime import datetime
from pathlib import Path
import paramiko
from PySide6.QtCore import Qt, QThread, Signal, QMimeData, QPoint
from PySide6.QtCore import Qt, QThread, Signal, QMimeData, QPoint, QFileSystemWatcher
from PySide6.QtGui import QDrag, QKeySequence, QShortcut
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QToolButton, QTreeWidget,
@@ -324,9 +325,11 @@ class FilePanel(QWidget):
act_rename.setEnabled(has_selection)
act_delete = menu.addAction("Supprimer (Suppr)")
act_delete.setEnabled(has_selection)
act_perms = None
act_perms = act_edit = None
if self.side == "remote":
menu.addSeparator()
act_edit = menu.addAction("Editer (renvoi automatique a l'enregistrement)")
act_edit.setEnabled(has_selection)
act_perms = menu.addAction("Permissions...")
act_perms.setEnabled(has_selection)
menu.addSeparator()
@@ -340,6 +343,8 @@ class FilePanel(QWidget):
self.rename_selected()
elif action == act_delete:
self.delete_selected()
elif act_edit is not None and action == act_edit:
self.edit_selected()
elif act_perms is not None and action == act_perms:
self.change_permissions()
elif action == act_mkdir:
@@ -361,6 +366,7 @@ class FilePanel(QWidget):
def rename_selected(self) -> None: ...
def delete_selected(self) -> None: ...
def change_permissions(self) -> None: ...
def edit_selected(self) -> None: ...
class LocalFilePanel(FilePanel):
@@ -453,6 +459,10 @@ class RemoteFilePanel(FilePanel):
def __init__(self, sftp: paramiko.SFTPClient, parent=None):
super().__init__("🌐 Distant", "remote", parent)
self.sftp = sftp
# fichiers ouverts pour edition : chemin local temporaire -> chemin distant
self._edited: dict[str, str] = {}
self._watcher = QFileSystemWatcher(self)
self._watcher.fileChanged.connect(self._on_edited_file_changed)
try:
home = self.sftp.normalize(".")
except Exception:
@@ -546,6 +556,55 @@ class RemoteFilePanel(FilePanel):
self.sftp.remove(sub)
self.sftp.rmdir(path)
# ---------- edition en place ----------
def edit_selected(self) -> None:
"""Telecharge le fichier, l'ouvre dans l'editeur par defaut du systeme et le
renvoie automatiquement a chaque enregistrement (facon WinSCP)."""
entries = [(n, d) for n, d in self.selected_entries() if not d]
if not entries:
QMessageBox.information(self, "Edition", "Selectionnez un fichier (pas un dossier).")
return
name = entries[0][0]
remote_path = self.join(self.current_path, name)
local_dir = Path(tempfile.gettempdir()) / "pyssh-edit"
local_dir.mkdir(parents=True, exist_ok=True)
local_path = local_dir / name
try:
self.sftp.get(remote_path, str(local_path))
except (IOError, OSError) as exc:
QMessageBox.critical(self, "Erreur", f"Telechargement impossible : {exc}")
return
self._edited[str(local_path)] = remote_path
self._watcher.addPath(str(local_path))
self.status.setText(f"Edition de {name} — enregistrez pour renvoyer sur le serveur")
try:
os.startfile(str(local_path)) # ouvre avec l'application associee
except OSError as exc:
QMessageBox.warning(
self, "Ouverture impossible",
f"Le fichier a ete telecharge dans :\n{local_path}\n\n"
f"Ouvrez-le manuellement ; il sera renvoye a chaque enregistrement.\n({exc})",
)
def _on_edited_file_changed(self, local_path: str) -> None:
remote_path = self._edited.get(local_path)
if not remote_path:
return
try:
self.sftp.put(local_path, remote_path)
except (IOError, OSError) as exc:
self.status.setText(f"Echec du renvoi : {exc}")
return
# Beaucoup d'editeurs enregistrent en remplacant le fichier : le chemin surveille
# disparait alors du watcher, il faut le reinscrire pour les enregistrements suivants.
if local_path not in self._watcher.files() and Path(local_path).exists():
self._watcher.addPath(local_path)
self.refresh()
# apres refresh() : sinon le message serait ecrase par le compteur d'elements
self.status.setText(f"↑ {posixpath.basename(remote_path)} renvoye sur le serveur")
def change_permissions(self) -> None:
entries = self.selected_entries()
if not entries: