zonza/systeme/windows.py

121 lines
4.8 KiB
Python

"""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):
# <cmd> n'existe pas sur Windows : pas d'équivalent à la touche Commande.
return "<ctrl>+<alt>+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)