ADR-061 — Architecture AirTerminal : modèle canonique + codecs (vision)
Statut : Accepté (2026-07-02, par délégation d’autonomie BDFL ; ADR de vision — gravé pour revue a posteriori). RFC de structure (ADR-015). Companion de ADR-060 (termios couche 0), ADR-046 (libc), ADR-001 (AirCom / IPC), ADR-003 (compositeur), ADR-007 (air-console), ADR-008 (air-tui), ADR-040 (Cap’n Proto).
Catégorie : Architecture (vision terminal, couche 1 → 2). Aucun engagement d’implémentation
au-delà de la forme (modèle + trait) ; les codecs riches et air-codec-1 sont couche 2, différés.
Contexte — le terminal legacy n’est plus adapté
Le PTY est un seul canal d’octets non typé : sortie appli (texte + séquences d’échappement),
entrée (frappes + souris encodée en fausses séquences + collage + focus), et négociation
(requêtes/réponses) — tout multiplexé en-bande. Modèle « téléscripteur » (POSIX termios 1988,
ECMA-48/VT102 1976-91), terminal-agnostique, incapable de décrire l’interaction riche (souris
multi-boutons, molette, tactile, pression, gestes, biométrie — terminfo n’a pas de vocabulaire
pour ça ; kitty & co. ont greffé des séquences faute d’autre canal). Air, qui possède tout le
stack (libc, IPC, compositeur, apps), peut faire mieux — non en encodant mieux la souris en
séquences, mais en n’ayant pas à le faire.
Résultat clé de la réflexion : sur un canal, « propre » et « legacy » ne cohabitent pas (le seul mécanisme est la greffe de séquences ignorables — la laideur même). ⇒ on sépare ; la cohabitation se fait au niveau système (objet), pas octet.
Décision — modèle canonique + codecs
-
AirTerminal(objet couche 1) = un tag (Legacy|Air) + un codec + un transport. Il représente le terminal-pair avec lequel Air converse. -
Un modèle interne canonique Air (la lingua franca) : les codecs ne se parlent jamais ; ils traduisent tous ce modèle. Conçu une fois, riche et tourné vers l’avenir ; chaque type de terminal = un codec de plus, jamais une refonte. Le codec legacy est un adaptateur dégradant (il jette ce que xterm ne sait pas), pas une contrainte sur le modèle.
#![allow(unused)] fn main() { #[non_exhaustive] enum TermIn { Keyboard(KeyboardEvent), Mouse(MouseEvent), Screen(ScreenEvent), /* +Touch/Gesture/Device… */ Unknown(RawEvent) } #[non_exhaustive] enum MouseEvent { Button{..}, Wheel{..}, Motion{..}, /* +Pressure/Tilt… */ Unknown{ subtype:u16, payload } } enum TermOut { Text{run,attrs}, Cursor{..}, Clear{..}, Scroll{..}, SetMode{..}, AttachSurface{ rect, surface_id } } // ⟵ la clé struct TermCaps { colors, mouse, keyboard_proto, surfaces:bool, .. } trait TerminalCodec { fn decode_in(&mut self, wire)->Vec<TermIn>; fn encode_out(&mut self, &TermOut)->Bytes; fn caps(&self)->TermCaps; } } -
Discipline unique d’extensibilité — « Annoncer → Sélectionner → dégrader-sur-inconnu ». Tout ce qui est extensible (catégories/sous-types d’events, champs, types de surface, codecs) suit : (1) un registre à IDs stables ; (2) chaque bout annonce ses capacités ; (3) le consommateur choisit dans l’intersection ; (4) l’inconnu est catégorisé et ignoré, jamais fatal. Mécanique Rust/fil :
#[non_exhaustive](compat à la recompilation, quand on ajoute une variante) + varianteUnknown(compat runtime, quand on reçoit plus récent que soi) + schéma évolutif Cap’n Proto (ADR-040 — ajouter un champ = compatible, inconnu skippé) pour l’axe « nouveau champ ». (Flux haute fréquence → batch par frame ; capnp donne la discipline d’évolution, pas forcément l’encodage par-événement.) -
AttachSurface= énumérer → sélectionner → lier. Une appli ne dessine pas une image en séquences : elle interroge les surfaces offertes (QuerySurfaces→SurfaceOffer{kinds}), choisit celle qu’elle sait piloter (intersection offert ∧ compris), lie (AttachSurface→SurfaceReady{surface_id}) et reçoit ses événements tagués. Le compositeur Air fournit une vraie surface (GPU, événements typés) — impossible sur Unix (pas d’autre canal), trivial sous Air.SurfaceKindest lui-même un registre extensible (même discipline). -
termios= affaire du codec legacy uniquement (discipline de ligne du PTY). Le chemin Air-natif esttermios-free (eventsKeytypés + éditeur-ligne userspace si besoin). -
La passerelle tombe toute seule : coller n’importe quel côté-appli à n’importe quel côté-display = composer deux codecs via le modèle (
bash[ansi]↔modèle↔display[kitty] ;air-cli[air]↔modèle↔xterm[ansi, dégrade]). L’interop legacy = « un bout utilise le codec ansi ». Découverte du tag distant : un handshake « es-tu Air ? » ignorable par le legacy (la seule greffe résiduelle, bornée), puis bascule sur le canal propre.
Discipline de portée (ne pas bloquer le sceau)
- Ce que la libc / le sceau couche 1 exige =
termios(couche 0, ADR-060) + un minceair-terminal(couche 1) dont le premier (et seul, au sceau) codec estansi/termios. - Le modèle canonique + le trait
TerminalCodec+ la règle d’extensibilité +AttachSurfacese gravent maintenant (design ; c’est l’assurance anti-double-travail). Le trait peut entrer dans la couche 1 (interface stable) ; les codecs riches,air-codec-1,AttachSurfaceimplémenté = couche 2, différés (conçus quand air-console/air-tui les réclament — pas avant, pour ne pas sur-designer). termcap/terminfo= hors libc (c’est ncurses/libtinfo, couche 2) ; si Air le fournit un jour, base xterm-family only (98 % du marché 2026), pas la base terminfo legacy complète.
Conséquences
air-terminalest bâti comme le premier codec d’un cadre déjà juste, pas un wrapper jetable.- L’interaction riche (souris fine, tactile, gestes, biométrie, graphique) passe par le stack
device/entrée d’Air (air-device, compositeur) via
AttachSurface/events typés — pas par le terminal. Le terminal reste texte, borné, compatible.