#include "rt-terminal.h"
#include "rt-utils.h"
#include <pango/pangocairo.h>
G_DEFINE_TYPE(RtTerminal, rt_terminal, GTK_TYPE_BOX)
enum {
SIGNAL_TITLE_CHANGED,
SIGNAL_BELL,
SIGNAL_CHILD_EXITED,
N_SIGNALS
};
static guint signals[N_SIGNALS] = { 0 };
static void rt_terminal_finalize(GObject *object) {
RtTerminal *terminal = RT_TERMINAL(object);
g_clear_object(&terminal->font_config);
g_clear_object(&terminal->theme);
g_clear_object(&terminal->pty);
g_free(terminal->current_directory);
g_free(terminal->window_title);
G_OBJECT_CLASS(rt_terminal_parent_class)->finalize(object);
}
static void rt_terminal_class_init(RtTerminalClass *klass) {
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = rt_terminal_finalize;
signals[SIGNAL_TITLE_CHANGED] = g_signal_new(
"title-changed",
G_TYPE_FROM_CLASS(klass),
G_SIGNAL_RUN_LAST,
0, NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_STRING);
signals[SIGNAL_BELL] = g_signal_new(
"bell",
G_TYPE_FROM_CLASS(klass),
G_SIGNAL_RUN_LAST,
0, NULL, NULL, NULL,
G_TYPE_NONE, 0);
signals[SIGNAL_CHILD_EXITED] = g_signal_new(
"child-exited",
G_TYPE_FROM_CLASS(klass),
G_SIGNAL_RUN_LAST,
0, NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_INT);
}
static void on_window_title_changed(VteTerminal *vte, gpointer user_data) {
RtTerminal *terminal = RT_TERMINAL(user_data);
g_free(terminal->window_title);
terminal->window_title = g_strdup(vte_terminal_get_window_title(vte));
g_signal_emit(terminal, signals[SIGNAL_TITLE_CHANGED], 0, terminal->window_title);
}
static void on_bell(VteTerminal *vte, gpointer user_data) {
(void)vte;
RtTerminal *terminal = RT_TERMINAL(user_data);
g_signal_emit(terminal, signals[SIGNAL_BELL], 0);
}
static void on_child_exited(VteTerminal *vte, gint status, gpointer user_data) {
(void)vte;
RtTerminal *terminal = RT_TERMINAL(user_data);
terminal->child_pid = 0;
g_signal_emit(terminal, signals[SIGNAL_CHILD_EXITED], 0, status);
}
static void on_current_directory_uri_changed(VteTerminal *vte, gpointer user_data) {
RtTerminal *terminal = RT_TERMINAL(user_data);
const gchar *uri = vte_terminal_get_current_directory_uri(vte);
if (uri) {
g_free(terminal->current_directory);
if (g_str_has_prefix(uri, "file://")) {
terminal->current_directory = g_strdup(uri + 7);
} else {
terminal->current_directory = g_strdup(uri);
}
}
}
static void apply_font_rendering(RtTerminal *terminal) {
PangoContext *context = gtk_widget_get_pango_context(GTK_WIDGET(terminal->vte));
cairo_font_options_t *options = cairo_font_options_create();
cairo_font_options_set_antialias(options, CAIRO_ANTIALIAS_SUBPIXEL);
cairo_font_options_set_hint_style(options, CAIRO_HINT_STYLE_FULL);
cairo_font_options_set_hint_metrics(options, CAIRO_HINT_METRICS_ON);
cairo_font_options_set_subpixel_order(options, CAIRO_SUBPIXEL_ORDER_RGB);
pango_cairo_context_set_font_options(context, options);
cairo_font_options_destroy(options);
pango_context_changed(context);
}
static void rt_terminal_init(RtTerminal *terminal) {
terminal->vte = NULL;
terminal->scrollbar = NULL;
terminal->font_config = NULL;
terminal->theme = NULL;
terminal->pty = NULL;
terminal->child_pid = 0;
terminal->current_directory = NULL;
terminal->window_title = NULL;
terminal->config = NULL;
gtk_orientable_set_orientation(GTK_ORIENTABLE(terminal), GTK_ORIENTATION_HORIZONTAL);
terminal->vte = VTE_TERMINAL(vte_terminal_new());
gtk_widget_set_hexpand(GTK_WIDGET(terminal->vte), TRUE);
gtk_widget_set_vexpand(GTK_WIDGET(terminal->vte), TRUE);
GtkAdjustment *adj = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(terminal->vte));
terminal->scrollbar = GTK_SCROLLBAR(gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, adj));
gtk_box_append(GTK_BOX(terminal), GTK_WIDGET(terminal->vte));
gtk_box_append(GTK_BOX(terminal), GTK_WIDGET(terminal->scrollbar));
g_signal_connect(terminal->vte, "window-title-changed", G_CALLBACK(on_window_title_changed), terminal);
g_signal_connect(terminal->vte, "bell", G_CALLBACK(on_bell), terminal);
g_signal_connect(terminal->vte, "child-exited", G_CALLBACK(on_child_exited), terminal);
g_signal_connect(terminal->vte, "current-directory-uri-changed", G_CALLBACK(on_current_directory_uri_changed), terminal);
}
RtTerminal* rt_terminal_new(RtConfig *config) {
RtTerminal *terminal = g_object_new(RT_TYPE_TERMINAL, NULL);
if (config) {
terminal->config = config;
terminal->font_config = rt_font_new(config->font_family, config->font_size);
terminal->theme = rt_theme_load_by_name(config->theme_name);
vte_terminal_set_scrollback_lines(terminal->vte, config->scrollback_lines);
vte_terminal_set_scroll_on_output(terminal->vte, config->scroll_on_output);
vte_terminal_set_scroll_on_keystroke(terminal->vte, config->scroll_on_keystroke);
vte_terminal_set_bold_is_bright(terminal->vte, config->bold_is_bright);
vte_terminal_set_audible_bell(terminal->vte, config->audible_bell);
if (g_strcmp0(config->cursor_shape, "block") == 0) {
vte_terminal_set_cursor_shape(terminal->vte, VTE_CURSOR_SHAPE_BLOCK);
} else if (g_strcmp0(config->cursor_shape, "ibeam") == 0) {
vte_terminal_set_cursor_shape(terminal->vte, VTE_CURSOR_SHAPE_IBEAM);
} else if (g_strcmp0(config->cursor_shape, "underline") == 0) {
vte_terminal_set_cursor_shape(terminal->vte, VTE_CURSOR_SHAPE_UNDERLINE);
}
if (config->cursor_blink) {
vte_terminal_set_cursor_blink_mode(terminal->vte, VTE_CURSOR_BLINK_ON);
} else {
vte_terminal_set_cursor_blink_mode(terminal->vte, VTE_CURSOR_BLINK_OFF);
}
if (config->word_chars) {
vte_terminal_set_word_char_exceptions(terminal->vte, config->word_chars);
}
PangoFontDescription *font_desc = rt_font_get_description(terminal->font_config);
vte_terminal_set_font(terminal->vte, font_desc);
rt_theme_apply_to_vte(terminal->theme, terminal->vte);
} else {
terminal->font_config = rt_font_new("Monospace", 11);
terminal->theme = rt_theme_create_dracula();
PangoFontDescription *font_desc = rt_font_get_description(terminal->font_config);
vte_terminal_set_font(terminal->vte, font_desc);
rt_theme_apply_to_vte(terminal->theme, terminal->vte);
vte_terminal_set_scrollback_lines(terminal->vte, 10000);
}
apply_font_rendering(terminal);
return terminal;
}
static void on_spawn_callback(VteTerminal *vte, GPid pid, GError *error, gpointer user_data) {
(void)vte;
RtTerminal *terminal = RT_TERMINAL(user_data);
if (error) {
g_warning("Failed to spawn shell: %s", error->message);
return;
}
terminal->child_pid = pid;
}
void rt_terminal_spawn_shell(RtTerminal *terminal, const gchar *working_dir) {
g_return_if_fail(RT_IS_TERMINAL(terminal));
gchar *shell = NULL;
if (terminal->config) {
shell = rt_config_get_shell(terminal->config);
} else {
shell = rt_utils_get_shell();
}
gchar **argv = rt_utils_get_shell_argv(shell);
gchar **envp = NULL;
if (terminal->config) {
envp = rt_config_get_environment(terminal->config);
} else {
envp = g_get_environ();
}
envp = g_environ_setenv(envp, "TERM", "xterm-256color", TRUE);
envp = g_environ_setenv(envp, "COLORTERM", "truecolor", TRUE);
const gchar *cwd = working_dir;
if (!cwd || !*cwd) {
cwd = g_get_home_dir();
}
vte_terminal_spawn_async(
terminal->vte,
VTE_PTY_DEFAULT,
cwd,
argv,
envp,
G_SPAWN_SEARCH_PATH | G_SPAWN_FILE_AND_ARGV_ZERO,
NULL, NULL, NULL,
-1,
NULL,
on_spawn_callback,
terminal
);
g_free(shell);
g_strfreev(argv);
g_strfreev(envp);
}
void rt_terminal_apply_font(RtTerminal *terminal, const gchar *font_desc) {
g_return_if_fail(RT_IS_TERMINAL(terminal));
if (terminal->font_config) {
g_object_unref(terminal->font_config);
}
terminal->font_config = rt_font_from_string(font_desc);
PangoFontDescription *desc = rt_font_get_description(terminal->font_config);
vte_terminal_set_font(terminal->vte, desc);
apply_font_rendering(terminal);
}
void rt_terminal_apply_theme(RtTerminal *terminal, RtTheme *theme) {
g_return_if_fail(RT_IS_TERMINAL(terminal));
g_return_if_fail(RT_IS_THEME(theme));
if (terminal->theme) {
g_object_unref(terminal->theme);
}
terminal->theme = g_object_ref(theme);
rt_theme_apply_to_vte(terminal->theme, terminal->vte);
}
void rt_terminal_set_dpi_scale(RtTerminal *terminal, gdouble scale) {
g_return_if_fail(RT_IS_TERMINAL(terminal));
if (terminal->font_config) {
rt_font_set_dpi_scale(terminal->font_config, scale);
PangoFontDescription *desc = rt_font_get_description(terminal->font_config);
vte_terminal_set_font(terminal->vte, desc);
apply_font_rendering(terminal);
}
}
void rt_terminal_copy_clipboard(RtTerminal *terminal) {
g_return_if_fail(RT_IS_TERMINAL(terminal));
vte_terminal_copy_clipboard_format(terminal->vte, VTE_FORMAT_TEXT);
}
void rt_terminal_paste_clipboard(RtTerminal *terminal) {
g_return_if_fail(RT_IS_TERMINAL(terminal));
vte_terminal_paste_clipboard(terminal->vte);
}
void rt_terminal_reset(RtTerminal *terminal) {
g_return_if_fail(RT_IS_TERMINAL(terminal));
vte_terminal_reset(terminal->vte, TRUE, TRUE);
}
void rt_terminal_feed_child(RtTerminal *terminal, const gchar *data, gsize length) {
g_return_if_fail(RT_IS_TERMINAL(terminal));
if (data && length > 0) {
vte_terminal_feed_child(terminal->vte, data, length);
}
}
gchar* rt_terminal_get_text_range(RtTerminal *terminal, glong start, glong end) {
g_return_val_if_fail(RT_IS_TERMINAL(terminal), NULL);
(void)start;
(void)end;
return NULL;
}
VteTerminal* rt_terminal_get_vte(RtTerminal *terminal) {
g_return_val_if_fail(RT_IS_TERMINAL(terminal), NULL);
return terminal->vte;
}
const gchar* rt_terminal_get_title(RtTerminal *terminal) {
g_return_val_if_fail(RT_IS_TERMINAL(terminal), NULL);
return terminal->window_title;
}