Xterm.js: Modules complémentaires personnalisés

Créé le 16 nov. 2017  ·  39Commentaires  ·  Source: xtermjs/xterm.js

Bonjour à tous!

J'essaie de créer un nouvel addon et la documentation ne semble pas très claire sur ce qu'il faut pour passer de 0 à 60.

Par exemple, 1) en quoi exactement l'API des modules complémentaires diffère-t-elle de l'API du terminal nominale ? Le fait-il du tout ? Le sera-t-il ?* De plus, 2) est-ce que la modification directe du prototype des terminaux est le moyen le plus approprié pour les modules complémentaires d'enregistrer des fonctionnalités ? Il semble que cela ne demande que des collisions. Existe-t-il un autre enregistrement d'espace de noms ou une installation de dic ? (Je suppose que peut-être même simplement en ajoutant l'objet addon, par exemple Terminal.MyAddon.method() pour la manière la plus simple, mais sûrement Terminal.addon('MyAddon').method() est beaucoup plus solide). 3) De plus, il ne semble pas clair comment ajouter un module complémentaire tiers, car les noms ont tous été codés en dur ... (j'ai commencé à étendre Terminal et à élargir loadAddon ( static loadAddon(String): void; )) Je fais des hypothèses, car la documentation ne dit pas si loadAddon est uniquement destiné à un usage privé (non tiers).

J'ai passé en revue certains des modules complémentaires existants, mais ils ne semblent pas avoir les implémentations les plus cohérentes.

Je crée quelques modules complémentaires (au moins un pour les codes d'échappement personnalisés) car je souhaite ajouter des fonctionnalités discrètes à xTerm.js. architectural de le faire au lieu de simplement créer une grande couche d'abstraction sur * au-dessus de xTerm.

Des détails

  • Version de tsc.exe : 2.6.1
  • xterm.js version : 3
areapi typenhancement

Commentaire le plus utile

Trouvez ci-dessous une proposition d'addons appropriés que j'ai rédigés en l'air ✈️ 😄. Tout commentaire serait grandement apprécié, car si vous faites des erreurs, il sera difficile de les changer.

/cc @xtermjs/core @jerch @vincentwoo @chabou @amejia1 @jluk

Meilleure proposition de modules complémentaires

Les modules complémentaires xterm.js sont des composants qui utilisent l'API xterm.js pour fournir des fonctionnalités supplémentaires. Ils suivent une structure particulière pour faciliter le développement d'un module complémentaire.

La différence entre l'écriture de fonctionnalités dans un module complémentaire et la simple utilisation de l'API est que le terminal est conscient des modules complémentaires et peut fournir des crochets/fonctionnalités supplémentaires que vous n'obtenez normalement pas lorsque vous programmez simplement avec l'API. Il est également facile de développer et de partager votre travail de manière cohérente avec la communauté.

Bien qu'il soit certainement possible d'écrire des addons en JavaScript, TypeScript est encouragé en raison des vérifications de type du compilateur supplémentaires et de la prise en charge de TS de première classe dans la bibliothèque principale.

Gestion des versions

Étant donné que xterm.js est un projet vivant et que des ruptures se produisent (sur les versions majeures), il est possible qu'un module complémentaire se brise.

Chargement des modules complémentaires

Au lieu de ce qui était fait précédemment, en enregistrant des addons dans la fonction statique Terminal.applyAddon , les addons sont maintenant passés à Terminal dans le constructeur en tant que 2ème argument facultatif :

interface ITerminalAddonConstructor<T> {
  // Used to make sure the same addon isn't instantiated twice
  readonly NAME: string;
  new(terminal: number): T;
}
class Terminal {
    constructor(
        options: ITerminalOptions,
        addons?: ITerminalAddonConstructor[]
    )
}

Cela permet à différents terminaux d'avoir un ensemble différent d'addons et fournit un moyen pratique de charger les addons pour chacun d'eux. Notez également que les typeof et ITerminalAddon sont fournis

import { Addon1 } from '...';
import { Addon2 } from '...';

const addons = [Addon1, Addon2];

const terminal = new Terminal({}, addons);

module npm xterm-base

Ce module contient des fichiers de déclaration qui définissent l'interface d'un addon. La seule raison pour laquelle ce module existe est que les addons n'ont pas besoin de dépendre du module xterm (et de créer une dépendance circulaire).

                 xterm.d.ts
                      ^
                      |
             -------------------
             ^        ^        ^
             |        |        |
           xterm  xterm-fit   ...

Cela peut être publié sur npm de la même manière que l'API vscode est publiée, la source de vérité reste dans le référentiel xterm.js (et est référencée directement par le référentiel xterm.js), mais est publiée en tant que module séparé pour les addons à consommer.

Interfaces

Les addons définissent un constructeur qui est déclenché pendant le constructeur de Terminal , d'autres hooks doivent être ajoutés à l'aide de Terminal.on .

interface ITerminalAddon {
    /**
     * The minimum version of xterm.js that this addon is compatible with, in the format
     * `[major, minor, patch]`. if this is higher than the version being used it will throw an
     * `Error` when the addon is loaded.
     */
    minimumVersion: [number, number, number];

    /**
     * The maximum version of xterm.js that this addon is compatible with, in the format
     * `[major, minor, patch]`. If this is defined and lower than the version being used it will
     * throw an `Error` when the addon is loaded.
     * TODO: Should we bother with this? Are people going to bother updating the addon to add this?
     */
    maximumVersion?: [number, number, number];

    // This can be a starting point, with more hooks added later
    constructor(terminal: ITerminal);

    // Terminal will call this when Terminal.dispose is called, this can clean up any
    // references/elements it needs to to shut down gracefully and eventually be
    // used to turn addons off without disposing the terminal
    dispose(): void;

    // We could add more hooks here if we want, or just let addons listen in on internals using
    // `Terminal.on` (which wouldn't be as nicely typed). See xtermjs/xterm.js#808
}

Pour réellement appeler des fonctions sur l'addon, vous devez acquérir l'instance d'addon d'un terminal. Cela peut être fait en tirant parti du Terminal.getAddon relativement complexe qui prend un constructeur d' addon et renvoie l'instance d'addon. Les éléments internes de getAddon devraient être faciles à implémenter :

interface ITerminalAddonConstructor<T> {
  new(terminal: number): T;
}
interface ITerminalAddon {
  a(): void;
}
class SearchAddon implements ITerminalAddon {
  findNext(): void {}
  a(): void {}
}
class FitAddon implements ITerminalAddon {
  fit(): void {}
  a(): void {}
}
class Terminal {
  getAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T {
    // Search internal addons list for that which matches addonConstructor
    return <T><any>1;
  }
}
const term = new Terminal();
const search = term.getAddon(SearchAddon);
search.findNext(); // Strongly typed simply by using the ctor
const fit = term.getAddon(FitAddon);
fit.fit(); // Strongly typed simply by using the ctor
term.getAddon({}); // error

Les addons "groupés" actuels

Les modules complémentaires fournis seront chacun déplacés vers les dépôts suivants et auront des modules npm publiés :

  • xtermjs/xterm-addon-attach
  • xtermjs/xterm-addon-fit
  • xtermjs/xterm-addon-fullscreen
  • xtermjs/xterm-addon-search
  • xtermjs/xterm-addon-terminado (arrêter en faveur de l'addon communautaire ?)
  • xtermjs/xterm-addon-web-links
  • xtermjs/xterm-addon-winpty-compat
  • xtermjs/xterm-addon-zmodem (arrêt au profit de l'addon communautaire ?)

Cela aura de nombreux avantages comme :

  • Réduisez le bruit dans le référentiel principal en consolidant les problèmes
  • Réduire les dépendances/la quantité de code dans le référentiel principal
  • Simplifiez le processus de construction - la démo dépendra toujours de certains mais ils n'auront pas besoin d'être construits

Les déplacer dans leurs propres dépôts devrait également nous encourager à ouvrir l'API afin que ces modules complémentaires de base n'exploitent plus l'API privée qui pourrait se briser. Ce serait également une bonne occasion d'ajouter une suite de tests d'API qui est un ensemble de tests qui n'utilisent que l'API publique.

Les pensées

  • Devrions-nous utiliser "engines": { "xterm": "..." } dans package.json ? Nous ne pouvons probablement pas sortir cela d'une manière agréable sans appliquer la façon dont les choses sont construites.
  • Nous pourrions répertorier les addons publiés sur npm en recherchant "xterm-addon-" et en recommandant cette convention de nommage ?
  • L'API de composant discutée dans #808 est vraiment plus une API de rendu qui peut avoir plus de réflexion après #791

Tous les 39 commentaires

quelque peu hors sujet : pour les codes d'échappement personnalisés, il n'y a, à mon humble avis, aucun moyen de se connecter à l'atm de l'analyseur par défaut. Vous pouvez toujours faire fonctionner ces codes personnalisés avec votre propre analyseur au préalable. La spécification ANSI permet des codes d'échappement personnalisés pour les commandes OSC et DCS. À moins que vous ne sachiez ce que vous faites, respectez-les pour rester compatible avec l'analyseur par défaut.

Bonjour @feamsr00; bons points.

  1. Les modules complémentaires doivent utiliser l'API publique de xterm.js uniquement pour fournir des fonctionnalités supplémentaires qui ne rentrent pas dans le noyau (par exemple, attacher sur un websocket)
  2. La modification directe du prototype n'est pas considérée comme une bonne pratique, mais cela a bien fonctionné jusqu'à présent. Il fournit une interface plus simple que l'utilisation d'un registre complémentaire. De plus, aucun des modules complémentaires du référentiel n'entre en conflit avec les autres, ce n'est donc pas un problème jusqu'à présent.
  3. En effet, il n'est pas documenté comment créer un module complémentaire, mais nous pouvons résoudre ce problème dans 3.0.
  4. ⚠️ En raison du #1018, nous abandonnerons loadAddon en faveur d'une nouvelle API plus propre. J'espère ouvrir un PR aujourd'hui

@jerch Merci pour la perspicacité. J'étais inquiet à ce sujet. Cela revient un peu à la question de l'API. Même sans API spéciale pour les modules complémentaires en soi, il n'est pas clair comment on pourrait simplement appeler un "Terminal.setParser()" (d'autant plus qu'aucune méthode de ce type ne semble exister) pour définir cette dépendance. De plus, il serait utile qu'il y ait un contrat d'interface pour Parser. Existe-t-il des analyseurs personnalisés dans la nature ou d'autres exemples sur la façon d'aller de l'avant ?
Puis-je suggérer une fonctionnalité pour au moins émettre des événements d'analyse/ctrl ?

@ feamsr00 actuellement, il n'y a pas de véritables addons externes, tout est intégré. La principale raison pour laquelle nous n'avons pas activement encouragé les addons de création de communauté pour le moment est tout ce qu'ils pourraient faire est d'appeler l'API standard dont vous ne pouvez pas vraiment faire des choses très intéressantes qui appelleraient un addon.

Un addon pourrait bien sûr faire appel à des membres privés, mais il se brisera probablement à l'avenir car nous ne nous engageons pas à maintenir la stabilité des API privées. Les modules complémentaires fournis avec le dépôt ont de meilleures chances d'être stables à un moment donné.

Il semble que cela ne demande que des collisions. Existe-t-il un autre enregistrement d'espace de noms ou une installation de dic ?

100%, je veux vraiment isoler les addons d'une manière meilleure et plus cohérente.

En effet, il n'est pas documenté comment créer un module complémentaire, mais nous pouvons résoudre ce problème dans 3.0.

Je préférerais que nous ne nous précipitions pas, mais que nous réfléchissions plutôt à https://github.com/xtermjs/xterm.js/issues/808 et en faisions le moyen standard de créer un module complémentaire. Nous sommes également à mi-chemin des discussions sur l'avenir de loadAddon , mieux vaut laisser cela cuire un moment pour voir s'il y a des bosses sur la route.

Existe-t-il des analyseurs personnalisés dans la nature ou d'autres exemples sur la façon d'aller de l'avant ?

L'échange des moteurs de rendu est quelque chose dont nous avons déjà parlé, pas les analyseurs, car l'analyseur est assez complexe. Je pense qu'il est prévu d'autoriser l'extension de l'analyseur avec https://github.com/xtermjs/xterm.js/issues/576


@ feamsr00 comme vous avez en fait des choses que vous voulez construire, vos commentaires seraient inestimables dans https://github.com/xtermjs/xterm.js/issues/808. L'espoir ici était de déplacer un tas de fonctionnalités intégrées pour utiliser cette API de composant agréable et modulaire, puis de la rendre disponible pour une utilisation externe dans des modules complémentaires.

Reportons cela afin que nous puissions sortir la v3

Et donc, la v3 est sortie maintenant (🎉). Quels sont les plans pour le système d'addon?

Pas de mise à jour, nous voulons toujours le faire finalement. Je tiens particulièrement à séparer les addons intégrés du référentiel principal.

Trouvez ci-dessous une proposition d'addons appropriés que j'ai rédigés en l'air ✈️ 😄. Tout commentaire serait grandement apprécié, car si vous faites des erreurs, il sera difficile de les changer.

/cc @xtermjs/core @jerch @vincentwoo @chabou @amejia1 @jluk

Meilleure proposition de modules complémentaires

Les modules complémentaires xterm.js sont des composants qui utilisent l'API xterm.js pour fournir des fonctionnalités supplémentaires. Ils suivent une structure particulière pour faciliter le développement d'un module complémentaire.

La différence entre l'écriture de fonctionnalités dans un module complémentaire et la simple utilisation de l'API est que le terminal est conscient des modules complémentaires et peut fournir des crochets/fonctionnalités supplémentaires que vous n'obtenez normalement pas lorsque vous programmez simplement avec l'API. Il est également facile de développer et de partager votre travail de manière cohérente avec la communauté.

Bien qu'il soit certainement possible d'écrire des addons en JavaScript, TypeScript est encouragé en raison des vérifications de type du compilateur supplémentaires et de la prise en charge de TS de première classe dans la bibliothèque principale.

Gestion des versions

Étant donné que xterm.js est un projet vivant et que des ruptures se produisent (sur les versions majeures), il est possible qu'un module complémentaire se brise.

Chargement des modules complémentaires

Au lieu de ce qui était fait précédemment, en enregistrant des addons dans la fonction statique Terminal.applyAddon , les addons sont maintenant passés à Terminal dans le constructeur en tant que 2ème argument facultatif :

interface ITerminalAddonConstructor<T> {
  // Used to make sure the same addon isn't instantiated twice
  readonly NAME: string;
  new(terminal: number): T;
}
class Terminal {
    constructor(
        options: ITerminalOptions,
        addons?: ITerminalAddonConstructor[]
    )
}

Cela permet à différents terminaux d'avoir un ensemble différent d'addons et fournit un moyen pratique de charger les addons pour chacun d'eux. Notez également que les typeof et ITerminalAddon sont fournis

import { Addon1 } from '...';
import { Addon2 } from '...';

const addons = [Addon1, Addon2];

const terminal = new Terminal({}, addons);

module npm xterm-base

Ce module contient des fichiers de déclaration qui définissent l'interface d'un addon. La seule raison pour laquelle ce module existe est que les addons n'ont pas besoin de dépendre du module xterm (et de créer une dépendance circulaire).

                 xterm.d.ts
                      ^
                      |
             -------------------
             ^        ^        ^
             |        |        |
           xterm  xterm-fit   ...

Cela peut être publié sur npm de la même manière que l'API vscode est publiée, la source de vérité reste dans le référentiel xterm.js (et est référencée directement par le référentiel xterm.js), mais est publiée en tant que module séparé pour les addons à consommer.

Interfaces

Les addons définissent un constructeur qui est déclenché pendant le constructeur de Terminal , d'autres hooks doivent être ajoutés à l'aide de Terminal.on .

interface ITerminalAddon {
    /**
     * The minimum version of xterm.js that this addon is compatible with, in the format
     * `[major, minor, patch]`. if this is higher than the version being used it will throw an
     * `Error` when the addon is loaded.
     */
    minimumVersion: [number, number, number];

    /**
     * The maximum version of xterm.js that this addon is compatible with, in the format
     * `[major, minor, patch]`. If this is defined and lower than the version being used it will
     * throw an `Error` when the addon is loaded.
     * TODO: Should we bother with this? Are people going to bother updating the addon to add this?
     */
    maximumVersion?: [number, number, number];

    // This can be a starting point, with more hooks added later
    constructor(terminal: ITerminal);

    // Terminal will call this when Terminal.dispose is called, this can clean up any
    // references/elements it needs to to shut down gracefully and eventually be
    // used to turn addons off without disposing the terminal
    dispose(): void;

    // We could add more hooks here if we want, or just let addons listen in on internals using
    // `Terminal.on` (which wouldn't be as nicely typed). See xtermjs/xterm.js#808
}

Pour réellement appeler des fonctions sur l'addon, vous devez acquérir l'instance d'addon d'un terminal. Cela peut être fait en tirant parti du Terminal.getAddon relativement complexe qui prend un constructeur d' addon et renvoie l'instance d'addon. Les éléments internes de getAddon devraient être faciles à implémenter :

interface ITerminalAddonConstructor<T> {
  new(terminal: number): T;
}
interface ITerminalAddon {
  a(): void;
}
class SearchAddon implements ITerminalAddon {
  findNext(): void {}
  a(): void {}
}
class FitAddon implements ITerminalAddon {
  fit(): void {}
  a(): void {}
}
class Terminal {
  getAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T {
    // Search internal addons list for that which matches addonConstructor
    return <T><any>1;
  }
}
const term = new Terminal();
const search = term.getAddon(SearchAddon);
search.findNext(); // Strongly typed simply by using the ctor
const fit = term.getAddon(FitAddon);
fit.fit(); // Strongly typed simply by using the ctor
term.getAddon({}); // error

Les addons "groupés" actuels

Les modules complémentaires fournis seront chacun déplacés vers les dépôts suivants et auront des modules npm publiés :

  • xtermjs/xterm-addon-attach
  • xtermjs/xterm-addon-fit
  • xtermjs/xterm-addon-fullscreen
  • xtermjs/xterm-addon-search
  • xtermjs/xterm-addon-terminado (arrêter en faveur de l'addon communautaire ?)
  • xtermjs/xterm-addon-web-links
  • xtermjs/xterm-addon-winpty-compat
  • xtermjs/xterm-addon-zmodem (arrêt au profit de l'addon communautaire ?)

Cela aura de nombreux avantages comme :

  • Réduisez le bruit dans le référentiel principal en consolidant les problèmes
  • Réduire les dépendances/la quantité de code dans le référentiel principal
  • Simplifiez le processus de construction - la démo dépendra toujours de certains mais ils n'auront pas besoin d'être construits

Les déplacer dans leurs propres dépôts devrait également nous encourager à ouvrir l'API afin que ces modules complémentaires de base n'exploitent plus l'API privée qui pourrait se briser. Ce serait également une bonne occasion d'ajouter une suite de tests d'API qui est un ensemble de tests qui n'utilisent que l'API publique.

Les pensées

  • Devrions-nous utiliser "engines": { "xterm": "..." } dans package.json ? Nous ne pouvons probablement pas sortir cela d'une manière agréable sans appliquer la façon dont les choses sont construites.
  • Nous pourrions répertorier les addons publiés sur npm en recherchant "xterm-addon-" et en recommandant cette convention de nommage ?
  • L'API de composant discutée dans #808 est vraiment plus une API de rendu qui peut avoir plus de réflexion après #791

Nous avons discuté avec créer des dépendances entre les addons en déclarant les noms de dépendances sur ITerminalAddon / ITerminalAddonConstructor qui seraient utilisés pour bloquer l'activation jusqu'à ce que les deps soient atteints. Une dépendance d'homologue peut être utilisée pour s'assurer que la dépendance est installée.

Les vérifications de version min et/ou max sont redondantes lors de l'utilisation de l'option de dépendance entre pairs package.json ? Je préférerais laisser npm gérer les plages de semver, c'est- 3.4.0 - 3.5.0 dire { min: [3,4,0], max: [3,5,0] } à la classe de mon module complémentaire. Faire de la gestion des dépendances semble être en contradiction avec la philosophie unix consistant à _faire une chose et la faire bien_.

Comment désactiveriez-vous un module complémentaire ?

Les vérifications de version min et/ou max sont redondantes lors de l'utilisation de l'option de dépendance entre pairs package.json

D'autres discussions avec @mofux sont arrivées à la même conclusion 😉

Comment désactiveriez-vous un module complémentaire ?

Vous ne pouvez pas le faire maintenant, mais si nous voulions soutenir cela, cela ressemblerait probablement à ceci :

class Terminal {
  disposeAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>);
}

interface ITerminalAddon {
  dispose?(): void;
}

Si je voulais développer un module complémentaire avec une dépendance facultative sur d'autres modules complémentaires, ce serait bien de pouvoir appeler getAddon('someOptionalAddon') pour obtenir une instance de module complémentaire. Dans cet esprit, je préférerais ce qui suit pour l'enregistrement.

class FitAddon implements ITerminalAddon {
  name: 'fit'
  fit(): void
}
xterm.useAddon(FitAddon, function optionalCallback(fit) {
  fit.fit();
});

// And maybe allow an optional name argument
xterm.useAddon('myfit', FitAddon); // Using the name 'fit' would throw here

const fit = xterm.getAddon('myfit');

Un getAddon surchargé fonctionnerait tout aussi bien.

@pro-src voici comment j'imagine le fonctionnement des interdépendances :

// fit.ts
import { ITerminal, ITerminalAddon } from 'xterm-base';
class FitAddon implements ITerminalAddon {
    // Still not 100% sure if we need this
    name: 'fit'
    constructor(private _terminal: ITerminal) {
    }
    fit(): void {
        // Do stuff with this._terminal
    }
}

// myFit.ts
import { ITerminal, ITerminalAddon } from 'xterm-base';
import { FitAddon } from 'xterm-addon-fit';
class MyFitAddon implements ITerminalAddon {
    name: 'myFit'
    // Will peerDependencies just work here and allow access to them?
    dependencies: [ FitAddon ]
    constructor(private _terminal: ITerminal) {
    }
    myFit(): void {
        const fit = this._terminal.getAddon(FitAddon)
    }
}

// app.ts
import { ITerminal, ITerminalAddon } from 'xterm-base';
import { FitAddon } from 'xterm-addon-fit';
import { MyFitAddon } from 'xterm-addon-myfit';
// FitAddon will initialize first, MyFitAddon will follow since it's
// dependencies are met
const term = new Terminal({}, [MyFitAddon, FitAddon]);
term.getAddon(FitAddon).fit();
term.getAddon(MyFitAddon).myFit();

LGTM :+1 :

Juste quelques questions qui me viennent à l'esprit :

  • L'interface ressemble beaucoup à la composition. Depuis que Typescript a introduit des fonctionnalités intéressantes de POO, pouvons-nous également utiliser l'héritage ? Un class XYAddon extends ZAddon ... s'enregistrerait-il toujours correctement ? Si oui sous quel nom ?
  • Un addon est-il extensible en place, changeant ainsi disons fitAddon et ne rompant pas avec les dépendances des autres ? Je ne sais pas si nous en avons vraiment besoin, j'ai vu des systèmes de plugins qui permettent des "injections" tout en gardant tout le reste intact.

@Tyriar

c'est ainsi que j'imagine le fonctionnement des interdépendances : ...

cool, cela semble également fonctionner pour les services optionnels.

const deps = [FitAddon];
Try { deps.push(require('optionalDep')); } catch(e) {}

Une classe XYAddon étendrait-elle ZAddon... s'enregistrerait-elle toujours correctement ? Si oui sous quel nom ?

Oui, je ne vois pas pourquoi cela ne fonctionnerait pas, je pensais que l'idée de la propriété name était nécessaire pour être utilisée comme identifiant afin que le nom soit remplacé par XYAddon .

Un addon est-il extensible en place, changeant ainsi, disons fitAddon et ne rompant pas avec les dépendances des autres ? Je ne sais pas si nous en avons vraiment besoin, j'ai vu des systèmes de plugins qui permettent des "injections" tout en gardant tout le reste intact.

Pas tout à fait clair pour moi ce que vous voulez dire ici?

Vraiment mieux pour Hyper de spécifier des add-ons pour une instance :+1 :

Pouvons-nous imaginer avoir une fonction addAddon(MyFitAddon) ou mieux une setAddons([MyFitAddon, FitAddon]) pour changer les add-ons à l'exécution ? Ce serait génial de les définir comme des options. Nous voulons vraiment nous assurer que nos plugins hyper sont chargeables à chaud.

Dans notre cas, lorsqu'un plugin est ajouté/supprimé, nous demanderons à chaque plugin son tableau de modules complémentaires de xterm , les fusionnerons et le passerons aux instances existantes de xterm (et futures nouvelles ceux).

La proposition de @Tyriar (https://github.com/xtermjs/xterm.js/issues/1128#issuecomment-394142177) me couvre complètement.

En outre, le report des vérifications de compatibilité pour xterm.js ou d'autres modules complémentaires à peerDependencies est idéal.

Enfin, mon opinion sur la désactivation des modules complémentaires et le chargement/déchargement à chaud devrait être laissée à une étape ultérieure, afin que nous puissions d'abord nous concentrer sur les bases.

Mettre à jour le système d'add-ons comme

@Tyriar
Ma deuxième question est également couverte par la chose de propriété de nom.

Concernant les addons interdépendants - comment les dépendances sont-elles résolues ? Avons-nous un problème de type MRO ou de cyclisme ici ? Je ne pense pas que les gens vont commencer à créer des tonnes d'addons dépendants, donc ce n'est peut-être qu'un problème académique lol.

Pouvons-nous imaginer avoir une fonction addAddon(MyFitAddon) ou mieux un setAddons([MyFitAddon, FitAddon]) pour changer les add-ons au moment de l'exécution ? Ce serait génial de les définir comme des options. Nous voulons vraiment nous assurer que nos hyper plugins sont (non)chargeables à chaud.

@chabou Je n'ai pas beaucoup envisagé la suppression/élimination des addons dans ces exemples mais cela pourrait être ajouté après un MVP, ce qui conduirait alors naturellement à une méthode pour pouvoir supprimer/ajouter à un terminal existant. Si nous voulons pouvoir ajouter un addon après new Terminal , cela ajoutera une certaine complexité à laquelle nous devrons faire face car l'événement open peut avoir été déclenché, ce qui, j'imagine, sera critique pour beaucoup addons.

Cela peut être aussi simple que des propriétés booléennes Terminal.isAttached ou Terminal.state .

Concernant les addons interdépendants - comment les dépendances sont-elles résolues ? Avons-nous un problème de type MRO ou de cyclisme ici ? Je ne pense pas que les gens vont commencer à créer des tonnes d'addons dépendants, donc ce n'est peut-être qu'un problème académique lol.

@jerch je pensais au "temps d'activation" faire ceci:

  Iterate over addons, activating any with dependencies matched
while (an addon was activated)

Je pense que cet algorithme simple devrait couvrir toutes les bases, les addons dépendants circulaires ne s'activeraient jamais mais c'est souhaité

J'ai ajouté une méthode dispose à la proposition ci-dessus :

interface ITerminalAddon {
    // Terminal will call this when Terminal.dispose is called, this can clean up any
    // references/elements it needs to to shut down gracefully and eventually be
    // used to turn addons off without disposing the terminal
    dispose(): void;
}

J'ai réalisé que nous en avions besoin après avoir vu attach tenir aux ressources après avoir appelé term.dispose() dans la démo.

Une autre idée:

Décourager les fuites de mémoire de dépendance circulaire

Considérez qu'un ITerminalAddonApi est passé dans l'addon au lieu de ITerminal :

class FitAddon implements ITerminalAddon {
    name: 'fit'
    constructor(private _api: ITerminalAddonApi) {
    }
    fit(): void {
        this._api.terminal.doStuff();
    }
}

L'implémentation réelle de l'API pourrait ressembler à ceci :

interface ITerminalAddonApi {
    readonly terminal: ITerminal
}

class TerminalAddonApi implements ITerminalAddonApi {
    constructor(private _terminal: ITerminal) {
    }
    public get terminal(): ITerminal {
        return this._terminal;
    }
    public dispose(): void {
        this._terminal = null;
    }
}

Cela découragera l'addon de conserver les références de ITerminal , leur permettant d'être libérées lorsque le terminal est supprimé (car Terminal.dispose déclenchera TerminalAddonApi.dispose ).

J'ai renommé le référentiel https://github.com/xtermjs/xterm-addon-ligatures pour l'aligner sur le schéma de nommage mentionné ici

Désolé d'être un peu en retard dans le jeu :smile: La proposition de @Tyriar dans l'ensemble est un grand pas en avant pour rendre xterm.js plus facilement extensible de manière structurée.

Quelques réflexions/considérations au fur et à mesure que j'ai lu la proposition et la discussion (sans ordre particulier). Aucun de ceux-ci n'est un gros problème ou un dealbreaker pour moi, mais je voulais faire connaître mes réflexions et mes impressions :

  1. Le package xterm-base me semble inutile. En prenant des systèmes tels que grunt/gulp comme exemple, il est déjà courant que les plugins prennent une peerDependency sur le package principal qu'ils étendent. Ceci est utile car il sert de protection contre le chargement d'addons pour une version de xterm qui ne fonctionnera pas avec eux sans introduire réellement une autre copie du package dans l'arborescence des dépendances. Lorsque j'ai besoin de dépendre des types d'un package répertorié comme dépendance de pair (ou que je souhaite tester avec lui), j'inclurai généralement également une version compatible de ce package en tant que devDependency afin qu'une copie soit disponible pour le développement. Cela permet d'importer les types lors du développement et de l'exécution sans l'introduction d'un nouveau package.

    Les deux principaux inconvénients d'avoir un package xterm-base dont tout le monde dépend sont (1) qu'il force une chaîne de publication en deux étapes chaque fois que les types changent et (2) qu'il perd les garanties d'alignement de version qu'un peerDependency fournit (puisque un addon peut dépendre de xterm-base@4 alors que la version de xterm utilisée s'aligne sur xterm-base@5 ).

  2. Les maximumVersion et minimumVersion sont redondants avec l'utilisation d'un peerDependency et nécessitent une maintenance supplémentaire. Un peerDependency seul devrait être suffisant ici. Cela semble avoir déjà été convenu mais pas encore mis à jour dans la proposition, alors j'ai pensé que je le mentionnerais également.

  3. Concernant les références circulaires entre les addons et le terminal, la tenue de références circulaires est-elle vraiment un problème ici ? Tant que les minuteurs/auditeurs/etc. sont arrêtés dans le cadre de l'élimination, le ramasse-miettes doit être capable de trouver et de nettoyer le groupe isolé de références circulaires. Même si la référence circulaire est un problème, ne suffit-il pas de simplement supprimer les références aux addons côté terminal après qu'ils aient appelé leur dispose() ? Ensuite, il y a toujours une référence sur le terminal, mais l'addon qui y fait référence n'a pas de références lui-même, donc la chaîne est nettoyée.

  4. Je n'aime pas imposer que les addons soient des classes ajoutées / référencées par la classe elle-même, mais cela semble être le meilleur / le seul moyen d'obtenir que Typescript renvoie automatiquement les bonnes informations de type lors de l'utilisation de quelque chose basé sur des chaînes sans passer des addons en tant qu'objet au lieu d'un tableau (qui lui-même pose problème si/quand l'enregistrement dynamique est possible). Cela dit, je ne pense pas que le champ de nom fournisse une valeur avec la conception actuelle, car les noms ne sont pas utilisés pour accéder à l'addon et seraient susceptibles de conflits de nom (surtout s'ils ne sont pas utilisés comme identifiant principal pour l'addon). Je voterais pour les supprimer pour le moment.

  5. La question des dépendances entre les addons et l'ordre de chargement est intéressante. La spécification des dépendances d'un addon est trompeuse car elle ne dispense pas l'utilisateur d'avoir à installer le package dépendant (les dépendances entre addons devraient être des dépendances entre pairs pour éviter les problèmes de duplication).

    À ce stade, je pense qu'il est logique de simplement demander à l'utilisateur de spécifier tous les modules complémentaires qu'il souhaite charger et l'ordre dans lequel il souhaite qu'il se charge (via l'ordre dans le tableau). Cela rend le comportement beaucoup plus facile à comprendre de l'extérieur et donne aux utilisateurs une flexibilité maximale. Cela simplifie également les choses lorsque les gens veulent décharger à chaud des addons, car la proposition actuelle semble exiger que xterm interdise ou ignore silencieusement la suppression d'un addon qui a encore des dépendances dans la liste des addons. Il est plus facile pour les utilisateurs de se tirer une balle dans le pied mais aussi plus facile de s'en sortir. Cette approche est généralement la façon dont je vois généralement les choses dans les systèmes de plug-in shell, par exemple.

    Dans ce cas, les addons pourraient toujours potentiellement spécifier leurs dépendances dans le cadre de leur interface (pour des capacités futures potentielles), mais avec cette approche, il n'y a pas de problème avec les dépendances circulaires entre les addons provoquant une boucle infinie ou des addons supprimés en silence car xterm ne serait plus besoin de construire un arbre de dépendance à partir d'un graphe. Si un module complémentaire ne peut pas fonctionner parce que ses dépendances ont été chargées dans le désordre, il devrait de toute façon échouer bruyamment.

@princjef merci beaucoup de vos retours, très précieux !

  1. Bons points, sonne comme la direction que nous devrions aller
  2. :+1:
  3. Bon point, je pense que c'est moi qui suis trop paranoïaque à propos des références circulaires après avoir traité https://github.com/xtermjs/xterm.js/pull/1525 , si c'est toujours un problème, nous pourrions le faire plus tard.
  4. Je ne pense pas que nous ayons besoin de nom si nous utilisons la méthode ITerminalAddonConstructor<T>
  5. Échouer bruyamment (console.warn/error/throw) lors de l'enregistrement/de l'élimination des modules complémentaires est probablement une bonne idée. Vous devriez en principe toujours le faire dans le bon ordre ou cela pourrait conduire à des bugs

J'ai mis en place une branche qui implémente la proposition et convertit les liens Web et les attache au nouveau format, vous pouvez la voir ici (la démo fonctionne ! 😮): https://github.com/xtermjs/xterm.js/compare /master...Tyriar :1128_addons?expand=1

Voici les modules :

Voici l'API :

  class Terminal {
    loadAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T;
    disposeAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): void;
    getAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T;
  }

  export interface ITerminalAddonConstructor<T extends ITerminalAddon> {
    new(terminal: Terminal): T;
  }

  export interface ITerminalAddon {
    /**
     * This function includes anything that needs to happen to clean up when
     * the addon is being disposed.
     */
    dispose(): void;
  }

Vous remarquerez peut-être l'API Terminal.disposeAddon , je pense que l'une des meilleures choses à faire est de gérer un cycle de vie des modules complémentaires afin qu'ils puissent être échangés sans recréer le Terminal . Une idée que j'avais était de déplacer le moteur de rendu WebGL vers un module complémentaire, principalement comme moyen de le tester d'une manière à très faible risque (opt-in et patcher les API privées), et potentiellement le garder sous cette forme car il y a de gros avantages en ne chargeant pas le moteur de rendu WebGL si vous ne l'utilisez pas.

Dites moi ce que vous en pensez

J'aime l'API en général 👍

(1) Je me demande - comment passerais-je une configuration à un module complémentaire ? Autant que je sache, le seul moyen est d'utiliser un appel séparé à l'instance de l'addon pour une méthode personnalisée comme init :

const myAddon = term.loadAddon(MyAddon);
myAddon.init({ /*...config*/ })

IMO passant une configuration à un plugin a besoin d'une meilleure histoire que celle-ci. Peut-être en ajoutant une configuration facultative en tant que deuxième paramètre à la méthode term.loadAddon , qui la transmet ensuite au constructeur de l'addon ?

// addon definition
class MyAddon implements ITerminalAddon {
  constructor(terminal, config) {
  }
  dispose() {
  }
}

// register addon
const myAddon = term.loadAddon(MyAddon, { /*...config*/ });

qui pour l'addon attach pourrait ressembler à ceci:

term.loadAddon(AttachAddon, { socket: mySocket });

et vous avez terminé.

(2) Serait-il judicieux d'ajouter un hook de cycle de vie à l'ouverture du terminal ( term.open ) ? Je pense que les addons qui ont besoin d'accéder aux dimensions de l'écran ou au DOM bénéficieraient de ce crochet.

@mofux

(2) Serait-il judicieux d'ajouter un hook de cycle de vie à l'ouverture du terminal (term.open) ? Je pense que les addons qui ont besoin d'accéder aux dimensions de l'écran ou au DOM bénéficieraient de ce crochet.

Excellente idée, onOpen , onDomAttach ? (après modification de https://github.com/xtermjs/xterm.js/issues/1505)

(1) Je me demande - comment passerais-je une configuration à un module complémentaire ?

J'y ai pensé un peu, mais j'ai pensé que cela pourrait devenir trop compliqué. Je viens de monter un prototype :

image

L'API ressemble à ceci :

  export class Terminal {
    loadAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T;
    loadAddonWithConfig<T extends ITerminalAddonWithConfig<K>, K>(addonConstructor: ITerminalAddonWithConfigConstructor<T, K>, config: K): T;
  }

  export interface ITerminalAddonConstructor<T extends ITerminalAddon> {
    new(terminal: Terminal): T;
  }

  export interface ITerminalAddonWithConfigConstructor<T extends ITerminalAddonWithConfig<K>, K> {
    new(terminal: Terminal, config: K): T;
  }

  export interface ITerminalAddon {
    dispose(): void;
  }

  // The types must be duplicated, extending ITerminalAddon means this could
  // be passed into Terminal.loadAddon
  export interface ITerminalAddonWithConfig<K> {
    dispose(): void;
  }

L'implément d'addon :

export interface IWebLinksAddonConfig {
  handler?: (event: MouseEvent, uri: string) => void;
  options?: ILinkMatcherOptions;
}

export class WebLinksAddonWithConfig implements ITerminalAddonWithConfig<IWebLinksAddonConfig> {
  // config can be omitted here without warning
  constructor(private _terminal: Terminal, config: IWebLinksAddonConfig) {
  }

  ...
}

Diff : https://github.com/Tyriar/xterm.js/commit/9822953d012ab167a0ad17a557595aea124ac97a

Les pensées?

Yupp API vraiment sympa jusqu'à présent. :+1:

J'aime l'idée des crochets de cycle de vie, de cette façon, ppl peut agir sur des états spécifiques du terminal (et même couvrir le cas du terminal hors écran lorsque nous le prendrons en charge un jour dans le futur). Choses qui me viennent à l'esprit (je n'ai jamais écrit d'addon moi-même, donc l'utilité de la liste est discutable):

  • aprèsInit
    Un peu auto-explicatif, chaque fois qu'une instance terminale est née. Un pur addon hors écran peut ignorer les hooks du DOM en toute sécurité.
  • afterDOMattach/didMount (style de réaction ?)
    Si un addons nécessite une configuration DOM, et le DOM devrait déjà être disponible.
  • avantDOMdetach/willUnmount
    Pas sûr de celui-ci, est-il même possible de s'accrocher à la chaîne d'élimination qui a un grain fin ? Cela peut toujours être utile pour un module complémentaire, s'il doit nettoyer l'état alors que le DOM est toujours accessible.
  • avantÉliminer
    Hmm - un addon a-t-il besoin d'une salle de nettoyage avant que l'ensemble de l'arborescence d'objets ne soit supprimé ? dispose peut-être déjà trop tard...

aprèsInit

Eh bien, c'est essentiellement le constructeur, le plan initial était de donner un tableau d'addons à Terminal.ctor mais cela semble trop compliquer les choses, d'autant plus que rien ne sera rendu jusqu'à ce que open soit appelé de toute façon.

Hmm - un addon a-t-il besoin d'une salle de nettoyage avant que l'ensemble de l'arborescence d'objets ne soit supprimé ? disposer peut-être déjà trop tard...

Pouvez-vous donner un exemple?

Aussi, j'ai pensé que nous n'avions même pas besoin de Terminal.disposeAddon et de Terminal.getAddon . Si vous souhaitez gérer vous-même le cycle de vie de l'addon (désactivation au moment de l'exécution) ou si vous devez interagir avec lui plus tard (par exemple, rechercher), gardez simplement une référence de loadAddon, Envelopper la fonction dispose au chargement rend disposeAddon imo redondant.

Pouvez-vous donner un exemple?

Eh bien, je n'ai actuellement aucune idée de l'ordre de disposition, donc c'est assez théorique. Que se passe-t-il si un module complémentaire repose sur un autre état de composant (quelque chose d'interne ou un autre module complémentaire) et doit nettoyer les choses, mais que l'autre composant est déjà parti ? L'exemple qui me vient à l'esprit est un module complémentaire qui sérialise l'état actuel du terminal pour pouvoir reprendre plus tard.
Avec un beforeDispose (pourrait être déclenché en tant que première tâche de terminal.dispose ), tout module complémentaire peut se préparer en toute sécurité pour la suppression à venir tant que l'environnement est toujours intact.

L'exemple qui me vient à l'esprit est un module complémentaire qui sérialise l'état actuel du terminal pour pouvoir reprendre plus tard.

Hmm, ajoutons si quelqu'un demande 🙂, cela pourrait être nécessaire si nous ajoutons des dépendances complémentaires

@Tyriar Désolé, j'oublie la commodité des options de configuration tapées dans mon post précédent. Ne pourrions-nous pas laisser l'utilisateur instancier l'addon, puis le laisser passer l'instance de l'addon à term.loadAddon ?

// addon definition
class MyAddon implements ITerminalAddon {

  // addon constructor
  constructor(config: IMyAddonConfig) {
  }

  // called when initiating the plugin
  onLoad(terminal: Terminal) {
  }

  // called when term.open() was called
  onOpen(terminal: Terminal) {
  }

  // called on term.dispose()
  dispose(terminal: Terminal) {
  }

}

// create addon instance (note: the consumer instanciates the addon, not us)
const myAddon = new MyAddon({ /*config*/ });
term.loadAddon(myAddon);

Donc onLoad serait essentiellement le crochet qui gérerait ce qui a été fait précédemment dans le constructor . Cela aurait également l'avantage de pouvoir créer un module complémentaire sans classe au moment de l'exécution (pas sûr que ce soit utile cependant):

term.loadAddon({
  onLoad(terminal) {
  },
  dispose(terminal) {
  }
});

Mettre à jour
Un autre avantage de cette approche est que l'on peut utiliser la même instance Addon pour plusieurs terminaux. Un exemple serait les addons qui maintiennent un cache interne, qui pourrait être facilement réutilisé avec cette approche ( constructor appelé une fois, onLoad appelé pour chaque instance)

Qu'est-ce que tu penses?

Ne pourrions-nous pas laisser l'utilisateur instancier l'addon, puis le laisser passer l'instance d'addon à term.loadAddon ?

Je suppose que nous pourrions nous débarrasser de tous les éléments du constructeur si nous n'avions qu'une seule API Terminal.loadAddon . Cela semble certainement plus simple.

Un autre avantage de cette approche est que l'on peut utiliser la même instance Addon pour plusieurs terminaux. Un exemple serait les addons qui maintiennent un cache interne, qui pourrait être facilement réutilisé avec cette approche (constructeur appelé une fois, onLoad appelé pour chaque instance)

C'est quelque chose que j'essayais d'éviter, cela signifierait qu'il existe des modules complémentaires qui fonctionnent avec plusieurs terminaux et d'autres non. Cela pourrait également encourager à faire ceci:

https://github.com/xtermjs/xterm.js/blob/509ce5fa3a698ee7847419117e9dd6b979b105bf/src/addons/attach/attach.ts#L23

Prenez le futur addon webgl par exemple, tout est assez gros sans ajouter de support pour gérer plusieurs terminaux dans le même addon, surtout quand il fonctionne juste quand il n'y a qu'un seul terminal chargé.

Et si on faisait quelque chose comme ça pour l'interdire ?

loadAddon(addon: ITerminalAddon): void {
  if (addon.__isLoaded) {
    throw ...
  }
  ...
  addon.__isLoaded = true;
}

Idée d'extensions groupées : https://github.com/xtermjs/xterm.js/pull/1714#issuecomment -454898319

Le nouveau modèle est beaucoup plus simple :

class Terminal {
    /**
     * Loads an addon into this instance of xterm.js.
     * <strong i="6">@param</strong> addon The addon to load.
     */
    loadAddon(addon: ITerminalAddon): void;
}

export interface ITerminalAddon {
    /**
     * This is called when the addon is activated within xterm.js.
     */
    activate(terminal: Terminal): void;

    /**
     * This function includes anything that needs to happen to clean up when
     * the addon is being disposed.
     */
    dispose(): void;
}

Cela permet à l'intégrateur de construire l'addon comme il le souhaite, et nous éliminons la complexité du système de ctor et les méthodes supplémentaires. Si quelqu'un souhaite conserver une référence, il suffit de conserver l'addon après votre inscription :

term.loadAddon(new WebLinksAddon());
const attachAddon = new AttachAddon();
term.loadAddon(attachAddon);

// ...

attachAddon.attach(...);

Je penche pour ne pas exposer un tas d'événements sur les addons eux-mêmes, mais plutôt pour permettre aux addons d'enregistrer leurs propres événements pendant l'événement activate. L'auteur de l'addon peut avoir besoin de vérifier l'état du terminal avant de faire tout :

const addon = {
  activate(term: Terminal): void {
    if (term.element) {
      // it's open
    } else {
      // handle open event
      term.onOpen(() => ...);
    }
  }
}

Il convient de souligner que je n'avais pas l'intention d'exporter des modules complémentaires sur window (voir https://github.com/xtermjs/xterm.js/issues/2015).

Cette page vous a été utile?
0 / 5 - 0 notes