This is the second article in the series of the articles where we are going to build the terminal in C using GTK4 and VTE. The series is divided into three articles.
tl;dc: Refer this gist to get the final version of terminal.c code.
- Let's Build the Terminal Pt. 1
- Let's Build the Terminal Pt. 2
- Let's Build the Terminal Pt. 3
Let's continue from where we left off in the last article. Following is the code we have in terminal.c file.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
And we know the command to compile and run the program.
gcc terminal.c `pkg-config --cflags --libs gtk4` && ./a.out
We will continue from here and let's start by adding a header bar using gtk_header_bar_new() function.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
Let's set the "Terminal" as title of the header bar. We need to pass the "Terminal" as label because gtk_header_bar_set_title_widget() expect it.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
The header bar is created. We need to call gtk_window_set_titlebar() function to actually set this header bar as titlebar for the window.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
Run the program using the above command and you should see a titlebar in the window.
We are now going to add a notebook. Notebook help you to have multi-tab terminal. Let's create a notebook using gtk_notebook_new() function. I will write it before the header bar code. I'll tell you the reason why I did this later.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Notebook. GtkWidget* notebook = gtk_notebook_new(); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
Notebook have pages or tab in our case. I'll use word "tab" or "page" interchangeably. Let's add two pages using gtk_notebook_append_page() function. This function accepts the content of the page and the title of the page.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Notebook. GtkWidget* notebook = gtk_notebook_new(); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 1 content goes here"), gtk_label_new("Page 1")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 2 content goes here"), gtk_label_new("Page 2")); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
I'm not going to add this notebook directly in the window. Instead, I'll use vertical box as the container and then add notebook to the box container for better layout management.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Container. GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Notebook. GtkWidget* notebook = gtk_notebook_new(); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 1 content goes here"), gtk_label_new("Page 1")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 2 content goes here"), gtk_label_new("Page 2")); gtk_box_append(GTK_BOX(box), notebook); gtk_window_set_child(GTK_WINDOW(window), box); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
Run the program to confirm the output. This is how it looks on my computer. Notice, the notebook is not taking all the available space.
We can fix this issue by expanding on both vertical and horizontal sides using gtk_widget_set_vexpand() and gtk_widget_set_hexpand() functions.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Container. GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Notebook. GtkWidget* notebook = gtk_notebook_new(); gtk_widget_set_vexpand(notebook, TRUE); gtk_widget_set_hexpand(notebook, TRUE); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 1 content goes here"), gtk_label_new("Page 1")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 2 content goes here"), gtk_label_new("Page 2")); gtk_box_append(GTK_BOX(box), notebook); gtk_window_set_child(GTK_WINDOW(window), box); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
Let's make things more interesting by adding a button in header bar on which if we click, it'll add a new tab in notebook. We'll do this in steps.
First, let's add a button to the header bar by creating a button using gtk_button_new_with_label() function and then packing the button to the end in header bar using gtk_header_bar_pack_end() function.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Container. GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Notebook. GtkWidget* notebook = gtk_notebook_new(); gtk_widget_set_vexpand(notebook, TRUE); gtk_widget_set_hexpand(notebook, TRUE); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 1 content goes here"), gtk_label_new("Page 1")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 2 content goes here"), gtk_label_new("Page 2")); gtk_box_append(GTK_BOX(box), notebook); gtk_window_set_child(GTK_WINDOW(window), box); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); // Button. GtkWidget* button = gtk_button_new_with_label("New Tab"); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
Let's attach the "clicked" signal to this button and write the stub callback function. We need to pass the notebook instance as the user data as we need to append the new tab to the notebook when clicked. As notebook is created before the header bar, we can directly pass it as user data. Now, you know the reason why I write the Notebook code before the header bar (Yes, this is quick fix but not a hack or anything)!
#include <gtk/gtk.h> static void _on_new_tab_clicked(GtkWidget* widget, gpointer data) { } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Container. GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Notebook. GtkWidget* notebook = gtk_notebook_new(); gtk_widget_set_vexpand(notebook, TRUE); gtk_widget_set_hexpand(notebook, TRUE); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 1 content goes here"), gtk_label_new("Page 1")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 2 content goes here"), gtk_label_new("Page 2")); gtk_box_append(GTK_BOX(box), notebook); gtk_window_set_child(GTK_WINDOW(window), box); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); // Button. GtkWidget* button = gtk_button_new_with_label("New Tab"); g_signal_connect(button, "clicked", G_CALLBACK(_on_new_tab_clicked), notebook); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
In the callback function, let's first cast the data to notebook type using GTK_NOTEBOOK() macro and then call the gtk_notebook_get_n_pages() function. This function will return total number of pages or tabs exists in given notebook. The reason to plan this function is to give the tab name like "Tab 3", "Tab 4", "Tab 5", .... e.g. increment the number by one and prefix the "Tab " string when clicked on "New Tab" button.
#include <gtk/gtk.h> static void _on_new_tab_clicked(GtkWidget* widget, gpointer data) { GtkNotebook* notebook = GTK_NOTEBOOK(data); int pages = gtk_notebook_get_n_pages(notebook); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Container. GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Notebook. GtkWidget* notebook = gtk_notebook_new(); gtk_widget_set_vexpand(notebook, TRUE); gtk_widget_set_hexpand(notebook, TRUE); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 1 content goes here"), gtk_label_new("Page 1")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 2 content goes here"), gtk_label_new("Page 2")); gtk_box_append(GTK_BOX(box), notebook); gtk_window_set_child(GTK_WINDOW(window), box); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); GtkWidget* button = gtk_button_new_with_label("New Tab"); g_signal_connect(button, "clicked", G_CALLBACK(_on_new_tab_clicked), notebook); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
Let's create a title in plain C code.
#include <gtk/gtk.h> static void _on_new_tab_clicked(GtkWidget* widget, gpointer data) { GtkNotebook* notebook = GTK_NOTEBOOK(data); int pages = gtk_notebook_get_n_pages(notebook); char title[32]; g_snprintf(title, sizeof(title), "Tab %d", pages + 1); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Container. GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Notebook. GtkWidget* notebook = gtk_notebook_new(); gtk_widget_set_vexpand(notebook, TRUE); gtk_widget_set_hexpand(notebook, TRUE); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 1 content goes here"), gtk_label_new("Page 1")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 2 content goes here"), gtk_label_new("Page 2")); gtk_box_append(GTK_BOX(box), notebook); gtk_window_set_child(GTK_WINDOW(window), box); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); GtkWidget* button = gtk_button_new_with_label("New Tab"); g_signal_connect(button, "clicked", G_CALLBACK(_on_new_tab_clicked), notebook); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
You can argue against this code as it is not future proof if someone create X number of tabs that overflow this size. Again, I'm not worry about such a scenario as one need to create many many tabs to overflow this limit and we can always fix such type of issues later.
Let's call gtk_notebook_append_page() to add a new tab with this title and dummy content. This function returns a page number that we need to focus using gtk_notebook_set_current_page() function.
#include <gtk/gtk.h> static void _on_new_tab_clicked(GtkWidget* widget, gpointer data) { GtkNotebook* notebook = GTK_NOTEBOOK(data); int pages = gtk_notebook_get_n_pages(notebook); char title[32]; g_snprintf(title, sizeof(title), "Tab %d", pages + 1); int new_page = gtk_notebook_append_page(notebook, gtk_label_new("New tab content goes here"), gtk_label_new(title)); gtk_notebook_set_current_page(notebook, new_page); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Terminal"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Container. GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Notebook. GtkWidget* notebook = gtk_notebook_new(); gtk_widget_set_vexpand(notebook, TRUE); gtk_widget_set_hexpand(notebook, TRUE); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 1 content goes here"), gtk_label_new("Page 1")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gtk_label_new("Page 2 content goes here"), gtk_label_new("Page 2")); gtk_box_append(GTK_BOX(box), notebook); gtk_window_set_child(GTK_WINDOW(window), box); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Terminal")); GtkWidget* button = gtk_button_new_with_label("New Tab"); g_signal_connect(button, "clicked", G_CALLBACK(_on_new_tab_clicked), notebook); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.terminal", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
Go ahead and run this program. You should see the following output when you click on 'New Tab' button.
Yay!




