Tags : une session peut porter plusieurs tags libres (Session.tags), saisis
comme une liste separee par des virgules dans l'editeur (normalises : espaces
retires, doublons insensibles a la casse elimines), avec autocompletion sur
les tags deja utilises. Affiches dans l'arbre des sessions, et filtrables via
un champ dedie au-dessus de l'arbre (masque les sessions ne correspondant pas,
sans toucher aux dossiers/terminaux locaux/hosts OpenSSH).
Parametres de groupe : choix architectural deliberement plus simple qu'une
resolution dynamique a la connexion. app/groups.py (GroupSettingsStore, meme
pattern que les autres stores) associe un dossier (Session.group) a des
valeurs par defaut (utilisateur, port, authentification, cle, bastion,
keepalive). Ces valeurs ne s'appliquent qu'au moment de la creation d'une
session, via un bouton explicite "↓ Heriter du groupe" dans SessionDialog -
jamais de resolution automatique/retroactive. Ce choix evite d'introduire des
valeurs sentinelles "herite vs. explicite" sur chaque champ (port=22 est a la
fois une valeur par defaut ET une vraie valeur possible, par exemple) dans
tout le reste du code (ConnectionController, SSHConnection...), pour une
fonctionnalite que la priorisation du projet place parmi les secondaires. Une
session reste ensuite un ensemble de valeurs normales, librement modifiables
- "surcharger une valeur heritee" est donc trivialement satisfait.
Clic droit sur un dossier dans le panneau lateral : "Nouvelle session dans ce
dossier..." (pre-remplit le champ Dossier) et "Parametres du groupe..."
(GroupSettingsDialog).
tests/test_tags_and_groups.py : persistance des tags, CRUD GroupSettingsStore,
normalisation de la saisie des tags, heritage applique/absent, filtrage dans
l'arbre.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Palette globale recherchant a travers sessions, hosts OpenSSH, workspaces,
snippets et actions courantes, avec recherche floue et navigation
entierement au clavier (fleches, Entree, Echap).
- app/services/fuzzy.py : correspondance par sous-sequence (comme fzf, sans
dependance externe), score favorisant les caracteres consecutifs et les
debuts de mot.
- QuickConnectDialog : collecte les resultats depuis les stores/controleurs
deja existants (aucune duplication de logique - activer une session appelle
ConnectionController.open_session(), un workspace WorkspaceManager.restore(),
un snippet insert_snippet() dans l'onglet courant, sans jamais l'executer
automatiquement). Les actions (Nouvelle session, Gestionnaires...) reutilisent
les methodes des controleurs/dialogues existants.
- Ctrl+K (menu Fichier > Recherche rapide...) ouvre la palette, reconstruite
a chaque ouverture pour refleter l'etat courant.
- insert_snippet()/execute_snippet() rendus publics sur BaseTerminalTab
(utilises depuis un autre module par la palette).
tests/test_fuzzy.py (scoring, tri par pertinence) et test_quick_connect.py
(collecte des 4 types de resultats + actions, filtrage, navigation clavier,
activation reelle d'une session et d'un snippet).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Un workspace capture les onglets ouverts (sessions SSH, terminaux locaux,
splits avec leurs tailles) et les restaure plus tard. Ne reference les
sessions que par id : aucun mot de passe stocke, redemande a la restauration
exactement comme une ouverture normale.
- app/workspace.py : Workspace/WorkspaceTab/WorkspaceItem + WorkspaceStore
(meme pattern que SessionStore/KeyManager : JSON atomique, schema_version).
- app/controllers/workspace_manager.py : capture() lit l'etat reel des onglets
de ConnectionController (types de widgets, layout et tailles d'un
SplitContainer) ; restore() rouvre chaque onglet via
ConnectionController.build_session_tab()/build_sftp_tab()/new_local_terminal()
(factorises hors de open_session()/open_sftp_session() pour etre reutilisables
ici) puis reconstruit les splits avec split_tab(). Un element non restaurable
(session supprimee depuis) est signale (compteur "ignores") plutot que de
faire echouer toute la restauration.
- WorkspaceDialog : creer / enregistrer (ecraser) / ouvrir / renommer /
supprimer, plus la case "Restaurer automatiquement le dernier workspace
ouvert au demarrage" (desactivee par defaut).
- app/app_settings.py : preferences globales (theme, taille de police,
dernier workspace) consolidees dans un seul module au lieu d'un mecanisme
de lecture/ecriture duplique par prefernce dans theme.py.
- SplitContainer expose layout_kind et get_layout_sizes()/set_layout_sizes()
(parcours recursif des QSplitter imbriques) pour la capture/restauration
fidele d'une grille 2x2.
tests/test_workspace.py : capture (simple + split), sauvegarde/rechargement
JSON, restauration complete et partielle, renommer/supprimer/re-enregistrer,
restauration automatique - avec app_settings.SETTINGS_FILE redirige vers un
fichier temporaire pour ne jamais toucher les vrais reglages de l'utilisateur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Layouts : 1 (normal), 2 verticaux (cote a cote), 2 horizontaux (empiles), 4
panneaux (grille 2x2), via des QSplitter imbriques (app/ui/split_pane.py).
Chaque panneau reutilise tel quel un widget d'onglet existant
(SessionTabWidget, LocalTerminalTabWidget) : aucune modification de ces
classes, seule une fine barre (menu ☰ + fermeture) les enveloppe.
- Menu contextuel d'un onglet (SSH ou terminal local, pas SFTP, pas
d'imbrication de splits) : "Diviser" -> choix du layout. Le contenu actuel
occupe le premier panneau, les autres recoivent un terminal local par
defaut.
- Menu ☰ d'un panneau : changer son contenu (autre terminal local), ou
"Deplacer un onglet ici" pour y faire migrer un onglet de premier niveau
existant sans fermer sa connexion.
- Fermer un panneau le retire de son QSplitter (qui redistribue l'espace tout
seul) ; s'il n'en reste qu'un, l'onglet redevient un onglet normal
(redescente automatique, pas de chrome de split residuel).
- Fermer l'onglet divise (bouton x) ferme toutes les connexions des panneaux.
- BroadcastManager (Multi-Send) atteint desormais aussi les panneaux a
l'interieur d'un split (MainWindow._iter_tabs les deplie).
Bug intercepte par les tests avant integration : reparenter le widget d'origine
dans son panneau retire automatiquement son onglet du QTabWidget (un widget ne
peut avoir qu'un seul parent), ce qui decale les onglets suivants ; un appel
explicite a removeTab() ensuite visait donc le mauvais onglet. Corrige en
laissant Qt gerer ce retrait implicitement plutot que de le dupliquer.
tests/test_split_terminal.py : SSHConnection.start()/LocalTerminalConnection.start()
neutralisees (teste l'agencement des panneaux et le cycle de vie des onglets,
pas une connexion reelle).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Support minimum : Host, HostName, User, Port, IdentityFile, ProxyJump,
ForwardAgent, ServerAliveInterval, ServerAliveCountMax.
- app/openssh/config.py : load_openssh_hosts() lit ~/.ssh/config via
paramiko.SSHConfig (gere nativement les blocs "Host *" fusionnes, les alias
multiples sur une meme ligne Host, et l'expansion ~ des IdentityFile).
Fichier absent = liste vide, pas une erreur. session_from_openssh_host()
convertit un Host en Session PySSH (groupe "OpenSSH", cle si IdentityFile
presente sinon authentification auto).
- Nouvelle section "Configuration OpenSSH" dans le panneau lateral
(SessionController), sous Sessions SSH : double-clic ou menu contextuel pour
se connecter directement ou importer comme session (editeur pre-rempli,
ajustable avant enregistrement) ; rafraichissement via le menu ⚙ ou le menu
contextuel de l'hote.
- ConnectionController.open_openssh_host() ouvre une connexion directe sans
enregistrer de session, comme une connexion rapide.
Le ProxyJump est detecte, affiche (badge "· via <bastion>", tooltip) et reporte
dans les notes d'une session importee, mais pas encore applique automatiquement
a la connexion : ce sera l'objet de la Phase 3 (Bastion / Jump Host), qui
prendra en charge toutes ses sources (OpenSSH, session PySSH, config manuelle)
via un mecanisme commun.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
main_window.py concentrait la fenetre principale, l'arbre des sessions, les 4
classes d'onglets, la detection de commandes ssh et le multi-send : 983 lignes
et une seule classe qui pilotait toute l'application.
- app/services/ssh_target.py : detection d'une cible ssh dans un terminal (pur,
sans dependance Qt)
- app/ui/tabs.py : BaseTerminalTab / SessionTabWidget / SFTPTabWidget /
LocalTerminalTabWidget (deplaces tels quels)
- app/controllers/session_controller.py : panneau lateral (arbre sessions +
terminaux locaux) et CRUD des sessions
- app/controllers/connection_controller.py : cycle de vie des onglets
(ouverture, connexion rapide, renommage, fermeture)
- app/controllers/broadcast_manager.py : bouton et logique du mode Multi-Send
main_window.py ne fait plus que construire ces composants et les relier par
signaux (983 -> ~200 lignes). Comportement inchange ; MainWindow.tabs et
detect_ssh_target restent importables depuis app.main_window pour la
compatibilite avec les tests existants.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Double-clic sur un onglet pour le renommer, ou clic droit pour le menu. Le menu
contextuel permet aussi de fermer l'onglet vise ou tous les autres.
Le nom saisi est conserve tel quel : un onglet renomme "Serveur prod" ne
retrouve pas son libelle d'origine, y compris apres une reconnexion.
Un nom vide ou uniquement compose d'espaces est refuse, et annuler la saisie
laisse le nom inchange.
La fermeture des autres onglets parcourt la liste a l'envers : retirer un
onglet decale l'index des suivants, un parcours en avant en sauterait.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le panneau des sessions etait declare Movable|Floatable mais pas Closable : il
n'avait donc aucun bouton de fermeture et ne pouvait pas etre masque. Sa largeur
plancher etait par ailleurs imposee par son contenu, ce qui empechait de le
retrecir vraiment.
- ajout de DockWidgetClosable, largeur minimale ramenee a 120 px
- bascule par F9, par le menu Affichage, ou par la croix du panneau, les trois
restant synchronises
- separateur elargi a 6 px et surligne au survol, pour etre attrape facilement
Generation de cles : ajout d'ECDSA sur les courbes NIST P-256, P-384 et P-521,
en plus d'Ed25519 et RSA. Ed25519 reste le choix par defaut : c'est aussi une
courbe elliptique (Curve25519), plus courte et plus rapide, et sans les
interrogations qui entourent le choix des parametres des courbes NIST. Chaque
type est verifie relisible par paramiko, donc utilisable pour se connecter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tunnels (redirection de ports), la specialite de Bitvise : un bouton dans la
barre d'un onglet SSH ouvre un gestionnaire de redirections.
- locale (-L) : un port de votre poste est relaye, via la connexion SSH, vers
une machine joignable depuis le serveur (typiquement un service qui n'ecoute
que sur la boucle locale du serveur)
- distante (-R) : l'inverse
Chaque tunnel tourne dans son propre thread, un relais bloque ne genant ni
l'interface ni les autres tunnels. Ils sont fermes avec la session.
Envoi simultane, façon MobaXterm : le bouton "Multi" diffuse la frappe vers
tous les onglets terminal. La diffusion ecrit directement dans les connexions
et non via les widgets terminal, sinon chaque onglet rediffuserait a son tour.
Reparation : un de mes scripts de correction avait reecrit main_window.py en
retraduisant deux fois les fins de ligne, produisant des sequences \r\r\n ;
le fichier avait ete committe ainsi (2538 lignes au lieu de 856, une ligne vide
inseree partout). Le contenu etait intact, seule la mise en forme etait touchee.
Le fichier est reconstitue par normalisation des fins de ligne, et un controle
d'integrite a ete ajoute a la verification de non-regression.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'emulateur de terminal etait deja le notre (pyte + QPainter) ; ce qui manquait
pour se rapprocher de MobaXterm, c'etait le *shell*. MobaXterm ne fournit pas
"son propre terminal" mais son propre bash, issu d'un Cygwin embarque : c'est ce
qui donne ls/grep/sed/awk/vi sous Windows.
Plutot que d'embarquer un binaire tiers, l'application detecte maintenant les
shells deja installes et les propose tous : PowerShell, cmd, Bash de Git for
Windows, MSYS2, Cygwin, WSL, et un busybox-w32 pose a cote de l'executable pour
les postes qui n'ont rien. Un shell Unix est choisi par defaut quand il y en a un.
Deux details qui bloquaient :
- pywinpty ne sait pas decouper une commande passee en chaine avec des
guillemets, et les chemins Windows en contiennent ("Program Files") : les
commandes sont donc des listes d'arguments.
- le bash de MSYS2 demarre sans jamais afficher d'invite si MSYSTEM n'est pas
definie ; chaque shell peut donc declarer ses variables d'environnement
(CHERE_INVOKING pour demarrer dans le dossier courant).
Les menus et l'arborescence sont generes depuis la detection, ils s'adaptent
donc au poste sans configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
Reunit en un seul outil les fonctions utiles de MobaXterm, Termius et Bitvise,
sans compte ni service tiers, avec toutes les donnees en local.
- Terminaux multi-onglets : sessions SSH et terminaux Windows locaux
(PowerShell / cmd) via la pseudo-console native ConPTY
- Gestionnaire de fichiers SFTP facon WinSCP : double panneau, colonnes
triables, glisser-deposer, renommage, permissions, file de transferts.
Ouvrable sur une connexion en cours sans creer de favori
- Gestion des cles SSH : generation Ed25519/RSA, import, export
- Gestion du fichier known_hosts, avec remplacement guide d'une cle d'hote
modifiee (cas frequent des serveurs en DHCP)
- Trois themes (clair, bleu nuit, noir), commutables a chaud
- Authentification automatique reprenant le comportement de la commande ssh
(agent SSH et cles ~/.ssh), mot de passe demande seulement en secours
Aucun mot de passe n'est stocke sur le disque.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>