zonza/systeme/macos.py
Ralph Mayola bfafc6a48c Le masque des modificateurs couvre enfin Command, et jamais Caps Lock
Deux defauts trouves en rendant la sonde injectable.

1. Le masque etait ecrit 0x000F0000 A LA MAIN. Il couvrait Shift, Control et Alt
   mais PAS Command (0x00100000) — pourtant la touche du raccourci de Zonza.
   Maintenir Cmd seul passait donc inapercu. Le masque est desormais COMPOSE
   depuis les constantes nommees de Quartz : un masque calcule de tete se trompe
   en silence, compose il ne peut plus.

2. Le test lisait le CLAVIER PHYSIQUE (`modificateurs_enfonces() is False`). Son
   resultat dependait des mains de l'utilisateur pendant la suite — un echec
   inreproductible a ete observe ce jour, puis 17 executions vertes. La sonde est
   rendue injectable, comme SystemeWindows l'avait deja, et les quatre
   modificateurs sont eprouves un par un. Un test verifie que la sonde reelle rend
   un booleen sans lever, SANS affirmer sa valeur.

Le repli hors Quartz excluait aussi le verrouillage majuscule : actif, il aurait
fait attendre 2 s avant CHAQUE collage, en permanence.

195 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 20:04:52 +02:00

82 lines
2.9 KiB
Python

"""Appels propres à macOS. Rien ici ne doit être importé directement par le noyau."""
import fcntl
import os
import subprocess
_SONS = {
"start": "/System/Library/Sounds/Pop.aiff",
"done": "/System/Library/Sounds/Glass.aiff",
"error": "/System/Library/Sounds/Basso.aiff",
}
def _masque_modificateurs():
"""Masque des modificateurs, COMPOSE depuis les constantes nommees de Quartz.
Il etait ecrit 0x000F0000 a la main : ce masque couvrait Shift, Control et Alt
mais PAS Command (0x00100000), pourtant la touche du raccourci de Zonza. Un
masque calcule de tete se trompe en silence ; compose depuis les constantes,
il ne peut plus. Repli sur la valeur complete si Quartz est indisponible.
"""
try:
from Quartz import (kCGEventFlagMaskAlternate, kCGEventFlagMaskCommand,
kCGEventFlagMaskControl, kCGEventFlagMaskShift)
return (kCGEventFlagMaskShift | kCGEventFlagMaskControl
| kCGEventFlagMaskAlternate | kCGEventFlagMaskCommand)
except Exception:
return 0x001E0000 # sans le verrouillage majuscule : il resterait actif en permanence
_MASQUE_MODIFICATEURS = _masque_modificateurs()
class SystemeMacOS:
"""Implémentation du contrat Systeme pour macOS."""
def jouer_son(self, nom):
chemin = _SONS.get(nom)
if not chemin:
return
try:
subprocess.Popen(["afplay", chemin])
except Exception:
pass # le son ne doit jamais casser la dictée
def __init__(self, sonde_modificateurs=None):
"""`sonde_modificateurs` est injectable : sans cela, tout test de cette
methode lirait le CLAVIER PHYSIQUE, et son resultat dependrait des mains
de l'utilisateur au moment ou la suite tourne. Meme forme que
SystemeWindows, qui recoit deja son clavier."""
self._sonde_modificateurs = sonde_modificateurs or self._drapeaux_reels
@staticmethod
def _drapeaux_reels():
from Quartz import CGEventSourceFlagsState
return CGEventSourceFlagsState(1)
def modificateurs_enfonces(self):
try:
return bool(self._sonde_modificateurs() & _MASQUE_MODIFICATEURS)
except Exception:
return False # dans le doute, ne jamais bloquer le collage
def raccourci_defaut(self):
return "<cmd>+<ctrl>+d"
def chemin_log(self):
return os.path.expanduser("~/Library/Logs/Zonza.log")
def dossier_donnees(self):
return os.path.expanduser("~/Library/Application Support/Zonza")
def acquerir_verrou_instance(self, chemin):
parent = os.path.dirname(chemin)
if parent:
os.makedirs(parent, exist_ok=True)
fichier = open(chemin, "a")
try:
fcntl.flock(fichier, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
fichier.close()
return None
return fichier