turbo-editors/turbo-golopublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-golo.git
git clone ssh://git@rickub.com/turbo-editors/turbo-golo.git

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

📦 Turbo Golo d710c1b · on v1.0.0 · k33g · 12h ago
terminal-windows.md · 72 lines · 9.7 KBmarkdown
Blame HistoryOpen raw

Fenêtres terminal — explication

De quoi s'agit-il ?

F8 ouvre une fenêtre contenant un shell. Cette phrase masque l'essentiel du travail : pour mettre un shell dans une fenêtre, un éditeur doit devenir un émulateur de terminal. Cette page raconte ce que cela a impliqué, et quelles solutions moins coûteuses ont été écartées en chemin.

Pourquoi un vrai pseudo-terminal

La version bon marché évidente consiste à lancer une commande avec exec.Command, à capturer sa sortie et à l'afficher dans un panneau en lecture seule. Beaucoup d'éditeurs livrent exactement cela, et cela échoue précisément sur ce pour quoi on veut un terminal.

Un programme se comporte différemment quand sa sortie est un tube plutôt qu'un terminal. golo --test abandonne ses couleurs. git log ne pagine pas. ls affiche un nom par ligne. Rien d'interactif ne fonctionne : ni vim, ni ssh, ni le REPL de golo, ni git rebase -i, ni la réponse à une invite, ni Ctrl-C — sans terminal de contrôle, il n'y a aucun signal à envoyer.

Le shell reçoit donc un vrai pseudo-terminal : /dev/ptmx sur les deux plateformes supportées, le fils dans une session à lui avec l'esclave comme terminal de contrôle, et TIOCSWINSZ à chaque redimensionnement de la fenêtre. Cela offre gratuitement le contrôle de tâches, isatty, SIGWINCH et la couleur, parce que ce sont les mêmes mécanismes que ceux de tous les autres terminaux.

Le prix à payer est que l'éditeur doit ensuite relire ce qu'un terminal est censé comprendre — c'est-à-dire l'émulateur.

Pourquoi écrire l'émulateur plutôt que d'en emprunter un

Go dispose de bibliothèques d'émulation de terminal. En prendre une aurait signifié une troisième dépendance, dans une bibliothèque qui en a exactement deux et qui affiche une réticence assumée à en ajouter une troisième.

Ce que l'on met en balance n'est pas « émulateur » contre « pas d'émulateur », mais quelle quantité d'émulateur. Ce dont ont besoin un shell, golo --test, git, less, htop et vim forme une liste bien délimitée : déplacement du curseur, la famille effacement / insertion-suppression, une région de défilement, SGR dans ses trois profondeurs de couleur, l'écran alternatif, le retour à la ligne automatique, la visibilité du curseur et les touches curseur application. Cela représente environ six cents lignes, c'est écrit noir sur blanc dans ECMA-48, et cela se teste en écrivant des octets en entrée et en lisant une grille en sortie — sans shell, sans temporisation, sans écran.

À comparer avec ce qu'apporte une bibliothèque généraliste : jeux de caractères, protocoles de rapport souris, sixel, collage entre crochets, rapports d'état DEC. Tout cela est réel, rien n'est nécessaire ici, et tout cela constitue de la surface à maintenir.

L'émulateur est donc écrit à la main et volontairement partiel, et la référence dit exactement où il s'arrête. Un programme qui demande quelque chose d'absent obtient le silence plutôt que de la corruption, ce qui est le bon mode d'échec : htop s'affiche, la sortie sixel n'apparaît simplement pas.

À qui revient la touche

C'est la décision qui pèse le plus sur la sensation d'usage de l'éditeur, et la première version s'était trompée.

Les raccourcis globaux de l'éditeur sont examinés avant que la fenêtre du premier plan ne voie quoi que ce soit. C'est juste pour un éditeur, et faux dès l'instant où cette fenêtre est un shell, parce que les deux revendiquent les mêmes touches. Ctrl-W ferme une fenêtre dans Turbo C et supprime un mot dans tous les shells. Ctrl-F est Rechercher ici et avancer-d'un-caractère dans readline. Ctrl-C est copier, et aussi le seul moyen d'arrêter une commande emballée.

La règle retenue inverse l'ordre habituel, mais uniquement pour les touches réellement disputées :

Un terminal ayant le focus reçoit tout, sauf les touches de fonction, Alt-X et Alt-0Alt-9.

Ces exceptions ne sont pas un compromis entre les deux revendications — ce sont la sortie. Un programme plein écran comme vim recouvre la fenêtre et s'empare de la souris ; sans touche réservée, il n'y aurait aucun moyen d'atteindre la barre de menus, de changer de fenêtre ou de quitter l'éditeur sans d'abord quitter le programme. Les touches de fonction sont la réservation naturelle parce que c'est vers elles qu'un utilisateur de terminal se tourne le moins, et Alt-X parce que quitter un éditeur ne devrait jamais faire de doute.

Ce que cela coûte est réel et mérite d'être nommé : Alt-B et Alt-F atteignent le shell, donc le déplacement par mot de readline fonctionne, mais un programme dans une fenêtre terminal ne verra jamais F1F12. Le menu par touches de fonction de htop est inaccessible. C'est l'arbitrage, et il a été rendu en faveur du fait de toujours pouvoir sortir.

Pourquoi fermer un terminal ne demande rien

Fermer un fichier modifié demande s'il faut l'enregistrer. Fermer un terminal ne demande rien du tout, et cette asymétrie est délibérée.

Une fenêtre au travail non enregistré contient quelque chose qui serait perdu. Un terminal contient un processus en cours, et fermer la fenêtre est la façon ordinaire de dire qu'on en a fini — comme on ferme l'onglet d'un émulateur de terminal. Demander « êtes-vous sûr ? » à chaque fois désapprendrait la réponse à quiconque, ce qui est le problème général des confirmations qui se déclenchent sur le cas courant.

Quitter l'éditeur ferme tous les terminaux pour la même raison, en sens inverse : une fenêtre est la seule prise sur ces shells, donc les laisser survivre à l'éditeur abandonnerait des processus que plus rien ne peut atteindre.

Pourquoi les redessins sont cadencés

Le shell écrit depuis une goroutine à lui ; l'éditeur dessine depuis la principale. Réveiller la boucle d'événements à chaque bloc de sortie semblait évident et se trompait deux fois.

Une commande bavarde écrit bien plus vite qu'un écran ne peut être utilement repeint : la plupart de ces redessins sont donc du gaspillage. Pire, le mécanisme de réveil de la boucle depuis une autre goroutine est le PostEvent de tcell, qui jette les événements quand sa file est pleine — de sorte que la rafale qui a le plus besoin d'un redessin est justement celle dont le réveil final est perdu, et la fenêtre se fige en pleine exécution sur un texte périmé. Ce bug exact avait déjà été rencontré une fois ailleurs dans cet éditeur, du côté du serveur de langage.

La vue positionne donc un drapeau, et une horloge demande un redessin soixante fois par seconde tant que le drapeau est levé. Un réveil perdu ne peut rien bloquer, puisque le tic suivant est à seize millisecondes.

Windows : une pseudo-console, et pourquoi c'est un fichier à part

Les pseudo-terminaux sont la seule partie non portable de tout ceci. Linux et macOS passent tous deux par /dev/ptmx et ne diffèrent que par l'ioctl qui accorde l'esclave. Windows n'a rien de tel : il a des pseudo-consoles — ConPTY, depuis Windows 10 version 1809 — un objet détenu par conhost.exe et relié à deux tubes de l'éditeur. Ce que le shell affiche arrive sur l'un des tubes sous la forme des mêmes séquences VT qu'un shell Unix écrit dans un pty, ce qui est la raison pour laquelle l'émulateur de ce côté n'a eu besoin d'aucun code Windows ; ce que l'éditeur écrit dans l'autre tube parvient au shell comme des frappes de touches.

Trois choses en ont fait un fichier à part plutôt qu'une variante du fichier Unix. Le processus doit être créé à la main, parce que l'attacher à une pseudo-console exige un enregistrement de démarrage étendu que l'os/exec de Go ne sait pas porter. Le shell est %COMSPEC% — cmd.exe — plutôt que $SHELL, et cmd.exe lit sa ligne de commande selon ses propres règles : la ligne qui lance une commande du menu est donc composée pour lui mot pour mot, la commande entre une seule paire de guillemets, au lieu d'être échappée comme tout autre programme l'attend. Et conhost.exe garde le tube de sortie ouvert jusqu'à la fermeture de la console, quoi que fasse le shell ; une goroutine attend donc la fin du shell puis ferme la console — c'est ce qui transforme une commande terminée en la fin d'entrée sur laquelle la fenêtre compte pour le dire. Le contrôle de tâches est celui de cmd.exe et non du noyau : Ctrl-C interrompt le programme en cours comme il le ferait dans une fenêtre de console.

Les fichiers par plateforme restent séparés pour que chaque plateforme ait une implémentation honnête derrière une petite interface, et qu'une plateforme qui n'a ni l'un ni l'autre — les BSD, aujourd'hui — reçoive ErrUnsupported, que F8 le dise clairement, et que rien d'autre dans l'éditeur ne soit affecté.

Le chemin Windows a été compilé et vérifié, pas exécuté. turbo-core est développé sous Linux et son auteur travaille sous macOS. Les parties pures — le bloc d'environnement, la ligne de commande que veut cmd.exe — sont testées unitairement sur toute plateforme, et les appels à l'API compilent et passent go vet sous GOOS=windows ; personne n'a encore appuyé sur F8 sur une machine Windows. Le guide dit quoi essayer en premier.

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
# Fenêtres terminal — explication

## De quoi s'agit-il ?

`F8` ouvre une fenêtre contenant un shell. Cette phrase masque l'essentiel du travail : pour mettre un shell dans une fenêtre, un éditeur doit devenir un émulateur de terminal. Cette page raconte ce que cela a impliqué, et quelles solutions moins coûteuses ont été écartées en chemin.

## Pourquoi un vrai pseudo-terminal

La version bon marché évidente consiste à lancer une commande avec `exec.Command`, à capturer sa sortie et à l'afficher dans un panneau en lecture seule. Beaucoup d'éditeurs livrent exactement cela, et cela échoue précisément sur ce pour quoi on veut un terminal.

Un programme se comporte différemment quand sa sortie est un tube plutôt qu'un terminal. `golo --test` abandonne ses couleurs. `git log` ne pagine pas. `ls` affiche un nom par ligne. Rien d'interactif ne fonctionne : ni `vim`, ni `ssh`, ni le REPL de `golo`, ni `git rebase -i`, ni la réponse à une invite, ni `Ctrl-C` — sans terminal de contrôle, il n'y a aucun signal à envoyer.

Le shell reçoit donc un vrai pseudo-terminal : `/dev/ptmx` sur les deux plateformes supportées, le fils dans une session à lui avec l'esclave comme terminal de contrôle, et `TIOCSWINSZ` à chaque redimensionnement de la fenêtre. Cela offre gratuitement le contrôle de tâches, `isatty`, `SIGWINCH` et la couleur, parce que ce sont les mêmes mécanismes que ceux de tous les autres terminaux.

Le prix à payer est que l'éditeur doit ensuite relire ce qu'un terminal est censé comprendre — c'est-à-dire l'émulateur.

## Pourquoi écrire l'émulateur plutôt que d'en emprunter un

Go dispose de bibliothèques d'émulation de terminal. En prendre une aurait signifié une troisième dépendance, dans une bibliothèque qui en a exactement deux et qui affiche une réticence assumée à en ajouter une troisième.

Ce que l'on met en balance n'est pas « émulateur » contre « pas d'émulateur », mais *quelle quantité* d'émulateur. Ce dont ont besoin un shell, `golo --test`, `git`, `less`, `htop` et `vim` forme une liste bien délimitée : déplacement du curseur, la famille effacement / insertion-suppression, une région de défilement, SGR dans ses trois profondeurs de couleur, l'écran alternatif, le retour à la ligne automatique, la visibilité du curseur et les touches curseur application. Cela représente environ six cents lignes, c'est écrit noir sur blanc dans ECMA-48, et cela se teste en écrivant des octets en entrée et en lisant une grille en sortie — sans shell, sans temporisation, sans écran.

À comparer avec ce qu'apporte une bibliothèque généraliste : jeux de caractères, protocoles de rapport souris, sixel, collage entre crochets, rapports d'état DEC. Tout cela est réel, rien n'est nécessaire ici, et tout cela constitue de la surface à maintenir.

L'émulateur est donc écrit à la main et volontairement partiel, et la [référence](../reference/terminal.md) dit exactement où il s'arrête. Un programme qui demande quelque chose d'absent obtient le silence plutôt que de la corruption, ce qui est le bon mode d'échec : `htop` s'affiche, la sortie `sixel` n'apparaît simplement pas.

## À qui revient la touche

C'est la décision qui pèse le plus sur la sensation d'usage de l'éditeur, et la première version s'était trompée.

Les raccourcis globaux de l'éditeur sont examinés avant que la fenêtre du premier plan ne voie quoi que ce soit. C'est juste pour un éditeur, et faux dès l'instant où cette fenêtre est un shell, parce que les deux revendiquent les mêmes touches. `Ctrl-W` ferme une fenêtre dans Turbo C et supprime un mot dans tous les shells. `Ctrl-F` est Rechercher ici et avancer-d'un-caractère dans readline. `Ctrl-C` est copier, et aussi le seul moyen d'arrêter une commande emballée.

La règle retenue inverse l'ordre habituel, mais uniquement pour les touches réellement disputées :

**Un terminal ayant le focus reçoit tout, sauf les touches de fonction, `Alt-X` et `Alt-0`…`Alt-9`.**

Ces exceptions ne sont pas un compromis entre les deux revendications — ce sont la *sortie*. Un programme plein écran comme `vim` recouvre la fenêtre et s'empare de la souris ; sans touche réservée, il n'y aurait aucun moyen d'atteindre la barre de menus, de changer de fenêtre ou de quitter l'éditeur sans d'abord quitter le programme. Les touches de fonction sont la réservation naturelle parce que c'est vers elles qu'un utilisateur de terminal se tourne le moins, et `Alt-X` parce que quitter un éditeur ne devrait jamais faire de doute.

Ce que cela coûte est réel et mérite d'être nommé : `Alt-B` et `Alt-F` atteignent le shell, donc le déplacement par mot de readline fonctionne, mais un programme dans une fenêtre terminal ne verra jamais `F1``F12`. Le menu par touches de fonction de `htop` est inaccessible. C'est l'arbitrage, et il a été rendu en faveur du fait de toujours pouvoir sortir.

## Pourquoi fermer un terminal ne demande rien

Fermer un fichier modifié demande s'il faut l'enregistrer. Fermer un terminal ne demande rien du tout, et cette asymétrie est délibérée.

Une fenêtre au travail non enregistré contient quelque chose qui serait *perdu*. Un terminal contient un processus en cours, et fermer la fenêtre est la façon ordinaire de dire qu'on en a fini — comme on ferme l'onglet d'un émulateur de terminal. Demander « êtes-vous sûr ? » à chaque fois désapprendrait la réponse à quiconque, ce qui est le problème général des confirmations qui se déclenchent sur le cas courant.

Quitter l'éditeur ferme tous les terminaux pour la même raison, en sens inverse : une fenêtre est la seule prise sur ces shells, donc les laisser survivre à l'éditeur abandonnerait des processus que plus rien ne peut atteindre.

## Pourquoi les redessins sont cadencés

Le shell écrit depuis une goroutine à lui ; l'éditeur dessine depuis la principale. Réveiller la boucle d'événements à chaque bloc de sortie semblait évident et se trompait deux fois.

Une commande bavarde écrit bien plus vite qu'un écran ne peut être utilement repeint : la plupart de ces redessins sont donc du gaspillage. Pire, le mécanisme de réveil de la boucle depuis une autre goroutine est le `PostEvent` de tcell, qui **jette** les événements quand sa file est pleine — de sorte que la rafale qui a le plus besoin d'un redessin est justement celle dont le réveil final est perdu, et la fenêtre se fige en pleine exécution sur un texte périmé. Ce bug exact avait déjà été rencontré une fois ailleurs dans cet éditeur, du côté du serveur de langage.

La vue positionne donc un drapeau, et une horloge demande un redessin soixante fois par seconde tant que le drapeau est levé. Un réveil perdu ne peut rien bloquer, puisque le tic suivant est à seize millisecondes.

## Windows : une pseudo-console, et pourquoi c'est un fichier à part

Les pseudo-terminaux sont la seule partie non portable de tout ceci. Linux et macOS passent tous deux par `/dev/ptmx` et ne diffèrent que par l'`ioctl` qui accorde l'esclave. Windows n'a rien de tel : il a des **pseudo-consoles** — ConPTY, depuis Windows 10 version 1809 — un objet détenu par `conhost.exe` et relié à deux tubes de l'éditeur. Ce que le shell affiche arrive sur l'un des tubes sous la forme des mêmes séquences VT qu'un shell Unix écrit dans un pty, ce qui est la raison pour laquelle l'émulateur de ce côté n'a eu besoin d'aucun code Windows ; ce que l'éditeur écrit dans l'autre tube parvient au shell comme des frappes de touches.

Trois choses en ont fait un fichier à part plutôt qu'une variante du fichier Unix. Le processus doit être créé à la main, parce que l'attacher à une pseudo-console exige un enregistrement de démarrage étendu que l'`os/exec` de Go ne sait pas porter. Le shell est `%COMSPEC%` — cmd.exe — plutôt que `$SHELL`, et cmd.exe lit sa ligne de commande selon ses propres règles : la ligne qui lance une commande du menu est donc composée pour lui mot pour mot, la commande entre une seule paire de guillemets, au lieu d'être échappée comme tout autre programme l'attend. Et `conhost.exe` garde le tube de sortie ouvert jusqu'à la fermeture de la console, quoi que fasse le shell ; une goroutine attend donc la fin du shell puis ferme la console — c'est ce qui transforme une commande terminée en la fin d'entrée sur laquelle la fenêtre compte pour le dire. Le contrôle de tâches est celui de cmd.exe et non du noyau : `Ctrl-C` interrompt le programme en cours comme il le ferait dans une fenêtre de console.

Les fichiers par plateforme restent séparés pour que chaque plateforme ait une implémentation honnête derrière une petite interface, et qu'une plateforme qui n'a ni l'un ni l'autre — les BSD, aujourd'hui — reçoive `ErrUnsupported`, que `F8` le dise clairement, et que rien d'autre dans l'éditeur ne soit affecté.

**Le chemin Windows a été compilé et vérifié, pas exécuté.** turbo-core est développé sous Linux et son auteur travaille sous macOS. Les parties pures — le bloc d'environnement, la ligne de commande que veut cmd.exe — sont testées unitairement sur toute plateforme, et les appels à l'API compilent et passent `go vet` sous `GOOS=windows` ; personne n'a encore appuyé sur `F8` sur une machine Windows. [Le guide](../how-to/use-a-terminal.md) dit quoi essayer en premier.

## Liens avec le reste

- La liste exacte de ce qui est implémenté : [référence des fenêtres terminal](../reference/terminal.md)
- En utiliser une : [Lancer des commandes shell sans quitter l'éditeur](../how-to/use-a-terminal.md)
-`terminal` se situe parmi les paquets, et pourquoi le graphe est orienté : [Architecture](architecture.md)
- Le décompte de dépendances que cette page ne cesse d'invoquer : [Décisions de conception](design-decisions.md)