121 lines
4.8 KiB
Python
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)
|