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