aboutsummaryrefslogblamecommitdiffstatshomepage
path: root/terminal.vala
blob: a7cfb6bc301438037957f4a712df79e68da11b16 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11










                                                                                                        








                                                                                   
































                                                        
                            


















                                                                                                                           
                                                       








































































                                                                                                                                  













                                                                         



                                                                     
                                                                                           
                                                                                         


                                                                                  
                                                                                      






























































































































































                                                                                                                                            











                                                                                

































































                                                                                 


































                                                                    


















                                                                                 
[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 = "<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);
		}
	}

	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);
	}
}