"""System tray wrapper for PaperDash. Runs the FastAPI server in a background thread and displays a tray icon with a quit option. This is the entry point for the compiled .exe. """ from __future__ import annotations import logging import multiprocessing import os import signal import subprocess import sys import time import pystray from PIL import Image, ImageDraw logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", ) logger = logging.getLogger("paperdash.tray") def create_icon() -> Image.Image: """Create a simple 16x16 tray icon.""" img = Image.new("RGB", (16, 16), color=(0, 0, 0)) draw = ImageDraw.Draw(img) # Dashboard frame draw.rectangle([1, 1, 15, 15], outline=(255, 255, 255), width=1) # Screen lines draw.line([3, 3, 13, 3], fill=(255, 255, 255), width=1) draw.line([3, 5, 13, 5], fill=(255, 255, 255), width=1) draw.line([3, 7, 8, 7], fill=(255, 255, 255), width=1) # Stand draw.line([6, 15, 10, 15], fill=(255, 255, 255), width=1) return img def _run_server() -> None: """Run the FastAPI server in this process (for multiprocessing).""" import uvicorn uvicorn.run( "main:app", host=CONFIG_HOST, port=CONFIG_PORT, log_level=CONFIG_LOG_LEVEL, reload=False, ) def _on_quit(icon: pystray.Icon, item: pystray.MenuItem) -> None: """Handle quit from tray menu.""" logger.info("Quit requested from tray") icon.stop() def _on_settings(icon: pystray.Icon, item: pystray.MenuItem) -> None: """Open the settings page in the default browser.""" logger.info("Opening settings page...") url = "http://127.0.0.1:8921/settings" try: if sys.platform == "win32": os.startfile(url) elif sys.platform == "darwin": subprocess.run(["open", url]) else: subprocess.run(["xdg-open", url]) except Exception as e: logger.error(f"Failed to open settings page: {e}") def main() -> None: """Entry point for the compiled .exe.""" logger.info("PaperDash starting...") # Start server in a subprocess server_process = multiprocessing.Process(target=_run_server, daemon=True) server_process.start() logger.info(f"Server process started (PID {server_process.pid})") # Wait briefly for server to be ready time.sleep(2) # Create and run tray icon icon_image = create_icon() menu = pystray.Menu( pystray.MenuItem("PaperDash", pystray.MenuItem.default, enabled=False), pystray.Menu.Separator(), pystray.MenuItem("Settings", _on_settings), pystray.Menu.Separator(), pystray.MenuItem("Quit", _on_quit), ) icon = pystray.Icon( name="PaperDash", image=icon_image, title="PaperDash", menu=menu, ) # Handle OS exit signals def _signal_handler(signum, frame): logger.info(f"Received signal {signum}, shutting down") icon.stop() server_process.terminate() signal.signal(signal.SIGINT, _signal_handler) signal.signal(signal.SIGTERM, _signal_handler) logger.info("PaperDash running in system tray. Quit from notification area.") icon.run() # Cleanup logger.info("Shutting down server process") server_process.terminate() server_process.join(timeout=5) def _load_config_values() -> tuple[str, int, str]: """Load config and return (host, port, log_level) for the server.""" from .config import config config.load() return config.host or "127.0.0.1", config.port, config.log_level.lower() # Load config at module level so _run_server uses detected values CONFIG_HOST, CONFIG_PORT, CONFIG_LOG_LEVEL = _load_config_values() if __name__ == "__main__": main()