Le module de thème publie deux palettes derrière les mêmes noms : `apply()` rebranche `BG`, `TEXT`, `TAB_CSS` sur celle qui est choisie au lancement, et les cent soixante-dix appels répartis dans l'interface continuent de lire `theme.BG` sans rien savoir du thème actif. Aucun widget n'a eu à changer — c'est ce que permettait le passage systématique par `theme.X` à l'usage. Le choix est lu une seule fois, au démarrage. Les feuilles de style sont posées dans les constructeurs des fenêtres ; les repeindre à chaud supposerait de reconstruire celle qui porte l'état du tunnel, ses fils d'exécution et ses minuteries. Le message de confirmation annonce donc la prise d'effet au prochain lancement plutôt que de laisser croire le contraire. Le thème sombre reste le défaut, et rend exactement les mêmes feuilles de style qu'avant. Un thème clair n'est pas une inversion, et trois familles de valeurs vivaient en dur là où elles étaient jusqu'ici invisibles. Dans la feuille de style commune, les variantes d'état des champs et des boutons étaient des voiles blancs translucides — c'est-à-dire rien du tout sur un fond clair. Le texte posé sur un aplat saturé — bouton plein, onglet actif, bandeau, sélection — portait `TEXT`, qui devient presque noir en thème clair et disparaîtrait sur l'accent : un jeton distinct le dit, blanc dans les deux thèmes. Enfin le graphe de bande passante peignait son fond en dur, soit un panneau sombre au milieu d'une page claire ; ses sept teintes sont devenues des jetons, comme quatre couleurs de survol de la fenêtre principale. Deux endroits figeaient par ailleurs le thème au chargement de leur module et auraient gardé les teintes sombres quel que soit le réglage : la feuille de style de la fenêtre d'historique, bâtie au niveau du module, et la table état → couleur du diagnostic, évaluée comme attribut de classe. La première devient une fonction, la seconde ne retient que la puce et demande la couleur au thème au moment de l'affichage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
182 lines
6.0 KiB
Python
182 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
WGSecure (WGS) — Interface graphique WireGuard avec MFA
|
|
"""
|
|
import sys
|
|
import argparse
|
|
|
|
from PyQt6.QtWidgets import QApplication, QMessageBox, QInputDialog, QLineEdit, QSystemTrayIcon
|
|
from PyQt6.QtCore import Qt
|
|
|
|
from app import __version__
|
|
from app.core.config import Config
|
|
from app.core import dns as dns_util
|
|
from app.core import log as conn_log
|
|
from app.core import shutdown as shutdown_guard
|
|
from app.core.single_instance import SingleInstanceGuard
|
|
from app.ui.main_window import MainWindow
|
|
from app.ui import theme
|
|
from app.ui.systray import SystemTray
|
|
from app.ui import icons
|
|
from app.utils.platform_utils import install_desktop_entry
|
|
|
|
|
|
def _install_taskbar_icon(app_icon) -> None:
|
|
"""Exporte l'icône en PNG 256 px et installe l'entrée .desktop."""
|
|
try:
|
|
import tempfile, os
|
|
tmp = os.path.join(tempfile.gettempdir(), "wgsecure_icon.png")
|
|
app_icon.pixmap(256, 256).save(tmp, "PNG")
|
|
install_desktop_entry(tmp)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def recover_dns_state(config: Config) -> None:
|
|
"""Répare la résolution DNS laissée détournée par une session précédente.
|
|
|
|
Si l'application (ou la machine) s'est arrêtée sans exécuter
|
|
`wg-quick down`, la configuration DNS du tunnel reste active alors que le
|
|
résolveur correspondant est injoignable : plus aucun nom ne se résout.
|
|
Le nettoyage n'a lieu que si un résidu est effectivement constaté.
|
|
"""
|
|
iface = config.wg.get("interface_name", "wgs0")
|
|
try:
|
|
repaired, msg = dns_util.repair_if_broken(iface, config.wg.get("dns", ""))
|
|
except Exception as e: # noqa: BLE001
|
|
repaired, msg = False, f"Vérification DNS impossible : {e}"
|
|
if msg:
|
|
conn_log.log_event(msg, "warning" if repaired else "error")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
prog="wgsecure",
|
|
description="WGSecure — WireGuard GUI avec MFA",
|
|
)
|
|
parser.add_argument(
|
|
"--admin",
|
|
action="store_true",
|
|
help="Lancer directement en mode Administrateur",
|
|
)
|
|
parser.add_argument(
|
|
"--no-tray",
|
|
action="store_true",
|
|
help="Désactiver l'icône systray",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def require_admin_auth(config: Config, app: QApplication) -> bool:
|
|
"""Vérifie le mot de passe admin si configuré. Retourne True si autorisé."""
|
|
if not config.has_admin_password():
|
|
return True
|
|
pw, ok = QInputDialog.getText(
|
|
None,
|
|
"Accès Administrateur",
|
|
"Mot de passe administrateur :",
|
|
QLineEdit.EchoMode.Password,
|
|
)
|
|
if not ok:
|
|
return False
|
|
if config.check_admin_password(pw):
|
|
return True
|
|
QMessageBox.warning(None, "WGSecure", "Mot de passe incorrect.")
|
|
return False
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
# Support DPI haute résolution
|
|
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
|
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
|
)
|
|
app = QApplication(sys.argv)
|
|
app.setApplicationName("WGSecure")
|
|
app.setApplicationVersion(__version__)
|
|
app.setOrganizationName("WGS")
|
|
|
|
# Une seule instance à la fois : un second lancement réveille la
|
|
# première plutôt que d'ouvrir une deuxième fenêtre sur le même tunnel.
|
|
guard = SingleInstanceGuard()
|
|
if not guard.try_acquire():
|
|
sys.exit(0)
|
|
# Icône globale multi-tailles — barre des tâches + alt-tab
|
|
app_icon = icons.icon_app()
|
|
app.setWindowIcon(app_icon)
|
|
app.setDesktopFileName("wgsecure")
|
|
# Installe .desktop + PNG pour que GNOME/KDE affiche l'icône sur la taskbar
|
|
_install_taskbar_icon(app_icon)
|
|
# Empêche la fermeture quand toutes les fenêtres sont cachées (tray)
|
|
app.setQuitOnLastWindowClosed(False)
|
|
|
|
config = Config()
|
|
|
|
# Avant la moindre fenêtre : les feuilles de style sont posées dans les
|
|
# constructeurs, et une palette changée après coup n'atteindrait plus
|
|
# rien. `apply` retombe seule sur le thème sombre si le réglage est
|
|
# inconnu — une configuration écrite à la main ne doit pas empêcher
|
|
# l'application de s'ouvrir.
|
|
theme.apply(config.get("ui", "theme") or "dark")
|
|
|
|
# Reste d'une session précédente mal terminée : DNS détourné vers un
|
|
# résolveur mort. À faire avant d'ouvrir la fenêtre pour rendre l'accès
|
|
# au réseau tout de suite.
|
|
recover_dns_state(config)
|
|
|
|
# Restauration de l'état réseau garantie sur toutes les sorties :
|
|
# Quitter, croix, SIGINT/SIGTERM, atexit, exception non rattrapée.
|
|
shutdown_guard.install(config, report=conn_log.log_event, app=app)
|
|
|
|
# Mode Admin direct (--admin)
|
|
if args.admin:
|
|
if not require_admin_auth(config, app):
|
|
sys.exit(0)
|
|
from app.ui.admin_window import AdminWindow
|
|
win = AdminWindow(config)
|
|
win.setWindowIcon(icons.icon_admin())
|
|
win.show()
|
|
guard.on_activate_requested(lambda: _raise_window(win))
|
|
sys.exit(app.exec())
|
|
|
|
# Mode User (défaut)
|
|
window = MainWindow(config)
|
|
guard.on_activate_requested(lambda: _raise_window(window))
|
|
|
|
# Systray
|
|
if not args.no_tray and QSystemTrayIcon_available():
|
|
tray = SystemTray(config, window)
|
|
tray.show()
|
|
# Démarrer réduit si déjà configuré
|
|
if config.configured:
|
|
window.show()
|
|
else:
|
|
window.show()
|
|
tray.showMessage(
|
|
"WGSecure",
|
|
"Bienvenue ! Ouvrez le panneau Administrateur pour configurer WireGuard.",
|
|
QSystemTrayIcon.MessageIcon.Information,
|
|
4000,
|
|
)
|
|
else:
|
|
window.show()
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
def _raise_window(window) -> None:
|
|
"""Ramène une fenêtre existante au premier plan (second lancement)."""
|
|
window.showNormal()
|
|
window.raise_()
|
|
window.activateWindow()
|
|
|
|
|
|
def QSystemTrayIcon_available() -> bool:
|
|
from PyQt6.QtWidgets import QSystemTrayIcon
|
|
return QSystemTrayIcon.isSystemTrayAvailable()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|