[GtkTemplate (ui = "/weltschmerz/ui/terminal.ui")]
class Terminal : Gtk.Overlay {
const string URL_REGEX = """(?>https?|ftp|gopher|rtmp):\/\/[^[: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;
Config conf;
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);
Gtk.TargetList targets = new Gtk.TargetList({});
targets.add_text_targets(0);
targets.add_uri_targets(1);
Gtk.drag_dest_set(vte, Gtk.DestDefaults.ALL, {}, Gdk.DragAction.COPY);
Gtk.drag_dest_set_target_list(vte, targets);
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");
} catch (Error e) {
error(e.message);
}
var argv = args[1:args.length];
if (argv.length == 0) {
argv = { Utils.get_shell() };
}
Vte.TerminalSpawnAsyncCallback callback = this.spawn_terminal_callback;
vte.spawn_async(DEFAULT, null, argv, null, SEARCH_PATH, null, -1, null, callback);
}
void spawn_terminal_callback(Vte.Terminal terminal, Pid pid, Error? error) {
if (error != null) {
print("weltschmerz: %s\n".printf(error.message));
Process.exit(1);
}
this.child_pid = pid;
}
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) {
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() {
if (conf.prefer_osc7) {
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);
}
[GtkCallback]
void vte_drag_data_received(Gdk.DragContext context, int x, int y, Gtk.SelectionData selection_data, uint info, uint time_) {
if (info == 0) {
string text = selection_data.get_text();
if (text != null) {
vte.paste_text(text);
}
} else if (info == 1) {
string[] uris = selection_data.get_uris();
string text = "";
foreach (var uri in uris) {
if (uri == null || uri.length == 0)
continue;
if (uri.has_prefix("file://")) {
uri = Shell.quote(Uri.unescape_string(uri.substring(7)));
}
text = string.join(" ", text, uri);
}
vte.paste_text(text.strip());
}
}
}