"""System-tray icon: green = healthy chain, red = broken/stopped, yellow = connecting.""" from __future__ import annotations import threading from typing import Any, Callable import pystray from PIL import Image, ImageDraw def _circle_icon(color: str, size: int = 64) -> Image.Image: img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) pad = 4 draw.ellipse([pad, pad, size - pad, size - pad], fill=color) return img ICONS = { "green": _circle_icon("#00e676"), "red": _circle_icon("#ff1744"), "yellow": _circle_icon("#ffab00"), "gray": _circle_icon("#6c757d"), } TIPS = { "green": "Proxy Chain: healthy", "red": "Proxy Chain: broken / stopped", "yellow": "Proxy Chain: connecting…", "gray": "Proxy Chain: idle", } class TrayIcon: def __init__( self, on_show: Callable[[], None], on_quit: Callable[[], None], on_rotate: Callable[[], None], ) -> None: self._on_show = on_show self._on_quit = on_quit self._on_rotate = on_rotate self._icon: pystray.Icon | None = None self._thread: threading.Thread | None = None self._state = "gray" def start(self) -> None: if self._thread and self._thread.is_alive(): return menu = pystray.Menu( pystray.MenuItem("Show", self._show, default=True), pystray.MenuItem("Rotate now", self._rotate), pystray.Menu.SEPARATOR, pystray.MenuItem("Quit", self._quit), ) self._icon = pystray.Icon( "ProxyChainManager", icon=ICONS[self._state], title=TIPS[self._state], menu=menu, ) self._thread = threading.Thread(target=self._icon.run, daemon=True) self._thread.start() def stop(self) -> None: if self._icon: try: self._icon.stop() except Exception: pass def set_state(self, state: str) -> None: """state: 'green', 'red', 'yellow', 'gray'.""" if state not in ICONS: state = "gray" self._state = state if self._icon: self._icon.icon = ICONS[state] self._icon.title = TIPS[state] def _show(self, icon: Any = None, item: Any = None) -> None: self._on_show() def _rotate(self, icon: Any = None, item: Any = None) -> None: self._on_rotate() def _quit(self, icon: Any = None, item: Any = None) -> None: self._on_quit()