diff options
Diffstat (limited to '')
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | LICENSE | 13 | ||||
-rw-r--r-- | Makefile | 22 | ||||
-rw-r--r-- | TODO | 2 | ||||
-rw-r--r-- | config.vala | 108 | ||||
-rw-r--r-- | meson.build | 17 | ||||
-rw-r--r-- | resources.xml | 7 | ||||
-rw-r--r-- | terminal.css | 8 | ||||
-rw-r--r-- | terminal.ui | 233 | ||||
-rw-r--r-- | terminal.vala | 387 | ||||
-rw-r--r-- | weltschmerz.1 | 203 | ||||
-rw-r--r-- | weltschmerz.vala | 36 |
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 @@ -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 @@ -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; +} |