"""Point d'entrée : application macOS de barre de menu (rumps). Possède la boucle d'événements Cocoa. Enregistre la hotkey globale, gère l'état du menu, lance le warmup du modèle en fond, et instancie le contrôleur + la fenêtre de pulsation. Zonza vit dans la barre de menu, jamais dans le Dock : c'est le LSUIElement de l'Info.plist (posé par build_app.sh) qui s'en charge. """ import threading import rumps from PyObjCTools import AppHelper from pynput import keyboard from controller import ZonzaController from core import CONFIG, validate_config from pulse_window import PulseWindow _STATE_TITLES = { "idle": "🎙️", "recording": "🔴", "transcribing": "⏳", } class ZonzaApp(rumps.App): def __init__(self): super().__init__("🎙️", quit_button=None) self.pulse = PulseWindow.alloc().init() self.controller = ZonzaController( CONFIG, self.pulse, on_state=self._set_state, run_on_main=AppHelper.callAfter, ) self.menu = [ rumps.MenuItem("Sons", callback=self._toggle_sounds), None, rumps.MenuItem("Quitter", callback=self._quit), ] self.menu["Sons"].state = CONFIG["sounds"] self._start_hotkey() self._warmup_async() def _set_state(self, state): # appelé depuis des threads de fond (transcription) ; toucher le titre du menu # (AppKit) doit se faire sur le thread principal → on marshale via callAfter. AppHelper.callAfter(self._apply_title, state) def _apply_title(self, state): self.title = _STATE_TITLES.get(state, "🎙️") def _toggle_sounds(self, sender): CONFIG["sounds"] = not CONFIG["sounds"] sender.state = CONFIG["sounds"] def _quit(self, _): rumps.quit_application() def _start_hotkey(self): # pynput exécute ce callback sur SON thread d'écoute, pas le thread principal. # toggle() touche NSWindow/NSTimer (Cocoa) → on marshale sur le thread principal. def on_activate(): AppHelper.callAfter(self.controller.toggle) self._listener = keyboard.GlobalHotKeys({CONFIG["hotkey"]: on_activate}) self._listener.start() def _warmup_async(self): def run(): try: self.controller.transcriber.warmup() except Exception as e: print(f"[erreur] warmup modèle : {e}") rumps.notification("Zonza", "Modèle", f"Échec du chargement : {e}") threading.Thread(target=run, daemon=True).start() def main(): validate_config(CONFIG) # PAS de setActivationPolicy_ ici : sur macOS Tahoe, forcer la politique avant # run() empêche rumps de créer l'icône de barre de menu. Le LSUIElement de # l'Info.plist suffit à masquer l'icône Dock pour l'app installée. ZonzaApp().run() if __name__ == "__main__": main()