Terminaux locaux : detecter et proposer les shells Unix du poste
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>
This commit is contained in:
@@ -15,7 +15,12 @@ Client SSH/SFTP personnel (type MobaXterm/Termius/Bitvise), 100% local, sans com
|
|||||||
avec des serveurs de test qui redemarrent souvent
|
avec des serveurs de test qui redemarrent souvent
|
||||||
- Les hotes deja approuves par votre commande `ssh` (`~/.ssh/known_hosts`) sont reconnus : pas de
|
- Les hotes deja approuves par votre commande `ssh` (`~/.ssh/known_hosts`) sont reconnus : pas de
|
||||||
double confirmation d'empreinte. Les nouvelles cles vont dans le fichier propre a l'application
|
double confirmation d'empreinte. Les nouvelles cles vont dans le fichier propre a l'application
|
||||||
- Terminaux locaux Windows (PowerShell / Invite de commandes) accessibles en un clic, **sans avoir besoin d'une session enregistree** — pratique pour un usage ponctuel, comme le terminal Windows natif
|
- Terminaux locaux accessibles en un clic, **sans session enregistree**. L'application detecte les
|
||||||
|
shells presents sur le poste et les propose tous : PowerShell, cmd, et surtout les **shells Unix**
|
||||||
|
s'ils sont installes (Bash de Git for Windows, MSYS2, Cygwin, WSL, ou un busybox pose a cote de
|
||||||
|
l'application). Comme le terminal local de MobaXterm, on retrouve alors `ls`, `grep`, `sed`, `awk`,
|
||||||
|
`vi`, `scp`... sous Windows. **Un shell Unix est choisi par defaut** quand il y en a un ;
|
||||||
|
sinon PowerShell. Rien n'est embarque : l'application reutilise ce qui est deja installe
|
||||||
- Carnet d'adresses de serveurs (JSON local, `data/sessions.json`) organise par dossiers, connexion en un clic
|
- Carnet d'adresses de serveurs (JSON local, `data/sessions.json`) organise par dossiers, connexion en un clic
|
||||||
- Gestion des cles SSH : generation (Ed25519 / RSA), import de cles existantes, export (cle publique/privee)
|
- Gestion des cles SSH : generation (Ed25519 / RSA), import de cles existantes, export (cle publique/privee)
|
||||||
- Client SFTP integre facon WinSCP : double panneau local/distant avec colonnes triables (nom, taille, date, droits),
|
- Client SFTP integre facon WinSCP : double panneau local/distant avec colonnes triables (nom, taille, date, droits),
|
||||||
|
|||||||
@@ -5,25 +5,110 @@ fonctionnel en un clic, comme le fait MobaXterm avec ses "terminaux locaux".
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import queue
|
import queue
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import winpty
|
import winpty
|
||||||
from PySide6.QtCore import QThread, Signal
|
from PySide6.QtCore import QThread, Signal
|
||||||
|
|
||||||
SHELL_POWERSHELL = "powershell"
|
SHELL_POWERSHELL = "powershell"
|
||||||
SHELL_CMD = "cmd"
|
SHELL_CMD = "cmd"
|
||||||
|
SHELL_GITBASH = "gitbash"
|
||||||
|
SHELL_MSYS2 = "msys2"
|
||||||
|
SHELL_CYGWIN = "cygwin"
|
||||||
|
SHELL_WSL = "wsl"
|
||||||
|
SHELL_BUSYBOX = "busybox"
|
||||||
|
|
||||||
SHELL_LABELS = {
|
# Shells Unix cherches sur le poste. Comme MobaXterm, on veut pouvoir travailler avec
|
||||||
SHELL_POWERSHELL: "PowerShell",
|
# ls/grep/sed/awk/vi sous Windows ; mais plutot que d'embarquer un binaire tiers, on
|
||||||
SHELL_CMD: "Invite de commandes (cmd)",
|
# reutilise ce qui est deja installe (Git for Windows en fournit un tres complet).
|
||||||
}
|
# CHERE_INVOKING : demarrer dans le dossier courant plutot que dans le home.
|
||||||
SHELL_COMMANDS = {
|
# MSYSTEM : sans elle, le bash de MSYS2 se lance mais n'affiche jamais d'invite.
|
||||||
SHELL_POWERSHELL: "powershell.exe -NoLogo",
|
_UNIX_CANDIDATES = [
|
||||||
SHELL_CMD: "cmd.exe",
|
(SHELL_GITBASH, "Bash (Git for Windows)", [
|
||||||
|
r"C:\Program Files\Git\bin\bash.exe",
|
||||||
|
r"C:\Program Files (x86)\Git\bin\bash.exe",
|
||||||
|
], ["--login", "-i"], {"CHERE_INVOKING": "1"}),
|
||||||
|
(SHELL_MSYS2, "Bash (MSYS2)", [r"C:\msys64\usr\bin\bash.exe"], ["--login", "-i"],
|
||||||
|
{"CHERE_INVOKING": "1", "MSYSTEM": "MSYS"}),
|
||||||
|
(SHELL_CYGWIN, "Bash (Cygwin)", [r"C:\cygwin64\bin\bash.exe"], ["--login", "-i"],
|
||||||
|
{"CHERE_INVOKING": "1"}),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Les commandes sont des listes d'arguments : pywinpty ne sait pas decouper une chaine
|
||||||
|
# contenant des guillemets, et les chemins Windows en contiennent ("Program Files").
|
||||||
|
_BASE_SHELLS = {
|
||||||
|
SHELL_POWERSHELL: ("PowerShell", ["powershell.exe", "-NoLogo"], {}),
|
||||||
|
SHELL_CMD: ("Invite de commandes (cmd)", ["cmd.exe"], {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _busybox_path() -> str | None:
|
||||||
|
"""busybox-w32 pose a cote de l'application (pour les postes sans Git installe)."""
|
||||||
|
from app.paths import DATA_DIR
|
||||||
|
|
||||||
|
for candidate in (
|
||||||
|
DATA_DIR.parent / "busybox.exe",
|
||||||
|
DATA_DIR.parent / "busybox64.exe",
|
||||||
|
shutil.which("busybox") or "",
|
||||||
|
):
|
||||||
|
if candidate and Path(candidate).exists():
|
||||||
|
return str(candidate)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _wsl_available() -> bool:
|
||||||
|
if not shutil.which("wsl.exe"):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
["wsl.exe", "-l", "-q"], capture_output=True, timeout=3,
|
||||||
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||||
|
)
|
||||||
|
# la sortie de wsl est en UTF-16
|
||||||
|
return bool(out.stdout.decode("utf-16-le", errors="ignore").strip())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def discover_shells() -> dict[str, tuple[str, list[str], dict[str, str]]]:
|
||||||
|
"""Renvoie les shells disponibles : {identifiant: (libelle, arguments, env)}."""
|
||||||
|
shells = dict(_BASE_SHELLS)
|
||||||
|
for shell_id, label, paths, args, env in _UNIX_CANDIDATES:
|
||||||
|
for path in paths:
|
||||||
|
if Path(path).exists():
|
||||||
|
shells[shell_id] = (label, [path] + args, env)
|
||||||
|
break
|
||||||
|
busybox = _busybox_path()
|
||||||
|
if busybox:
|
||||||
|
shells[SHELL_BUSYBOX] = ("Shell BusyBox", [busybox, "sh"], {})
|
||||||
|
if _wsl_available():
|
||||||
|
shells[SHELL_WSL] = ("WSL (Linux)", ["wsl.exe"], {})
|
||||||
|
return shells
|
||||||
|
|
||||||
|
|
||||||
|
SHELLS = discover_shells()
|
||||||
|
SHELL_LABELS = {k: v[0] for k, v in SHELLS.items()}
|
||||||
|
SHELL_COMMANDS = {k: v[1] for k, v in SHELLS.items()}
|
||||||
|
SHELL_ENV = {k: v[2] for k, v in SHELLS.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def default_shell() -> str:
|
||||||
|
"""Shell ouvert par defaut : un shell Unix s'il y en a un sur le poste (on y gagne
|
||||||
|
ls/grep/sed/awk/ssh/scp, comme le terminal local de MobaXterm), sinon PowerShell."""
|
||||||
|
for candidate in (SHELL_GITBASH, SHELL_MSYS2, SHELL_CYGWIN, SHELL_BUSYBOX):
|
||||||
|
if candidate in SHELLS:
|
||||||
|
return candidate
|
||||||
|
return SHELL_POWERSHELL
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SHELL = default_shell()
|
||||||
|
|
||||||
|
|
||||||
class LocalTerminalConnection(QThread):
|
class LocalTerminalConnection(QThread):
|
||||||
"""Meme interface de signaux que SSHConnection, pour reutiliser TerminalWidget tel quel."""
|
"""Meme interface de signaux que SSHConnection, pour reutiliser TerminalWidget tel quel."""
|
||||||
|
|
||||||
@@ -83,7 +168,7 @@ class LocalTerminalConnection(QThread):
|
|||||||
finally:
|
finally:
|
||||||
self._cleanup()
|
self._cleanup()
|
||||||
|
|
||||||
def _spawn(self, command: str) -> winpty.PtyProcess:
|
def _spawn(self, command: list[str]) -> winpty.PtyProcess:
|
||||||
"""Ouvre la pseudo-console, en privilegiant ConPTY.
|
"""Ouvre la pseudo-console, en privilegiant ConPTY.
|
||||||
|
|
||||||
ConPTY est l'API pseudo-console native de Windows (10 1809+) : c'est un vrai
|
ConPTY est l'API pseudo-console native de Windows (10 1809+) : c'est un vrai
|
||||||
@@ -93,11 +178,14 @@ class LocalTerminalConnection(QThread):
|
|||||||
artefacts de curseur qui saute et de saisie reecrite. On ne retombe dessus que si
|
artefacts de curseur qui saute et de saisie reecrite. On ne retombe dessus que si
|
||||||
ConPTY est indisponible (Windows trop ancien).
|
ConPTY est indisponible (Windows trop ancien).
|
||||||
"""
|
"""
|
||||||
|
extra = SHELL_ENV.get(self.shell) or {}
|
||||||
|
env = {**os.environ, **extra} if extra else None
|
||||||
try:
|
try:
|
||||||
return winpty.PtyProcess.spawn(
|
return winpty.PtyProcess.spawn(
|
||||||
command,
|
command,
|
||||||
dimensions=(self.rows, self.cols),
|
dimensions=(self.rows, self.cols),
|
||||||
cwd=self.cwd,
|
cwd=self.cwd,
|
||||||
|
env=env,
|
||||||
backend=winpty.Backend.ConPTY,
|
backend=winpty.Backend.ConPTY,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -105,6 +193,7 @@ class LocalTerminalConnection(QThread):
|
|||||||
command,
|
command,
|
||||||
dimensions=(self.rows, self.cols),
|
dimensions=(self.rows, self.cols),
|
||||||
cwd=self.cwd,
|
cwd=self.cwd,
|
||||||
|
env=env,
|
||||||
backend=winpty.Backend.WinPTY,
|
backend=winpty.Backend.WinPTY,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
1710
app/main_window.py
1710
app/main_window.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user