from PyQt6.QtWidgets import QSystemTrayIcon, QMenu, QApplication from PyQt6.QtGui import QAction from PyQt6.QtCore import QTimer from app.core.config import Config from app.core import wireguard as wg_core from app.ui import icons class SystemTray(QSystemTrayIcon): def __init__(self, config: Config, main_window, parent=None): super().__init__(icons.icon_disconnected(), parent) self._cfg = config self._win = main_window self._connected = False self._build_menu() self.setToolTip("WGSecure — Déconnecté") self.activated.connect(self._on_activated) self._poll = QTimer(parent) self._poll.timeout.connect(self._update_status) self._poll.start(5000) self._update_status() def _build_menu(self): menu = QMenu() # Ligne de statut (désactivée) self._action_status = QAction("● Déconnecté", self) self._action_status.setEnabled(False) menu.addAction(self._action_status) menu.addSeparator() # Afficher la fenêtre self._action_show = QAction(icons.icon_app(), "Afficher la fenêtre", self) self._action_show.triggered.connect(self._show_window) menu.addAction(self._action_show) menu.addSeparator() # Connexion / déconnexion self._action_toggle = QAction(icons.icon_disconnected(), "Se connecter", self) self._action_toggle.triggered.connect(self._toggle_connection) menu.addAction(self._action_toggle) # Saisie du code d'accès : le tunnel monte en quarantaine et # l'autorisation expire d'elle-même, il faut donc pouvoir y revenir # sans passer par une déconnexion. Masquée quand elle n'a pas lieu # d'être — pas d'identifiant VPN, tunnel à l'arrêt, accès déjà ouvert. self._action_access = QAction("🔐 Saisir le code d'accès", self) self._action_access.triggered.connect(self._win.request_access_code) self._action_access.setVisible(False) menu.addAction(self._action_access) menu.addSeparator() # Panneau de configuration self._action_config = QAction(icons.icon_admin(), "Configuration", self) self._action_config.triggered.connect(self._win._open_admin) menu.addAction(self._action_config) menu.addSeparator() # Quitter self._action_quit = QAction("Quitter", self) self._action_quit.triggered.connect(self._quit) menu.addAction(self._action_quit) self.setContextMenu(menu) def _on_activated(self, reason: QSystemTrayIcon.ActivationReason): if reason == QSystemTrayIcon.ActivationReason.Trigger: self._show_window() def _show_window(self): self._win.show() self._win.raise_() self._win.activateWindow() def _update_status(self): prev = self._connected try: self._connected = wg_core.is_connected(self._cfg) except Exception: self._connected = False # La fenêtre principale est seule à savoir si le serveur a ouvert # l'accès : elle tient cet état, qu'aucune commande WireGuard ne # rapporte — le tunnel est identique, ouvert ou en quarantaine. self._action_access.setVisible(self._win.access_code_needed()) if self._connected: self.setIcon(icons.icon_connected()) self.setToolTip("WGSecure — Connecté") self._action_status.setText("● Connecté") self._action_toggle.setIcon(icons.icon_connected()) self._action_toggle.setText("Se déconnecter") if not prev: srv = self._cfg.wg.get("server_endpoint", "") self.showMessage( "WGSecure — Connecté ✓", f"Tunnel WireGuard actif{(' → ' + srv) if srv else ''}", QSystemTrayIcon.MessageIcon.Information, 3000, ) else: self.setIcon(icons.icon_disconnected()) self.setToolTip("WGSecure — Déconnecté") self._action_status.setText("● Déconnecté") self._action_toggle.setIcon(icons.icon_disconnected()) self._action_toggle.setText("Se connecter") if prev: self.showMessage( "WGSecure — Déconnecté", "Le tunnel WireGuard a été désactivé.", QSystemTrayIcon.MessageIcon.Warning, 3000, ) def _toggle_connection(self): self._win._on_connect() # La (dé)connexion est désormais asynchrone : l'icône sera mise à jour # par la scrutation périodique une fois l'opération terminée. def _quit(self): self._poll.stop() self.hide() # prepare_quit() arrête les minuteries, attend les threads en cours, # démonte le tunnel et retire la configuration DNS qu'il avait posée. # Sans cela, quitter en étant connecté laissait la machine sans # résolution de noms. self._win.prepare_quit() QApplication.quit()