aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/terminal.vala
diff options
context:
space:
mode:
Diffstat (limited to 'terminal.vala')
-rw-r--r--terminal.vala387
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);
+ }
+}