Un lanceur Qt pour Windows, et un assemblage partage

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 20:00:03 +02:00
co-authored by Claude Opus 5
parent 3e269dfb5e
commit aa63e94239
5 changed files with 578 additions and 26 deletions
+30 -26
View File
@@ -25,7 +25,6 @@ if "--diagnostic" in sys.argv:
print(mettre_en_forme(diagnostiquer()))
sys.exit(0)
import os
import threading
from objc import super
@@ -41,11 +40,8 @@ from Foundation import NSObject, NSTimer
from PyObjCTools import AppHelper
from pynput import keyboard
from controller import ZonzaController
from core import CONFIG, modele_retenu, validate_config
from interface import classe_overlay_pour
from moteur import choisir_transcripteur, detecter_moteur
from systeme import choisir_systeme
from assemblage import acquerir_verrou, construire
from core import CONFIG
_STATE_TITLES = {
"idle": "🎙️",
@@ -55,19 +51,21 @@ _STATE_TITLES = {
class ZonzaApp(NSObject):
"""Délégué applicatif : status item + menu + hotkey + contrôleur."""
"""Délégué applicatif : status item + menu + hotkey.
def initWithTranscripteur_systeme_(self, transcripteur, systeme):
Le contrôleur (donc le transcripteur, l'overlay et le système) vient déjà
assemblé d'`assemblage.construire()` — voir `main()`. Ce délégué ne fait
plus QUE ce qui est propre à Cocoa : le NSStatusItem, le menu, et le
marshalage `AppHelper.callAfter` vers le thread principal.
"""
def initWithController_(self, controller):
self = super().init()
if self is None:
return None
self._item = None
self._sounds_item = None
self.pulse = classe_overlay_pour().alloc().init()
self.controller = ZonzaController(
CONFIG, self.pulse, transcripteur, systeme, on_state=self._set_state,
run_on_main=AppHelper.callAfter,
)
self.controller = controller
self._listener = None
return self
@@ -144,23 +142,18 @@ class ZonzaApp(NSObject):
def main():
# une seule instance a la fois : sinon deux modeles en RAM et la meme
# hotkey captee deux fois (cf systeme.acquerir_verrou_instance).
# hotkey captee deux fois (cf systeme.acquerir_verrou_instance). La prise
# du verrou reste ICI (pas dans assemblage.construire) : c'est au lanceur
# de décider quoi faire d'un refus, avant même de construire quoi que ce
# soit d'autre.
from systeme import choisir_systeme
systeme = choisir_systeme()
lock = systeme.acquerir_verrou_instance(
os.path.join(systeme.dossier_donnees(), "instance.lock")
)
lock = acquerir_verrou(systeme)
if lock is None:
print("[garde] une autre instance de Zonza tourne deja — arret.")
return
main._lock = lock # garder le verrou vivant tant que le process vit
validate_config(CONFIG)
moteur = detecter_moteur()
transcripteur = choisir_transcripteur(
nom=moteur,
modele=modele_retenu(moteur, CONFIG["model"]),
langue=CONFIG["language"],
amorce=CONFIG["initial_prompt"],
)
# si l'Accessibilité n'est pas accordée, macOS affiche sa popup officielle
# (« Zonza souhaite contrôler cet ordinateur ») et ajoute l'app à la liste —
# bien plus fiable qu'un ajout manuel via « + » dans les Réglages.
@@ -170,8 +163,19 @@ def main():
)
trusted = AXIsProcessTrustedWithOptions({kAXTrustedCheckOptionPrompt: True})
print(f"[diag] accessibilité accordée : {bool(trusted)}")
app = NSApplication.sharedApplication()
delegate = ZonzaApp.alloc().initWithTranscripteur_systeme_(transcripteur, systeme)
# Dépendance circulaire : `on_state` doit marshaler vers `delegate._apply_title`,
# mais `delegate` a besoin du contrôleur déjà construit pour naître. Cassée
# avec une case remplie APRÈS coup, comme dans lanceur_qt.py — la fermeture
# ne lit `case["delegate"]` qu'au moment de l'appel, jamais à la construction.
case = {}
assemblage = construire(
run_on_main=AppHelper.callAfter,
on_state=lambda s: case["delegate"]._set_state(s),
)
delegate = ZonzaApp.alloc().initWithController_(assemblage["controller"])
case["delegate"] = delegate
delegate.setup()
# référence gardée pour toute la vie du processus (sinon GC → icône disparaît)
main._delegate = delegate