Écrire son propre émulateur de terminal

Vincent Bernat

J’étais un uti­li­sa­teur heu­reux de rxvt-uni­code jusqu’au jour où j’ai eu un por­table avec un écran « haute den­sité ». Pas­ser d’un écran ex­terne clas­sique à l’écran in­té­gré était alors as­sez pé­nible : il fal­lait soit ajus­ter la fonte sur chaque ter­mi­nal, soit re­dé­mar­rer l’en­semble des terminaux.

VTE est une bi­blio­thèque pour construire un ému­la­teur de ter­mi­nal uti­li­sant GTK+ qui sait gé­rer le chan­ge­ment de den­sité. Elle est uti­li­sée par de nom­breux ému­la­teurs tels que GNOME Ter­mi­nal, evilvte, sa­kura, ter­mit et ROXTerm. Elle est plu­tôt simple d’em­ploi si on se li­mite aux fonc­tion­na­li­tés prévues.

Un simple émulateur de terminal#

Com­men­çons en se conten­tant des fonc­tion­na­li­tés par dé­faut. Nous uti­li­se­rons le C. Une autre op­tion bien sup­por­tée est le lan­gage Vala.

#include <vte/vte.h>

static void
child_ready(VteTerminal *terminal, GPid pid, GError *error, gpointer user_data)
{
    if (!terminal) return;
    if (pid == -1) gtk_main_quit();
}

int
main(int argc, char *argv[])
{
    GtkWidget *window, *terminal;

    /* Initialise GTK, the window and the terminal */
    gtk_init(&argc, &argv);
    terminal = vte_terminal_new();
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "myterm");

    /* Start a new shell */
    gchar **envp = g_get_environ();
    gchar **command = (gchar *[]){g_strdup(g_environ_getenv(envp, "SHELL")), NULL };
    g_strfreev(envp);
    vte_terminal_spawn_async(VTE_TERMINAL(terminal),
        VTE_PTY_DEFAULT,
        NULL,         /* working directory  */
        command,      /* command */
        NULL,         /* environment */
        0,            /* spawn flags */
        NULL, NULL,   /* child setup */
        NULL,         /* child pid */
        -1,           /* timeout */
        NULL,         /* cancellable */
        child_ready,  /* callback */
        NULL);        /* user_data */

    /* Connect some signals */
    g_signal_connect(window, "delete-event", gtk_main_quit, NULL);
    g_signal_connect(terminal, "child-exited", gtk_main_quit, NULL);

    /* Put widgets together and run the main loop */
    gtk_container_add(GTK_CONTAINER(window), terminal);
    gtk_widget_show_all(window);
    gtk_main();
}

Ce pro­gramme se com­pile avec la com­mande suivante :

gcc -O2 -Wall $(pkg-config --cflags vte-2.91) term.c -o term $(pkg-config --libs vte-2.91)

En lan­çant ./term, nous ob­te­nons ceci :

Terminal simple basé sur VTE
Terminal simple basé sur VTE

Fonctionnalités supplémentaires#

L’étape sui­vante est de re­gar­der la do­cu­men­ta­tion afin d’ajou­ter quelques fonc­tion­na­li­tés ou de mo­di­fier le com­por­te­ment. Voici trois exemples.

Couleurs#

Il est pos­sible de re­dé­fi­nir les 16 cou­leurs de base avec le code suivant :

#define CLR_R(x)   (((x) & 0xff0000) >> 16)
#define CLR_G(x)   (((x) & 0x00ff00) >>  8)
#define CLR_B(x)   (((x) & 0x0000ff) >>  0)
#define CLR_16(x)  ((double)(x) / 0xff)
#define CLR_GDK(x) (const GdkRGBA){ .red = CLR_16(CLR_R(x)), \
                                    .green = CLR_16(CLR_G(x)), \
                                    .blue = CLR_16(CLR_B(x)), \
                                    .alpha = 0 }
vte_terminal_set_colors(VTE_TERMINAL(terminal),
    &CLR_GDK(0xffffff),
    &(GdkRGBA){ .alpha = 0.85 },
    (const GdkRGBA[]){
        CLR_GDK(0x111111),
        CLR_GDK(0xd36265),
        CLR_GDK(0xaece91),
        CLR_GDK(0xe7e18c),
        CLR_GDK(0x5297cf),
        CLR_GDK(0x963c59),
        CLR_GDK(0x5E7175),
        CLR_GDK(0xbebebe),
        CLR_GDK(0x666666),
        CLR_GDK(0xef8171),
        CLR_GDK(0xcfefb3),
        CLR_GDK(0xfff796),
        CLR_GDK(0x74b8ef),
        CLR_GDK(0xb85e7b),
        CLR_GDK(0xA3BABF),
        CLR_GDK(0xffffff)
}, 16);

Bien que cela ne soit pas vi­sible sur la cap­ture d’écran sui­vante1, le fond est lé­gè­re­ment transparent :

Rendu des couleurs
Rendu des couleurs

Paramètres divers#

VTE dis­pose de nom­breux ré­glages. Voici quelques exemples :

vte_terminal_set_scrollback_lines(VTE_TERMINAL(terminal), 0);
vte_terminal_set_scroll_on_output(VTE_TERMINAL(terminal), FALSE);
vte_terminal_set_scroll_on_keystroke(VTE_TERMINAL(terminal), TRUE);
vte_terminal_set_mouse_autohide(VTE_TERMINAL(terminal), TRUE);

Res­pec­tivement :

  • désac­ti­va­tion de l’his­to­ri­sa­tion des lignes ;
  • pas de dé­fi­le­ment en cas de nou­velle sortie ;
  • dé­fi­le­ment en bas s’il y a in­ter­ac­tion de l’uti­lisateur ;
  • ca­cher le cur­seur de la sou­ris quand le cla­vier est utilisé.

Titre de la fenêtre#

Une ap­pli­ca­tion peut mo­di­fier le titre de la fe­nêtre en uti­li­sant les sé­quences de contrôle XTerm (par exemple, avec printf "\e]2;${title}\a"). Pour que ce chan­ge­ment im­pacte la fe­nêtre GTK, il faut dé­fi­nir cette fonction :

static gboolean
on_title_changed(GtkWidget *terminal, gpointer user_data)
{
    GtkWindow *window = user_data;
    gtk_window_set_title(window,
        vte_terminal_get_window_title(VTE_TERMINAL(terminal))?:"Terminal");
    return TRUE;
}

Il convient en­suite de la connec­ter au si­gnal ap­pro­prié de­puis la fonc­tion main() :

g_signal_connect(terminal, "window-title-changed",
    G_CALLBACK(on_title_changed), GTK_WINDOW(window));

Conclusion#

Je n’ai guère be­soin de plus étant donné que j’uti­lise tmux à l’in­té­rieur de chaque ter­mi­nal. J’ai éga­le­ment im­plé­menté une com­plé­tion dy­na­mique per­met­tant de com­plé­ter le mot sous le cur­seur à par­tir des mots pré­sents dans le ter­mi­nal cou­rant ou un autre. Cela a né­ces­sité de gé­rer toutes les fe­nêtres de­puis un pro­ces­sus unique, comme avec urxvtcd.

Il est donc très simple de construire un ému­la­teur de ter­mi­nal de « zéro »2. Ce­pen­dant, ce n’est pas for­cé­ment une bonne idée. Il est fa­cile de com­pi­ler evilvte avec les fonc­tion­na­li­tés que l’on dé­sire et d’ar­ri­ver à un ré­sul­tat si­mi­laire. Consi­dé­rez d’abord une telle so­lu­tion. Hon­nê­te­ment, je ne sais même plus pour­quoi je ne l’ai pas fait.

Mise à jour (02.2017)

evilvte n’a pas été mis à jour de­puis 2014. Son sup­port GTK+3 est non fonc­tion­nel. Il ne sup­porte pas les der­nières ver­sions de VTE. Il ne s’agit donc pas d’une bonne re­com­mendation.

Un autre élé­ment cru­cial est que VTE est une bi­blio­thèque de sup­port pour GNOME Ter­mi­nal uni­que­ment. Une fonc­tion­na­lité qui n’est pas uti­li­sée dans GNOME Ter­mi­nal ne sera pas im­plé­men­tée. Elle sera même re­ti­rée si elle existe déjà.