Skip to content

presse_papiers_spatial

presse_papiers_spatial

Ce qu'on tient dans la main — le presse-papiers spatial (§17, §22).

Spatial Mesh, gestes, phase 5 — 25 août 2026. Le geste exprime une INTENTION ; il ne transporte rien. Fermer le poing ne déplace aucun octet : il désigne ce que l'écran affiche à cet instant — ou le fichier explicitement préparé dans le dialogue natif — et le retient, le temps d'un geste. Ouvrir la main dit où le déposer.

Le §2 du cahier des charges le dit mieux que moi : ne jamais confondre l'effet visuel et l'architecture. Ici, le presse-papiers est la seule mémoire du geste, et elle est volatile, minuscule, et honnête — elle ne contient jamais les octets d'un fichier, seulement son chemin local vérifié, que le transfert relira par morceaux après le choix de l'appareil.

Classes

ObjetSpatial dataclass

ObjetSpatial(
    type: str,
    id: str,
    titre: str,
    ecran: str = "",
    quand: float = 0.0,
    chemin: str = "",
    taille: int = 0,
    type_mime: str = "",
)

Ce qui a été attrapé — une IDENTITÉ, jamais une copie (§19).

Une tâche n'est pas sérialisée puis renvoyée : on transporte son type et son identifiant, et l'appareil d'arrivée va chercher la ressource par ses propres moyens. C'est ce qui distingue un handoff d'une duplication.

Functions:

preparer_fichier

preparer_fichier(chemin: Path | str) -> ObjetSpatial

Désigner UN fichier local que le prochain poing pourra attraper.

Le choix vient du dialogue natif de l'application. On vérifie tout de même ici : une chaîne reçue par HTTP n'est jamais une preuve qu'un fichier existe, qu'il est régulier, ni qu'il respecte le plafond du transfert.

Source code in src/diapason/desktop/presse_papiers_spatial.py
def preparer_fichier(chemin: Path | str) -> ObjetSpatial:
    """Désigner UN fichier local que le prochain poing pourra attraper.

    Le choix vient du dialogue natif de l'application. On vérifie tout de
    même ici : une chaîne reçue par HTTP n'est jamais une preuve qu'un fichier
    existe, qu'il est régulier, ni qu'il respecte le plafond du transfert.
    """
    global _prepare

    from diapason.mesh.transfert import TAILLE_MAX_DEFAUT

    try:
        resolu = Path(chemin).expanduser().resolve(strict=True)
    except (OSError, RuntimeError) as exc:
        raise ValueError("Ce fichier n'existe plus.") from exc
    if not resolu.is_file():
        raise ValueError("Choisis un fichier, pas un dossier.")
    taille = resolu.stat().st_size
    if taille > TAILLE_MAX_DEFAUT:
        raise ValueError("Ce fichier dépasse la limite de 2 Gio.")
    _prepare = ObjetSpatial(
        type="file",
        # Un jeton opaque, jamais le chemin. L'interface a besoin d'une clé
        # stable pour son rendu, pas de savoir où vit le fichier.
        id=f"file_{secrets.token_urlsafe(9)}",
        titre=resolu.name,
        quand=time.monotonic(),
        chemin=str(resolu),
        taille=taille,
        type_mime=mimetypes.guess_type(resolu.name)[0] or "",
    )
    return _prepare

fichier_prepare

fichier_prepare() -> Optional[ObjetSpatial]

Le fichier prêt à être attrapé, s'il n'a pas été oublié.

Source code in src/diapason/desktop/presse_papiers_spatial.py
def fichier_prepare() -> Optional[ObjetSpatial]:
    """Le fichier prêt à être attrapé, s'il n'a pas été oublié."""
    global _prepare
    if _prepare is None:
        return None
    if (time.monotonic() - _prepare.quand) >= PREPARATION_TTL_S:
        _prepare = None
        return None
    # Le fichier peut disparaître entre le dialogue et le geste. Le garder
    # affiché « prêt » dans ce cas serait une promesse déjà fausse.
    if not Path(_prepare.chemin).is_file():
        _prepare = None
        return None
    return _prepare

attraper

attraper() -> Optional[ObjetSpatial]

Prendre ce que l'écran affiche à cet instant — ou rien.

Rien n'est deviné : sans contexte d'application frais, la main se referme sur du vide, et le dire vaut mieux qu'attraper au hasard un objet que l'utilisateur ne regardait pas.

Source code in src/diapason/desktop/presse_papiers_spatial.py
def attraper() -> Optional[ObjetSpatial]:
    """Prendre ce que l'écran affiche à cet instant — ou rien.

    Rien n'est deviné : sans contexte d'application frais, la main se
    referme sur du vide, et le dire vaut mieux qu'attraper au hasard un
    objet que l'utilisateur ne regardait pas.
    """
    global _prepare, _tenu
    prepare = fichier_prepare()
    if prepare is not None:
        _tenu = ObjetSpatial(
            type=prepare.type,
            id=prepare.id,
            titre=prepare.titre,
            quand=time.monotonic(),
            chemin=prepare.chemin,
            taille=prepare.taille,
            type_mime=prepare.type_mime,
        )
        _prepare = None
        logger.info("attrapé : fichier « %s » (%d octets)", _tenu.titre, _tenu.taille)
        return _tenu
    from diapason.desktop.contexte_app import dernier_contexte

    vue = dernier_contexte()
    if vue is None:
        _tenu = None
        return None
    if vue.ressource_id and vue.ressource_type:
        _tenu = ObjetSpatial(
            type=vue.ressource_type,
            id=vue.ressource_id,
            titre=vue.ressource_titre or vue.ressource_id,
            ecran=vue.ecran,
            quand=time.monotonic(),
        )
    else:
        # Aucun élément sélectionné : c'est l'ÉCRAN qu'on attrape. « Reprends
        # ça là-bas » a un sens même sans élément précis.
        _tenu = ObjetSpatial(
            type="screen",
            id=vue.chemin,
            titre=vue.ecran,
            ecran=vue.ecran,
            quand=time.monotonic(),
        )
    logger.info("attrapé : %s « %s »", _tenu.type, _tenu.titre)
    return _tenu

tenu

tenu() -> Optional[ObjetSpatial]

Ce qui est tenu, s'il est encore frais.

Source code in src/diapason/desktop/presse_papiers_spatial.py
def tenu() -> Optional[ObjetSpatial]:
    """Ce qui est tenu, s'il est encore frais."""
    if _tenu is not None and _tenu.frais:
        return _tenu
    return None

lacher

lacher() -> Optional[ObjetSpatial]

Rendre ce qui était tenu, et ouvrir la main.

Source code in src/diapason/desktop/presse_papiers_spatial.py
def lacher() -> Optional[ObjetSpatial]:
    """Rendre ce qui était tenu, et ouvrir la main."""
    global _tenu
    objet, _tenu = tenu(), None
    return objet

decrire

decrire(objet: ObjetSpatial) -> str

Ce qui est dans la main, en une phrase, pour le contexte du modèle.

Miroir strict de contexte_app.decrire : même préfixe court, mêmes guillemets, une seule phrase, aucun impératif. Le contexte ÉNONCE un fait ; l'instruction, elle, vit dans la description de l'outil.

Le « (geste) » n'est pas décoratif. Ces deux phrases se suivent souvent dans le même contexte, et « Dans la main » et « Dans Diapason » se ressemblent assez pour que le modèle envoie l'un en croyant l'autre — c'est exactement le défaut que ce raccordement existe pour corriger.

Source code in src/diapason/desktop/presse_papiers_spatial.py
def decrire(objet: ObjetSpatial) -> str:
    """Ce qui est dans la main, en une phrase, pour le contexte du modèle.

    Miroir strict de ``contexte_app.decrire`` : même préfixe court, mêmes
    guillemets, une seule phrase, aucun impératif. Le contexte ÉNONCE un
    fait ; l'instruction, elle, vit dans la description de l'outil.

    Le « (geste) » n'est pas décoratif. Ces deux phrases se suivent souvent
    dans le même contexte, et « Dans la main » et « Dans Diapason » se
    ressemblent assez pour que le modèle envoie l'un en croyant l'autre —
    c'est exactement le défaut que ce raccordement existe pour corriger.
    """
    if objet.type == "screen":
        return f"Dans la main (geste) : l'écran {objet.titre}."
    if objet.type == "file":
        return f"Dans la main (geste) : le fichier « {objet.titre} »."
    quoi = {
        "project": "le projet",
        "note": "la note",
        "task": "la tâche",
    }.get(objet.type, "l'élément")
    return f"Dans la main (geste) : {quoi} « {objet.titre} »."