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>
182 lines
7.0 KiB
Python
182 lines
7.0 KiB
Python
"""Themes de l'application : palette d'interface (QSS) + palette du terminal.
|
|
|
|
Trois themes sont fournis (clair par defaut, bleu nuit, noir). Ils partagent le meme
|
|
gabarit `resources/theme.qss`, dont les $variables sont remplacees par la palette
|
|
choisie : une seule feuille de style a maintenir au lieu de trois quasi identiques.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from string import Template
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtGui import QIcon, QPixmap, QPainter, QColor, QFont, QLinearGradient
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
from app.paths import RESOURCES_DIR, DATA_DIR # noqa: E402 (chemins valables aussi en .exe)
|
|
STYLESHEET_PATH = RESOURCES_DIR / "theme.qss"
|
|
SETTINGS_FILE = DATA_DIR / "settings.json"
|
|
|
|
THEME_LIGHT = "light"
|
|
THEME_MIDNIGHT = "midnight"
|
|
THEME_BLACK = "black"
|
|
DEFAULT_THEME = THEME_LIGHT
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Palettes d'interface
|
|
# ---------------------------------------------------------------------------
|
|
UI_PALETTES: dict[str, dict[str, str]] = {
|
|
THEME_LIGHT: {
|
|
"window": "#f4f6f9", "base": "#ffffff", "panel": "#eef1f6", "elevated": "#e4e9f0",
|
|
"input": "#ffffff", "hover": "#dfe6f2", "pressed": "#d2dae8",
|
|
"border": "#d3dae4", "border_strong": "#b9c3d1",
|
|
"text": "#1c2430", "text_dim": "#5c6875", "text_disabled": "#a3adba",
|
|
"accent": "#2f6fed", "accent_hover": "#1f5ed6", "on_accent": "#ffffff",
|
|
"scroll": "#c3ccd9", "scroll_hover": "#a7b3c4",
|
|
},
|
|
THEME_MIDNIGHT: {
|
|
"window": "#0a1220", "base": "#0c1526", "panel": "#0e1a30", "elevated": "#1c2c47",
|
|
"input": "#101c30", "hover": "#172542", "pressed": "#16233b",
|
|
"border": "#1c2c47", "border_strong": "#2a3c5c",
|
|
"text": "#e6ecf6", "text_dim": "#93a4c2", "text_disabled": "#55627c",
|
|
"accent": "#2f6fed", "accent_hover": "#4f8cff", "on_accent": "#ffffff",
|
|
"scroll": "#2a3c5c", "scroll_hover": "#3a527d",
|
|
},
|
|
THEME_BLACK: {
|
|
"window": "#000000", "base": "#0a0a0a", "panel": "#141414", "elevated": "#1f1f1f",
|
|
"input": "#141414", "hover": "#262626", "pressed": "#0f0f0f",
|
|
"border": "#2b2b2b", "border_strong": "#3d3d3d",
|
|
"text": "#e8e8e8", "text_dim": "#9a9a9a", "text_disabled": "#5a5a5a",
|
|
"accent": "#4a9eff", "accent_hover": "#6fb3ff", "on_accent": "#000000",
|
|
"scroll": "#3a3a3a", "scroll_hover": "#525252",
|
|
},
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Palettes du terminal (16 couleurs ANSI + defauts + curseur)
|
|
# ---------------------------------------------------------------------------
|
|
TERMINAL_PALETTES: dict[str, dict] = {
|
|
THEME_LIGHT: {
|
|
"fg": "#1c2430", "bg": "#ffffff", "cursor": (47, 111, 237, 110),
|
|
"ansi": {
|
|
"black": "#1c2430", "red": "#c0392b", "green": "#2e7d32", "brown": "#a06800",
|
|
"blue": "#1f5ed6", "magenta": "#8e24aa", "cyan": "#00838f", "white": "#5c6875",
|
|
"brightblack": "#8a94a3", "brightred": "#e04b3a", "brightgreen": "#3ca142",
|
|
"brightbrown": "#c98a00", "brightblue": "#2f6fed", "brightmagenta": "#a944c7",
|
|
"brightcyan": "#0aa2b0", "brightwhite": "#1c2430",
|
|
},
|
|
},
|
|
THEME_MIDNIGHT: {
|
|
"fg": "#e6ecf6", "bg": "#0b1220", "cursor": (79, 140, 255, 140),
|
|
"ansi": {
|
|
"black": "#0b1220", "red": "#ff6b81", "green": "#8ce99a", "brown": "#f2c94c",
|
|
"blue": "#4f8cff", "magenta": "#c792ea", "cyan": "#63d4e0", "white": "#dbe4f5",
|
|
"brightblack": "#3a4a68", "brightred": "#ff8a9b", "brightgreen": "#a9e6b5",
|
|
"brightbrown": "#f5d778", "brightblue": "#7aa9ff", "brightmagenta": "#dcb3f0",
|
|
"brightcyan": "#8fe3ec", "brightwhite": "#ffffff",
|
|
},
|
|
},
|
|
THEME_BLACK: {
|
|
"fg": "#e8e8e8", "bg": "#000000", "cursor": (74, 158, 255, 140),
|
|
"ansi": {
|
|
"black": "#000000", "red": "#e05561", "green": "#8cc265", "brown": "#d18f52",
|
|
"blue": "#4aa5f0", "magenta": "#c162de", "cyan": "#42b3c2", "white": "#d0d0d0",
|
|
"brightblack": "#5a5a5a", "brightred": "#ff6b78", "brightgreen": "#a5e075",
|
|
"brightbrown": "#e5a663", "brightblue": "#6fb3ff", "brightmagenta": "#d178ee",
|
|
"brightcyan": "#5fc9d8", "brightwhite": "#ffffff",
|
|
},
|
|
},
|
|
}
|
|
|
|
THEME_LABELS = {
|
|
THEME_LIGHT: "Clair",
|
|
THEME_MIDNIGHT: "Bleu nuit",
|
|
THEME_BLACK: "Noir",
|
|
}
|
|
|
|
_current_theme = DEFAULT_THEME
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Preference persistante
|
|
# ---------------------------------------------------------------------------
|
|
def load_saved_theme() -> str:
|
|
try:
|
|
data = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
|
|
theme = data.get("theme")
|
|
if theme in UI_PALETTES:
|
|
return theme
|
|
except (OSError, json.JSONDecodeError):
|
|
pass
|
|
return DEFAULT_THEME
|
|
|
|
|
|
def save_theme(theme: str) -> None:
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
data = {}
|
|
try:
|
|
data = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
pass
|
|
data["theme"] = theme
|
|
try:
|
|
SETTINGS_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def current_theme() -> str:
|
|
return _current_theme
|
|
|
|
|
|
def terminal_palette(theme: str | None = None) -> dict:
|
|
return TERMINAL_PALETTES.get(theme or _current_theme, TERMINAL_PALETTES[DEFAULT_THEME])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Application du theme
|
|
# ---------------------------------------------------------------------------
|
|
def apply_theme(app: QApplication, theme: str = DEFAULT_THEME) -> None:
|
|
"""Bascule sur le style Fusion (necessaire pour que la QSS s'applique
|
|
uniformement, y compris aux widgets stylises nativement par Windows) puis
|
|
construit la feuille de style a partir de la palette demandee."""
|
|
global _current_theme
|
|
if theme not in UI_PALETTES:
|
|
theme = DEFAULT_THEME
|
|
_current_theme = theme
|
|
|
|
app.setStyle("Fusion")
|
|
try:
|
|
template = Template(STYLESHEET_PATH.read_text(encoding="utf-8"))
|
|
app.setStyleSheet(template.safe_substitute(UI_PALETTES[theme]))
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def build_app_icon() -> QIcon:
|
|
"""Genere une icone simple (pas de fichier externe requis) : carre + prompt '>_'"""
|
|
size = 64
|
|
pixmap = QPixmap(size, size)
|
|
pixmap.fill(Qt.transparent)
|
|
|
|
painter = QPainter(pixmap)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
gradient = QLinearGradient(0, 0, size, size)
|
|
gradient.setColorAt(0, QColor("#182a4a"))
|
|
gradient.setColorAt(1, QColor("#0a1220"))
|
|
painter.setBrush(gradient)
|
|
painter.setPen(QColor("#2a3c5c"))
|
|
painter.drawRoundedRect(2, 2, size - 4, size - 4, 14, 14)
|
|
|
|
font = QFont("Consolas")
|
|
font.setBold(True)
|
|
font.setPointSize(24)
|
|
painter.setFont(font)
|
|
painter.setPen(QColor("#4f8cff"))
|
|
painter.drawText(pixmap.rect(), Qt.AlignCenter, ">_")
|
|
|
|
painter.end()
|
|
return QIcon(pixmap)
|