Progmar Marcin Załęczny

Jak użyć kontrolkę terminala VTE we własnych programach

W tym krótkim tutorialu pokażę jak używać widget VTE we własnych programach. Najpierw utworzymy w programie Glade interfejs dla naszej przykładowej aplikacji a następnie w odpowiednie miejsce layoutu podepniemy wyżej wspomniany widget. Pokażę jak załadować shell do terminala i jak zabezpieczyć się przed opuszczeniem shella przy pomocy komendy exit lub kombinacji klawiszy Ctrl+D.

A więc do dzieła!

Zaczniemy od instalacji biblioteki developerskiej umożliwiającej wykorzystanie kontrolki VTE. W tym celu zainstaluj pakiet libvte-2.90-dev:

sudo apt-get install libvte-2.90-dev

Następnie otwórz program Glade i utwórz layout według podanego poniżej opisu.

  1. Utwórz obiekt Window i ustaw mu następujące właściwości:
    Identyfikator: mainWindow,
    Tytuł: Terminal.
  2. Do okna głównego dodaj box o rozmiarze 2 (id: box1).
  3. Do górnego placeholdera boksu box1 dodaj widget Frame (id: frame1) i ustaw mu atrybut Rozszerzanie na 1.
  4. W hierarchi widgetów rozwiń obiekt frame1 i kliknij w widget alignment1 i ustaw mu następujące właściwości:
    Dopełnienie u dołu: 16,
    Dopełnienie z lewej strony: 16,
    Dopełnienie z prawej strony: 16.
    Następnie kliknij widget label1 i wyczyść mu atrybut etykieta.
  5. Do dolnego placeholdera boksu box1 dodaj widget Box (id: box2) o rozmiarze dwóch elementów i ustaw mu atrybut Ułożenie na Horizontal.
  6. Do lewego placeholdera boksu box2 dodaj widget Text Entry i ustaw mu właściwości:
    Identyfikator: edtCommand,
    Rozszerzanie: 1.
  7. Do prawego placeholdera boksu box2 dodaj widget Button i ustaw mu właściwości:
    Identyfikator: btnExecute,
    Etykieta z opcjonalnym obrazem: Wykonaj.

Po wykonaniu powyższych kroków nasze okno powinno wyglądać jak na screenshocie poniżej:

Zapiszmy teraz nasz plik interfejsu jako main_window.ui i przejdźmy do tworzenia kodu naszej aplikacji. Zacznijmy od zainkludowania potrzebnych plików nagłówkowych:

#include <gtk/gtk.h>
#include <vte/vte.h>
#include <vte/reaper.h>

Plik gtk.h jest głównym plikiem nagłówkowym biblioteki gtk i jest wymagany dla każdej aplikacji korzystającej z tej biblioteki.
Plik vte.h jest plikiem nagłówkowym definiującym kontrolkę vte i pozwalającym używać ją w naszych aplikacjach.
Plik reaper.h pozwala przechwytywać zdarzenia zakończenia wykonywania się procesu załadowanego do widgeta vte.

Zdefiniujmy teraz zmienne globalne i funkcje, które wykorzystamy w naszej aplikacji:

GtkBuilder *builder;
GtkWidget *mainWindow;
GtkWidget *vte;

gboolean loadGui();
void create_terminal();
void run_shell_in_terminal();
void vte_child_exited(VteReaper *vtereaper, gint arg1, gint arg2, gpointer user_data);
void on_execute_command_click(GtkWidget *widget, gpointer user_data);

Zmienna builder będzie przechowywała wskaźnik na obiekt odpowiedzialny za wczytanie interfejsu aplikacji z utworzonego wcześniej pliku main_window.ui.
Zmienna mainWindow jest wskaźnikiem na okno główne naszej aplikacji.
Natomiast zmienna vte będzie przechowywała wskaźnik na obiekt terminala VTE.

Funkcję główną możemy zdefiniować następująco:

gint main(gint argc, gchar *argv[])
{
    gtk_init(&argc, &argv);
    
    if (loadGui()) {
        mainWindow = GTK_WIDGET(gtk_builder_get_object(builder, "mainWindow"));
        GtkWidget *btnExecute = GTK_WIDGET(gtk_builder_get_object(builder, "btnExecute"));
        if (mainWindow && btnExecute) {
            g_signal_connect(G_OBJECT(mainWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
            g_signal_connect(G_OBJECT(btnExecute), "clicked", G_CALLBACK(on_execute_command_click), NULL);
            create_terminal();            
            gtk_widget_show_all(mainWindow);
            gtk_main();
        }
    }
    
    if (builder) {
        g_object_unref(builder);
    }
    
    return 0;
}

Za pomocą funkcji gtk_init inicjalizujemy bibliotekę GTK. Do funkcji tej przekazujemy zmienne argc i argv określające parametry aplikacji przekazane w linii poleceń oraz ich ilość.
Następnie jeśli interfejs został poprawnie wczytany - pobieramy przy pomocy funkcji gtk_builder_get_object wskaźniki na widgety okna głównego i przycisku "Wykonaj". Jeśli udało się pobrać te wskaźniki podpinamy funkcję obsługi sygnału destroy okna głównego, odpowiedzialną za zakończenie aplikacji (funkcja systemowa gtk_main_quit) oraz funkcję obsługi sygnału clicked przycisku btnExecute - zdefiniowana przez nas funkcja on_execute_command_click.
Następnie wywołujemy funkcję tworzącą terminal i pokazujemy okno główne na ekranie. Na koniec uruchamiamy funkcję systemową gtk_main odpowiedzialną za przetwarzanie w pętli wszystkich sygnałów i zdarzeń otrzymywanych przez aplikację.
Przed opuszczeniem funkcji głównej pamiętamy o zwolnieniu obiektu buildera przy pomocy funkcji g_object_unref.

gboolean loadGui() {
    GError *error;
    
    builder = gtk_builder_new();
	error = NULL;
	gtk_builder_add_from_file(builder, "main_window.ui", &error);
	if (error) {
		g_print("Wystąpił błąd: %s\n", error->message);
		g_error_free(error);
		return FALSE;
	}
    return TRUE;
}

Funkcja loadGui odpowiada za utworzenie interfejsu aplikacji. Zwraca TRUE w przypadku sukcesu i FALSE w przypadku niepowodzenia. W funkcji tej najpierw tworzymy obiekt buildera przy pomocy funkcji gtk_builder_new a następnie wywołując funkcję gtk_builder_add_from_file próbujemy wczytać interfejs z pliku main_window.ui (z bieżącego katalogu roboczego). Jeśli wystąpił błąd, to wyświetlamy stosowny komunikat na standardowym wyjściu.


void run_shell_in_terminal() {
    GError *error;        
    char *startterm[2] = {0, 0};
    startterm[0] = vte_get_user_shell();
    
    error = NULL;
    vte_terminal_fork_command_full(VTE_TERMINAL(vte),
           VTE_PTY_DEFAULT, // Tryb uruchomienia shella
           NULL,  // Katalog roboczy; ustawiony na NULL przyjmuje wartość bieżącego katalogu roboczego
           startterm, // Polecenie do wykonania
           NULL, // NULL lub lista zmiennych środowiskowych ustawianych w terminalu
           (GSpawnFlags)(G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH),  // Flagi dla tworzonego procesu
           NULL, // Funkcja konfiguracyjna wywoływana przed wykonaniem funkcji exec
           NULL, // Dane przekazywane do funkcji konfiguracyjnej
           NULL, // NULL albo zmienna przechowująca PID potomka
           &error // Zmienna przechowująca informację o błędzie
           );
    if (error) {
        g_print("Wystąpił błąd: %s\n", error->message);
        g_error_free(error);
    }
    g_free(startterm[0]);
}

Funkcja run_shell_in_terminal odpowiada za uruchomienie w terminalu bieżącego shella użytkownika. Pełną ścieżkę do tego shella otrzymujemy przy pomocy funkcji vte_get_user_shell i przechowujemy w tablicy startterm. Następnie uruchamiamy ten shell przy pomocy funkcji vte_terminal_fork_command_full. Poszczególne parametry tej funkcji opisałem w komentarzach przy wywołaniu.


void create_terminal() {
    GtkWidget *frame_alignment = GTK_WIDGET(gtk_builder_get_object(builder, "alignment1"));
    if (frame_alignment) {
        vte = vte_terminal_new();
        g_signal_connect(G_OBJECT(vte), "child-exited", G_CALLBACK(vte_child_exited), NULL);
        gtk_container_add(GTK_CONTAINER(frame_alignment), vte);
        
        run_shell_in_terminal();
    }
}

Funkcja create_terminal odpowiada za utworzenie widgeta terminala VTE i dodanie go do załadowanego wcześniej interfejsu użytkownika. Najpierw pobieramy przy pomocy funkcji gtk_builder_get_object pobieramy widget-pojemnik, do którego dodamy obiekt terminala. Jest to widget alignment1. Następnie tworzymy terminal funkcją vte_terminal_new, podpinamu obsługę sygnału "child-exited" (wywoływany w momencie opuszczenia procesu shella) oraz dodajemy terminal do pojemnika. Na koniec uruchamiamy w terminalu shell użytkownika.

void vte_child_exited(VteReaper *vtereaper, gint arg1, gint arg2, gpointer user_data) {
    // if user exits shell by pressing Ctrl+D or executing "exit" command run shell again
    run_shell_in_terminal();
}

Funkcja vte_child_exited wywoływana po zamknięciu shella uruchamia go po prostu jeszcze raz. Dzięki temu blokujemy użytkownikowi możliwość całkowitego zamknięcia shella w terminalu.

void on_execute_command_click(GtkWidget *widget, gpointer user_data) {
    GtkWidget *edtCommand = GTK_WIDGET(gtk_builder_get_object(builder, "edtCommand"));
    if (edtCommand) {
        const gchar *command = gtk_entry_get_text(GTK_ENTRY(edtCommand));
        gchar *command_with_newline = g_strdup_printf("%s\r", command);
        
        vte_terminal_feed_child(VTE_TERMINAL(vte), command_with_newline, -1);
        
        g_free(command_with_newline);
    }
}

Na koniec definiujemy funkcję obsługi kliknięcia przycisku. Funkcja ta najpierw pobiera wskaźnik na pole tekstowe z komendą do uruchomienia a następnie pobiera tę komendę przy pomocy funkcji gtk_entry_get_text, dokleja do niej znacznik wciśnięcia klawisza ENTER (\r) i wywołuje w terminalu (funkcja vte_terminal_feed_child). Na koniec oczywiście pamiętamy o zwolnieniu zasobów.

Kompletny kod aplikacji można pobrać poniżej lub na samej górze strony.