L'état du bureau — ce qui tourne, ce qui est devant.
Demandé le 23 août 2026 : « si l'application est déjà ouverte, regarde,
et mets-la au premier plan » — un compagnon fluide perçoit l'état du Mac
avant d'agir et de parler. Ce module est cette perception : une lecture
System Events (l'app au premier plan + les apps visibles), un cache court
pour que la voix n'attende jamais, et une phrase française prête pour le
contexte du modèle.
Le cache a deux lectures : etat_du_bureau() rafraîchit si le cliché
est vieux (≈ 100 ms d'osascript), dernier_etat_connu() rend le cliché
tel quel sans jamais bloquer — c'est lui que le tour de parole consulte,
pendant qu'un rafraîchissement part en tâche de fond.
Functions:
onglet_actif
onglet_actif(
app: str, runner: Optional[Callable[[str], str]] = None
) -> str
Le titre de l'onglet actif d'un navigateur connu — "" sinon.
Rien ne s'invente : un navigateur inconnu, une fenêtre absente ou un
refus d'automatisation rendent la chaîne vide, et decrire() se tait.
Source code in src/diapason/desktop/etat_bureau.py
| def onglet_actif(app: str, runner: Optional[Callable[[str], str]] = None) -> str:
"""Le titre de l'onglet actif d'un navigateur connu — "" sinon.
Rien ne s'invente : un navigateur inconnu, une fenêtre absente ou un
refus d'automatisation rendent la chaîne vide, et decrire() se tait.
"""
script = _ONGLET_PAR_NAVIGATEUR.get(app)
if script is None:
return ""
try:
return " ".join((runner or _executer)(script).split())
except Exception: # noqa: BLE001 - la perception est un bonus, jamais une porte
logger.debug("onglet actif illisible pour %s", app, exc_info=True)
return ""
|
interpreter
interpreter(
sortie: str,
runner: Optional[Callable[[str], str]] = None,
) -> Optional[EtatBureau]
La sortie du script, en état — None si elle ne ressemble à rien.
Source code in src/diapason/desktop/etat_bureau.py
| def interpreter(
sortie: str, runner: Optional[Callable[[str], str]] = None
) -> Optional[EtatBureau]:
"""La sortie du script, en état — None si elle ne ressemble à rien."""
lignes = (sortie or "").strip("\n").split("\n")
if not lignes or not lignes[0].strip():
return None
premier = lignes[0].strip()
apps = tuple(
nom.strip()
for nom in (lignes[1] if len(lignes) > 1 else "").split("|")
if nom.strip()
)
titre = " ".join(lignes[2].split()) if len(lignes) > 2 else ""
# L'onglet coûte un SECOND appel osascript : on ne le paie que pour un
# navigateur, et seulement quand il est déjà devant.
onglet = onglet_actif(premier, runner) if premier in _ONGLET_PAR_NAVIGATEUR else ""
return EtatBureau(
premier_plan=premier,
en_marche=apps,
quand=time.monotonic(),
titre_fenetre=titre,
onglet=onglet,
)
|
etat_du_bureau
etat_du_bureau(
*,
ttl_s: float = TTL_S,
runner: Optional[Callable[[str], str]] = None,
horloge: Callable[[], float] = monotonic,
) -> Optional[EtatBureau]
Le cliché courant, rafraîchi s'il a dépassé son âge. None hors macOS.
Source code in src/diapason/desktop/etat_bureau.py
| def etat_du_bureau(
*,
ttl_s: float = TTL_S,
runner: Optional[Callable[[str], str]] = None,
horloge: Callable[[], float] = time.monotonic,
) -> Optional[EtatBureau]:
"""Le cliché courant, rafraîchi s'il a dépassé son âge. None hors macOS."""
global _cache
if runner is None and sys.platform != "darwin":
return None
if _cache is not None and horloge() - _cache.quand < ttl_s:
return _cache
try:
etat = interpreter((runner or _executer)(_SCRIPT), runner)
except Exception: # noqa: BLE001 - la perception est un bonus, jamais une porte
logger.debug("état du bureau illisible", exc_info=True)
return _cache
if etat is not None:
_cache = etat
return _cache
|
dernier_etat_connu
dernier_etat_connu() -> Optional[EtatBureau]
Le cliché tel quel, sans lecture — jamais une milliseconde d'attente.
Source code in src/diapason/desktop/etat_bureau.py
| def dernier_etat_connu() -> Optional[EtatBureau]:
"""Le cliché tel quel, sans lecture — jamais une milliseconde d'attente."""
return _cache
|
premier_plan
premier_plan(
runner: Optional[Callable[[str], str]] = None,
) -> str
L'app au premier plan, à l'instant même (lecture directe, ~80 ms).
Source code in src/diapason/desktop/etat_bureau.py
| def premier_plan(runner: Optional[Callable[[str], str]] = None) -> str:
"""L'app au premier plan, à l'instant même (lecture directe, ~80 ms)."""
script = (
'tell application "System Events" to get name of '
"first process whose frontmost is true"
)
try:
return (runner or _executer)(script).strip()
except Exception: # noqa: BLE001
return ""
|
decrire
decrire(etat: EtatBureau, limite: int = _APPS_MAX) -> str
L'état en une phrase française, prête pour le contexte du modèle.
La phrase de base est INCHANGÉE quand titre et onglet sont vides : elle
est comparée à l'égalité par des tests, et surtout un cliché plus bavard
qu'il n'a de matière ferait deviner le modèle.
Source code in src/diapason/desktop/etat_bureau.py
| def decrire(etat: EtatBureau, limite: int = _APPS_MAX) -> str:
"""L'état en une phrase française, prête pour le contexte du modèle.
La phrase de base est INCHANGÉE quand titre et onglet sont vides : elle
est comparée à l'égalité par des tests, et surtout un cliché plus bavard
qu'il n'a de matière ferait deviner le modèle.
"""
autres = [nom for nom in etat.en_marche if nom != etat.premier_plan]
coupe = autres[:limite]
suite = f" (+{len(autres) - limite})" if len(autres) > limite else ""
en_marche = ", ".join(coupe) + suite if coupe else "rien d'autre"
# « Safari » ne dit rien ; « Safari — Gmail, brouillon à Julie » dit tout.
precision = etat.onglet or etat.titre_fenetre
devant = (
f"{etat.premier_plan} — {precision[:120]}" if precision else etat.premier_plan
)
return f"État du bureau : au premier plan, {devant}. Aussi en marche : {en_marche}."
|