"""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. La politique d'activation est forcée en « accessory » : Zonza vit dans la barre de menu, jamais dans le Dock — même quand la fenêtre de pulsation Cocoa s'affiche. """ import threading import rumps from AppKit import NSApplication, NSApplicationActivationPolicyAccessory 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) # barre de menu uniquement, jamais d'icône Dock — même en lançant depuis les sources NSApplication.sharedApplication().setActivationPolicy_( NSApplicationActivationPolicyAccessory ) ZonzaApp().run() if __name__ == "__main__": main()