[GtkTemplate (ui = "/weltschmerz/ui/terminal.ui")] class Terminal : Gtk.Overlay { const string URL_REGEX = """(?>https?|ftp):\/\/[^\s\$.?#].(?>[^\s()"]*|\([^\s]*\)|"[^\s"]*")"""; 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; struct PaletteEntry { string name; string normal; string bright; } const PaletteEntry[] DEFAULT_PALETTE = { { "black", "black", "grey50" }, { "red", "red3", "red" }, { "green", "green3", "green" }, { "yellow", "yellow3", "yellow" }, { "blue", "blue2", "#5c5cff" }, { "magenta", "magenta3", "magenta" }, { "cyan", "cyan3", "cyan" }, { "white", "grey90", "white" }, }; public Gtk.Window window { get; construct set; } [GtkChild] Gtk.Button search_button_down; [GtkChild] Gtk.Button search_button_up; [GtkChild] Gtk.InfoBar infobar; [GtkChild] Gtk.Label infobar_label; [GtkChild] Gtk.Menu standard_context_menu; [GtkChild] Gtk.Menu url_context_menu; [GtkChild] Gtk.MenuItem copy_item; [GtkChild] Gtk.Revealer search_revealer; [GtkChild] Gtk.ScrolledWindow scrolled_window; [GtkChild] Gtk.SearchEntry search_entry; [GtkChild] Vte.Terminal vte; bool has_search; string url_match; uint? infobar_timeout_id; double scroll_delta; public Terminal(string[] args, Gtk.Container parent, Gtk.Window window) { Object(parent: parent, window: window); load_config(false); search_update_sensitivity(); vte.search_set_wrap_around(true); url_context_menu.attach_to_widget(vte, null); standard_context_menu.attach_to_widget(vte, null); 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 = { get_shell() }; } vte.spawn_sync(DEFAULT, null, argv, null, SEARCH_PATH, null, null, null); } catch (Error e) { error(e.message); } } public void load_config(bool reload) { var conf = new Config(); Gtk.PolicyType policy = conf.boolean("misc", "scrollbar", true) ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER; scrolled_window.set_policy(policy, policy); vte.set_font(Pango.FontDescription.from_string(conf.value("misc", "font", "Monospace 12"))); var geometry = Gdk.Geometry() { // This must be kept in sync with the padding size in terminal.css base_width = 2 * 2, base_height = 2 * 2, width_inc = (int)vte.get_char_width(), height_inc = (int)vte.get_char_height() }; window.set_geometry_hints(null, geometry, BASE_SIZE | RESIZE_INC); vte.set_mouse_autohide(conf.boolean("misc", "autohide-mouse", false)); vte.set_scrollback_lines(conf.integer("misc", "scrollback", 10000)); var cursor = conf.value("misc", "cursor-shape", "block"); switch (cursor) { case "block": vte.set_cursor_shape(BLOCK); break; case "beam": vte.set_cursor_shape(IBEAM); break; case "underline": vte.set_cursor_shape(UNDERLINE); break; default: conf.append_warning("invalid cursor '%s'".printf(cursor)); vte.set_cursor_shape(BLOCK); break; } var foreground = conf.colour("foreground", null); var background = conf.colour("background", null); var palette = new Gdk.RGBA[16]; for (int i = 0; i < DEFAULT_PALETTE.length; i++) { var entry = DEFAULT_PALETTE[i]; palette[i] = conf.colour("normal." + entry.name, entry.normal); palette[i + 8] = conf.colour("bright." + entry.name, entry.bright); } vte.set_colors(foreground, background, palette); vte.set_color_bold(conf.colour("bold", null)); vte.set_color_cursor_foreground(conf.colour("cursor.foreground", null)); vte.set_color_cursor(conf.colour("cursor.background", null)); vte.set_color_highlight_foreground(conf.colour("selection.foreground", null)); vte.set_color_highlight(conf.colour("selection.background", null)); var warnings = conf.done(); if (warnings.length > 0) { string header = "Configuration loaded with warnings:\n"; infobar_show(header + string.joinv("\n", warnings), Gtk.MessageType.WARNING); } else if (reload) { infobar_show("Configuration loaded successfully.", Gtk.MessageType.INFO, 3); } } string get_shell() { var env_shell = Environment.get_variable("SHELL"); if (env_shell != null && env_shell.length > 0){ return env_shell; } unowned Posix.Passwd pw = Posix.getpwuid(Posix.getuid()); if (pw != null && pw.pw_shell.length > 0) { return pw.pw_shell; } return "/bin/sh"; } 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 url_match_open() { try { AppInfo.launch_default_for_uri(url_match, get_display().get_app_launch_context()); } catch (Error e) { warning(e.message); infobar_show(e.message, Gtk.MessageType.ERROR); } } [GtkCallback] void url_match_copy() { if (url_match == null) { return; } Gtk.Clipboard clipboard = Gtk.Clipboard.get_default(window.get_display()); clipboard.set_text(url_match, -1); } 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); } [GtkCallback] void vte_copy() { if (vte.get_has_selection()) { vte.copy_clipboard_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); if (url_match != null) { url_context_menu.popup_at_pointer(event); } else { copy_item.set_sensitive(vte.get_has_selection()); 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); if (url_match != null && !vte.get_has_selection()) { url_match_open(); } } 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, 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 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); } }