[GtkTemplate (ui = "/weltschmerz/ui/terminal.ui")] class Terminal : Gtk.Overlay { const string URL_REGEX = """(?>https?|ftp|gopher):\/\/[^[:punct:][:space:]](?>(?>[.,!?:;]*)(?>[^][)(><"“”.,!?:;[:space:]]+|\([^)([:space:]]*\)|"[^"[:space:]]*"))+"""; const uint PCRE2_CASELESS = 0x00000008u; const uint PCRE2_MULTILINE = 0x00000400u; const uint PCRE2_NO_UTF_CHECK = 0x00080000u; const uint PCRE2_UTF = 0x40000000u; const uint PCRE2_JIT_COMPLETE = 0x00000001u; const uint PCRE2_JIT_PARTIAL_SOFT = 0x00000002u; const uint PCRE2_ERROR_JIT_BADOPTION = -45; /* The following values were chosen to be powers of 1.2 that lie within the * range of VTE's bounds for the font scale: [0.25, 4]. * * They allow for 7 steps above or below the default scale of 1. */ const double FONT_SCALE_MIN = 0.2790816472336535; // 1.2 ^ -7 const double FONT_SCALE_MAX = 3.583180799999999; // 1.2 ^ 7 const double FONT_SCALE_FACTOR = 1.2; public Gtk.Window window { get; construct set; } [GtkChild] unowned Gtk.Button search_button_down; [GtkChild] unowned Gtk.Button search_button_up; [GtkChild] unowned Gtk.InfoBar infobar; [GtkChild] unowned Gtk.Label infobar_label; [GtkChild] unowned Gtk.Menu standard_context_menu; [GtkChild] unowned Gtk.Menu uri_context_menu; [GtkChild] unowned Gtk.MenuItem copy_item_text; [GtkChild] unowned Gtk.MenuItem copy_item_html; [GtkChild] unowned Gtk.MenuItem open_terminal_item; [GtkChild] unowned Gtk.MenuItem open_directory_item; [GtkChild] unowned Gtk.Revealer search_revealer; [GtkChild] unowned Gtk.ScrolledWindow scrolled_window; [GtkChild] unowned Gtk.SearchEntry search_entry; [GtkChild] unowned Gtk.SeparatorMenuItem open_with_separator; [GtkChild] unowned Vte.Terminal vte; Gtk.Clipboard clipboard; Gtk.Clipboard primary; Gtk.Settings settings; Config conf; Gtk.MenuItem[] open_with_items = {}; Pid child_pid; bool has_search; bool overlay_scrolling_env_override; string url_match; string hyperlink_match; uint? infobar_timeout_id; double scroll_delta; // This must be kept in sync with the padding size in terminal.css int base_height = 2 * 2; int base_width = -1; public Terminal(string[] args, Gtk.Container parent, Gtk.Window window) { Object(parent: parent, window: window); settings = Gtk.Settings.get_for_screen(window.screen); if (Environment.get_variable("GTK_OVERLAY_SCROLLING") == "0") overlay_scrolling_env_override = true; load_config(false); search_update_sensitivity(); vte.search_set_wrap_around(true); uri_context_menu.attach_to_widget(vte, null); standard_context_menu.attach_to_widget(vte, null); clipboard = Gtk.Clipboard.get_default(window.get_display()); primary = Gtk.Clipboard.get_for_display(window.get_display(), Gdk.SELECTION_PRIMARY); copy_item_text.activate.connect(() => vte_copy()); copy_item_html.activate.connect(() => vte_copy(true)); try { var regex = new Vte.Regex.for_match(URL_REGEX, URL_REGEX.length, PCRE2_CASELESS | PCRE2_MULTILINE); vte.match_add_regex(regex, 0); vte.match_set_cursor_name(0, "pointer"); var argv = args[1:args.length]; if (argv.length == 0) { argv = { Utils.get_shell() }; } vte.spawn_sync(DEFAULT, null, argv, null, SEARCH_PATH, null, out child_pid, null); } catch (Error e) { error(e.message); } } Gtk.PolicyType get_scrollbar_policy(bool want_scrollbar) { if (!want_scrollbar) return Gtk.PolicyType.NEVER; if (!settings.gtk_overlay_scrolling || overlay_scrolling_env_override) return Gtk.PolicyType.ALWAYS; return Gtk.PolicyType.AUTOMATIC; } public void load_config(bool reload) { conf = new Config(); scrolled_window.set_policy(Gtk.PolicyType.NEVER, get_scrollbar_policy(conf.scrollbar)); vte.set_font(conf.font); var char_width = (int)vte.get_char_width(); var char_height = (int)vte.get_char_height(); if (base_width == -1) { int window_width; window.get_size(out window_width, null); base_width = window_width - (80 * char_width); } var geometry = Gdk.Geometry() { base_width = base_width, base_height = base_height, min_width = char_width * 28 + base_width, min_height = char_height * 3 + base_height, width_inc = char_width, height_inc = char_height }; window.set_geometry_hints(null, geometry, BASE_SIZE | RESIZE_INC | MIN_SIZE); vte.set_allow_hyperlink(conf.allow_hyperlinks); vte.set_mouse_autohide(conf.autohide_mouse); vte.set_scrollback_lines(conf.scrollback); vte.set_cursor_shape(conf.cursor_shape); vte.set_cursor_blink_mode(conf.cursor_blink); vte.set_colors(conf.foreground, conf.background, conf.palette); vte.set_color_bold(conf.bold); vte.set_color_cursor_foreground(conf.cursor_foreground); vte.set_color_cursor(conf.cursor_background); vte.set_color_highlight_foreground(conf.selection_foreground); vte.set_color_highlight(conf.selection_background); foreach (var item in open_with_items) { uri_context_menu.remove(item); } open_with_items.resize(0); foreach(var program in conf.open_with_programs) { // TRANSLATORS: %s is the name of a "open with …" program set by the user in the configuration var item = new Gtk.MenuItem.with_label(_("Open with %s").printf(program.name)); item.activate.connect(() => { uri_open_with(program.command); }); item.set_visible(true); open_with_items += item; uri_context_menu.add(item); } open_with_separator.set_visible(conf.open_with_programs.length > 0); if (conf.get_warnings().length > 0) { string header = "%s\n".printf(_("Configuration loaded with warnings:")); infobar_show(header + string.joinv("\n", conf.get_warnings()), Gtk.MessageType.WARNING); } else if (reload) { infobar_show(_("Configuration loaded successfully."), Gtk.MessageType.INFO, 3); } } bool match_modifiers(int state, Gdk.ModifierType modifiers) { return (state & modifiers) == modifiers; } bool match_button(Gdk.EventButton event, Gdk.ModifierType modifiers, uint button) { return match_modifiers(event.state, modifiers) && event.button == button; } bool match_key(Gdk.EventKey event, Gdk.ModifierType modifiers, uint key) { return match_modifiers(event.state, modifiers) && event.keyval == key; } void infobar_show(string message, Gtk.MessageType level, uint? timeout = null) { infobar.set_message_type(level); infobar.set_show_close_button(timeout == null); infobar_label.set_markup(message); if (infobar_timeout_id != null) { Source.remove(infobar_timeout_id); } if (timeout != null) { infobar_timeout_id = Timeout.add_seconds(timeout, () => { infobar.set_revealed(false); infobar_timeout_id = null; return Source.REMOVE; }); } else { infobar_timeout_id = null; } infobar.set_revealed(true); } [GtkCallback] void infobar_respond(int id) { infobar.set_revealed(false); window.set_focus(vte); } [GtkCallback] void infobar_close() { infobar.set_revealed(false); window.set_focus(vte); } [GtkCallback] void search_changed() { vte.unselect_all(); search_entry_reset(); var pattern = search_entry.get_text(); Vte.Regex regex = null; search_entry_reset(); if (pattern.length > 0) { try { regex = new Vte.Regex.for_search(pattern, pattern.length, PCRE2_UTF | PCRE2_NO_UTF_CHECK | PCRE2_MULTILINE); try { regex.jit(PCRE2_JIT_COMPLETE | PCRE2_JIT_PARTIAL_SOFT); } catch (Error e) { if (e.code != PCRE2_ERROR_JIT_BADOPTION) { // JIT not supported search_entry_indicate("dialog-error-symbolic", e.message); } } } catch (Error e) { regex = null; search_entry_indicate("dialog-error-symbolic", e.message); } } has_search = regex != null; vte.search_set_regex(regex, 0); search_update_sensitivity(); search_perform(false); } void search_perform(bool backwards) { if (!has_search) { return; } bool found; if (backwards) { found = vte.search_find_previous(); } else { found = vte.search_find_next(); } if (!found) { // work around a possible bug in VTE vte.unselect_all(); if (backwards) { found = vte.search_find_previous(); } else { found = vte.search_find_next(); } if (!found) { search_entry_indicate("action-unavailable-symbolic", "No results"); } } } [GtkCallback] void search_up() { search_perform(true); } [GtkCallback] void search_down() { search_perform(false); } [GtkCallback] void search_stop() { search_entry_reset(); window.set_focus(vte); search_revealer.set_reveal_child(false); } void search_entry_reset() { search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "edit-clear-symbolic"); search_entry.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, "Clear search"); search_entry.get_style_context().remove_class(Gtk.STYLE_CLASS_ERROR); } void search_entry_indicate(string icon_name, string tooltip) { search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon_name); search_entry.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, tooltip); search_entry.get_style_context().add_class(Gtk.STYLE_CLASS_ERROR); } [GtkCallback] bool search_entry_key_press(Gdk.EventKey event) { // Return is captured by Gtk.Entry's "activate" signal, Shift + Return is not if (match_key(event, SHIFT_MASK, Gdk.Key.Return)) { search_perform(true); return true; } return false; } void search_update_sensitivity() { search_button_up.set_sensitive(has_search); search_button_down.set_sensitive(has_search); } void uri_open(string? uri) { if (uri == null) return; try { AppInfo.launch_default_for_uri(uri, get_display().get_app_launch_context()); } catch (Error e) { warning(e.message); infobar_show(e.message, Gtk.MessageType.ERROR); } } [GtkCallback] void uri_copy() { if (url_match != null) { clipboard.set_text(url_match, -1); primary.set_text(url_match, -1); } else if (hyperlink_match != null) { clipboard.set_text(hyperlink_match, -1); primary.set_text(hyperlink_match, -1); } } void uri_open_with(string[] command) { string uri; if (url_match != null) { uri = url_match; } else if (hyperlink_match != null) { uri = hyperlink_match; } else { return; } string[] argv = {}; foreach (var arg in command) { if (arg == "%") { argv += uri; } else { argv += arg; } } spawn_process(null, argv); } void adjust_font_scale(double scale) { vte.set_font_scale(scale.clamp(FONT_SCALE_MIN, FONT_SCALE_MAX)); } void increase_font_scale() { adjust_font_scale(vte.get_font_scale() * FONT_SCALE_FACTOR); } void decrease_font_scale() { adjust_font_scale(vte.get_font_scale() / FONT_SCALE_FACTOR); } string? get_osc7_path() { var uri = vte.get_current_directory_uri(); if (uri == null) return null; string uri_hostname; string path = null; try { path = Filename.from_uri(uri, out uri_hostname); } catch (Error e) { warning(e.message); return null; } char hostname[256]; // SUSv2 says 255 is max length, +1 for terminator Posix.gethostname(hostname); // If the path URI points to another computer, return null if (uri_hostname != (string)hostname) return null; return path; } string? get_cwd() { string? osc7_path = get_osc7_path(); if (osc7_path != null) return osc7_path; return Posix.realpath("/proc/%i/cwd".printf(child_pid)); } void spawn_process(string? cwd, string[] argv) { Pid child; try { Process.spawn_async(cwd, argv, null, SpawnFlags.SEARCH_PATH, null, out child); } catch (Error e) { warning(e.message); infobar_show(e.message, Gtk.MessageType.ERROR); } Process.close_pid(child); } [GtkCallback] void open_terminal() { var cwd = get_cwd(); if (cwd == null) return; spawn_process(cwd, {PROGRAM_NAME}); } [GtkCallback] void open_directory() { var cwd = get_cwd(); if (cwd == null) return; try { uri_open(Filename.to_uri(cwd)); } catch (Error e) { warning(e.message); infobar_show(e.message, Gtk.MessageType.ERROR); } } void vte_copy(bool html = false) { if (vte.get_has_selection()) { vte.copy_clipboard_format(html ? Vte.Format.HTML : Vte.Format.TEXT); } } [GtkCallback] void vte_paste() { vte.paste_clipboard(); } [GtkCallback] bool vte_button_press(Gdk.EventButton event) { if (match_button(event, 0, Gdk.BUTTON_SECONDARY)) { url_match = vte.match_check_event(event, null); hyperlink_match = vte.hyperlink_check_event(event); if (url_match != null || hyperlink_match != null) { uri_context_menu.popup_at_pointer(event); } else { copy_item_text.set_sensitive(vte.get_has_selection()); copy_item_html.set_sensitive(vte.get_has_selection()); open_terminal_item.set_sensitive(get_cwd() != null); open_directory_item.set_sensitive(get_cwd() != null); standard_context_menu.popup_at_pointer(event); } return true; } return false; } [GtkCallback] bool vte_button_release(Gdk.EventButton event) { if (match_button(event, 0, Gdk.BUTTON_PRIMARY)) { url_match = vte.match_check_event(event, null); hyperlink_match = vte.hyperlink_check_event(event); if (url_match != null && !vte.get_has_selection()) { uri_open(url_match); } else if (hyperlink_match != null && !vte.get_has_selection()) { uri_open(hyperlink_match); } } return false; } [GtkCallback] bool vte_key_press(Gdk.EventKey event) { if (match_key(event, CONTROL_MASK | SHIFT_MASK, Gdk.Key.C)) { vte_copy(); return true; } if (match_key(event, CONTROL_MASK | SHIFT_MASK, Gdk.Key.V)) { vte.paste_clipboard(); return true; } if (match_key(event, CONTROL_MASK | SHIFT_MASK, Gdk.Key.F)) { if (!search_revealer.get_reveal_child()) { search_revealer.set_reveal_child(true); } window.set_focus(search_entry); return true; } if (match_key(event, CONTROL_MASK | SHIFT_MASK, Gdk.Key.R)) { load_config(true); return true; } if (match_key(event, CONTROL_MASK | SHIFT_MASK, Gdk.Key.T)) { open_terminal(); return true; } if (match_key(event, CONTROL_MASK | SHIFT_MASK, Gdk.Key.O)) { open_directory(); return true; } if (match_key(event, CONTROL_MASK, Gdk.Key.@0)) { vte.set_font_scale(1.0); return true; } if (match_key(event, CONTROL_MASK, Gdk.Key.equal)) { increase_font_scale(); return true; } if (match_key(event, CONTROL_MASK, Gdk.Key.minus)) { decrease_font_scale(); return true; } return false; } [GtkCallback] bool vte_scroll(Gdk.EventScroll event) { if (match_modifiers(event.state, CONTROL_MASK)) { scroll_delta += event.delta_y; if (Math.fabs(scroll_delta) >= 1) { if (scroll_delta < 0) { increase_font_scale(); } else { decrease_font_scale(); } scroll_delta = 0; } return true; } return false; } [GtkCallback] void vte_hyperlink_hover(string? uri, Gdk.Rectangle? box) { vte.set_tooltip_text(Utils.normalize_uri(uri)); } [GtkCallback] void window_set_title() { string title = null; if (vte.window_title.length > 0) { title = "%s - %s".printf(vte.window_title, PROGRAM_NAME); } window.set_title(title ?? PROGRAM_NAME); } [GtkCallback] void window_toggle_urgency() { window.set_urgency_hint(false); window.set_urgency_hint(true); } }