22
33import sys
44import traceback
5+ from typing import TYPE_CHECKING
6+
7+ if TYPE_CHECKING :
8+ from pystray import Icon
59
610
711def show_error (exc : Exception , shutdown : bool = True ) -> None :
@@ -21,11 +25,11 @@ def show_error(exc: Exception, shutdown: bool = True) -> None:
2125 import win32api
2226
2327 win32api .MessageBox (0 , text , title )
24- elif sys .platform == 'Linux ' :
28+ elif sys .platform == 'linux ' :
2529 import subprocess
2630
2731 subprocess .Popen (['xmessage' , '-center' , text ]) # noqa: S603, S607
28- elif sys .platform == 'Darwin ' :
32+ elif sys .platform == 'darwin ' :
2933 import subprocess
3034
3135 subprocess .Popen (['/usr/bin/osascript' , '-e' , text ]) # noqa: S603
@@ -152,6 +156,46 @@ def load_torrent_uri(parsed_args: Arguments) -> str | None:
152156 return torrent_uri
153157
154158
159+ async def mac_event_loop () -> None :
160+ """
161+ Consume Mac events on the asyncio main thread.
162+
163+ WARNING: sendEvent_ can block on some events. In particular, while the tray menu is open.
164+ """
165+ from AppKit import NSApp , NSEventMaskAny
166+ from Foundation import NSDate , NSDefaultRunLoopMode
167+
168+ while True :
169+ event = NSApp ().nextEventMatchingMask_untilDate_inMode_dequeue_ (NSEventMaskAny , NSDate .now (),
170+ NSDefaultRunLoopMode , True )
171+ if event is None :
172+ await asyncio .sleep (0.5 )
173+ else :
174+ NSApp ().sendEvent_ (event )
175+ await asyncio .sleep (0.01 )
176+
177+
178+ def spawn_tray_icon (session : Session , config : TriblerConfigManager ) -> Icon :
179+ """
180+ Create the tray icon.
181+ """
182+ import pystray
183+ image_path = tribler .get_webui_root () / "public" / "tribler.png"
184+ image = Image .open (image_path .resolve ())
185+ api_port = session .rest_manager .get_api_port ()
186+ url = f"http://{ config .get ('api/http_host' )} :{ api_port } /ui/#/downloads/all?key={ config .get ('api/key' )} "
187+ menu = (pystray .MenuItem ('Open' , lambda : webbrowser .open_new_tab (url )),
188+ pystray .MenuItem ('Quit' , lambda : session .shutdown_event .set ()))
189+ icon = pystray .Icon ("Tribler" , icon = image , title = "Tribler" , menu = menu )
190+ webbrowser .open_new_tab (url )
191+ if sys .platform == "darwin" :
192+ icon .run_detached (None )
193+ asyncio .ensure_future (mac_event_loop ()) # noqa: RUF006
194+ else :
195+ threading .Thread (target = icon .run ).start ()
196+ return icon
197+
198+
155199async def main () -> None :
156200 """
157201 The main script entry point.
@@ -185,16 +229,7 @@ async def main() -> None:
185229 if server_url and torrent_uri :
186230 await start_download (config , server_url , torrent_uri )
187231 if not headless :
188- import pystray
189- image_path = tribler .get_webui_root () / "public" / "tribler.png"
190- image = Image .open (image_path .resolve ())
191- api_port = session .rest_manager .get_api_port ()
192- url = f"http://{ config .get ('api/http_host' )} :{ api_port } /ui/#/downloads/all?key={ config .get ('api/key' )} "
193- menu = (pystray .MenuItem ('Open' , lambda : webbrowser .open_new_tab (url )),
194- pystray .MenuItem ('Quit' , lambda : session .shutdown_event .set ()))
195- icon = pystray .Icon ("Tribler" , icon = image , title = "Tribler" , menu = menu )
196- webbrowser .open_new_tab (url )
197- threading .Thread (target = icon .run ).start ()
232+ icon = spawn_tray_icon (session , config )
198233
199234 await session .shutdown_event .wait ()
200235 await session .shutdown ()
0 commit comments