"""Entry point for ``python -m wled_controller``. Starts the uvicorn server and, on Windows when *pystray* is installed, shows a system-tray icon with **Show UI** / **Exit** actions. """ import asyncio import os import socket import sys import threading import time import webbrowser from pathlib import Path import uvicorn from wled_controller.config import get_config from wled_controller.server_ref import set_server, set_tray from wled_controller.tray import PYSTRAY_AVAILABLE, TrayManager from wled_controller.utils import setup_logging, get_logger setup_logging() logger = get_logger(__name__) _ICON_PATH = Path(__file__).parent / "static" / "icons" / "icon-192.png" def _run_server(server: uvicorn.Server) -> None: """Run uvicorn in a dedicated asyncio event loop (background thread).""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(server.serve()) def _open_browser(port: int, delay: float = 2.0) -> None: """Open the UI in the default browser after a short delay.""" time.sleep(delay) webbrowser.open(f"http://localhost:{port}") def _is_restart() -> bool: """Detect if this is a restart (vs first launch).""" return os.environ.get("WLED_RESTART", "") == "1" def _check_port(host: str, port: int) -> None: """Exit with a clear message if the port is already in use.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(1) try: sock.bind((host, port)) except OSError: logger.error("Port %d is already in use on %s", port, host) sys.exit(1) def main() -> None: config = get_config() _check_port(config.server.host, config.server.port) uv_config = uvicorn.Config( "wled_controller.main:app", host=config.server.host, port=config.server.port, log_level=config.server.log_level.lower(), ) server = uvicorn.Server(uv_config) set_server(server) use_tray = PYSTRAY_AVAILABLE and (sys.platform == "win32" or _force_tray()) if use_tray: logger.info("Starting with system tray icon") # Uvicorn in a background thread server_thread = threading.Thread( target=_run_server, args=(server,), daemon=True, ) server_thread.start() # Browser after a short delay (skip on restart — user already has a tab) if not _is_restart(): threading.Thread( target=_open_browser, args=(config.server.port,), daemon=True, ).start() # Tray on main thread (blocking) tray = TrayManager( icon_path=_ICON_PATH, port=config.server.port, on_exit=lambda: _request_shutdown(server), ) set_tray(tray) tray.run() # Tray exited — wait for server to finish its graceful shutdown server_thread.join(timeout=10) else: if not PYSTRAY_AVAILABLE: logger.info("System tray not available (install pystray for tray support)") server.run() def _request_shutdown(server: uvicorn.Server) -> None: """Signal uvicorn to perform a graceful shutdown.""" server.should_exit = True def _force_tray() -> bool: """Allow forcing tray on non-Windows via WLED_TRAY=1.""" import os return os.environ.get("WLED_TRAY", "").strip() in ("1", "true", "yes") if __name__ == "__main__": main()