Skip to content

etat_bureau

etat_bureau

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}."