"""System tray runner — connects tray data classes to pystray. Provides a cross-platform system tray icon with menu items for controlling the I2P router. Requires pystray (optional dependency). Usage: from i2p_apps.desktop.runner import TrayRunner runner = TrayRunner() runner.run() # blocks until user quits """ from __future__ import annotations import logging import subprocess import sys import webbrowser from typing import Any from i2p_apps.desktop.tray import MenuItem, TrayConfig, TrayMenu logger = logging.getLogger(__name__) try: import pystray from PIL import Image _HAS_PYSTRAY = True except ImportError: _HAS_PYSTRAY = False def _create_icon_image(size: int = 64) -> Any: """Create a simple I2P-themed icon (purple square with white 'I2P' text). Falls back to a solid colored square if PIL text rendering is unavailable. """ if not _HAS_PYSTRAY: return None img = Image.new("RGB", (size, size), color=(88, 44, 131)) try: from PIL import ImageDraw draw = ImageDraw.Draw(img) # Draw a simple centered marker margin = size // 4 draw.rectangle( [margin, margin, size - margin, size - margin], fill=(255, 255, 255), ) draw.rectangle( [margin + 2, margin + 2, size - margin - 2, size - margin - 2], fill=(88, 44, 131), ) except ImportError: pass return img class TrayRunner: """Cross-platform system tray icon using pystray.""" def __init__( self, config: TrayConfig | None = None, menu: TrayMenu | None = None, ) -> None: if not _HAS_PYSTRAY: raise RuntimeError( "pystray is required for system tray. " "Install with: pip install 'i2p-python[gui]'" ) self._config = config or TrayConfig() self._menu = menu or TrayMenu() self._icon: pystray.Icon | None = None def _dispatch_action(self, action: str) -> None: """Dispatch a menu item action.""" if action == "open_console": webbrowser.open(self._config.console_url) elif action == "restart": logger.info("Restart requested via tray menu") self._signal_router("restart") elif action == "shutdown_graceful": logger.info("Graceful shutdown requested via tray menu") self._signal_router("shutdown") elif action == "shutdown": logger.info("Immediate shutdown requested via tray menu") self._signal_router("shutdown-now") elif action == "cancel_shutdown": logger.info("Shutdown cancelled via tray menu") self._signal_router("cancel-shutdown") elif action == "quit": if self._icon: self._icon.stop() else: logger.warning("Unknown tray action: %s", action) @staticmethod def _signal_router(command: str) -> None: """Send a command to the router via its health endpoint.""" import urllib.request import urllib.error try: req = urllib.request.Request( "http://127.0.0.1:9701/api/v1/router/" + command, method="POST", data=b"", ) with urllib.request.urlopen(req, timeout=5) as resp: logger.info("Router %s: %d", command, resp.status) except urllib.error.URLError as exc: logger.warning("Could not reach router: %s", exc) def _build_pystray_menu(self) -> pystray.Menu: """Convert TrayMenu items to pystray menu items.""" items = [] for mi in self._menu.items: if mi.is_separator: items.append(pystray.Menu.SEPARATOR) else: action = mi.action def on_click(_icon: Any, _item: Any, act: str = action) -> None: self._dispatch_action(act) items.append(pystray.MenuItem(mi.label, on_click)) # Always add Quit at the end items.append(pystray.Menu.SEPARATOR) items.append( pystray.MenuItem( "Quit Tray", lambda _icon, _item: self._dispatch_action("quit"), ) ) return pystray.Menu(*items) def run(self) -> None: """Run the system tray icon. Blocks until the user quits.""" image = _create_icon_image() menu = self._build_pystray_menu() self._icon = pystray.Icon( name="i2p-router", icon=image, title=self._config.tooltip, menu=menu, ) logger.info("Starting system tray icon") self._icon.run() def stop(self) -> None: """Stop the tray icon programmatically.""" if self._icon: self._icon.stop() def main() -> None: """Entry point for i2p-router-gui command.""" logging.basicConfig( level=logging.INFO, format="%(levelname)s %(name)s: %(message)s", ) try: runner = TrayRunner() runner.run() except RuntimeError as exc: print(str(exc), file=sys.stderr) sys.exit(1)