aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--LICENSE13
-rw-r--r--Makefile22
-rw-r--r--TODO2
-rw-r--r--config.vala108
-rw-r--r--meson.build17
-rw-r--r--resources.xml7
-rw-r--r--terminal.css8
-rw-r--r--terminal.ui233
-rw-r--r--terminal.vala387
-rw-r--r--weltschmerz.1203
-rw-r--r--weltschmerz.vala36
12 files changed, 1039 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4aad62a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+build/*
+*.c
+weltschmerz
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ef05c3f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright Wolfgang Müller <vehk@vehk.de>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..39463aa
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,22 @@
+PREFIX ?= /usr/local
+EXEC_PREFIX ?= ${PREFIX}
+BINDIR ?= ${EXEC_PREFIX}/bin
+DATAROOTDIR ?= ${PREFIX}/share
+MANDIR ?= ${DATAROOTDIR}/man
+VALAC ?= valac
+
+weltschmerz: weltschmerz.vala terminal.vala config.vala resources.c
+ ${VALAC} --pkg posix --pkg gtk+-3.0 --pkg vte-2.91 --gresources resources.xml \
+ weltschmerz.vala terminal.vala config.vala resources.c
+
+resources.c: resources.xml terminal.ui terminal.css
+ glib-compile-resources $< --target=$@ --generate-source
+
+install: weltschmerz weltschmerz.1
+ install -D -m 755 -t '${DESTDIR}${BINDIR}' weltschmerz
+ install -D -m 644 -t '${DESTDIR}${MANDIR}/man1' weltschmerz.1
+
+clean:
+ rm -f weltschmerz resources.c
+
+.PHONY: install clean
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..26cbc45
--- /dev/null
+++ b/TODO
@@ -0,0 +1,2 @@
+- Upstream recommends spawn_async() instead of spawn_sync(), but the bindings
+ for it are missing, see https://bugzilla.gnome.org/show_bug.cgi?id=784232
diff --git a/config.vala b/config.vala
new file mode 100644
index 0000000..c36d533
--- /dev/null
+++ b/config.vala
@@ -0,0 +1,108 @@
+class Config {
+
+ KeyFile? keyfile = new KeyFile();
+ string[] warnings = {};
+
+ public Config() {
+ var path = Path.build_filename(Environment.get_user_config_dir(), PROGRAM_NAME, "config");
+ try {
+ keyfile.load_from_file(path, NONE);
+ } catch (Error e) {
+ // We want to ignore a legitimately missing file, since we fall back to defaults.
+ if (!(e is FileError.NOENT)) {
+ append_warning(path + ": " + e.message);
+ keyfile = null;
+ }
+ }
+ }
+
+ public void append_warning(string message) {
+ warning(message);
+
+ warnings += "• " + Markup.escape_text(message);
+ }
+
+ void check_error(KeyFileError e) {
+ if (!(e is KeyFileError.KEY_NOT_FOUND || e is KeyFileError.GROUP_NOT_FOUND)) {
+ append_warning(e.message);
+ }
+ }
+
+ public string[] done() {
+ if (keyfile == null) {
+ return warnings;
+ }
+
+ string[] keys = {};
+ try {
+ foreach(var group in keyfile.get_groups()) {
+ foreach(var key in keyfile.get_keys(group)) {
+ keys += string.join(".", group, key);
+ }
+ }
+ } catch (KeyFileError e) {
+ // purposefully ignored
+ }
+
+ if (keys.length > 0) {
+ string k = keys.length > 1 ? "keys" : "key";
+ append_warning("Unknown %s in config: %s".printf(k, string.joinv(", ", keys)));
+ }
+
+ return warnings;
+ }
+
+ public string? value(string group, string key, string? fallback) {
+ if (keyfile == null) { return fallback; }
+
+ try {
+ string value = keyfile.get_value(group, key);
+ keyfile.remove_key(group, key);
+ return value;
+ } catch (KeyFileError e) {
+ check_error(e);
+ return fallback;
+ }
+ }
+
+ public int? integer(string group, string key, int? fallback) {
+ if (keyfile == null) { return fallback; }
+
+ try {
+ int integer = keyfile.get_integer(group, key);
+ keyfile.remove_key(group, key);
+ return integer;
+ } catch (KeyFileError e) {
+ check_error(e);
+ return fallback;
+ }
+ }
+
+ public bool? boolean(string group, string key, bool? fallback) {
+ if (keyfile == null) { return fallback; }
+
+ try {
+ bool boolean = keyfile.get_boolean(group, key);
+ keyfile.remove_key(group, key);
+ return boolean;
+ } catch (KeyFileError e) {
+ check_error(e);
+ return fallback;
+ }
+ }
+
+ public Gdk.RGBA? colour(string key, string? fallback) {
+ string value = value("colours", key, fallback);
+ if (value == null) {
+ return null;
+ }
+
+ var rgba = Gdk.RGBA();
+ if (!rgba.parse(value)) {
+ append_warning("invalid colour: " + value);
+ return null;
+ }
+
+ return rgba;
+ }
+}
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..a36157b
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,17 @@
+project('weltschmerz', 'vala', 'c')
+
+gnome = import('gnome')
+valac = meson.get_compiler('vala')
+
+dependencies = [
+ valac.find_library('posix'),
+ dependency('gtk+-3.0'),
+ dependency('vte-2.91'),
+]
+
+sources = files('weltschmerz.vala', 'terminal.vala', 'config.vala')
+sources += gnome.compile_resources('resources', 'resources.xml')
+
+executable('weltschmerz', sources, dependencies: dependencies, install: true)
+
+install_man('weltschmerz.1')
diff --git a/resources.xml b/resources.xml
new file mode 100644
index 0000000..05f7109
--- /dev/null
+++ b/resources.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/weltschmerz">
+ <file alias="ui/terminal.ui" compressed="true" preprocess="xml-stripblanks">terminal.ui</file>
+ <file alias="css/terminal.css" compressed="true">terminal.css</file>
+ </gresource>
+</gresources>
diff --git a/terminal.css b/terminal.css
new file mode 100644
index 0000000..51ead0d
--- /dev/null
+++ b/terminal.css
@@ -0,0 +1,8 @@
+vte-terminal {
+ /* This must be kept in sync with the geometry hints in terminal.vala */
+ padding: 2px;
+}
+
+scrolledwindow undershoot {
+ background-image: none;
+}
diff --git a/terminal.ui b/terminal.ui
new file mode 100644
index 0000000..7885ecc
--- /dev/null
+++ b/terminal.ui
@@ -0,0 +1,233 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <requires lib="vte-2.91" version="0.54"/>
+ <object class="GtkImage" id="copy_url_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">edit-copy</property>
+ </object>
+ <object class="GtkMenu" id="url_context_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkImageMenuItem" id="copy_url_item">
+ <property name="label" translatable="yes">_Copy URL</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">Copy the URL to the clipboard</property>
+ <property name="use_underline">True</property>
+ <property name="image">copy_url_image</property>
+ <property name="use_stock">False</property>
+ <property name="always_show_image">True</property>
+ <signal name="activate" handler="url_match_copy" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <object class="GtkImage" id="search_image_down">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">go-down-symbolic</property>
+ </object>
+ <object class="GtkImage" id="search_image_up">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">go-up-symbolic</property>
+ </object>
+ <template class="Terminal" parent="GtkOverlay">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="window_placement_set">False</property>
+ <property name="propagate_natural_width">True</property>
+ <property name="propagate_natural_height">True</property>
+ <child>
+ <object class="VteTerminal" id="vte">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscroll_policy">natural</property>
+ <property name="vscroll_policy">natural</property>
+ <property name="encoding">UTF-8</property>
+ <property name="scroll_on_keystroke">True</property>
+ <property name="scroll_on_output">False</property>
+ <signal name="bell" handler="window_toggle_urgency" swapped="no"/>
+ <signal name="button-press-event" handler="vte_button_press" swapped="no"/>
+ <signal name="button-release-event" handler="vte_button_release" swapped="no"/>
+ <signal name="child-exited" handler="gtk_main_quit" swapped="no"/>
+ <signal name="key-press-event" handler="vte_key_press" swapped="no"/>
+ <signal name="window-title-changed" handler="window_set_title" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="index">-1</property>
+ </packing>
+ </child>
+ <child type="overlay">
+ <object class="GtkRevealer" id="search_revealer">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="valign">end</property>
+ <property name="transition_type">slide-up</property>
+ <child>
+ <object class="GtkBox" id="search_container">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">5</property>
+ <property name="margin_right">5</property>
+ <property name="margin_top">5</property>
+ <property name="margin_bottom">5</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">out</property>
+ <property name="primary_icon_name">edit-find-symbolic</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">False</property>
+ <property name="placeholder_text" translatable="yes">Search...</property>
+ <signal name="activate" handler="search_down" swapped="no"/>
+ <signal name="key-press-event" handler="search_entry_key_press" swapped="no"/>
+ <signal name="next-match" handler="search_down" swapped="no"/>
+ <signal name="previous-match" handler="search_up" swapped="no"/>
+ <signal name="search-changed" handler="search_changed" swapped="no"/>
+ <signal name="stop-search" handler="search_stop" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="search_button_down">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Go to next result</property>
+ <property name="image">search_image_down</property>
+ <property name="always_show_image">True</property>
+ <signal name="clicked" handler="search_down" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="search_button_up">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Go to previous result</property>
+ <property name="image">search_image_up</property>
+ <property name="always_show_image">True</property>
+ <signal name="clicked" handler="search_up" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="index">1</property>
+ </packing>
+ </child>
+ <child type="overlay">
+ <object class="GtkInfoBar" id="infobar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">end</property>
+ <property name="show_close_button">True</property>
+ <property name="revealed">False</property>
+ <signal name="close" handler="infobar_close" swapped="no"/>
+ <signal name="response" handler="infobar_respond" swapped="no"/>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="infobar_buttons">
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <property name="layout_style">end</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child internal-child="content_area">
+ <object class="GtkBox" id="infobar_content">
+ <property name="can_focus">False</property>
+ <property name="spacing">16</property>
+ <child>
+ <object class="GtkLabel" id="infobar_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Sample Error Message</property>
+ <property name="use_markup">True</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="index">2</property>
+ </packing>
+ </child>
+ </template>
+ <object class="GtkMenu" id="standard_context_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkImageMenuItem" id="copy_item">
+ <property name="label">gtk-copy</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="vte_copy" swapped="no"/>
+ <accelerator key="c" signal="activate" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="paste_item">
+ <property name="label">gtk-paste</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="vte_paste" swapped="no"/>
+ <accelerator key="v" signal="activate" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ </object>
+</interface>
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);
+ }
+}
diff --git a/weltschmerz.1 b/weltschmerz.1
new file mode 100644
index 0000000..c66bbad
--- /dev/null
+++ b/weltschmerz.1
@@ -0,0 +1,203 @@
+.Dd January 17, 2019
+.Dt WELTSCHMERZ 1
+.Os
+.Sh NAME
+.Nm weltschmerz
+.Nd a small VTE-based terminal emulator
+.Sh SYNOPSIS
+.Nm
+.Oo
+.Ar command
+.Op Ar argument...
+.Oc
+.Sh DESCRIPTION
+.Nm
+is a terminal emulator using the VTE widget.
+It supports clickable URLs, contains basic search functionality, and can
+reload its configuration whilst running.
+.Pp
+.Nm
+executes the given command, or the program specified in the
+.Em SHELL
+environment variable if no command was given.
+.Pp
+The clipboard can be copied to and pasted from with
+.Sy CTRL + Shift + C
+and
+.Sy CTRL + Shift + V ,
+respectively.
+.Sh SEARCH OVERLAY
+The search overlay can be opened by pressing
+.Sy CTRL + Shift + F .
+The search will be updated automatically as the user types in the search bar.
+.Pp
+The key bindings for the overlay are as follows:
+.Bl -tag -width Ds
+.It Sy Enter , CTRL + G
+Go to next search result.
+.It Sy Shift + Enter , CTRL + Shift + G
+Go to previous search result.
+.It Sy Escape
+Close search overlay.
+.El
+.Sh CONFIGURATION
+Configuration is done using a configuration file.
+.Nm
+will reread that file when receiving the hangup signal (SIGHUP), or
+when the user presses
+.Sy CTRL + Shift + R .
+.Pp
+.Nm
+uses GLib's key-value file parser.
+The file format consists of key-value pairs collected in groups:
+.Bd -literal -offset indent
+[misc]
+font = Iosevka Light 16
+
+[colours]
+foreground = #000000
+background = #ffffff
+.Ed
+.Pp
+Refer to the GLib documentation for detailed information on this format.
+.Pp
+The options for the
+.Em misc
+group are as follows:
+.Bl -tag -width Ds
+.It Sy autohide-mouse
+When set to
+.Sy true ,
+the mouse cursor will be hidden once the user presses a key, and shown
+once the user moves the mouse.
+When set to
+.Sy false ,
+the mouse cursor will always be shown.
+The default is
+.Sy false .
+.It Sy cursor-shape
+Specifies the shape of the terminal cursor.
+Possible values are
+.Sy beam ,
+.Sy block ,
+and
+.Sy underline .
+The default is
+.Sy block .
+.It Sy font
+Specifies the font used to draw text, in the form of a Pango font
+description.
+Consists of one or more font families, zero or more style options, and
+the size in points (or in pixels if given a suffix of
+.Dq px ) :
+.Bd -literal -offset indent
+Monospace 12
+Iosevka, DejaVu Sans Mono bold italic 16
+Gohu GohuFont 11px
+.Ed
+.Pp
+Refer to the Pango documentation for detailed information.
+.Pp
+The default is
+.Sy Monospace 12 .
+.It Sy scrollback
+Specifies how many lines of scrollback to keep.
+A value of -1 means infinite scrollback.
+The default is
+.Sy 10000 .
+.It Sy scrollbar
+When set to
+.Sy true ,
+.Nm
+will draw a scrollbar at the right side of the terminal window.
+When set to
+.Sy false ,
+no scrollbar is drawn.
+The default is
+.Sy true .
+.El
+.Pp
+The
+.Em colours
+group contains the palette and colour overrides.
+All keys in this group take a colour representation as their value:
+.Bd -filled -offset indent
+.TS
+tab(/);
+l |l
+lb |l.
+Representation/Example value
+=
+Hexadecimal/#00ffff
+RGB/rgb(0, 255, 255)
+RGBA/rgba(0, 255, 255, 1)
+X11 colour/cyan
+.TE
+.Ed
+.Pp
+The palette defines the 16 base colours available to the terminal.
+Keys for the palette are in the form of:
+.Bd -literal -offset indent
+<colour type>.<colour name>
+.Ed
+.Pp
+The colour type is either
+.Dq normal
+or
+.Dq bright ,
+and the possible colour names along with their default representations
+are as follows:
+.Bd -filled -offset indent
+.TS
+tab(/);
+l |l |l
+lb |l |l.
+Colour name/Default (normal)/Default (bright)
+=
+black/black/grey50
+red/red3/red
+green/green3/green
+yellow/yellow3/yellow
+blue/blue2/#5c5cff
+magenta/magenta3/magenta
+cyan/cyan3/cyan
+white/grey90/white
+.TE
+.Ed
+.Pp
+The colour overrides specify which colour to use for certain parts of
+the terminal.
+The following table contains all possible keys for the colour overrides,
+along with the default behaviour if the override is not set:
+.Bd -filled -offset indent
+.TS
+tab(/);
+l |l
+lb |il.
+Key/Default
+=
+foreground/normal.white
+background/normal.black
+cursor.foreground/reverse video
+cursor.background/reverse video
+selection.foreground/reverse video
+selection.background/reverse video
+bold/inherit colour
+.TE
+.Ed
+.Sh FILES
+.Bl -tag -width Ds
+.It Em $XDG_CONFIG_HOME/weltschmerz/config
+The configuration file for
+.Nm .
+.El
+.Pp
+.Nm
+adheres to the XDG Base Directory Specification.
+If $XDG_CONFIG_HOME is unset or empty, it will default to
+.Em ~/.config
+.Sh AUTHORS
+.An -nosplit
+.Nm
+was written by
+.An Wolfgang Müller Aq Mt vehk@vehk.de
diff --git a/weltschmerz.vala b/weltschmerz.vala
new file mode 100644
index 0000000..dac2291
--- /dev/null
+++ b/weltschmerz.vala
@@ -0,0 +1,36 @@
+const string PROGRAM_NAME = "weltschmerz";
+
+void warning(string message) {
+ stderr.printf("%s: %s\n", PROGRAM_NAME, message);
+}
+
+static int main(string[] args) {
+ unowned string[]? nullargs = null;
+ Gtk.init(ref nullargs);
+
+ var window = new Gtk.Window();
+
+ window.destroy.connect(Gtk.main_quit);
+ window.set_icon_name("utilities-terminal");
+
+ var visual = window.screen.get_rgba_visual();
+ if (visual != null) {
+ window.set_visual(visual);
+ }
+
+ var css_provider = new Gtk.CssProvider();
+ css_provider.load_from_resource("/weltschmerz/css/terminal.css");
+ Gtk.StyleContext.add_provider_for_screen(window.screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER);
+
+ var terminal = new Terminal(args, window, window);
+
+ Unix.signal_add(Posix.Signal.USR1, () => {
+ terminal.load_config(true);
+ return Source.CONTINUE;
+ });
+
+ window.show_all();
+
+ Gtk.main();
+ return 0;
+}