Files
pyssh-client/app/theme.py
snoopy b977842186 Client SSH/SFTP personnel pour Windows
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>
2026-09-04 06:50:10 +02:00

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)