Spécification air-tui — noyau 1.0
Document de spécification — couche 4
Rôle du document
Ce document précise le noyau opérationnel du framework air-tui après les
décisions d’architecture actées dans :
- ADR-008 — trois backends ;
- ADR-009 — API déclarative sans coordonnées physiques ;
- ADR-018 — modèle cellulaire pour le mode console ;
- ADR-026 — contrat 1.0 d’
air-tui.
Ce document ne fige pas encore une API Rust complète ni les signatures exactes des types publics. Il fixe en revanche trois contrats sans lesquels le framework ne peut pas être implémenté proprement :
- le modèle
Capabilities; - le modèle de focus ;
- le noyau des commandes sémantiques.
1. Périmètre
air-tui est le framework console de la couche 4. Il s’appuie sur :
- un tronc déclaratif commun avec
air-ui(air-view) ; - un renderer cellulaire (
air-ui-render-tui) ; - trois backends de transport :
- backend console local via
air-console; - backend terminal Air via protocole privé ;
- backend compatible terminal tiers/SSH via Kitty keyboard protocol ou ANSI.
- backend console local via
Le présent document couvre uniquement la sémantique commune à ces trois backends. Tout détail spécifique à un backend ne peut exister que comme capacité optionnelle ou extension interne, jamais comme hypothèse implicite du framework.
2. Modèle Capabilities
2.1 Objectif
Le type Capabilities décrit ce que l’environnement d’exécution garantit à une
application air-tui.
Il ne décrit pas :
- les préférences utilisateur ;
- les entitlements applicatifs ;
- l’état dynamique de l’application ;
- les permissions de sécurité système.
Il décrit seulement les capacités d’interaction et de rendu du backend TUI.
2.2 Principes
Le modèle suit cinq principes :
- Une capacité décrit une garantie positive ou une absence explicite, jamais une supposition implicite.
- Une capacité doit être stable sémantiquement à travers les backends.
- Une capacité doit être interrogeable au runtime.
- Une capacité peut être consommée au build pour du capability-gating statique ultérieur, mais le runtime reste la source de vérité.
- Une capacité ne doit jamais prétendre émuler honnêtement ce qu’un backend ne sait pas fournir.
2.3 Forme conceptuelle
Le modèle conceptuel attendu est :
- un profil global de backend ;
- un ensemble de drapeaux ou enums fins par famille ;
- une distinction entre
guaranteed,optional,unsupportedquand cela a du sens.
Exemple purement illustratif :
#![allow(unused)]
fn main() {
struct Capabilities {
backend_profile: BackendProfile,
keyboard: KeyboardCapabilities,
pointer: PointerCapabilities,
text: TextCapabilities,
clipboard: ClipboardCapabilities,
focus: FocusCapabilities,
viewport: ViewportCapabilities,
}
}
Les noms exacts restent ouverts ; la structure logique, non.
2.4 Profils globaux
Trois profils globaux sont normatifs.
FullConsole
Profil local premium sous air-console.
Garanties minimales :
KeyDownetKeyUpfiables ;- modifiers complets et layout-aware ;
- souris native ;
- resize fiable ;
- focus structuré ;
- couleurs riches ;
- Unicode sérieux ;
- paste fiable ;
- scroll événementiel réel.
AirTerminal
Profil transporté via terminal Air.
Garanties minimales :
- mêmes invariants applicatifs que
FullConsole; - latence et qualité proches ;
- transport indirect sans perte sémantique substantielle.
Différence clé :
- le backend n’est pas le compositeur local lui-même.
CompatibleTerminal
Profil dégradé sur terminal tiers ou SSH.
Garanties minimales :
- texte rendu en grille ;
KeyDownpour un sous-ensemble significatif du clavier ;- resize généralement disponible ;
- paste et souris possibles mais non garantis ;
KeyUppotentiellement absent ;- modifiers parfois incomplets ;
- rendu Unicode et largeur de cellules dépendants du terminal.
2.5 Familles de capacités
Le framework doit exposer au minimum les familles suivantes.
Capacités clavier
Doivent permettre de répondre explicitement aux questions suivantes :
KeyUpest-il garanti ?- les modifiers gauche/droite sont-ils distingués ?
- les touches de navigation sont-elles fiables ?
- la répétition clavier est-elle distinguée d’une nouvelle pression ?
- les combinaisons avec
Alt,Ctrl,Shift,Supersont-elles fiables ? - la composition IME est-elle disponible ?
Capacités pointeur
Doivent permettre de répondre explicitement aux questions suivantes :
- un pointeur est-il disponible ?
- les clics sont-ils disponibles ?
- le mouvement est-il disponible ?
- le scroll est-il disponible ?
- la capture/glisse continue est-elle disponible ?
Capacités texte
Doivent permettre de répondre explicitement aux questions suivantes :
- Unicode est-il pris en charge sérieusement ?
- la largeur de cellule est-elle fiable pour les caractères larges ?
- le shaping complexe est-il pris en charge ?
- le bidi est-il pris en charge ?
- la coloration riche est-elle disponible ?
Le framework ne doit pas promettre le même niveau de sophistication textuelle
entre FullConsole et CompatibleTerminal.
Capacités viewport
Doivent permettre de répondre explicitement aux questions suivantes :
- la taille de grille est-elle fiable ?
- les événements de resize sont-ils fiables ?
- les régions invalidées partielles sont-elles honorées ?
- le mode écran alternatif est-il disponible ?
Capacités clipboard / paste
Doivent distinguer au minimum :
unsupportedpaste_onlyread_write
Capacités focus
Doivent permettre de répondre explicitement aux questions suivantes :
- le framework peut-il garantir un focus unique stable ?
- les changements de focus peuvent-ils être annoncés fiablement ?
- un focus pointeur et un focus clavier coexistent-ils ou non ?
2.6 Règles de dégradation
Une application air-tui doit pouvoir :
- interroger ses capacités au démarrage ;
- réagir à leur variation si le backend le permet ;
- publier des dégradations contrôlées par composant ;
- refuser proprement un mode si une capacité indispensable est absente.
Une vue peut exiger une capacité forte, mais elle doit alors :
- le déclarer explicitement ;
- fournir un fallback ;
- ou échouer proprement avec diagnostic.
Le framework ne doit jamais transformer silencieusement une garantie absente en succédané trompeur.
2.7 API conceptuelle de Capabilities
L’API publique exacte reste à écrire, mais Capabilities doit offrir au
minimum les opérations conceptuelles suivantes.
Identification du profil
L’application doit pouvoir connaître le profil courant :
backend_profile() -> FullConsole | AirTerminal | CompatibleTerminal
Cette information sert :
- au diagnostic ;
- à la télémétrie locale éventuelle de debug ;
- aux choix de fallback de haut niveau ;
- aux tests de conformité inter-backends.
Elle ne doit pas être utilisée comme substitut aux capacités fines quand une question précise peut être posée directement.
Interrogation des familles de capacités
L’application doit pouvoir accéder à des sous-objets ou vues immuables sur :
keyboard()pointer()text()clipboard()focus()viewport()
Chaque sous-objet doit offrir des prédicats ou enums stables. Exemples :
keyboard.key_up()keyboard.left_right_modifiers()pointer.scroll()text.bidi()viewport.alternate_screen()
Le document ne fige pas la forme exacte (bool, enum, bitflags), mais fige la
nécessité d’une interrogation explicite, lisible et stable.
Vérification de prérequis
Le framework doit offrir une manière standard de vérifier qu’un ensemble de capacités minimales est satisfait.
Exemples conceptuels :
supports(requirements)missing(requirements)assert_supported(requirements)
Cette couche est importante pour :
- éviter la répétition de conditions ad hoc dans les apps ;
- produire des diagnostics homogènes ;
- documenter les prérequis d’une vue ou d’un widget.
Observation des changements
Quand le backend supporte des changements dynamiques, l’application doit pouvoir être notifiée.
Cas typiques :
- resize ;
- bascule clipboard disponible / indisponible ;
- variation du mode pointeur ;
- perte d’une capacité sur transport dégradé.
Le contrat minimal est :
- soit les capacités sont statiques pour la session ;
- soit les changements sont observables explicitement.
Un changement silencieux non observable est interdit pour une capacité exposée.
Exigences déclaratives des vues
Une vue ou un widget doit pouvoir déclarer :
- des capacités requises ;
- des capacités améliorant le rendu ou l’interaction ;
- un fallback quand les capacités requises ne sont pas remplies.
Le framework devra donc fournir un mécanisme conceptuel du type :
requires(...)prefers(...)fallback(...)
La syntaxe exacte reste ouverte, le contrat non.
Diagnostic standard
Le framework doit pouvoir produire un diagnostic lisible à partir de
Capabilities.
Ce diagnostic doit au minimum répondre à :
- quel backend suis-je en train d’utiliser ?
- quelles capacités majeures sont garanties ?
- quelles capacités manquent par rapport au mode premium ?
- pourquoi telle vue s’est-elle dégradée ?
Le diagnostic standard est un outil de debug, de support utilisateur, et de test.
3. Modèle de focus
3.1 Objectif
Le focus est un concept de premier rang dans air-tui. Il ne peut pas être un
effet de bord du rendu ni un simple index de widget actif.
Le modèle de focus sert cinq objectifs :
- navigation clavier déterministe ;
- accessibilité ;
- dispatch des commandes ;
- cohérence entre backends ;
- testabilité.
3.2 Invariants
Les invariants suivants sont normatifs.
- Une scène
air-tuipossède au plus un focus clavier primaire. - Le focus clavier primaire appartient soit à un élément focusable, soit à aucun élément.
- Un élément non focusable ne peut jamais devenir focus primaire.
- La perte du focus sur un élément doit générer une transition explicite.
- Le focus ne dépend jamais de coordonnées physiques publiques.
- L’ordre de tabulation doit être déterministe pour un arbre donné.
- Un conteneur peut déléguer le focus à un descendant, jamais le dupliquer.
3.3 Types conceptuels de focus
Le framework doit distinguer conceptuellement :
keyboard focus— destination des événements clavier et commandes ;pointer hover— élément survolé quand un pointeur existe ;active scope— sous-arbre actuellement actif pour les commandes ;modal focus— capture temporaire par dialogue, palette ou menu.
Ces concepts peuvent coexister, mais ne doivent pas être confondus.
3.4 Éléments focusables
Un élément focusable doit déclarer au minimum :
- s’il accepte le focus ;
- s’il accepte l’activation ;
- s’il participe à l’ordre de tabulation ;
- s’il expose un rôle d’accessibilité cohérent ;
- quelles commandes il consomme prioritairement.
Exemples d’éléments focusables :
- bouton ;
- champ de texte ;
- éditeur ;
- liste sélectionnable ;
- table navigable ;
- arbre navigable ;
- palette de commandes.
Exemples d’éléments non focusables par défaut :
- texte statique ;
- séparateur ;
- conteneur purement décoratif.
3.5 Navigation de focus
Le framework doit fournir un noyau de navigation indépendant du backend :
- focus suivant ;
- focus précédent ;
- focus premier ;
- focus dernier ;
- entrée dans un scope ;
- sortie d’un scope ;
- focus vers panneau voisin ;
- restauration du focus précédent lors de fermeture d’un modal.
Ces transitions deviennent ensuite des commandes sémantiques, jamais des conventions cachées.
3.6 Scopes de focus
Un focus scope est un sous-arbre déclaratif qui :
- définit une frontière de navigation ;
- peut mémoriser son dernier descendant actif ;
- peut piéger temporairement le focus ;
- peut se déclarer modal ou non modal.
Cas typiques :
- dialogue ;
- palette de commandes ;
- panneau latéral ;
- split view ;
- formulaire.
3.7 Politique clavier / souris
Le framework est keyboard-first.
Cela implique :
- un clic pointeur peut déplacer le focus si la capacité pointeur existe ;
- aucun élément critique ne doit être accessible uniquement à la souris ;
- toute action de souris importante doit avoir une équivalence clavier ;
- l’absence de pointeur ne doit pas casser l’interaction fondamentale.
3.8 API conceptuelle de FocusScope
FocusScope est la brique de structuration du focus. L’API publique exacte
reste ouverte, mais les responsabilités minimales sont fixées ici.
Création et rôle
Un scope doit pouvoir être déclaré comme :
- scope standard ;
- scope mémorisant son dernier focus descendant ;
- scope modal ;
- scope piégeant la navigation ;
- scope racine logique d’un panneau ou d’un dialogue.
Le scope n’est pas un simple conteneur visuel. C’est une frontière d’interaction.
Insertion dans l’arbre
Un scope doit pouvoir :
- contenir des descendants focusables ou d’autres scopes ;
- participer à la hiérarchie de dispatch ;
- annoncer s’il est actif, modal, ou dormant ;
- définir s’il veut une navigation séquentielle, directionnelle, ou les deux.
Opérations minimales
Le modèle conceptuel doit supporter au minimum :
contains(focus_target)is_active()is_modal()activate()deactivate()focus_first()focus_last()focus_next()focus_previous()restore_last_focus()clear_focus()
Les noms exacts restent ouverts ; les opérations, non.
Mémoire du focus
Un scope peut mémoriser son dernier descendant focalisé pour permettre :
- restauration à la réouverture d’un panneau ;
- retour cohérent après fermeture d’un modal ;
- navigation plus naturelle dans les vues fractionnées.
Cette mémoire doit être explicite, jamais implicite ou accidentelle.
Piège modal
Un scope modal doit pouvoir :
- capturer le focus primaire ;
- empêcher la fuite de navigation hors du scope ;
- relâcher le focus proprement à sa fermeture ;
- restaurer le focus précédent si possible.
Les dialogues et palettes de commandes utilisent ce comportement par défaut.
Politique de sortie
Un scope doit pouvoir préciser ce qu’il advient quand la navigation atteint sa frontière :
- boucle interne ;
- sortie vers le parent ;
- blocage explicite ;
- délégation à une politique de scène.
Cette politique doit être déterministe et testable.
Relation avec les commandes
Un scope doit pouvoir enregistrer ou résoudre des commandes locales :
- commandes de navigation internes ;
- commandes de confirmation/annulation ;
- commandes propres à un conteneur complexe.
Lors du dispatch, le scope actif constitue un niveau intermédiaire stable entre le widget focalisé et la scène.
4. Modèle d’événements bruts
4.1 Objectif
Le modèle d’événements bruts est la couche d’entrée commune à tous les
backends air-tui.
Il sert à :
- transporter les faits d’interaction observés ;
- alimenter la résolution des commandes sémantiques ;
- fournir une base de tests inter-backends ;
- exprimer honnêtement les dégradations des terminaux tiers.
Un événement brut n’est ni une commande, ni une intention applicative. C’est un fait d’entrée structuré.
4.2 Principes
Le modèle suit six principes :
- Un événement brut décrit ce qui s’est produit, pas ce que l’application doit faire.
- La sémantique d’un événement doit rester stable entre backends.
- Une information absente ne doit pas être inventée silencieusement.
- Les timestamps et métadonnées d’origine sont autorisés, mais secondaires par rapport au sens interactionnel.
- Les événements doivent être utilisables en replay de test.
- Le backend doit traduire son protocole local vers ce modèle, jamais exposer directement un flux ANSI ou propriétaire au framework.
4.3 Familles d’événements
Le noyau 1.0 doit couvrir au minimum les familles suivantes :
- clavier ;
- pointeur ;
- texte/paste ;
- focus ;
- viewport ;
- système d’interaction.
4.4 Événements clavier
Les événements clavier minimaux sont :
KeyDownKeyUpKeyRepeatModifiersChanged
Un événement clavier doit pouvoir exprimer au minimum :
- une identité de touche stable ;
- l’état des modifiers ;
- éventuellement le texte produit ;
- l’origine backend ;
- le caractère répété ou non.
Règles :
KeyRepeatne doit pas être confondu avec une succession deKeyDownsi le backend sait faire la distinction ;- en
CompatibleTerminal, l’absence deKeyUpdoit être déclarée parCapabilities, pas cachée ; - la résolution en commande n’a pas lieu à ce niveau.
4.5 Événements pointeur
Les événements pointeur minimaux sont :
MouseDownMouseUpMouseMoveScrollMouseEnterMouseLeave
Un événement pointeur doit pouvoir exprimer au minimum :
- position logique dans le viewport ;
- bouton ou direction pertinente ;
- état des modifiers ;
- granularité du scroll si connue.
Règles :
- un backend qui ne sait pas fournir
MouseEnter/MouseLeavene doit pas les simuler à l’aveugle ; - un scroll continu et un scroll discret peuvent partager la même famille d’événements tout en gardant une granularité distincte.
4.6 Événements texte et paste
Les événements minimaux sont :
PasteImePreeditsi supporté plus tardImeCommit
Règles :
Pastetransporte un payload textuel brut ;Pastene doit pas être réémis sous forme de succession artificielle deKeyDownsi le backend sait distinguer le collage ;ImeCommitdoit rester distinct de toute commande d’insertion tant qu’aucun binding n’a été résolu.
4.7 Événements de focus
Les événements minimaux sont :
FocusGainedFocusLostHoverGainedHoverLost
Règles :
FocusGained/FocusLostconcernent le focus clavier ;HoverGained/HoverLostconcernent le survol pointeur ;- le backend ne décide pas seul de la politique de focus globale ; il signale seulement les faits observables.
4.8 Événements de viewport
Les événements minimaux sont :
ResizeViewportExposedsi une exposition partielle existe
Resize doit exprimer au minimum :
- taille logique en cellules du viewport ;
- information de validité/cohérence ;
- éventuellement la cause si connue.
4.9 Événements système d’interaction
Le noyau 1.0 doit permettre au moins :
CapabilitiesChangedBackendSuspendedBackendResumed
Ces événements ne sont pas des commandes et ne doivent pas être dispatchés comme telles.
4.10 Type conceptuel RawEvent
La future API devra matérialiser un type conceptuel RawEvent couvrant
l’ensemble des familles ci-dessus.
Exigences minimales :
- somme typée ou équivalent ;
- inspectable ;
- sérialisable pour tests ;
- utilisable en replay ;
- non ambigu entre texte, clavier, focus, pointeur et système.
4.11 EventContext
La future API devra aussi matérialiser un contexte léger associé aux événements
bruts, distinct de CommandContext.
EventContext sert à fournir :
- horodatage éventuel ;
- backend d’origine ;
- viewport courant ;
- capabilities courantes ;
- métadonnées de tracing ou de replay.
Règle importante :
EventContextappartient à la couche de traitement des événements ;CommandContextappartient à la couche de dispatch des commandes ;- les deux peuvent partager des données, mais ne doivent pas être fusionnés sans nécessité.
4.12 Pipeline normative
La pipeline minimale devient :
RawEvent -> normalisation backend -> focus/hit-testing -> binding -> Command -> dispatch
Les étapes peuvent être implémentées différemment, mais aucune ne peut être supprimée conceptuellement sans réintroduire de l’ambiguïté.
4.13 Règles de tests
Le modèle d’événements doit être testable au minimum sur :
- distinction
KeyDown/KeyRepeat/KeyUpquand disponible ; - absence honnête de
KeyUpen terminal tiers ; - cohérence des événements
Resize; - distinction
Pastevs saisie clavier ; - conservation de l’ordre des événements en replay.
5. Commandes sémantiques
5.1 Objectif
Les commandes sémantiques forment le contrat d’action de air-tui.
Une commande n’est pas une touche. C’est une intention stable, rebindable, journalisable, testable, et dispatchable dans l’arbre de vues.
5.2 Pipeline
La pipeline normative est :
événement brut -> binding -> commande -> dispatch -> action
Le binding dépend :
- du backend ;
- du layout clavier ;
- du contexte de focus ;
- des préférences utilisateur ;
- du scope modal actif.
La commande produite doit être indépendante de ces détails.
5.3 Niveaux de commandes
Le framework doit distinguer au moins quatre niveaux :
- commandes d’application ;
- commandes de scène ;
- commandes de conteneur ;
- commandes de widget.
Commandes d’application
Exemples :
app.quitapp.helpapp.preferencesapp.refresh
Commandes de scène
Exemples :
scene.focus_nextscene.focus_previousscene.cancelscene.confirmscene.open_palette
Commandes de conteneur
Exemples :
pane.focus_leftpane.focus_rightsplit.resize_increasetabs.nexttabs.previous
Commandes de widget
Exemples :
list.move_nextlist.move_previouslist.page_downtree.expandtree.collapseeditor.insert_newlineeditor.delete_backwardeditor.save
5.4 Noyau minimal 1.0
Le noyau minimal de commandes garanties par le framework doit couvrir au moins les familles suivantes.
Navigation générale
scene.focus_nextscene.focus_previousscene.confirmscene.cancelscene.open_palette
Navigation directionnelle
nav.upnav.downnav.leftnav.rightnav.page_upnav.page_downnav.homenav.end
Édition de texte
text.inserttext.delete_backwardtext.delete_forwardtext.move_lefttext.move_righttext.move_word_lefttext.move_word_righttext.select_alltext.copytext.cuttext.paste
Listes / tables / arbres
collection.move_nextcollection.move_previouscollection.activatecollection.expandcollection.collapsecollection.select_toggle
Fenêtrage logique / panneaux
pane.focus_leftpane.focus_rightpane.focus_uppane.focus_downpane.close
5.5 Dispatch
Le dispatch suit l’ordre conceptuel suivant :
- widget focalisé ;
- scope de focus actif ;
- conteneurs parents ;
- scène ;
- application.
Une commande non gérée peut :
- être ignorée explicitement ;
- déclencher un feedback standard ;
- remonter jusqu’à l’application ;
- être réinterprétée par un fallback déclaré.
Le framework ne doit pas inventer de comportement caché non inspectable.
5.6 Binding par défaut
Les bindings concrets ne sont pas tous figés ici, mais les règles suivantes le sont :
- ils doivent rester cohérents avec les conventions TUI modernes ;
- ils doivent être remappables ;
- ils ne doivent pas dépendre exclusivement de touches indisponibles sur
CompatibleTerminal; - ils doivent avoir des alternatives quand une capacité manque.
Exemple :
- si
KeyUpn’existe pas, aucun binding critique ne doit en dépendre exclusivement ; - si la souris n’existe pas, aucune commande de navigation primaire ne doit lui être réservée.
6. Types publics conceptuels
Cette section introduit les types conceptuels minimaux que la future API
publique air-tui devra matérialiser. Les noms exacts et signatures exactes
restent ouverts, mais leur rôle ne l’est plus.
6.1 CapabilityRequirements
Rôle
CapabilityRequirements représente un ensemble déclaratif d’exigences qu’une
vue, un widget, ou une fonctionnalité veut imposer à son environnement
d’exécution.
Ce type sert à :
- documenter les prérequis d’un composant ;
- centraliser la logique de support/fallback ;
- produire des diagnostics homogènes ;
- éviter les branches ad hoc fondées directement sur le backend.
Contenu conceptuel minimal
CapabilityRequirements doit pouvoir exprimer :
- des capacités requises ;
- des capacités préférées ;
- des capacités explicitement non nécessaires ;
- éventuellement un niveau minimal de profil quand c’est justifié.
Le type ne doit pas être une simple liste de booléens. Il doit permettre une description structurée par famille :
- clavier ;
- pointeur ;
- texte ;
- clipboard ;
- focus ;
- viewport.
Opérations minimales
Le modèle conceptuel doit supporter au minimum :
empty()require(...)prefer(...)merge(...)is_satisfied_by(capabilities)missing_from(capabilities)
Résultat de vérification
La vérification d’un CapabilityRequirements ne doit pas se limiter à un
booléen quand un diagnostic plus riche est possible.
Le framework doit pouvoir produire au minimum :
- satisfait / non satisfait ;
- liste des capacités manquantes ;
- liste des capacités seulement dégradées ;
- raison normalisée du fallback.
Règles
- Un widget du noyau 1.0 ne doit pas exiger un backend nominal si une exigence plus fine suffit.
- Une exigence doit être formulée en termes de capacité, pas de marqueur d’implémentation.
- Les exigences doivent être composables entre vues parentes et enfants.
6.2 FocusTarget
Rôle
FocusTarget représente une cible focusable adressable par le système de
focus.
Il n’est pas nécessairement un widget visible autonome ; il peut aussi représenter :
- un sous-élément focusable d’un widget complexe ;
- un point d’entrée logique dans un scope ;
- une cible virtuelle restaurable.
Invariants
Un FocusTarget doit être :
- stable pour la durée de vie logique de son élément ;
- comparable pour déterminer l’égalité de cible ;
- utilisable dans les diagnostics et tests ;
- indépendant de coordonnées physiques publiques.
Propriétés conceptuelles minimales
Un FocusTarget doit pouvoir exposer ou permettre d’obtenir :
- son scope parent ;
- sa focusability actuelle ;
- son rôle d’interaction ;
- son ordre relatif de navigation ;
- son état activable ou non.
Opérations minimales
Le modèle conceptuel doit supporter au minimum :
is_focusable()scope()role()activation_kind()
Un FocusTarget n’a pas à muter directement le focus lui-même ; cette
responsabilité appartient au moteur de focus et aux scopes.
Cas des widgets complexes
Un widget complexe comme Table, Tree, Editor, ou CommandPalette peut
choisir entre deux stratégies :
- un focus target unique avec curseur interne ;
- plusieurs focus targets descendants explicites.
La stratégie choisie doit rester cohérente avec :
- l’accessibilité ;
- la testabilité ;
- la stabilité des commandes ;
- la simplicité de navigation.
6.3 Command
Rôle
Command représente une intention sémantique dispatchable.
Il ne représente ni :
- une touche ;
- un événement brut ;
- une callback liée à un widget particulier.
Structure conceptuelle minimale
Un Command doit pouvoir exprimer au minimum :
- une identité stable ;
- une catégorie ;
- éventuellement une payload typée ou contextualisée ;
- une origine ou cause logique si utile au debug.
Exemples :
- commande sans payload :
scene.cancel - commande paramétrée :
text.insert(character) - commande contextualisée :
collection.activate(current_selection)
Identité
L’identité d’une commande doit être :
- stable dans le temps ;
- sérialisable pour debug/tests ;
- utilisable dans les diagnostics, la palette de commandes et les bindings.
Le document ne fige pas encore si cette identité est :
- une enum fermée ;
- un identifiant hiérarchique string-like ;
- un couple catégorie + verbe ;
- ou une combinaison de ces approches.
Mais il fige qu’elle doit être stable et inspectable.
Opérations minimales
Le modèle conceptuel doit supporter au minimum :
id()category()payload()is_repeatable()is_text_input()
Ces opérations n’imposent pas la forme finale, seulement la sémantique attendue.
Règles
- Une commande du noyau 1.0 doit pouvoir être remappée.
- Une commande critique doit avoir un sens stable entre backends.
- Une commande ne doit pas dépendre d’un widget concret pour exister.
6.4 CommandContext
Rôle
CommandContext représente l’environnement sémantique dans lequel une commande
est évaluée et dispatchée.
Il sert à éviter que les widgets ou l’application aillent puiser directement des informations globales dispersées.
Contenu conceptuel minimal
Un CommandContext doit fournir au minimum l’accès à :
Capabilitiesactives ;- focus primaire courant ;
- scope actif ;
- informations de viewport utiles ;
- état modal éventuel ;
- métadonnées d’origine de la commande ;
- canal de feedback ou de rejet standard.
Opérations minimales
Le modèle conceptuel doit supporter au minimum :
capabilities()focused_target()active_scope()viewport()origin()reject(reason)emit_feedback(feedback)
Origine de commande
Le contexte doit distinguer conceptuellement l’origine d’une commande :
- clavier ;
- pointeur ;
- paste ;
- accessibilité ;
- automation/test ;
- commande programmatiquement injectée.
Cette distinction est utile pour :
- diagnostiquer des comportements ;
- adapter certains feedbacks ;
- tester la cohérence inter-backends.
Règles
CommandContextne doit pas devenir un objet fourre-tout applicatif.- Il transporte le strict nécessaire au dispatch et au feedback.
- Toute donnée métier applicative doit rester hors de ce type.
6.5 Relations entre les types
Les relations minimales suivantes sont normatives :
CapabilityRequirementss’évalue contreCapabilities.FocusTargetvit dans unFocusScope.Commandest dispatchée à partir d’unFocusTargetet d’unCommandContext.CommandContextexpose leFocusTargetfocalisé et lesCapabilitiesactives.
Schéma conceptuel :
Capabilities ----> CapabilityRequirements
|
v
CommandContext ---> FocusScope ---> FocusTarget
|
v
Command
6.6 Non-objectifs à ce stade
Cette section ne fige pas encore :
- les traits Rust exacts ;
- le niveau de généricité ;
- la sérialisation wire de ces types ;
- la séparation exacte entre types
air-viewcommuns et typesair-tuispécifiques.
Ces points viendront plus tard. Le but ici est de verrouiller la sémantique, pas la syntaxe API.
7. Relations avec les widgets 1.0
Le noyau de widgets défini par ADR-026 impose déjà quelques attentes.
Widgets devant obligatoirement participer au focus
InputEditorListTableTreeTabsDialogCommandPalette
Widgets pouvant être focusables selon configuration
Block/PanelStatusBarProgressquand interactif
Widgets non focusables par défaut
TextSpanChartpurement passif
8. Premier lot de widgets 1.0
Cette section précise le premier lot de widgets à considérer comme noyau
implémentable d’air-tui. Le but n’est pas d’épuiser le futur widget set, mais
de fixer un socle cohérent avec Capabilities, FocusScope et les commandes
sémantiques.
Pour chaque widget, quatre dimensions comptent :
- rôle principal ;
- contrat de focus ;
- commandes minimales ;
- dégradations acceptées.
8.1 Text
Rôle. Affichage de texte statique ou riche en spans stylés.
Focus. Non focusable par défaut.
Commandes minimales. Aucune commande d’interaction obligatoire.
Dégradations acceptées.
- réduction de richesse colorimétrique ;
- wrapping différent selon backend ;
- shaping/bidi réduit en
CompatibleTerminal.
Dégradations interdites en silence.
- texte tronqué sans signalement quand une vue exige explicitement l’intégrité ;
- largeur de cellule incohérente cassant la mise en page.
8.2 Block / Panel
Rôle. Conteneur logique et visuel.
Focus. Non focusable par défaut ; peut devenir focusable s’il représente un panneau navigable ou sélectionnable.
Commandes minimales.
- aucune en mode passif ;
scene.focus_next/scene.focus_previoussi focusable ;pane.focus_*quand utilisé comme panneau d’une vue fractionnée.
Dégradations acceptées.
- bordures simplifiées ;
- styles de box-drawing réduits ;
- titres plus sobres.
8.3 Input
Rôle. Champ texte mono-ligne.
Focus. Toujours focusable.
Commandes minimales.
scene.confirmscene.canceltext.inserttext.delete_backwardtext.delete_forwardtext.move_lefttext.move_righttext.move_word_lefttext.move_word_righttext.select_alltext.copytext.cuttext.paste
Dégradations acceptées.
- sélection visuelle plus simple ;
- clipboard limité ;
- IME absent sur backend dégradé.
Dégradations interdites en silence.
- perte d’édition clavier de base ;
- focus instable ;
- paste destructif ou ambigu sans signalement.
Machine d’état minimale.
idle: champ non focalisé.focused: champ focalisé, curseur visible.editing: insertion/suppression active.selecting: sélection active si supportée.disabled: visible mais non éditable.
Le passage focused -> editing peut être implicite dès la première entrée
texte. Le passage focused -> disabled n’est autorisé que sur changement
d’état explicite de l’application.
Modèle de focus.
- un
Inputcorrespond par défaut à un uniqueFocusTarget; - l’entrée dans le focus place le curseur selon la politique du composant : début, fin, ou dernière position mémorisée ;
- la sortie du focus doit figer proprement la sélection et le curseur ;
- un
Inputdans unDialogne doit pas pouvoir perdre le focus hors du scope modal sans commande explicite.
Sémantique de commandes.
text.insertajoute le contenu au point d’insertion courant ;text.delete_backwardettext.delete_forwardopèrent de manière déterministe sur sélection ou curseur ;scene.confirmvalide le champ si le contexte l’autorise, sinon remonte ;scene.cancelannule l’édition en cours seulement si le composant le supporte explicitement ; sinon la commande remonte.
Feedback minimal.
- focus visible ;
- curseur visible ;
- état désactivé perceptible ;
- erreur de validation perceptible mais non bloquante par défaut.
Pré-requis de capacités.
- requis :
keyboard,focus,viewport.resizefiable ; - préférés :
clipboard,text.bidi,ime.
8.4 Editor
Rôle. Édition texte multi-ligne.
Focus. Toujours focusable ; constitue généralement son propre scope d’interaction.
Commandes minimales.
- toutes celles de
Input; nav.upnav.downnav.page_upnav.page_downnav.homenav.endeditor.insert_newlineeditor.savesi l’éditeur expose cette action
Dégradations acceptées.
- rendu de sélection simplifié ;
- gouttière ou numéros de ligne absents ;
- shortcuts avancés indisponibles sur backend dégradé.
Dégradations interdites en silence.
- désynchronisation curseur / texte ;
- perte de navigation fondamentale ;
- modifications de texte non déterministes.
Machine d’état minimale.
idle: éditeur non focalisé ;focused: éditeur focalisé ;editing: insertion/suppression active ;selecting: sélection active ;navigating: déplacement sans mutation ;readonly: navigation autorisée, mutation interdite ;disabled: ni édition ni navigation active.
Un éditeur ne doit jamais mélanger silencieusement readonly et disabled.
Stratégie de focus.
Deux stratégies sont permises :
- un unique
FocusTargetavec curseur interne ; - plusieurs cibles de focus internes si l’éditeur expose des sous-zones distinctes.
Pour le noyau 1.0, la stratégie recommandée est le focus target unique avec curseur interne, plus simple et plus stable pour les commandes.
Sémantique de navigation.
nav.up/nav.downdéplacent le curseur verticalement ;nav.page_up/nav.page_downdéplacent le viewport logique ;nav.home/nav.endagissent au minimum sur la ligne courante ;- le maintien d’une colonne désirée lors des déplacements verticaux doit être déterministe.
Sémantique d’édition.
text.inserteteditor.insert_newlinedoivent produire des mutations ordonnées et reproductibles ;- les suppressions doivent avoir une sémantique définie sur sélection, caractère, ou unité logique future ;
editor.saven’est disponible que si le composant annonce explicitement cette action ; sinon la commande remonte.
Feedback minimal.
- curseur visible ;
- viewport cohérent autour du curseur ;
- état
readonlyperceptible ; - sélection perceptible quand elle est supportée ;
- erreur ou rejet de commande signalé via feedback standard.
Pré-requis de capacités.
- requis :
keyboard,focus,viewport.resize; - préférés :
clipboard,text.wide_cell_width,text.bidi,ime.
8.5 List
Rôle. Collection linéaire navigable.
Focus. Focusable si interactive.
Commandes minimales.
collection.move_nextcollection.move_previousnav.page_upnav.page_downnav.homenav.endcollection.activatecollection.select_togglesi multi-sélection
Dégradations acceptées.
- marqueurs de sélection simplifiés ;
- hover absent ;
- scroll moins fin.
Machine d’état minimale.
idle: liste non focalisée ;focused: liste focalisée ;navigating: déplacement de l’élément actif ;selecting: changement de sélection ;empty: liste vide ;disabled: liste visible mais non interactive.
Une liste peut être focused et empty ; elle doit alors rester navigable au
sens du focus global, sans action de collection disponible.
Modèle interne minimal.
Une List interactive doit distinguer explicitement :
- l’élément actif ;
- l’ensemble sélectionné si la multi-sélection existe ;
- l’ancre de scroll logique.
L’élément actif et la sélection ne doivent pas être confondus.
Stratégie de focus.
Pour le noyau 1.0, la stratégie recommandée est :
- un unique
FocusTargetpour la liste ; - un curseur/élément actif interne.
La stratégie “un focus target par ligne” est autorisée plus tard, mais alourdit fortement l’accessibilité, le dispatch et les tests.
Sémantique de commandes.
collection.move_next/collection.move_previousdéplacent l’élément actif sans ambiguïté ;collection.activateactive l’élément courant ;collection.select_togglemodifie la sélection sans casser l’élément actif ;nav.page_up/nav.page_downdéplacent au moins l’ancre visible.
Feedback minimal.
- élément actif perceptible ;
- sélection perceptible si présente ;
- état vide perceptible ;
- impossibilité d’activation signalée proprement.
Pré-requis de capacités.
- requis :
keyboard,focus; - préférés :
pointer,viewport.resize,pointer.scroll.
8.6 Table
Rôle. Données tabulaires navigables.
Focus. Focusable si interactive.
Commandes minimales.
- toutes les commandes de
List; nav.leftnav.right
Dégradations acceptées.
- colonnes tronquées ou repliées ;
- absence de resize interactif des colonnes ;
- styles de séparation simplifiés.
Dégradations interdites en silence.
- ambiguïté non signalée sur cellule active ;
- navigation incohérente ligne/colonne.
Machine d’état minimale.
idle: table non focalisée ;focused: table focalisée ;navigating_rows: déplacement vertical actif ;navigating_columns: déplacement horizontal actif ;selecting: changement de sélection si supporté ;empty: table vide ;disabled: table visible mais non interactive.
Modèle interne minimal.
Une Table interactive doit distinguer explicitement :
- la ligne active ;
- la colonne active ou cellule active selon le mode ;
- la sélection courante ;
- l’ancre de scroll verticale ;
- l’ancre de scroll horizontale.
Le modèle “ligne active” et le modèle “cellule active” sont tous deux permis, mais le composant doit en choisir un explicitement et le maintenir de façon cohérente.
Stratégie de focus.
Pour le noyau 1.0, la stratégie recommandée est :
- un unique
FocusTargetpour la table ; - une cellule ou ligne active gérée en interne.
Sémantique de commandes.
collection.move_next/collection.move_previousdéplacent au minimum la ligne active ;nav.left/nav.rightdéplacent la colonne ou cellule active ;collection.activateagit sur l’élément logique actuellement actif ;nav.page_up/nav.page_downdéplacent la fenêtre visible de façon déterministe.
Feedback minimal.
- ligne ou cellule active perceptible ;
- séparation visuelle suffisante des colonnes ;
- état vide perceptible ;
- ambiguïté de navigation signalée si le mode change.
Pré-requis de capacités.
- requis :
keyboard,focus,viewport.resize; - préférés :
pointer,pointer.scroll,text.wide_cell_width.
8.7 Tree
Rôle. Hiérarchie expandable/collapsible.
Focus. Focusable si interactive.
Commandes minimales.
collection.move_nextcollection.move_previouscollection.expandcollection.collapsecollection.activate
Dégradations acceptées.
- glyphes d’expansion simplifiés ;
- indentation plus sobre ;
- absence d’animation d’ouverture/fermeture.
Machine d’état minimale.
idle: arbre non focalisé ;focused: arbre focalisé ;navigating: déplacement du nœud actif ;expanding: ouverture logique d’un nœud ;collapsing: fermeture logique d’un nœud ;empty: arbre vide ;disabled: arbre visible mais non interactif.
Modèle interne minimal.
Un Tree interactive doit distinguer explicitement :
- le nœud actif ;
- l’état ouvert/fermé de chaque nœud concerné ;
- la profondeur logique ;
- l’ancre de scroll visible ;
- la sélection si elle existe.
Stratégie de focus.
Comme pour List, la stratégie recommandée est :
- un unique
FocusTargetpour l’arbre ; - un nœud actif interne.
Sémantique de commandes.
collection.move_next/collection.move_previousnaviguent dans l’ordre visible actuel ;collection.expandouvre le nœud actif s’il est ouvrable ;collection.collapseferme le nœud actif s’il est ouvert, ou remonte au parent selon la politique du composant ;collection.activateactive le nœud courant.
Feedback minimal.
- nœud actif perceptible ;
- état ouvert/fermé perceptible ;
- hiérarchie perceptible malgré les dégradations visuelles ;
- impossibilité d’expansion/collapse signalée proprement.
Pré-requis de capacités.
- requis :
keyboard,focus; - préférés :
pointer,pointer.scroll,viewport.resize.
8.8 ScrollView
Rôle. Fenêtre défilante sur un contenu plus grand que le viewport.
Focus. Peut être focusable ou déléguer le focus à son contenu.
Commandes minimales.
nav.upnav.downnav.page_upnav.page_downnav.homenav.end
Dégradations acceptées.
- scroll seulement par page ;
- indicateurs de scroll absents ;
- inertie inexistante.
Machine d’état minimale.
idle: non focalisée si le composant prend le focus ;focused: focalisée ;scrolling: déplacement du viewport logique ;delegating: laisse le focus ou les commandes au contenu ;disabled: visible mais non interactive.
Rôle de focus.
Deux modes sont normatifs :
focus owner: laScrollViewreçoit le focus et interprète les commandes de scroll ;focus delegator: le contenu reçoit le focus, laScrollViewne fait que clipper et translater.
Le composant doit déclarer explicitement quel mode il adopte.
Sémantique de commandes.
nav.up/nav.downdéplacent le viewport ou sont déléguées au contenu selon le mode ;nav.page_up/nav.page_downdéplacent au minimum d’un viewport logique ;nav.home/nav.enddéplacent à l’origine ou à l’extrémité logique.
Feedback minimal.
- position de scroll perceptible si possible ;
- début et fin de contenu détectables ;
- impossibilité de scroller davantage signalée proprement.
Pré-requis de capacités.
- requis :
keyboardsi focus owner ; - préférés :
pointer.scroll,viewport.resize.
8.9 Tabs
Rôle. Sélection entre plusieurs panneaux mutuellement exclusifs.
Focus. Focusable ; constitue souvent un scope de focus avec restauration du dernier focus par onglet.
Commandes minimales.
tabs.nexttabs.previousscene.confirm
Dégradations acceptées.
- apparence de tab simplifiée ;
- raccourcis directs aux onglets absents ;
- overflow transformé en liste ou palette.
Machine d’état minimale.
idle: onglets non focalisés ;focused: barrette ou sélecteur d’onglets focalisé ;switching: transition logique entre deux onglets ;disabled: visible mais non interactive.
Modèle interne minimal.
Un Tabs doit distinguer explicitement :
- l’onglet sélectionné ;
- l’onglet focalisé si distinct ;
- le dernier focus descendant mémorisé pour chaque panneau quand cette fonctionnalité est activée.
Stratégie de focus.
Le modèle recommandé est :
- un
FocusTargetpour la barrette d’onglets ; - un
FocusScopepour le panneau actif ; - restauration du dernier focus du panneau lors du retour sur un onglet.
Sémantique de commandes.
tabs.next/tabs.previouschangent l’onglet sélectionné ;scene.confirmpeut entrer dans le panneau actif si la barrette a le focus ;scene.focus_next/scene.focus_previouspeuvent alterner entre barrette et contenu selon la politique du composant.
Feedback minimal.
- onglet sélectionné perceptible ;
- onglet focalisé perceptible s’il diffère ;
- overflow perceptible et navigable.
Pré-requis de capacités.
- requis :
keyboard,focus; - préférés :
viewport.resize,pointer.
8.10 StatusBar
Rôle. Affichage d’état, de hints de commandes, ou de messages contextuels.
Focus. Non focusable par défaut ; peut devenir focusable si interactive.
Commandes minimales.
- aucune en mode passif ;
scene.confirmou commande dédiée si elle expose des actions.
8.11 Dialog
Rôle. Interaction modale courte : confirmation, alerte, petit formulaire.
Focus. Toujours encapsulé dans un FocusScope modal.
Commandes minimales.
scene.confirmscene.cancelscene.focus_nextscene.focus_previous
Dégradations acceptées.
- bordure ou ombrage simplifiés ;
- réduction du chrome visuel ;
- mise en page plus compacte.
Dégradations interdites en silence.
- fuite du focus hors du modal ;
- impossibilité de confirmer ou d’annuler au clavier.
Machine d’état minimale.
opening: apparition logique du dialogue ;active: dialogue modal actif ;submitting: validation en cours si l’action n’est pas immédiate ;closing: fermeture logique ;closed: hors scène active.
Même sans animation visuelle, ces états logiques doivent exister pour cadrer le focus et les commandes.
Scope modal.
Un Dialog crée par défaut un FocusScope modal qui :
- capture le focus primaire à l’ouverture ;
- mémorise le focus précédent ;
- piège
scene.focus_next/scene.focus_previousà l’intérieur ; - restaure le focus précédent à la fermeture si la cible existe encore.
Structure interne minimale.
Un Dialog 1.0 peut contenir au minimum :
- un titre optionnel ;
- un contenu textuel ou un petit arbre de widgets ;
- une zone d’actions explicites ;
- éventuellement un widget focalisable primaire (
Input,List, etc.).
Sémantique de commandes.
scene.confirmdéclenche l’action par défaut si elle existe ;scene.cancelferme le dialogue ou remonte un rejet explicite ;scene.focus_next/scene.focus_previousnaviguent dans le scope modal ;- les commandes non locales remontent seulement si la politique du dialogue le permet explicitement.
Feedback minimal.
- frontière modale perceptible ;
- action par défaut identifiable ;
- fermeture ou rejet perceptible ;
- erreur de validation ou d’envoi perceptible.
Pré-requis de capacités.
- requis :
keyboard,focus; - préférés :
pointer,viewport.resize.
8.12 CommandPalette
Rôle. Point d’entrée universel des commandes et de la navigation globale.
Focus. Scope modal dédié.
Commandes minimales.
scene.cancelscene.confirmcollection.move_nextcollection.move_previoustext.inserttext.delete_backward
Dégradations acceptées.
- matching plus simple ;
- absence de preview ;
- rendu de liste simplifié.
Dégradations interdites en silence.
- impossibilité d’invoquer la palette ;
- résultats activables uniquement à la souris.
Machine d’état minimale.
closed: hors scène active ;opening: entrée logique dans la modalité ;querying: saisie active de la requête ;browsing: navigation dans les résultats ;executing: exécution logique de la commande choisie ;closing: fermeture logique ;
Scope modal.
La CommandPalette crée un FocusScope modal par défaut, au même titre qu’un
Dialog, avec capture du focus primaire et restauration du focus précédent à
la fermeture.
Structure interne minimale.
La palette contient au minimum :
- un champ de requête ;
- une liste de résultats ;
- une sélection active ;
- un mécanisme d’activation de résultat.
Le champ et la liste peuvent être modélisés comme sous-composants internes, mais le widget doit rester cohérent comme unité d’interaction unique.
Sémantique de commandes.
text.insertettext.delete_backwardmodifient la requête ;collection.move_next/collection.move_previousdéplacent le résultat actif ;scene.confirmexécute le résultat actif ;scene.cancelferme la palette sans exécution.
Feedback minimal.
- requête visible ;
- résultat actif perceptible ;
- absence de résultat perceptible ;
- exécution ou rejet perceptible.
Pré-requis de capacités.
- requis :
keyboard,focus; - préférés :
viewport.resize,clipboard,pointer.
8.13 Progress
Rôle. Indication d’avancement déterminé ou indéterminé.
Focus. Non focusable par défaut ; focusable seulement si l’élément expose des actions.
Commandes minimales. Aucune en mode passif.
Dégradations acceptées.
- style de barre simplifié ;
- mode indéterminé réduit à indicateur textuel ;
- absence d’animation.
8.14 Chart
Rôle. Visualisation textuelle simple : barres, sparklines, jauges, petits indicateurs.
Focus. Non focusable par défaut ; peut devenir focusable si des points ou séries sont inspectables.
Commandes minimales.
- aucune en mode passif ;
nav.left/nav.right/scene.confirmsi inspectable.
Dégradations acceptées.
- résolution visuelle réduite ;
- couleurs simplifiées ;
- légende tronquée ou repliée.
9. Contrats transverses du lot 1.0
Tous les widgets 1.0 interactifs doivent respecter les règles suivantes.
9.1 Clavier intégral
Toute action critique doit être réalisable au clavier.
9.2 Focus visible
Un widget focusable doit avoir un état focalisé perceptible dans tous les profils supportés, même si la richesse visuelle varie.
9.3 Commandes inspectables
Les commandes consommées par un widget doivent pouvoir être documentées et, à terme, introspectées par le framework.
9.4 Dégradation explicable
Quand un widget se dégrade, le framework doit pouvoir expliquer :
- quelle capacité manque ;
- quelle variante a été choisie ;
- si cette variante reste pleinement fonctionnelle ou non.
10. Patterns composites 1.0
Les widgets de base ne suffisent pas à décrire une application air-tui
crédible. Le framework doit fournir ou au minimum normaliser quelques patterns
composites de haut niveau, récurrents dans air-base.
Un pattern composite :
- assemble plusieurs widgets du noyau ;
- impose une politique de focus cohérente ;
- expose un petit contrat de commandes ;
- définit ses dégradations acceptées.
10.1 Form
Rôle. Saisie structurée de plusieurs champs, sections et actions.
Composition minimale.
- texte d’aide ou labels ;
Inputet/ouEditor;- actions
confirm/cancel; - sections optionnelles.
Focus.
- scope dédié ;
- navigation séquentielle stable ;
- possibilité de mémoriser le dernier champ invalide ou focalisé.
Commandes minimales.
scene.focus_nextscene.focus_previousscene.confirmscene.cancel
Dégradations acceptées.
- layout plus compact ;
- aides inline déplacées en dessous ;
- indicateurs visuels de validation simplifiés.
Machine d’état minimale.
idle: formulaire présent mais non focalisé ;focused: un champ ou une action interne détient le focus ;editing: au moins un champ est en interaction active ;validating: validation locale ou globale en cours ;submitting: soumission logique en cours ;invalid: au moins une contrainte empêche la soumission ;disabled: formulaire visible mais non interactif.
Le formulaire peut être focused et invalid en même temps. L’état invalid
ne doit jamais être implicite : il doit pouvoir être dérivé d’erreurs de
validation explicites.
Modèle interne minimal.
Un Form doit distinguer explicitement :
- l’ordre de tabulation des champs ;
- les sections éventuelles ;
- le premier champ invalide ;
- les actions primaires et secondaires ;
- l’état sale (
dirty) si la vue choisit de l’exposer.
Focus et validation.
- le focus se déplace séquentiellement entre champs et actions ;
scene.confirmsur un champ n’implique pas automatiquement soumission globale si le composant ne l’annonce pas ;- à échec de validation, le focus doit pouvoir être déplacé de manière déterministe vers le premier champ invalide ou rester sur le champ courant selon la politique du formulaire ;
- un formulaire dans un
Dialoghérite du scope modal parent sans dupliquer sa logique.
Sémantique de commandes.
scene.focus_next/scene.focus_previousnaviguent entre champs et actions ;scene.confirmsoumet si le contexte l’autorise et si la validation passe ;scene.cancelannule ou remonte selon la politique du conteneur ;- les commandes widget-level (
text.*,editor.*) sont traitées par le champ focalisé avant toute logique de formulaire.
Feedback minimal.
- champ focalisé perceptible ;
- champs invalides perceptibles ;
- état de soumission ou de validation perceptible ;
- action primaire identifiable.
Pré-requis de capacités.
- requis :
keyboard,focus; - préférés :
viewport.resize,clipboard,ime,text.bidi.
10.2 NavigationSplit
Rôle. Navigation maître/détail en mode texte.
Composition minimale.
- un panneau latéral de navigation (
ListouTree) ; - un panneau de détail (
Text,Form,Editor, etc.) ; - éventuellement une barre de séparation logique.
Focus.
- deux scopes principaux ;
- commandes
pane.focus_left/pane.focus_rightnormatives ; - restauration du dernier focus dans chaque panneau recommandée.
Dégradations acceptées.
- collapse du panneau latéral ;
- passage temporaire en navigation empilée ;
- séparation visuelle simplifiée.
Machine d’état minimale.
idle: composite visible mais aucun panneau focalisé ;master_focused: panneau de navigation focalisé ;detail_focused: panneau de détail focalisé ;collapsed_master: panneau maître replié ;disabled: composite visible mais non interactif.
Modèle interne minimal.
Un NavigationSplit doit distinguer explicitement :
- le panneau maître ;
- le panneau détail ;
- le dernier focus interne de chaque panneau ;
- l’item de navigation actuellement sélectionné ;
- la politique de collapse du panneau maître.
Politique de focus.
pane.focus_leftetpane.focus_rightsont des commandes normatives ;- le changement de sélection dans le panneau maître peut remplacer le contenu du détail sans déplacer automatiquement le focus ;
- quand le focus revient dans un panneau, la restauration du dernier focus interne est recommandée ;
- en mode
collapsed_master, le panneau maître doit rester accessible via une commande explicite.
Sémantique de commandes.
- les commandes
collection.*dans le panneau maître s’appliquent à la vue de navigation ; - les commandes du panneau détail sont résolues localement tant que ce panneau garde le focus ;
scene.confirmdans le panneau maître peut soit activer l’élément, soit déplacer le focus au détail selon la politique du composant.
Feedback minimal.
- panneau focalisé perceptible ;
- item maître sélectionné perceptible ;
- état collapsed / expanded perceptible ;
- séparation logique entre navigation et détail perceptible même en dégradé.
Pré-requis de capacités.
- requis :
keyboard,focus; - préférés :
viewport.resize,pointer.
10.3 SettingsView
Rôle. Présentation ordonnée de préférences et réglages.
Composition minimale.
- groupes ou sections ;
Form;- aides contextuelles ;
- actions de validation si nécessaire.
Focus.
- scope principal ;
- navigation séquentielle et éventuellement par sections.
Commandes minimales.
- celles de
Form; - navigation directionnelle si panneau latéral de catégories.
Machine d’état minimale.
browsing_categories: catégorie active en navigation ;editing_section: section de réglages active ;validating: validation locale ou globale ;submitting: application ou enregistrement en cours.
Politique de focus.
- si une liste de catégories existe, elle forme un scope latéral analogue à
NavigationSplit; - sinon, la vue se comporte comme un
Formétendu.
Dégradations acceptées.
- catégories repliées en liste simple ;
- aide contextuelle déplacée en dessous ;
- validation différée moins riche visuellement.
10.4 HelpView
Rôle. Aide textuelle structurée, raccourcis, documentation in-app.
Composition minimale.
Textriche ;ScrollView;- éventuellement
TabsouListde rubriques.
Focus.
- peut être passive ou interactive ;
- si interactive, focus owner généralement au niveau
ScrollViewou de la liste de rubriques.
Dégradations acceptées.
- styles typographiques simplifiés ;
- table des matières repliée ;
- recherche absente en backend dégradé.
Machine d’état minimale.
reading: lecture du contenu principal ;navigating_index: navigation dans la table des matières si présente ;searching: recherche active si le composant la supporte.
Politique de focus.
- en version passive, aucun focus propre hors conteneur parent ;
- en version interactive, le focus alterne entre index, recherche et contenu scrollable selon la structure choisie.
Commandes minimales.
nav.upnav.downnav.page_upnav.page_downscene.focus_nextscene.focus_previous
10.5 CommandBrowser
Rôle. Variante persistante ou non modale de la CommandPalette pour
explorer les actions disponibles.
Composition minimale.
Inputde filtre optionnel ;ListouTablede commandes ;- panneau d’aide ou description optionnel.
Focus.
- un ou plusieurs scopes selon la mise en page ;
- la stratégie recommandée est un focus principal sur la liste avec filtre accessible par commande dédiée.
10.6 Règles communes aux composites
- un composite ne doit pas introduire de commandes cachées incohérentes avec le noyau ;
- un composite doit définir sa politique de focus en termes de
FocusScopeetFocusTarget; - un composite doit pouvoir expliquer ses dégradations via
Capabilities.
11. Architecture des crates air-tui
Cette section traduit les contrats précédents en découpage d’implémentation. Elle ne fige pas encore tous les noms exacts des modules internes, mais fixe les responsabilités de chaque crate et leurs relations de dépendance.
Le principe directeur est simple :
- séparer sémantique, rendu et transport ;
- ne pas laisser un backend dicter l’API publique ;
- garder les types de contrat au-dessus des détails de protocole ;
- permettre des tests croisés du même comportement sur plusieurs backends.
11.1 Vue d’ensemble
Le découpage cible est le suivant :
air-view
|
+--> air-tui
| |
| +--> air-tui-widgets
| +--> air-ui-render-tui
| +--> air-tui-runtime
|
+--> air-tui-backend-console
+--> air-tui-backend-terminal
+--> air-tui-backend-ansi
Ce schéma doit se lire ainsi :
air-viewporte le tronc déclaratif commun ;air-tuiexpose la façade framework console ;air-tui-widgetsporte le widget set TUI ;air-ui-render-tuiporte le renderer cellulaire ;air-tui-runtimeporte la boucle d’input, la normalisation d’événements, le binding et le dispatch ;- les crates
air-tui-backend-*adaptent chacune un environnement d’exécution concret.
11.2 air-tui
Rôle. Façade publique du framework console.
La crate air-tui doit réexporter :
- les widgets TUI publics ;
- les types de contrat publics (
Capabilities,CapabilityRequirements,Command,CommandContext,FocusScope,FocusTarget,RawEventselon la répartition retenue) ; - les points d’entrée de runtime nécessaires à une application ;
- le minimum de traits ou fonctions de bootstrap nécessaires à l’exécution.
air-tui ne doit pas :
- contenir directement un backend concret ;
- dépendre d’un terminal ANSI concret ;
- contenir de logique de protocole
air-console; - exposer des détails de transport comme surface publique principale.
11.3 air-tui-widgets
Rôle. Implémentation du widget set TUI standard.
Cette crate porte au minimum :
- les widgets du lot 1.0 ;
- les patterns composites 1.0 ;
- leurs machines d’état ;
- leur logique de focus locale ;
- leur traduction des
Commanden mutations d’état.
Elle dépend conceptuellement de :
air-viewpour la composition ;air-tui-runtimepour les contrats d’interaction ;air-ui-render-tuiseulement via interfaces stables, jamais via couplage au backend concret.
air-tui-widgets ne doit pas embarquer directement de logique spécifique à
air-console, au terminal Air, ou à l’ANSI.
11.4 air-ui-render-tui
Rôle. Renderer cellulaire.
Cette crate reçoit un arbre de vues ou une représentation normalisée et produit une scène cellulaire rendable par les backends.
Responsabilités minimales :
- layout cellulaire ;
- glyphes, attributs et styles ;
- clipping ;
- dirty regions ;
- scroll logique ;
- calcul de hit-testing logique si cette responsabilité n’est pas ailleurs.
Sortie conceptuelle :
- grille de cellules ;
- métadonnées de focus/hover si nécessaires au debug ;
- informations de diff pour éviter le repaint complet.
air-ui-render-tui ne doit pas :
- parser un flux terminal ;
- parler AirCom directement ;
- gérer un PTY ;
- décider de la politique de binding clavier.
11.5 air-tui-runtime
Rôle. Cœur interactionnel du framework console.
C’est la crate la plus structurante après air-tui lui-même. Elle doit porter
le pipeline suivant :
- réception des événements bruts ;
- normalisation backend ;
- mise à jour des
Capabilities; - focus et scopes ;
- hit-testing logique ;
- binding événement -> commande ;
- dispatch de commande ;
- feedback et rejet standard.
air-tui-runtime est le bon lieu pour matérialiser ou héberger conceptuellement
les types suivants, sous une forme ou une autre :
RawEventEventContextCommandCommandContextFocusScopeFocusTargetCapabilities
Cette crate ne doit pas dépendre d’un backend concret. Les backends doivent dépendre d’elle, ou implémenter les interfaces qu’elle expose.
11.6 air-tui-backend-console
Rôle. Adaptateur du mode plein bypass via air-console.
Responsabilités minimales :
- connexion au protocole privé d’
air-console; - réception des événements riches natifs ;
- émission de la grille cellulaire ou de son diff ;
- signalement fidèle des capacités
FullConsole.
Ce backend est la référence premium pour le mode texte Air.
Il ne doit pas contenir :
- la logique de widget ;
- la politique de focus globale ;
- des fallbacks ANSI.
11.7 air-tui-backend-terminal
Rôle. Adaptateur du terminal Air riche, hors air-console direct.
Responsabilités minimales :
- parler le protocole privé du terminal Air ;
- traduire les événements et capacités en profil
AirTerminal; - transmettre la sortie du renderer.
Ce backend doit fournir une sémantique applicative proche de
air-tui-backend-console, avec seule différence notable : la couche de
transport.
11.8 air-tui-backend-ansi
Rôle. Adaptateur du terminal tiers ou SSH.
Responsabilités minimales :
- parler ANSI/VT et Kitty keyboard protocol quand disponible ;
- détecter honnêtement les capacités réellement présentes ;
- produire des
RawEventdégradés mais cohérents ; - transmettre la sortie rendue sous une forme acceptable par le terminal tiers.
Ce backend est explicitement un backend de compatibilité, pas le centre de gravité du design.
11.9 Dépendances et direction du flux
Les dépendances conceptuelles doivent respecter au minimum les règles suivantes :
air-tuidépend deair-view,air-tui-runtime,air-tui-widgets,air-ui-render-tui;air-tui-widgetsdépend deair-viewetair-tui-runtime;air-ui-render-tuine dépend d’aucun backend concret ;- chaque
air-tui-backend-*dépend deair-tui-runtimeet consomme la sortie du renderer ; - aucun backend ne doit être requis pour compiler les types centraux du framework.
Flux conceptuel d’exécution :
backend -> RawEvent -> air-tui-runtime -> Command -> widgets/state
widgets/state -> air-ui-render-tui -> cell grid -> backend
11.10 Testabilité du découpage
Ce découpage est choisi aussi pour ses propriétés de test :
- tests unitaires de
air-tui-runtimesans backend réel ; - tests de widgets sans protocole terminal ;
- tests du renderer sur snapshots de grilles ;
- tests d’adaptateurs backend avec fixtures de flux d’entrée/sortie ;
- tests end-to-end identiques sur
console,terminal,ansi.
11.11 Points laissés ouverts
Restent ouverts à ce stade :
- l’existence ou non d’une crate
air-tui-typesdédiée ; - la séparation exacte entre runtime, focus et commandes en sous-crates ;
- la place exacte de certains types communs dans
air-viewvsair-tui-runtime; - la stratégie de réexport depuis la façade
air-tui.
12. Architecture interne de air-tui-runtime
Cette section descend d’un cran sous le niveau crate. Elle décrit les
sous-systèmes conceptuels de air-tui-runtime, leur ordre de responsabilité,
et les points où ils s’articulent avec les widgets, le renderer et les
backends.
Principe directeur :
- normaliser tôt ;
- décider tard ;
- ne jamais mélanger transport, focus, binding et rendu ;
- rendre chaque étape testable isolément.
12.1 Vue d’ensemble
Le runtime peut se lire comme une pipeline en anneau :
backend adapter
-> event ingress
-> normalizer
-> capability tracker
-> focus engine
-> hit-testing / target resolution
-> binding engine
-> command dispatcher
-> state mutation / feedback
-> invalidation scheduler
-> renderer bridge
-> backend adapter
Toutes ces étapes peuvent cohabiter dans une seule crate, mais elles doivent rester séparables conceptuellement et testables individuellement.
12.2 event ingress
Rôle. Point d’entrée des événements provenant d’un backend.
Responsabilités minimales :
- recevoir des événements source dans l’ordre ;
- préserver l’ordre logique ;
- associer un
EventContextminimal ; - rejeter les événements manifestement invalides ;
- alimenter la phase de normalisation.
event ingress ne doit pas :
- résoudre des commandes ;
- décider du focus global ;
- déclencher du rendu directement.
12.3 normalizer
Rôle. Convertir les événements backend-spécifiques en RawEvent
canoniques.
Responsabilités minimales :
- transformer les protocoles de backend en événements normés ;
- distinguer honnêtement
KeyDown,KeyRepeat,KeyUp,Paste, etc. ; - enrichir les événements avec les métadonnées stables disponibles ;
- éliminer les ambiguïtés de transport sans inventer d’information absente.
Le normalizer est le point où se matérialise le contrat “les backends ne
dictent pas l’API”.
12.4 capability tracker
Rôle. Maintenir l’état courant de Capabilities.
Responsabilités minimales :
- exposer le profil courant ;
- suivre les capacités stables et dynamiques ;
- émettre
CapabilitiesChangedsi nécessaire ; - fournir des snapshots cohérents au reste du runtime.
Le capability tracker doit être la source de vérité interne pour toute
question du type “peut-on faire X sur ce backend maintenant ?”.
12.5 focus engine
Rôle. Maintenir et faire évoluer l’état de focus.
Responsabilités minimales :
- conserver le focus primaire ;
- gérer les
FocusScope; - appliquer les politiques d’entrée/sortie de scope ;
- restaurer les focus mémorisés ;
- arbitrer les transitions
FocusGained/FocusLost.
Le focus engine ne doit pas être un effet secondaire du renderer. Il est un
sous-système autonome.
12.6 hit-testing / target resolution
Rôle. Résoudre les cibles logiques concernées par un événement ou une commande.
Responsabilités minimales :
- associer un événement pointeur à un
FocusTargetou sous-arbre logique ; - déterminer le widget focalisé ou survolé ;
- coopérer avec le renderer pour les positions logiques si nécessaire ;
- fournir au binding et au dispatch un contexte de cible stable.
Cette étape peut dépendre de métadonnées calculées au rendu, mais ne doit pas vivre dans le backend.
12.7 binding engine
Rôle. Transformer RawEvent + contexte en Command.
Responsabilités minimales :
- appliquer les bindings par défaut ;
- respecter les remappings utilisateur ;
- distinguer insertion de texte, navigation, activation, annulation ;
- tenir compte du scope modal actif ;
- produire zéro, une, ou plusieurs commandes si le modèle le permet.
Le binding engine ne doit pas muter directement l’état des widgets. Il produit
des intentions, pas des effets.
12.8 command dispatcher
Rôle. Acheminer une Command vers le bon niveau de l’arbre.
Responsabilités minimales :
- construire le
CommandContext; - envoyer d’abord la commande au widget focalisé ;
- remonter au scope actif, aux parents, à la scène, puis à l’application ;
- recueillir rejet, consommation, ou remapping ;
- déclencher le feedback standard quand rien ne gère la commande.
Le command dispatcher est le gardien de la cohérence de propagation.
12.9 state mutation / feedback
Rôle. Appliquer les mutations d’état résultant du dispatch et produire les signaux de retour.
Responsabilités minimales :
- muter les machines d’état des widgets concernés ;
- produire des rejets standardisés ;
- produire les feedbacks visuels ou sonores abstraits ;
- marquer les zones ou sous-arbres invalidés.
Le runtime ne doit pas imposer la logique métier d’application, mais il doit fournir le cadre pour que les mutations d’UI restent cohérentes.
12.10 invalidation scheduler
Rôle. Décider quoi rerendre, quand, et avec quel niveau de granularité.
Responsabilités minimales :
- agréger les invalidations ;
- distinguer invalidation locale et rerendu complet ;
- coopérer avec les dirty regions du renderer ;
- éviter les rerenders inutiles ;
- garantir qu’une mutation visible mène à un rendu cohérent.
L’invalidation est un sous-système important parce que le mode texte riche peut facilement dégénérer en rafraîchissements complets coûteux si elle n’est pas structurée.
12.11 renderer bridge
Rôle. Frontière entre runtime et air-ui-render-tui.
Responsabilités minimales :
- préparer l’arbre ou snapshot à rendre ;
- transmettre les contraintes de viewport ;
- récupérer la grille ou son diff ;
- transmettre au runtime les métadonnées utiles au hit-testing et au debug.
Cette frontière doit rester assez mince pour éviter le couplage fort entre runtime et renderer.
12.12 Boucle d’exécution
La boucle d’exécution conceptuelle d’air-tui-runtime est :
- recevoir un événement backend ;
- le normaliser en
RawEvent; - mettre à jour les capacités si nécessaire ;
- résoudre focus et cible ;
- produire une ou plusieurs commandes ;
- dispatcher les commandes ;
- appliquer mutations et feedback ;
- planifier l’invalidation ;
- demander un rendu si nécessaire ;
- pousser la sortie vers le backend.
Cette boucle peut être synchrone ou intégrée à un runtime async plus large, mais sa sémantique doit rester stable.
12.13 États internes minimaux
Le runtime doit maintenir au minimum :
- snapshot courant de
Capabilities; - focus primaire et scopes actifs ;
- état de hover si pertinent ;
- queue ou flux d’événements entrants ;
- invalidations en attente ;
- dernier snapshot rendu ou diff de référence si nécessaire ;
- configuration de bindings active.
12.14 Interfaces attendues entre sous-systèmes
Les interfaces conceptuelles minimales sont :
- backend ->
event ingress: émission d’événements source ordonnés ; normalizer->capability tracker: métadonnées de capacité observées ;focus engine<->hit-testing: cibles et transitions ;binding engine->command dispatcher:Command+ origine ;state mutation->invalidation scheduler: sous-arbres ou régions sales ;renderer bridge-> backend : grille ou diff rendus.
12.15 Testabilité interne
Chaque sous-système doit être testable séparément :
normalizeravec fixtures de backend ;capability trackeravec transitions de profil ;focus engineavec arbres et scopes synthétiques ;binding engineavec matrices d’événements et de remappings ;command dispatcheravec arbres de widgets factices ;invalidation scheduleravec scénarios de mutations locales.
12.16 Points laissés ouverts
Restent ouverts à ce stade :
- architecture mono-crate interne vs sous-modules très marqués ;
- éventuelle scission future en
air-tui-focus,air-tui-command, etc. ; - politique exacte de buffering des événements ;
- articulation précise avec le runtime async de couche 1/2 ;
- place exacte du hit-testing entre runtime et renderer.
13. Tests attendus
L’implémentation future doit être testée au minimum selon quatre axes.
Tests de capacités
- capacité correctement détectée par backend ;
- absence de fausse déclaration ;
- fallback correct d’une vue selon capacité manquante.
Tests de focus
- unicité du focus primaire ;
- ordre de tabulation déterministe ;
- restauration correcte après fermeture d’un modal ;
- navigation cohérente entre scopes.
Tests de commandes
- binding produit la bonne commande ;
- dispatch remonte correctement dans l’arbre ;
- commande critique toujours accessible dans les profils supportés.
Tests inter-backends
Une même app de test doit être exercée sur :
FullConsoleAirTerminalCompatibleTerminal
avec vérification que :
- le comportement critique reste cohérent ;
- seules les dégradations explicitement autorisées apparaissent.
14. Questions laissées ouvertes
Les points suivants restent à instruire plus tard sans remettre en cause cette spec :
- noms exacts des types publics ;
- sérialisation précise de l’arbre logique pour debug/tests ;
- stratégie IME détaillée en mode console ;
- politique de shaping/bidi exacte selon backend ;
- modèle de thèmes et de styles textuels ;
- widgets additionnels après le noyau 1.0.
Licence du document : MPL 2.0 Statut : Spécification de design pré-implémentation. Édition directe autorisée en phase de design pré-ouverture publique ; après ouverture publique, évolution via RFC si impact contractuel.