/* svte: simple virtual terminal emulator: minimal, tabbed, VTE-based terminal * Copyright 2010 mutantturkey and svte contributors. * * This file is part of svte. * * svte is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "defaults.h" #include #include #include #include #include typedef struct window { GtkWidget *win; GtkWidget *notebook; gchar *title; } window; typedef struct term { GtkWidget *vte; GtkWidget *label; GPid pid; struct window *w; } term; typedef struct { gboolean audible_bell; gboolean autohide_mouse; gboolean allow_bold; gchar *browser_command; gchar *font; gboolean fullscreen; gint num_scrollback_lines; gboolean scroll_on_keystroke; gboolean scroll_on_output; gboolean bg_transparent; gdouble bg_saturation; gchar *bg_image; gchar *url_regex; gboolean show_tabbar; gboolean visible_bell; gint window_height; gint window_width; gchar *word_chars; GdkColor foreground; GdkColor background; GdkColor *colour_palette; gchar *cursor; } Settings; static void quit(); gboolean event_key(GtkWidget *widget, GdkEventKey *event, window *w); gboolean event_button(GtkWidget *widget, GdkEventButton *button_event); static void tab_close(VteTerminal *term, struct window *w); static char* tab_get_cwd(struct term* t); static void tab_switch(gboolean b, struct window *w); static void tab_title(); static void tab_geometry_hints(); static void tab_new(struct window *w); static void tab_togglebar(struct window *w); static void new_window(); static void tab_focus(GtkNotebook *notebook, GtkNotebookPage *page, guint page_num, struct window *w); static void set_window_title(term *t); static void launch_url(char *url); static void zoom(gboolean b, struct term *t); static inline term* get_current_term(window *w); static inline term* get_nth_term(window *w, guint page); static GQuark term_data_id = 0; static Settings *config; static gchar *config_file = NULL; const static gchar *start_program = NULL; static gboolean show_version = FALSE; static GOptionEntry options[] = { { "config", 'c', 0, G_OPTION_ARG_FILENAME, &config_file, "Path to configuration file to use.", NULL }, { "execute", 'e', 0, G_OPTION_ARG_STRING, &start_program, "execute this command.", NULL }, { "version", 'v', 0, G_OPTION_ARG_NONE, &show_version, "Print version information and exit", NULL }, { NULL } }; /* quit helper function */ static void quit() { gtk_main_quit(); } /* return the nth page */ static inline term* get_nth_term(window *w, guint page) { return (struct term*)g_object_get_qdata(G_OBJECT(gtk_notebook_get_nth_page((GtkNotebook*)w->notebook, page) ), term_data_id); } /* return current page */ static inline term* get_current_term(window *w){ gint page = gtk_notebook_get_current_page(GTK_NOTEBOOK(w->notebook)); struct term *t; t = get_nth_term(w, page); return t; } static void launch_url(char *url) { g_spawn_command_line_async(g_strconcat(config->browser_command, " ", url, NULL), NULL); } static void zoom(gboolean b, struct term *t) { int size = -2000; if(b) size = 2000; PangoFontDescription *font = vte_terminal_get_font(VTE_TERMINAL(t->vte)); pango_font_description_set_size(font, pango_font_description_get_size(font) + size); vte_terminal_set_font(VTE_TERMINAL(t->vte), font); } /* key event handler */ gboolean event_key(GtkWidget *widget, GdkEventKey *event, window *w) { guint(g) = event->keyval; if ((event->state & (GDK_CONTROL_MASK|GDK_SHIFT_MASK)) == (GDK_CONTROL_MASK|GDK_SHIFT_MASK)) { if (g == GDK_N) { new_window(); return TRUE; } if (g == GDK_T) { tab_new(w); return TRUE; } if (g == GDK_H) { tab_togglebar(w); return TRUE; } if (g == GDK_W) { tab_close(NULL, w); return TRUE; } if (g == GDK_V) { vte_terminal_paste_clipboard(VTE_TERMINAL(get_current_term(w)->vte)); return TRUE; } if (g == GDK_plus) { zoom(TRUE, get_current_term(w)); return TRUE; } if (g == GDK_underscore) { zoom(FALSE, get_current_term(w)); return TRUE; } if (g == GDK_C) { vte_terminal_copy_clipboard(VTE_TERMINAL(get_current_term(w)->vte)); return TRUE; } } if ((event->state & (GDK_MOD1_MASK) ) == (GDK_MOD1_MASK)) { if (g == GDK_Left) { tab_switch(FALSE, w); return TRUE; } if (g == GDK_Right) { tab_switch(TRUE, w); return TRUE; } if (g == GDK_F11) { if(config->fullscreen) { gtk_window_unfullscreen(GTK_WINDOW(widget)); config->fullscreen = FALSE; } else { gtk_window_fullscreen(GTK_WINDOW(widget)); config->fullscreen = TRUE; } return TRUE; } } if(g == GDK_KEY_Forward) { tab_switch(TRUE, w); return TRUE; } if(g == GDK_KEY_Back) { tab_switch(FALSE, w); return TRUE; } return FALSE; } /* button event handler */ gboolean event_button(GtkWidget *widget, GdkEventButton *button_event) { int ret = 0; gchar *match; if(button_event->button == 1) { match = vte_terminal_match_check(VTE_TERMINAL(widget), button_event->x / vte_terminal_get_char_width (VTE_TERMINAL (widget)), button_event->y / vte_terminal_get_char_height (VTE_TERMINAL (widget)), &ret); if (match) { launch_url(match); return TRUE; } } return FALSE; } /* function that closes the current window */ static void window_close(struct window *w) { gtk_widget_destroy(w->notebook); gtk_widget_destroy(w->win); g_free(w); } /* event handler for closing windows close program when all windows are closed */ static void window_destroy(GtkWidget *widget){ GList *list = gtk_window_list_toplevels(); /* 3 is 1 for the window being destroyed and 1 for the remaining tooltip widget */ if(g_list_length(list) < 3 ) { quit(); } } /* function closes the current tab */ static void tab_close(VteTerminal *term, struct window *w) { gint page = gtk_notebook_get_current_page(GTK_NOTEBOOK(w->notebook)); struct term *t = get_nth_term(w, page); gtk_notebook_remove_page(GTK_NOTEBOOK(w->notebook), page); g_free(t); if (gtk_notebook_get_n_pages(GTK_NOTEBOOK(w->notebook)) == 1) { gtk_notebook_set_show_tabs(GTK_NOTEBOOK(w->notebook), FALSE); gtk_widget_grab_focus( gtk_notebook_get_nth_page(GTK_NOTEBOOK(w->notebook), gtk_notebook_get_current_page(GTK_NOTEBOOK(w->notebook)))); } if (gtk_notebook_get_n_pages(GTK_NOTEBOOK(w->notebook)) == 0) { window_close(w); } } /* toggle the visibility of the notebook tab */ static void tab_togglebar(struct window *w) { if(gtk_notebook_get_show_tabs(GTK_NOTEBOOK(w->notebook))) { gtk_notebook_set_show_tabs(GTK_NOTEBOOK(w->notebook), FALSE); } else { if (gtk_notebook_get_n_pages(GTK_NOTEBOOK(w->notebook)) != 1) { gtk_notebook_set_show_tabs(GTK_NOTEBOOK(w->notebook), TRUE); } } } /* Retrieve the cwd of the specified term page. * Original function was from terminal-screen.c of gnome-terminal, copyright (C) 2001 Havoc Pennington * Adapted by Hong Jen Yee, non-linux shit removed by David Gómez */ static char* tab_get_cwd(struct term* t) { char *cwd = NULL; if (t->pid >= 0) { char *file; char buf[255+1]; int len; file = g_strdup_printf ("/proc/%d/cwd", t->pid); len = readlink (file, buf, sizeof (buf) - 1); if (len > 0 && buf[0] == '/') { buf[len] = '\0'; cwd = g_strdup(buf); } g_free(file); } return cwd; } /* callback for when tabs switch */ static void tab_switch(gboolean b, struct window *w) { gint(current) = gtk_notebook_get_current_page(GTK_NOTEBOOK(w->notebook)); if(b) { if (current == gtk_notebook_get_n_pages(GTK_NOTEBOOK(w->notebook)) -1 ) { current = 0; } else { current = current + 1; } } else { if (current == 0) { current = gtk_notebook_get_n_pages(GTK_NOTEBOOK(w->notebook)) - 1; } else { current = current -1; } } gtk_notebook_set_current_page(GTK_NOTEBOOK(w->notebook), current); } /* setup the whacky geometry hints for gtk */ static void tab_geometry_hints(term *t) { // I dont need to call this every time, since the char width only changes // once, maybe I'll make hints and border global and reuse them GdkGeometry hints; GtkBorder *border; gint char_width, char_height; gtk_widget_style_get(GTK_WIDGET(t->vte), "inner-border", &border, NULL); char_width = vte_terminal_get_char_width(VTE_TERMINAL(t->vte)); char_height = vte_terminal_get_char_height(VTE_TERMINAL(t->vte)); hints.min_width = char_width + border->left + border->right; hints.min_height = char_height + border->top + border->bottom; hints.base_width = border->left + border->right; hints.base_height = border->top + border->bottom; hints.width_inc = char_width; hints.height_inc = char_height; gtk_window_set_geometry_hints( GTK_WINDOW(t->w->win), GTK_WIDGET(t->vte), &hints, GDK_HINT_RESIZE_INC | GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE); } /* set a tab title */ static void tab_title(GtkWidget *widget, term *t) { gtk_label_set_text( GTK_LABEL(t->label), vte_terminal_get_window_title(VTE_TERMINAL(t->vte))); if(t == get_current_term(t->w)) { set_window_title(t); } } /* set the window title */ static void set_window_title(term *t){ gint number_of_pages = 0; const char *term_title = vte_terminal_get_window_title(VTE_TERMINAL(t->vte)); if (term_title == NULL) { term_title = "svte"; } number_of_pages = gtk_notebook_get_n_pages(GTK_NOTEBOOK(t->w->notebook)); if ( number_of_pages > 1) { char *title = g_strdup_printf("%s (%d/%d)", term_title, gtk_notebook_page_num(GTK_NOTEBOOK(t->w->notebook), t->vte) + 1, number_of_pages); gtk_window_set_title(GTK_WINDOW(t->w->win), title); g_free(title); } else { gtk_window_set_title(GTK_WINDOW(t->w->win), term_title); } } /* focus the tab */ static void tab_focus(GtkNotebook *notebook, GtkNotebookPage *page, guint page_num, struct window *w) { struct term *t; t = get_nth_term(w, page_num); set_window_title(t); } /* create a new tab */ static void tab_new(struct window *w) { term *t; int tmp; char **args = 0; // if the user declares a program to execute then just do that but otherwise // but otherwise use our default shell. if(start_program == NULL) { start_program = g_getenv("SHELL"); if (!start_program) { start_program = "sh"; } } // Execute the program but then reset start_program to null so that the start // progam will only execute one time. g_shell_parse_argv(start_program, 0, &args, 0); start_program = NULL; t = g_new0(term, 1); t->label = gtk_label_new(""); t->w = w; t->vte = vte_terminal_new(); int index = gtk_notebook_append_page(GTK_NOTEBOOK(w->notebook), t->vte, t->label); gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(w->notebook), t->vte, TRUE); if (index == 0) { gtk_notebook_set_show_tabs(GTK_NOTEBOOK(w->notebook), FALSE); vte_terminal_fork_command_full(VTE_TERMINAL(t->vte), VTE_PTY_DEFAULT, NULL, args, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &t->pid, NULL); tab_geometry_hints(t); } else { struct term *previous = get_nth_term(w, gtk_notebook_get_current_page(GTK_NOTEBOOK(w->notebook))); vte_terminal_fork_command_full(VTE_TERMINAL(t->vte), VTE_PTY_DEFAULT, tab_get_cwd(previous), args, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &t->pid, NULL); if(config->show_tabbar == TRUE) gtk_notebook_set_show_tabs(GTK_NOTEBOOK(w->notebook), TRUE); } g_object_set_qdata_full(G_OBJECT(gtk_notebook_get_nth_page( (GtkNotebook*)w->notebook, index)), term_data_id, t, NULL); g_signal_connect(G_OBJECT(t->vte), "child-exited", G_CALLBACK(tab_close), w); g_signal_connect(G_OBJECT(t->vte), "window-title-changed", G_CALLBACK(tab_title), t); g_signal_connect(G_OBJECT(t->vte), "button-press-event", G_CALLBACK(event_button), NULL); vte_terminal_set_allow_bold(VTE_TERMINAL(t->vte), config->allow_bold); vte_terminal_set_audible_bell(VTE_TERMINAL(t->vte), config->audible_bell); vte_terminal_set_background_transparent(VTE_TERMINAL(t->vte), config->bg_transparent); vte_terminal_set_background_saturation(VTE_TERMINAL(t->vte), config->bg_saturation); vte_terminal_set_background_image_file(VTE_TERMINAL(t->vte), config->bg_image); vte_terminal_set_font_from_string(VTE_TERMINAL(t->vte), config->font); vte_terminal_set_mouse_autohide(VTE_TERMINAL(t->vte), config->autohide_mouse); vte_terminal_set_scroll_on_keystroke(VTE_TERMINAL(t->vte), config->scroll_on_keystroke); vte_terminal_set_scroll_on_output(VTE_TERMINAL(t->vte), config->scroll_on_output); vte_terminal_set_scrollback_lines(VTE_TERMINAL(t->vte), config->num_scrollback_lines); vte_terminal_set_visible_bell(VTE_TERMINAL(t->vte), config->visible_bell); vte_terminal_set_word_chars(VTE_TERMINAL(t->vte), config->word_chars); vte_terminal_set_colors(VTE_TERMINAL(t->vte), &config->foreground, &config->background, config->colour_palette, DEFAULT_PALETTE_SIZE); tmp = vte_terminal_match_add_gregex( VTE_TERMINAL(t->vte), g_regex_new(config->url_regex, G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY, NULL), 0); vte_terminal_match_set_cursor_type(VTE_TERMINAL(t->vte), tmp, GDK_HAND2); gtk_widget_show_all(w->notebook); gtk_notebook_set_current_page(GTK_NOTEBOOK(w->notebook), index); gtk_widget_grab_focus(t->vte); } /* setup the main window */ static void new_window() { window *w = g_new0(window, 1); term_data_id = g_quark_from_static_string("svte"); w->notebook = gtk_notebook_new(); gtk_notebook_set_show_border(GTK_NOTEBOOK(w->notebook), FALSE); gtk_notebook_set_scrollable(GTK_NOTEBOOK(w->notebook), TRUE); w->win = gtk_window_new(GTK_WINDOW_TOPLEVEL); if (config->fullscreen) { gtk_window_fullscreen(GTK_WINDOW(w->win)); } gtk_window_set_default_size(GTK_WINDOW(w->win), config->window_width, config->window_height); gtk_container_add(GTK_CONTAINER(w->win), w->notebook); tab_new(w); gtk_widget_show_all(w->win); /* add the callback signals */ g_signal_connect(G_OBJECT(w->win), "key-press-event", G_CALLBACK(event_key), w); g_signal_connect(G_OBJECT(w->notebook), "switch-page", G_CALLBACK(tab_focus), w); g_signal_connect(G_OBJECT(w->win), "destroy", G_CALLBACK(window_destroy), NULL); set_window_title(get_current_term(w)); } /* handle the command line arguments */ static gboolean parse_command_line_options(int argc, char* argv[]) { gboolean retval = TRUE; GError *error = NULL; GOptionContext *context; context = g_option_context_new("- a simple, tabbed, VTE based terminal"); g_option_context_add_main_entries(context, options, NULL); g_option_context_add_group(context, gtk_get_option_group(TRUE)); if (!g_option_context_parse(context, &argc, &argv, &error)) { g_error("Error parsing command-line options: %s\n", error->message); retval = FALSE; } return retval; } /* parse the config file, using the Settings struct */ static void parse_config_file(gchar *config_file) { GKeyFile *keyfile; GError *error = NULL; gchar *addid; int i = 0; addid = (gchar *) g_malloc (3); keyfile = g_key_file_new(); if (!g_key_file_load_from_file(keyfile, config_file, G_KEY_FILE_NONE, &error)) { g_warning("Error parsing config file %s: %s\n", config_file, error->message); } config = g_slice_new(Settings); /* General Settings */ config->browser_command = g_key_file_get_string( keyfile, "general", "browser", NULL); /* UI Settings */ config->audible_bell = g_key_file_get_boolean( keyfile, "ui", "audible_bell", NULL); config->autohide_mouse = g_key_file_get_boolean( keyfile, "ui", "autohide_mouse", NULL); config->allow_bold = g_key_file_get_boolean( keyfile, "ui", "allow_bold", NULL); config->font = g_key_file_get_string( keyfile, "ui", "font", NULL); config->fullscreen = g_key_file_get_boolean( keyfile, "ui", "fullscreen", NULL); config->num_scrollback_lines = g_key_file_get_integer( keyfile, "ui", "num_scrollback_lines", NULL); config->scroll_on_keystroke = g_key_file_get_boolean( keyfile, "ui", "scroll_on_keystroke", NULL); config->scroll_on_output = g_key_file_get_boolean( keyfile, "ui", "scroll_on_output", NULL); config->bg_transparent = g_key_file_get_boolean( keyfile, "ui", "bg_transparent", NULL); config->bg_image = g_key_file_get_string( keyfile, "ui", "bg_image", NULL); config->bg_saturation = g_key_file_get_double( keyfile, "ui", "bg_saturation", NULL); config->url_regex = g_key_file_get_string( keyfile, "ui", "url_regex", NULL); config->show_tabbar = g_key_file_get_boolean( keyfile, "ui", "show_tabbar", NULL); config->visible_bell = g_key_file_get_boolean( keyfile, "ui", "visible_bell", NULL); config->window_height = g_key_file_get_integer( keyfile, "ui", "window_height", NULL); config->window_width = g_key_file_get_integer( keyfile, "ui", "window_width", NULL); config->word_chars = g_key_file_get_string( keyfile, "ui", "word_chars", NULL); /* Color Scheme Settings */ config->cursor = g_key_file_get_string( keyfile, "colour scheme", "cursor", NULL); config->colour_palette = (GdkColor *) g_malloc(sizeof(GdkColor) * DEFAULT_PALETTE_SIZE); for (i=0; i < DEFAULT_PALETTE_SIZE; i++){ g_snprintf(addid, 3, "%d", i); gdk_color_parse(g_key_file_get_string(keyfile, "colour scheme", addid , NULL), &config->colour_palette[i]); } if (!gdk_color_parse(g_key_file_get_string( keyfile, "colour scheme", "foreground", NULL), &config->foreground)){ gdk_color_parse(DEFAULT_FOREGROUND_COLOR, &config->foreground); g_warning("Using default foreground color"); } if (!gdk_color_parse(g_key_file_get_string( keyfile, "colour scheme", "background", NULL), &config->background)){ gdk_color_parse(DEFAULT_BACKGROUND_COLOR, &config->background); g_warning("Using default background color"); } if (NULL == config->font) { config->font = DEFAULT_FONT; } if (0 == config->window_width || 0 == config->window_height) { config->window_width = DEFAULT_WINDOW_WIDTH; config->window_height = DEFAULT_WINDOW_HEIGHT; } if(NULL == config->browser_command) { config->browser_command = DEFAULT_BROWSER_COMMAND; } if (NULL == config->url_regex) { config->url_regex = DEFAULT_URL_REGEX; } g_free(addid); g_key_file_free(keyfile); } int main(int argc, char* argv[]) { gtk_init(&argc, &argv); if(!parse_command_line_options(argc, argv)) { return 1; } if (show_version) { printf("%s \n", VERSION); return 0; } if (config_file == NULL) { config_file = DEFAULT_CONFIG_FILE; } parse_config_file(config_file); new_window(); gtk_main(); return 0; }