diff options
Diffstat (limited to '')
-rw-r--r-- | terminal.vala | 387 |
1 files changed, 387 insertions, 0 deletions
diff --git a/terminal.vala b/terminal.vala new file mode 100644 index 0000000..d0f9480 --- /dev/null +++ b/terminal.vala @@ -0,0 +1,387 @@ +[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; + + 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; + + 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 = { Environment.get_variable("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 = "<b>Configuration loaded with warnings:</b>\n"; + infobar_show(header + string.joinv("\n", warnings), Gtk.MessageType.WARNING); + } else if (reload) { + infobar_show("Configuration loaded successfully.", Gtk.MessageType.INFO, 3); + } + } + + bool match_button(Gdk.EventButton event, Gdk.ModifierType modifiers, uint button) { + return (event.state & modifiers) == modifiers && event.button == button; + } + + bool match_key(Gdk.EventKey event, Gdk.ModifierType modifiers, uint key) { + return (event.state & modifiers) == 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); + } + + [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; + } + + 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); + } +} |