90 lines
2.5 KiB
Python
90 lines
2.5 KiB
Python
"""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()
|