"""Appels propres à Windows. Rien ici ne doit être importé directement par le noyau. Miroir de `systeme/macos.py`, mais avec une contrainte que macOS n'a pas : `winsound`, `msvcrt` et `ctypes.windll` n'EXISTENT PAS sur macOS. Un import en tête de fichier rendrait ce module impossible à charger — donc impossible à tester — depuis la machine de développement (aucune machine Windows n'est disponible pour ce portage). Les trois dépendances sont donc INJECTABLES : `jouer`, `clavier`, `verrouiller`. Par défaut (None), elles se résolvent paresseusement vers l'API Windows réelle au moment de l'appel, jamais à l'import. Les tests injectent des doublures et n'exécutent donc jamais une ligne de code Windows — ils éprouvent la LOGIQUE de cette classe (mapping des sons, masque des touches, construction des chemins, séquence d'acquisition du verrou), pas le comportement réel de winsound/msvcrt/user32, qu'aucune machine ici ne peut vérifier. """ import os # Alias système Windows joués par winsound.PlaySound(alias, SND_ALIAS | SND_ASYNC). _SONS = { "start": "SystemAsterisk", "done": "SystemExclamation", "error": "SystemHand", } # Virtual-key codes Windows des cinq touches modificatrices à surveiller. # ctrl, shift, alt (menu), windows gauche, windows droite. _TOUCHES_MODIFICATRICES = (0x11, 0x10, 0x12, 0x5B, 0x5C) # Dans le retour de GetAsyncKeyState, le bit de poids fort indique que la # touche est enfoncée EN CE MOMENT (les bits bas concernent un état passé # qui ne nous intéresse pas ici). _BIT_ENFONCEE = 0x8000 class SystemeWindows: """Implémentation du contrat Systeme pour Windows. jouer(alias) : joue un son. clavier(vk) : rend l'état d'une touche. verrouiller(fichier) : pose un verrou exclusif non bloquant sur `fichier`, lève OSError s'il est déjà pris. Les trois par défaut à None : résolus paresseusement vers l'API Windows réelle au premier usage. """ def __init__(self, jouer=None, clavier=None, verrouiller=None): self._jouer = jouer self._clavier = clavier self._verrouiller = verrouiller # --- son ------------------------------------------------------------ def jouer_son(self, nom): alias = _SONS.get(nom) if not alias: return try: (self._jouer or self._jouer_reel)(alias) except Exception: pass # le son ne doit jamais casser la dictée (même raison qu'en macOS) @staticmethod def _jouer_reel(alias): import winsound winsound.PlaySound(alias, winsound.SND_ALIAS | winsound.SND_ASYNC) # --- clavier ---------------------------------------------------------- def modificateurs_enfonces(self): sonder = self._clavier or self._clavier_reel try: return any(sonder(vk) & _BIT_ENFONCEE for vk in _TOUCHES_MODIFICATRICES) except Exception: return False # dans le doute, ne jamais bloquer le collage (idem macOS) @staticmethod def _clavier_reel(vk): import ctypes return ctypes.windll.user32.GetAsyncKeyState(vk) # --- raccourci -------------------------------------------------------- def raccourci_defaut(self): # n'existe pas sur Windows : pas d'équivalent à la touche Commande. return "++d" # --- chemins ------------------------------------------------------------ def chemin_log(self): return os.path.join(self._dossier_local_appdata(), "Zonza", "Zonza.log") def dossier_donnees(self): return os.path.join(self._dossier_local_appdata(), "Zonza") @staticmethod def _dossier_local_appdata(): # os.environ est standard : lisible (et falsifiable par les tests via # monkeypatch.setenv) depuis n'importe quelle plateforme, contrairement # à winsound/ctypes.windll/msvcrt. return os.environ.get("LOCALAPPDATA", os.path.expanduser("~")) # --- verrou d'instance -------------------------------------------------- def acquerir_verrou_instance(self, chemin): parent = os.path.dirname(chemin) if parent: os.makedirs(parent, exist_ok=True) fichier = open(chemin, "a+") fichier.seek(0) # msvcrt.locking verrouille un OCTET à partir de la position courante ; # il exige que cet octet existe déjà dans le fichier, d'où cette garde. if os.fstat(fichier.fileno()).st_size == 0: fichier.write("0") fichier.flush() fichier.seek(0) try: (self._verrouiller or self._verrouiller_reel)(fichier) except OSError: fichier.close() return None return fichier @staticmethod def _verrouiller_reel(fichier): import msvcrt fichier.seek(0) msvcrt.locking(fichier.fileno(), msvcrt.LK_NBLCK, 1)