A Python port of the Invisible Internet Project (I2P)
at main 171 lines 5.3 kB view raw
1"""System tray runner — connects tray data classes to pystray. 2 3Provides a cross-platform system tray icon with menu items for 4controlling the I2P router. Requires pystray (optional dependency). 5 6Usage: 7 from i2p_apps.desktop.runner import TrayRunner 8 runner = TrayRunner() 9 runner.run() # blocks until user quits 10""" 11 12from __future__ import annotations 13 14import logging 15import subprocess 16import sys 17import webbrowser 18from typing import Any 19 20from i2p_apps.desktop.tray import MenuItem, TrayConfig, TrayMenu 21 22logger = logging.getLogger(__name__) 23 24try: 25 import pystray 26 from PIL import Image 27 28 _HAS_PYSTRAY = True 29except ImportError: 30 _HAS_PYSTRAY = False 31 32 33def _create_icon_image(size: int = 64) -> Any: 34 """Create a simple I2P-themed icon (purple square with white 'I2P' text). 35 36 Falls back to a solid colored square if PIL text rendering is unavailable. 37 """ 38 if not _HAS_PYSTRAY: 39 return None 40 41 img = Image.new("RGB", (size, size), color=(88, 44, 131)) 42 try: 43 from PIL import ImageDraw 44 45 draw = ImageDraw.Draw(img) 46 # Draw a simple centered marker 47 margin = size // 4 48 draw.rectangle( 49 [margin, margin, size - margin, size - margin], 50 fill=(255, 255, 255), 51 ) 52 draw.rectangle( 53 [margin + 2, margin + 2, size - margin - 2, size - margin - 2], 54 fill=(88, 44, 131), 55 ) 56 except ImportError: 57 pass 58 return img 59 60 61class TrayRunner: 62 """Cross-platform system tray icon using pystray.""" 63 64 def __init__( 65 self, 66 config: TrayConfig | None = None, 67 menu: TrayMenu | None = None, 68 ) -> None: 69 if not _HAS_PYSTRAY: 70 raise RuntimeError( 71 "pystray is required for system tray. " 72 "Install with: pip install 'i2p-python[gui]'" 73 ) 74 self._config = config or TrayConfig() 75 self._menu = menu or TrayMenu() 76 self._icon: pystray.Icon | None = None 77 78 def _dispatch_action(self, action: str) -> None: 79 """Dispatch a menu item action.""" 80 if action == "open_console": 81 webbrowser.open(self._config.console_url) 82 elif action == "restart": 83 logger.info("Restart requested via tray menu") 84 self._signal_router("restart") 85 elif action == "shutdown_graceful": 86 logger.info("Graceful shutdown requested via tray menu") 87 self._signal_router("shutdown") 88 elif action == "shutdown": 89 logger.info("Immediate shutdown requested via tray menu") 90 self._signal_router("shutdown-now") 91 elif action == "cancel_shutdown": 92 logger.info("Shutdown cancelled via tray menu") 93 self._signal_router("cancel-shutdown") 94 elif action == "quit": 95 if self._icon: 96 self._icon.stop() 97 else: 98 logger.warning("Unknown tray action: %s", action) 99 100 @staticmethod 101 def _signal_router(command: str) -> None: 102 """Send a command to the router via its health endpoint.""" 103 import urllib.request 104 import urllib.error 105 106 try: 107 req = urllib.request.Request( 108 "http://127.0.0.1:9701/api/v1/router/" + command, 109 method="POST", 110 data=b"", 111 ) 112 with urllib.request.urlopen(req, timeout=5) as resp: 113 logger.info("Router %s: %d", command, resp.status) 114 except urllib.error.URLError as exc: 115 logger.warning("Could not reach router: %s", exc) 116 117 def _build_pystray_menu(self) -> pystray.Menu: 118 """Convert TrayMenu items to pystray menu items.""" 119 items = [] 120 for mi in self._menu.items: 121 if mi.is_separator: 122 items.append(pystray.Menu.SEPARATOR) 123 else: 124 action = mi.action 125 126 def on_click(_icon: Any, _item: Any, act: str = action) -> None: 127 self._dispatch_action(act) 128 129 items.append(pystray.MenuItem(mi.label, on_click)) 130 131 # Always add Quit at the end 132 items.append(pystray.Menu.SEPARATOR) 133 items.append( 134 pystray.MenuItem( 135 "Quit Tray", 136 lambda _icon, _item: self._dispatch_action("quit"), 137 ) 138 ) 139 return pystray.Menu(*items) 140 141 def run(self) -> None: 142 """Run the system tray icon. Blocks until the user quits.""" 143 image = _create_icon_image() 144 menu = self._build_pystray_menu() 145 self._icon = pystray.Icon( 146 name="i2p-router", 147 icon=image, 148 title=self._config.tooltip, 149 menu=menu, 150 ) 151 logger.info("Starting system tray icon") 152 self._icon.run() 153 154 def stop(self) -> None: 155 """Stop the tray icon programmatically.""" 156 if self._icon: 157 self._icon.stop() 158 159 160def main() -> None: 161 """Entry point for i2p-router-gui command.""" 162 logging.basicConfig( 163 level=logging.INFO, 164 format="%(levelname)s %(name)s: %(message)s", 165 ) 166 try: 167 runner = TrayRunner() 168 runner.run() 169 except RuntimeError as exc: 170 print(str(exc), file=sys.stderr) 171 sys.exit(1)