aboutsummaryrefslogblamecommitdiffstatshomepage
path: root/terminal.vala
blob: 10059962f487e09ef5546bf9257159e0358aa65a (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|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;

	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) {
		var 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 = "<b>%s</b>\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);
	}
}