#!/usr/bin/env python3 """WebView2 settings window for Clawdmeter (Phase 7, M1). Opens a native Windows WebView2 window (via pywebview) onto the local control panel the tray process already serves on 127.0.0.1. Launched as a SEPARATE PROCESS from the tray — ``Clawdmeter.exe --panel`` when frozen, ``python -m daemon.panel`` in source — because pywebview and pystray each need to own the main thread and cannot coexist in one process (see tray_windows._on_settings). The port comes from CLAWDMETER_PANEL_PORT (set by the tray when it spawns us) and falls back to the config default, so the window always points at the server the tray actually started. """ from __future__ import annotations import os import sys import time import urllib.request # Make the `daemon` package importable when run as a bare script or frozen exe, # mirroring tray_windows.py's bootstrap (logon autostart starts us with cwd = # System32, and the frozen exe loads the package from the bundle root). if getattr(sys, "frozen", False): _REPO_ROOT = sys._MEIPASS # type: ignore[attr-defined] else: _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if _REPO_ROOT not in sys.path: sys.path.insert(0, _REPO_ROOT) _VENV_SITE = os.path.join(_REPO_ROOT, ".venv", "Lib", "site-packages") if os.path.isdir(_VENV_SITE): import site site.addsitedir(_VENV_SITE) try: from daemon import config as cfgmod except ImportError: # running as a plain script (cwd on path) import config as cfgmod WINDOW_TITLE = "Clawdmeter" WINDOW_W = 860 WINDOW_H = 720 WINDOW_MIN = (640, 560) BRAND_BG = "#131211" # paint the chrome brand-dark so there's no white flash def _port() -> int: raw = os.environ.get("CLAWDMETER_PANEL_PORT") if raw: try: return int(raw) except ValueError: pass return cfgmod.DEFAULT_PORT def _wait_for_server(url: str, timeout: float = 6.0) -> bool: """Poll the server's status endpoint until it answers or the timeout elapses. The tray starts the HTTP server in a thread a moment before it can spawn us, so a freshly-clicked Settings might briefly beat the socket. Polling avoids a blank window in that race; a miss just means we open anyway and the UI's own fetch retries. """ deadline = time.time() + timeout probe = url.rstrip("/") + "/api/status" while time.time() < deadline: try: with urllib.request.urlopen(probe, timeout=1.0) as r: if r.status == 200: return True except Exception: time.sleep(0.25) return False def run() -> None: """Open the control-panel window and block until the user closes it.""" import webview # imported here so the tray never pays for it unless --panel url = f"http://127.0.0.1:{_port()}/" _wait_for_server(url) webview.create_window( WINDOW_TITLE, url, width=WINDOW_W, height=WINDOW_H, min_size=WINDOW_MIN, background_color=BRAND_BG, ) # gui defaults to auto-detect; on Windows 11 that resolves to EdgeChromium # (WebView2), which ships with the OS — the modern engine the brand CSS needs. # start() blocks on the native GUI loop until the window closes. webview.start() if __name__ == "__main__": run()