turbo-editors/turbo-pythonpublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-python.git
git clone ssh://git@rickub.com/turbo-editors/turbo-python.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

📦 Turbo Python 6fc62ea · on v1.0.2 · k33g · 11h ago
python-tools.md · 117 lines · 15.6 KBmarkdown
Blame HistoryOpen raw

Outils Python — explication

De quoi s'agit-il ?

Un menu Python dont les commandes viennent d'un fichier TOML, chacune lancée dans une fenêtre terminal, et les fichiers ouverts relus ensuite. Cette page explique pourquoi chacun de ces trois points est ce qu'il est.

Pourquoi la sortie a trois destinations, et une popup par défaut

La première version mettait chaque commande dans une fenêtre terminal, et c'était le mauvais défaut pour cinq des six.

Un terminal est la bonne réponse quand le programme est interactif ou long : uv run sur quelque chose qui lit l'entrée standard doit pouvoir recevoir une réponse, et une compilation qui s'avère durer une minute doit pouvoir être interrompue par Ctrl-C. Ni l'un ni l'autre n'est vrai de uv run ruff check ., qui affiche quatre lignes et s'arrête. Lui donner une fenêtre entière — qu'il faut ensuite fermer, sur un bureau où les fenêtres se recouvrent et sont numérotées — est plus de cérémonie que le résultat n'en mérite.

Une popup est la bonne réponse pour une commande qu'on lance, qu'on lit et qu'on referme. Elle est modale, ce qui est un coût réel et nommé dans le guide : un go build qu'on n'attendait pas lent immobilise l'éditeur jusqu'à sa fin ou jusqu'à Échap. Ce coût a été accepté exprès, parce que l'alternative — un dialogue surgissant trois secondes plus tard — avale ce qu'on était en train de taper à cet instant.

La popup s'ouvre donc immédiatement et se remplit. On voit la progression, rien ne surprend, et Échap la ferme et arrête la commande — le seul moyen d'interrompre quelque chose dont la sortie n'est pas dans un terminal.

Une fenêtre d'édition est la bonne réponse pour une sortie qu'on va éplucher : un long go test -v, un rapport de couverture. C'est un buffer ordinaire, donc Ctrl-F y cherche et Save as le conserve. Elle est remplie à la fin de la commande plutôt qu'au fil de l'eau, parce qu'un buffer qui grandit sous le curseur pendant qu'on y cherche est l'inverse de ce que ce mode vise.

Aucune des trois ne convient à tout, et c'est pourquoi output est dans le fichier et non dans le code. Run en est l'exemple travaillé : c'est la seule commande du fichier de départ qui dit terminal, et le commentaire à côté dit pourquoi.

Pourquoi la fenêtre terminal reste

L'éditeur en avait déjà une — un vrai pseudo-terminal avec émulateur VT, construit pour les fenêtres F8 — donc output = "terminal" coûte un champ dans ses options et offre gratuitement les couleurs, la pagination, Ctrl-C, l'entrée clavier et l'historique, parce que ce sont les mêmes mécanismes que ceux de tous les autres terminaux.

La fenêtre reste après la fin de la commande, et c'est le but : la sortie est ce qu'on a demandé, et une fenêtre qui disparaîtrait avec elle serait inutile.

Cela a demandé un correctif à part. Une vue terminal consommait toute touche qu'on lui donnait et l'écrivait au shell ; une fois le shell parti, l'écriture échouait en silence et la touche était consommée quand même — Ctrl-W ne pouvait donc jamais fermer une fenêtre terminée, et la souris était la seule issue. Une vue terminée ne prend plus que les touches de défilement et laisse passer le reste vers l'éditeur.

Pourquoi le code de sortie est toujours dans le titre

uv sync qui réussit n'affiche rien du tout. Une popup au corps vide et au titre neutre est indistinguable d'une popup dont la commande n'a pas démarré, et le lecteur en est réduit à deviner la seule chose qu'il voulait savoir.

Le titre porte donc le verdict — — ok ou — exit 1 — et un corps vide affiche (no output) une fois la commande terminée. Pendant qu'elle tourne, le corps reste vide : « (no output) » est un verdict, et une commande en cours n'y est pas parvenue.

Pourquoi les commandes sont dans un fichier

Six commandes codées en dur auraient répondu à la demande. Elles auraient aussi été fausses en une semaine.

Toutes les commandes du fichier de départ passent par uv, qui crée l'environnement, résout les dépendances et lance les outils dedans — aucune n'exige donc qu'un environnement ait été activé au préalable. C'est un défaut défendable, ce n'est la réponse universelle de personne. Un projet sous Poetry veut poetry run. Un projet sous pip avec un .venv fait à la main veut la commande nue, l'environnement déjà sur le PATH. Un projet qui a standardisé sur black et flake8 veut ceux-là plutôt que ruff. uv run pytest suppose pytest ; un projet sous unittest veut python -m unittest. Un projet avec un Makefile veut make check. Rien de cela n'est connaissable d'ici, et tout cela fait une ligne dans un fichier.

Les six sont donc des défauts, pas du code : c'est le contenu du fichier de départ qu'écrit Python ▸ Create tools file, et en changer une consiste à éditer un fichier plutôt qu'à recompiler un éditeur. Le fichier est relu à chaque ouverture du menu, pour la même raison que le menu Snippets : une modification doit prendre effet aussitôt, et le fichier est souvent ouvert dans la fenêtre derrière le menu.

Les commandes passent par sh -ccmd.exe /S /C sous Windows — plutôt que d'être découpées en argv ici. Le fichier est celui de l'utilisateur, donc les tubes, les globs et && sont des fonctionnalités plutôt que des dangers, et une entrée peut être uv run ruff format . && uv run ruff check . && uv run pytest. Découper un argv supposerait d'inventer des règles de citation pour une chaîne écrite à la main.

Pourquoi il n'y a pas de fichier d'outils utilisateur

Les snippets sont lus depuis deux fichiers — le vôtre et celui du projet — parce que vos snippets sont vos habitudes et doivent vous suivre.

Les outils ne sont pas ainsi. Ils appartiennent à la chaîne d'outils propre à un projet : un fichier d'outils global proposerait uv run pytest dans un dépôt qui n'a jamais entendu parler d'uv, et un projet épinglé sur Poetry hériterait des habitudes de quelqu'un d'autre dans son menu. Le fichier est par projet, et c'est toute la règle.

Pourquoi un outil peut nommer son propre menu

Un menu nommé Python contenant docker compose up ment sur ce qu'il est. Le premier fichier d'outils que l'on écrit déborde de Python, parce que les commandes d'un projet ne parlent pas toutes du langage dans lequel il est écrit : conteneurs, bases de données, déploiements, une cible de Makefile ajoutée en 2019.

Deux formes ont été envisagées. Un second menu fixe nommé Tools — tout ce qui est Python dans Python, le reste dans Tools — c'est une clé de plus dans le format et aucun problème de nommage, mais cela ne fait que déplacer le mensonge : un menu Tools contenant docker compose up, psql et un script de déploiement est tout aussi indifférencié, et dès qu'il y a dix entrées personne n'en retrouve une. Et un second fichier, menus.toml, garde le fichier d'outils simple au prix de deux fichiers qui doivent s'accorder sur les outils qui existent.

Le menu est donc un nom libre porté par l'outil, dans l'unique fichier : menu = "Docker". Un nom que rien d'autre n'emploie crée le menu ; omettre la clé signifie Python. Il n'y a pas de liste de noms autorisés, parce qu'une liste serait la liste des projets de quelqu'un d'autre.

Python reste fixe sur la barre plutôt que de devenir un nom parmi d'autres venu du fichier. Python ▸ Create tools file doit être atteignable dans un projet qui n'a aucun fichier d'outils — c'est précisément le projet qui en a besoin — et un menu qui n'existe qu'une fois le fichier créé ne peut pas proposer de créer le fichier.

Pourquoi la touche d'accès n'appartient pas au fichier

L'auteur d'un fichier d'outils ne peut pas savoir quelles lettres sont libres. Il voit File, Edit, Search, Run, Options, Window, Snippets, Python et Help sur la barre, mais seulement en comptant les soulignements, et un projet partagé à plusieurs dépendrait alors du fait que personne n'ajoute un menu qui entre en collision.

Les collisions sont ici silencieuses, et c'est ce qui vaut qu'on se prémunisse contre elles. La barre répond au premier menu dont la touche correspond ; un second menu revendiquant la même lettre n'est pas une erreur et se dessine normalement — il ne s'ouvre simplement jamais. Le piège s'est déjà refermé une fois dans cet éditeur : Snippets et Search voulaient tous deux le S, Snippets était celui qu'on ne pouvait pas ouvrir, et tous les tests passaient. Le correctif d'alors fut de déplacer Snippets sur N à la main. Laisser un fichier nommer des menus en ferait un danger permanent plutôt qu'une erreur isolée, donc l'attribution revient à l'éditeur : la première lettre du nom que rien d'autre ne revendique.

Les tildes écrits dans le nom sont honorés quand la lettre est libre, et écartés sans bruit sinon. Refuser le fichier était l'alternative, et elle est pire : la collision dépend des menus qui existent, donc un fichier d'outils qui marchait cesserait de marcher le jour où une version de l'éditeur ajoute un menu. Entre un menu sur une lettre que vous n'avez pas demandée et un menu que vous ne pouvez pas ouvrir, la première est la moindre perte.

Quand toutes les lettres d'un nom sont prises, le menu n'a pas de touche d'accès du tout. F10, les flèches et la souris l'atteignent encore, et l'alternative — aller chercher une lettre qui n'est pas dans le nom — mettrait un soulignement sous rien.

Pourquoi la barre est reconstruite depuis un stat

Menu.OnOpen remplit les entrées d'un menu juste avant qu'il ne se déroule, et c'est ainsi que les menus Python et Snippets suivent leurs fichiers sans redémarrage. Cela ne suffit pas ici : l'ensemble des menus appartient à la barre et non à un menu, et ajouter menu = "Docker" au fichier doit poser Docker sur la barre.

Lire et analyser le fichier à chaque tour de la boucle d'événements y parviendrait, et ferait aussi ce travail pour rien à chaque frappe dans un fichier que personne n'a modifié. La barre porte donc la taille et la date de modification du fichier d'outils dont elle est issue, et un stat par tour décide s'il faut reconstruire. Modifier le fichier dans la fenêtre devant soi, l'enregistrer et voir la barre changer, c'est le cas visé.

Pourquoi les fichiers ouverts sont relus, et seulement certains

Format réécrit les fichiers sur le disque — y compris celui qu'on regarde. Sans rien de plus, l'éditeur resterait assis sur une copie périmée, et le F2 suivant réécrirait votre version non formatée par-dessus le travail de ruff format. Ce n'est pas une aspérité : c'est la fonctionnalité qui se défait toute seule, en silence.

À la fin d'une commande, l'éditeur relit donc chaque fichier ouvert. La partie intéressante est ceux qu'il refuse de toucher.

Un fichier ayant des modifications non enregistrées est laissé tel quel, et la barre d'état dit combien ont été ignorés. Le recharger jetterait un travail que l'utilisateur n'a pas enregistré, ce qu'aucune commodité ne justifie. Et le conflit est réel : le formateur et la modification non enregistrée ne sont pas d'accord sur ce que le fichier doit dire, et l'éditeur n'est pas en position de trancher. Le nommer et s'arrêter est l'issue honnête — l'utilisateur peut enregistrer et relancer, ou continuer à éditer et formater plus tard.

Deux décisions plus petites à l'intérieur :

  • Le curseur reste où il était, borné à ce que le fichier contient désormais. Un formateur déplace les lignes ; remettre le curseur en haut ferait perdre sa place au lecteur pour rien.
  • L'historique d'annulation est jeté. Annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus, ce qui est pire que de ne pas pouvoir annuler.

Pourquoi le rechargement a lieu sur la boucle d'événements

La fin de la commande est remarquée par la goroutine qui lit le terminal, laquelle ne peut toucher ni un buffer ni le bureau. Elle positionne donc un drapeau, et le rechargement se fait en tête du tour suivant de la boucle.

C'est la quatrième chose construite ainsi dans cet éditeur — l'annonce au serveur de langage, les redessins de terminal, l'échéance d'autosave, et maintenant ceci. La règle qu'elles partagent mérite d'être énoncée une fois de plus : le réveil peut être perdu, l'état ne doit pas l'être. PostEvent jette ce qui ne tient pas dans sa file, donc tout ce qui dépend de l'arrivée d'un message est un bug qui attend un moment de charge. Un drapeau que la boucle vérifie elle-même ne peut pas disparaître.

Pourquoi une commande peut demander une valeur, et pourquoi en doubles accolades

uv venv a besoin d'un répertoire. uv run a besoin d'un script. uv add a besoin d'un nom de paquet, et pytest -k d'un motif. Aucun de ces éléments ne peut vivre dans le fichier d'outils sous forme de chaîne figée, parce que la réponse change à chaque fois — et un outil qui ne peut pas demander est un outil qu'il faut éditer avant chaque usage, ce qui n'est pas un outil.

Un {{libellé}} dans une commande est donc une valeur que l'éditeur demande d'abord, dans une boîte portant le nom de l'outil.

Une seule accolade était l'écriture évidente, et elle est fausse. awk '{print $1}' et find . -exec rm {} + sont des choses ordinaires à mettre dans un fichier d'outils, et lire la première comme un libellé transforme une commande qui marche en une boîte demandant « print $1 ». Les doubles accolades n'entrent en collision presque avec rien, et la seule construction avec laquelle elles le font — un bloc imbriqué en awk — est assez rare pour être signalée plutôt que contournée par la conception.

La valeur est protégée par défaut, parce que l'inverse échoue en silence. Un chemin contenant une espace, substitué tel quel, devient deux arguments et la commande parle d'un fichier qui n'existe pas. La protection fait marcher ce cas et rend l'autre — « ajoute ces trois options à la fin » — impossible ; c'est pourquoi un ... dans les accolades demande la valeur telle quelle. Deux comportements, tous deux documentés, plutôt qu'un seul faux une fois sur deux.

Rien n'est retenu sur le disque. La boîte repart de ce qui avait été tapé, pour la session. L'écrire dans le répertoire propre au projet a été envisagé puis rejeté : ce répertoire contient ce que le projet a décidé, et un filtre tapé en poursuivant un test n'en fait pas partie. Ce serait aussi la première chose qui y changerait sans que personne ne modifie un fichier.

Un fichier illisible est refusé à la lecture, pas au moment où l'outil est choisi. Un {{ non fermé atteignant le shell donne une commande qui échoue avec des accolades dedans, ce qui ne nomme ni l'outil ni le fichier ; refuser au chargement nomme les deux. C'est la règle que suit déjà une valeur d'output inconnue.

La boîte est refusée quand elle ne tient pas. Un outil demandant plus de valeurs que le terminal n'a de lignes donnerait une boîte dont le bouton OK est sous le bas de l'écran — à laquelle on ne peut répondre que par Échap, qui annule. Dire « celui-ci demande douze valeurs et neuf tiennent » n'est pire que rien que si l'on préfère l'apprendre en essayant.

Liens avec le reste

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# Outils Python — explication

## De quoi s'agit-il ?

Un menu **Python** dont les commandes viennent d'un fichier TOML, chacune lancée dans une fenêtre terminal, et les fichiers ouverts relus ensuite. Cette page explique pourquoi chacun de ces trois points est ce qu'il est.

## Pourquoi la sortie a trois destinations, et une popup par défaut

La première version mettait chaque commande dans une fenêtre terminal, et c'était le mauvais défaut pour cinq des six.

Un terminal est la bonne réponse quand le programme est *interactif ou long* : `uv run` sur quelque chose qui lit l'entrée standard doit pouvoir recevoir une réponse, et une compilation qui s'avère durer une minute doit pouvoir être interrompue par `Ctrl-C`. Ni l'un ni l'autre n'est vrai de `uv run ruff check .`, qui affiche quatre lignes et s'arrête. Lui donner une fenêtre entière — qu'il faut ensuite fermer, sur un bureau où les fenêtres se recouvrent et sont numérotées — est plus de cérémonie que le résultat n'en mérite.

Une popup est la bonne réponse pour une commande qu'on lance, qu'on lit et qu'on referme. Elle est modale, ce qui est un coût réel et nommé dans le [guide](../how-to/run-uv-commands.md) : un `go build` qu'on n'attendait pas lent immobilise l'éditeur jusqu'à sa fin ou jusqu'à `Échap`. Ce coût a été accepté exprès, parce que l'alternative — un dialogue surgissant trois secondes plus tard — avale ce qu'on était en train de taper à cet instant.

La popup s'ouvre donc **immédiatement et se remplit**. On voit la progression, rien ne surprend, et `Échap` la ferme et arrête la commande — le seul moyen d'interrompre quelque chose dont la sortie n'est pas dans un terminal.

Une fenêtre d'édition est la bonne réponse pour une sortie qu'on va éplucher : un long `go test -v`, un rapport de couverture. C'est un buffer ordinaire, donc `Ctrl-F` y cherche et `Save as` le conserve. Elle est remplie à la fin de la commande plutôt qu'au fil de l'eau, parce qu'un buffer qui grandit sous le curseur pendant qu'on y cherche est l'inverse de ce que ce mode vise.

Aucune des trois ne convient à tout, et c'est pourquoi `output` est dans le fichier et non dans le code. `Run` en est l'exemple travaillé : c'est la seule commande du fichier de départ qui dit `terminal`, et le commentaire à côté dit pourquoi.

## Pourquoi la fenêtre terminal reste

L'éditeur en avait déjà une — un vrai pseudo-terminal avec émulateur VT, construit pour les fenêtres `F8` — donc `output = "terminal"` coûte un champ dans ses options et offre gratuitement les couleurs, la pagination, `Ctrl-C`, l'entrée clavier et l'historique, parce que ce sont les mêmes mécanismes que ceux de tous les autres terminaux.

La fenêtre reste après la fin de la commande, et c'est le but : la sortie est ce qu'on a demandé, et une fenêtre qui disparaîtrait avec elle serait inutile.

Cela a demandé un correctif à part. Une vue terminal consommait toute touche qu'on lui donnait et l'écrivait au shell ; une fois le shell parti, l'écriture échouait en silence et la touche était consommée quand même — `Ctrl-W` ne pouvait donc jamais fermer une fenêtre terminée, et la souris était la seule issue. Une vue terminée ne prend plus que les touches de défilement et laisse passer le reste vers l'éditeur.

## Pourquoi le code de sortie est toujours dans le titre

`uv sync` qui réussit n'affiche rien du tout. Une popup au corps vide et au titre neutre est indistinguable d'une popup dont la commande n'a pas démarré, et le lecteur en est réduit à deviner la seule chose qu'il voulait savoir.

Le titre porte donc le verdict — `— ok` ou `— exit 1` — et un corps vide affiche `(no output)` une fois la commande terminée. Pendant qu'elle tourne, le corps reste vide : « (no output) » est un verdict, et une commande en cours n'y est pas parvenue.

## Pourquoi les commandes sont dans un fichier

Six commandes codées en dur auraient répondu à la demande. Elles auraient aussi été fausses en une semaine.

Toutes les commandes du fichier de départ passent par `uv`, qui crée l'environnement, résout les dépendances et lance les outils dedans — aucune n'exige donc qu'un environnement ait été activé au préalable. C'est un défaut défendable, ce n'est la réponse universelle de personne. Un projet sous Poetry veut `poetry run`. Un projet sous pip avec un `.venv` fait à la main veut la commande nue, l'environnement déjà sur le PATH. Un projet qui a standardisé sur `black` et `flake8` veut ceux-là plutôt que `ruff`. `uv run pytest` suppose pytest ; un projet sous `unittest` veut `python -m unittest`. Un projet avec un `Makefile` veut `make check`. Rien de cela n'est connaissable d'ici, et tout cela fait une ligne dans un fichier.

Les six sont donc des **défauts, pas du code** : c'est le contenu du fichier de départ qu'écrit **Python ▸ Create tools file**, et en changer une consiste à éditer un fichier plutôt qu'à recompiler un éditeur. Le fichier est relu à chaque ouverture du menu, pour la même raison que le menu Snippets : une modification doit prendre effet aussitôt, et le fichier est souvent ouvert dans la fenêtre derrière le menu.

Les commandes passent par `sh -c``cmd.exe /S /C` sous Windows — plutôt que d'être découpées en argv ici. Le fichier est celui de l'utilisateur, donc les tubes, les globs et `&&` sont des fonctionnalités plutôt que des dangers, et une entrée peut être `uv run ruff format . && uv run ruff check . && uv run pytest`. Découper un argv supposerait d'inventer des règles de citation pour une chaîne écrite à la main.

## Pourquoi il n'y a pas de fichier d'outils utilisateur

Les snippets sont lus depuis deux fichiers — le vôtre et celui du projet — parce que vos snippets sont vos habitudes et doivent vous suivre.

Les outils ne sont pas ainsi. Ils appartiennent à la chaîne d'outils propre à un projet : un fichier d'outils global proposerait `uv run pytest` dans un dépôt qui n'a jamais entendu parler d'uv, et un projet épinglé sur Poetry hériterait des habitudes de quelqu'un d'autre dans son menu. Le fichier est par projet, et c'est toute la règle.

## Pourquoi un outil peut nommer son propre menu

Un menu nommé **Python** contenant `docker compose up` ment sur ce qu'il est. Le premier fichier d'outils que l'on écrit déborde de Python, parce que les commandes d'un projet ne parlent pas toutes du langage dans lequel il est écrit : conteneurs, bases de données, déploiements, une cible de `Makefile` ajoutée en 2019.

Deux formes ont été envisagées. Un **second menu fixe** nommé Tools — tout ce qui est Python dans Python, le reste dans Tools — c'est une clé de plus dans le format et aucun problème de nommage, mais cela ne fait que déplacer le mensonge : un menu Tools contenant `docker compose up`, `psql` et un script de déploiement est tout aussi indifférencié, et dès qu'il y a dix entrées personne n'en retrouve une. Et un **second fichier**, `menus.toml`, garde le fichier d'outils simple au prix de deux fichiers qui doivent s'accorder sur les outils qui existent.

Le menu est donc un **nom libre porté par l'outil**, dans l'unique fichier : `menu = "Docker"`. Un nom que rien d'autre n'emploie crée le menu ; omettre la clé signifie Python. Il n'y a pas de liste de noms autorisés, parce qu'une liste serait la liste des projets de quelqu'un d'autre.

Python reste fixe sur la barre plutôt que de devenir un nom parmi d'autres venu du fichier. **Python ▸ Create tools file** doit être atteignable dans un projet qui n'a aucun fichier d'outils — c'est précisément le projet qui en a besoin — et un menu qui n'existe qu'une fois le fichier créé ne peut pas proposer de créer le fichier.

## Pourquoi la touche d'accès n'appartient pas au fichier

L'auteur d'un fichier d'outils ne peut pas savoir quelles lettres sont libres. Il voit `File`, `Edit`, `Search`, `Run`, `Options`, `Window`, `Snippets`, `Python` et `Help` sur la barre, mais seulement en comptant les soulignements, et un projet partagé à plusieurs dépendrait alors du fait que personne n'ajoute un menu qui entre en collision.

Les collisions sont ici **silencieuses**, et c'est ce qui vaut qu'on se prémunisse contre elles. La barre répond au premier menu dont la touche correspond ; un second menu revendiquant la même lettre n'est pas une erreur et se dessine normalement — il ne s'ouvre simplement jamais. Le piège s'est déjà refermé une fois dans cet éditeur : `Snippets` et `Search` voulaient tous deux le `S`, `Snippets` était celui qu'on ne pouvait pas ouvrir, et tous les tests passaient. Le correctif d'alors fut de déplacer Snippets sur `N` à la main. Laisser un fichier nommer des menus en ferait un danger permanent plutôt qu'une erreur isolée, donc l'attribution revient à l'éditeur : la première lettre du nom que rien d'autre ne revendique.

Les tildes écrits dans le nom sont honorés **quand la lettre est libre**, et écartés sans bruit sinon. Refuser le fichier était l'alternative, et elle est pire : la collision dépend des menus qui existent, donc un fichier d'outils qui marchait cesserait de marcher le jour où une version de l'éditeur ajoute un menu. Entre un menu sur une lettre que vous n'avez pas demandée et un menu que vous ne pouvez pas ouvrir, la première est la moindre perte.

Quand toutes les lettres d'un nom sont prises, le menu n'a pas de touche d'accès du tout. `F10`, les flèches et la souris l'atteignent encore, et l'alternative — aller chercher une lettre qui n'est pas dans le nom — mettrait un soulignement sous rien.

## Pourquoi la barre est reconstruite depuis un stat

`Menu.OnOpen` remplit les entrées d'un menu juste avant qu'il ne se déroule, et c'est ainsi que les menus Python et Snippets suivent leurs fichiers sans redémarrage. Cela ne suffit pas ici : l'*ensemble* des menus appartient à la barre et non à un menu, et ajouter `menu = "Docker"` au fichier doit poser Docker sur la barre.

Lire et analyser le fichier à chaque tour de la boucle d'événements y parviendrait, et ferait aussi ce travail pour rien à chaque frappe dans un fichier que personne n'a modifié. La barre porte donc la taille et la date de modification du fichier d'outils dont elle est issue, et un `stat` par tour décide s'il faut reconstruire. Modifier le fichier dans la fenêtre devant soi, l'enregistrer et voir la barre changer, c'est le cas visé.

## Pourquoi les fichiers ouverts sont relus, et seulement certains

`Format` réécrit les fichiers sur le disque — y compris celui qu'on regarde. Sans rien de plus, l'éditeur resterait assis sur une copie périmée, et le `F2` suivant réécrirait votre version non formatée par-dessus le travail de `ruff format`. Ce n'est pas une aspérité : c'est la fonctionnalité qui se défait toute seule, en silence.

À la fin d'une commande, l'éditeur relit donc chaque fichier ouvert. La partie intéressante est ceux qu'il refuse de toucher.

**Un fichier ayant des modifications non enregistrées est laissé tel quel**, et la barre d'état dit combien ont été ignorés. Le recharger jetterait un travail que l'utilisateur n'a pas enregistré, ce qu'aucune commodité ne justifie. Et le conflit est réel : le formateur et la modification non enregistrée ne sont pas d'accord sur ce que le fichier doit dire, et l'éditeur n'est pas en position de trancher. Le nommer et s'arrêter est l'issue honnête — l'utilisateur peut enregistrer et relancer, ou continuer à éditer et formater plus tard.

Deux décisions plus petites à l'intérieur :

- **Le curseur reste où il était**, borné à ce que le fichier contient désormais. Un formateur déplace les lignes ; remettre le curseur en haut ferait perdre sa place au lecteur pour rien.
- **L'historique d'annulation est jeté.** Annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus, ce qui est pire que de ne pas pouvoir annuler.

## Pourquoi le rechargement a lieu sur la boucle d'événements

La fin de la commande est remarquée par la goroutine qui lit le terminal, laquelle ne peut toucher ni un buffer ni le bureau. Elle positionne donc un drapeau, et le rechargement se fait en tête du tour suivant de la boucle.

C'est la quatrième chose construite ainsi dans cet éditeur — l'annonce au serveur de langage, les redessins de terminal, l'échéance d'autosave, et maintenant ceci. La règle qu'elles partagent mérite d'être énoncée une fois de plus : **le réveil peut être perdu, l'état ne doit pas l'être.** `PostEvent` jette ce qui ne tient pas dans sa file, donc tout ce qui dépend de l'arrivée d'un message est un bug qui attend un moment de charge. Un drapeau que la boucle vérifie elle-même ne peut pas disparaître.

## Pourquoi une commande peut demander une valeur, et pourquoi en doubles accolades

`uv venv` a besoin d'un répertoire. `uv run` a besoin d'un script. `uv add` a besoin d'un nom de paquet, et `pytest -k` d'un motif. Aucun de ces éléments ne peut vivre dans le fichier d'outils sous forme de chaîne figée, parce que la réponse change à chaque fois — et un outil qui ne peut pas demander est un outil qu'il faut éditer avant chaque usage, ce qui n'est pas un outil.

Un `{{libellé}}` dans une commande est donc une valeur que l'éditeur demande d'abord, dans une boîte portant le nom de l'outil.

**Une seule accolade était l'écriture évidente, et elle est fausse.** `awk '{print $1}'` et `find . -exec rm {} +` sont des choses ordinaires à mettre dans un fichier d'outils, et lire la première comme un libellé transforme une commande qui marche en une boîte demandant « print $1 ». Les doubles accolades n'entrent en collision presque avec rien, et la seule construction avec laquelle elles le font — un bloc imbriqué en awk — est assez rare pour être signalée plutôt que contournée par la conception.

**La valeur est protégée par défaut**, parce que l'inverse échoue en silence. Un chemin contenant une espace, substitué tel quel, devient deux arguments et la commande parle d'un fichier qui n'existe pas. La protection fait marcher ce cas et rend l'autre — « ajoute ces trois options à la fin » — impossible ; c'est pourquoi un `...` dans les accolades demande la valeur telle quelle. Deux comportements, tous deux documentés, plutôt qu'un seul faux une fois sur deux.

**Rien n'est retenu sur le disque.** La boîte repart de ce qui avait été tapé, pour la session. L'écrire dans le répertoire propre au projet a été envisagé puis rejeté : ce répertoire contient ce que le projet a décidé, et un filtre tapé en poursuivant un test n'en fait pas partie. Ce serait aussi la première chose qui y changerait sans que personne ne modifie un fichier.

**Un fichier illisible est refusé à la lecture**, pas au moment où l'outil est choisi. Un `{{` non fermé atteignant le shell donne une commande qui échoue avec des accolades dedans, ce qui ne nomme ni l'outil ni le fichier ; refuser au chargement nomme les deux. C'est la règle que suit déjà une valeur d'`output` inconnue.

**La boîte est refusée quand elle ne tient pas.** Un outil demandant plus de valeurs que le terminal n'a de lignes donnerait une boîte dont le bouton OK est sous le bas de l'écran — à laquelle on ne peut répondre que par Échap, qui annule. Dire « celui-ci demande douze valeurs et neuf tiennent » n'est pire que rien que si l'on préfère l'apprendre en essayant.

## Liens avec le reste

- Toutes les clés du fichier et toutes les règles : [Référence des outils go](../reference/python-tools.md)
- L'utiliser : [Lancer les commandes uv depuis l'éditeur](../how-to/run-uv-commands.md)
- Les fenêtres qu'emploie `output = "terminal"`, et pourquoi ce sont de vrais terminaux : [Fenêtres terminal](terminal-windows.md)
- L'autre menu construit depuis un fichier : [Snippets](snippets.md)