aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/interface-gtk/interface.c
diff options
context:
space:
mode:
authorRobin Haberkorn <robin.haberkorn@googlemail.com>2021-05-30 02:38:43 +0200
committerRobin Haberkorn <robin.haberkorn@googlemail.com>2021-05-30 03:12:56 +0200
commit432ad24e382681f1c13b07e8486e91063dd96e2e (patch)
tree51838adac822767bd5884b9383cd4c72f29d3840 /src/interface-gtk/interface.c
parent524bc3960e6a6e5645ce904e20f72479e24e0a23 (diff)
downloadsciteco-432ad24e382681f1c13b07e8486e91063dd96e2e.tar.gz
THE GREAT CEEIFICATION EVENT
This is a total conversion of SciTECO to plain C (GNU C11). The chance was taken to improve a lot of internal datastructures, fix fundamental bugs and lay the foundations of future features. The GTK user interface is now in an useable state! All changes have been squashed together. The language itself has almost not changed at all, except for: * Detection of string terminators (usually Escape) now takes the string building characters into account. A string is only terminated outside of string building characters. In other words, you can now for instance write I^EQ[Hello$world]$ This removes one of the last bits of shellisms which is out of place in SciTECO where no tokenization/lexing is performed. Consequently, the current termination character can also be escaped using ^Q/^R. This is used by auto completions to make sure that strings are inserted verbatim and without unwanted sideeffects. * All strings can now safely contain null-characters (see also: 8-bit cleanliness). The null-character itself (^@) is not (yet) a valid SciTECO command, though. An incomplete list of changes: * We got rid of the BSD headers for RB trees and lists/queues. The problem with them was that they used a form of metaprogramming only to gain a bit of type safety. It also resulted in less readble code. This was a C++ desease. The new code avoids metaprogramming only to gain type safety. The BSD tree.h has been replaced by rb3ptr by Jens Stimpfle (https://github.com/jstimpfle/rb3ptr). This implementation is also more memory efficient than BSD's. The BSD list.h and queue.h has been replaced with a custom src/list.h. * Fixed crashes, performance issues and compatibility issues with the Gtk 3 User Interface. It is now more or less ready for general use. The GDK lock is no longer used to avoid using deprecated functions. On the downside, the new implementation (driving the Gtk event loop stepwise) is even slower than the old one. A few glitches remain (see TODO), but it is hoped that they will be resolved by the Scintilla update which will be performed soon. * A lot of program units have been split up, so they are shorter and easier to maintain: core-commands.c, qreg-commands.c, goto-commands.c, file-utils.h. * Parser states are simply structs of callbacks now. They still use a kind of polymorphy using a preprocessor trick. TECO_DEFINE_STATE() takes an initializer list that will be merged with the default list of field initializers. To "subclass" states, you can simply define new macros that add initializers to existing macros. * Parsers no longer have a "transitions" table but the input_cb() may use switch-case statements. There are also teco_machine_main_transition_t now which can be used to implement simple transitions. Additionally, you can specify functions to execute during transitions. This largely avoids long switch-case-statements. * Parsers are embeddable/reusable now, at least in parse-only mode. This does not currently bring any advantages but may later be used to write a Scintilla lexer for TECO syntax highlighting. Once parsers are fully embeddable, it will also be possible to run TECO macros in a kind of coroutine which would allow them to process string arguments in real time. * undo.[ch] still uses metaprogramming extensively but via the C preprocessor of course. On the downside, most undo token generators must be initiated explicitly (theoretically we could have used embedded functions / trampolines to instantiate automatically but this has turned out to be dangereous). There is a TECO_DEFINE_UNDO_CALL() to generate closures for arbitrary functions now (ie. to call an arbitrary function at undo-time). This simplified a lot of code and is much shorter than manually pushing undo tokens in many cases. * Instead of the ridiculous C++ Curiously Recurring Template Pattern to achieve static polymorphy for user interface implementations, we now simply declare all functions to implement in interface.h and link in the implementations. This is possible since we no longer hace to define interface subclasses (all state is static variables in the interface's *.c files). * Headers are now significantly shorter than in C++ since we can often hide more of our "class" implementations. * Memory counting is based on dlmalloc for most platforms now. Unfortunately, there is no malloc implementation that provides an efficient constant-time memory counter that is guaranteed to decrease when freeing memory. But since we use a defined malloc implementation now, malloc_usable_size() can be used safely for tracking memory use. malloc() replacement is very tricky on Windows, so we use a poll thread on Windows. This can also be enabled on other supported platforms using --disable-malloc-replacement. All in all, I'm still not pleased with the state of memory limiting. It is a mess. * Error handling uses GError now. This has the advantage that the GError codes can be reused once we support error catching in the SciTECO language. * Added a few more test suite cases. * Haiku is no longer supported as builds are instable and I did not manage to debug them - quite possibly Haiku bugs were responsible. * Glib v2.44 or later are now required. The GTK UI requires Gtk+ v3.12 or later now. The GtkFlowBox fallback and sciteco-wrapper workaround are no longer required. * We now extensively use the GCC/Clang-specific g_auto feature (automatic deallocations when leaving the current code block). * Updated copyright to 2021. SciTECO has been in continuous development, even though there have been no commits since 2018. * Since these changes are so significant, the target release has been set to v2.0. It is planned that beginning with v3.0, the language will be kept stable.
Diffstat (limited to 'src/interface-gtk/interface.c')
-rw-r--r--src/interface-gtk/interface.c1203
1 files changed, 1203 insertions, 0 deletions
diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c
new file mode 100644
index 0000000..afc8fe3
--- /dev/null
+++ b/src/interface-gtk/interface.c
@@ -0,0 +1,1203 @@
+/*
+ * Copyright (C) 2012-2021 Robin Haberkorn
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <stdarg.h>
+#include <string.h>
+#include <signal.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#ifdef G_OS_UNIX
+#include <glib-unix.h>
+#endif
+
+#include <gdk/gdk.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#include <gtk/gtk.h>
+
+#include <gio/gio.h>
+
+#include <Scintilla.h>
+#include <ScintillaWidget.h>
+
+#include "teco-gtk-info-popup.h"
+#include "teco-gtk-label.h"
+
+#include "sciteco.h"
+#include "error.h"
+#include "string-utils.h"
+#include "cmdline.h"
+#include "qreg.h"
+#include "ring.h"
+#include "interface.h"
+
+//#define DEBUG
+
+static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget,
+ GdkRectangle *allocation,
+ gpointer user_data);
+static gboolean teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event,
+ gpointer user_data);
+static gboolean teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event,
+ gpointer user_data);
+static gboolean teco_interface_sigterm_handler(gpointer user_data) G_GNUC_UNUSED;
+
+#define UNNAMED_FILE "(Unnamed)"
+
+#define USER_CSS_FILE ".teco_css"
+
+/** printf() format for CSS RGB colors given as guint32 */
+#define CSS_COLOR_FORMAT "#%06" G_GINT32_MODIFIER "X"
+
+/** Style used for the asterisk at the beginning of the command line */
+#define STYLE_ASTERISK 16
+
+/** Indicator number used for control characters in the command line */
+#define INDIC_CONTROLCHAR (INDIC_CONTAINER+0)
+/** Indicator number used for the rubbed out part of the command line */
+#define INDIC_RUBBEDOUT (INDIC_CONTAINER+1)
+
+/** Convert Scintilla-style BGR color triple to RGB. */
+static inline guint32
+teco_bgr2rgb(guint32 bgr)
+{
+ return GUINT32_SWAP_LE_BE(bgr) >> 8;
+}
+
+/*
+ * NOTE: The teco_view_t pointer is reused to directly
+ * point to the ScintillaObject.
+ * This saves one heap object per view.
+ */
+
+static void
+teco_view_scintilla_notify(ScintillaObject *sci, gint id,
+ struct SCNotification *notify, gpointer user_data)
+{
+ teco_interface_process_notify(notify);
+}
+
+teco_view_t *
+teco_view_new(void)
+{
+ ScintillaObject *sci = SCINTILLA(scintilla_new());
+ /*
+ * We don't want the object to be destroyed
+ * when it is removed from the vbox.
+ */
+ g_object_ref_sink(sci);
+
+ scintilla_set_id(sci, 0);
+
+ gtk_widget_set_size_request(GTK_WIDGET(sci), 500, 300);
+
+ /*
+ * This disables mouse and key events on this view.
+ * For some strange reason, masking events on
+ * the event box does NOT work.
+ *
+ * NOTE: Scroll events are still allowed - scrolling
+ * is currently not under direct control of SciTECO
+ * (i.e. it is OK the side effects of scrolling are not
+ * tracked).
+ */
+ gtk_widget_set_can_focus(GTK_WIDGET(sci), FALSE);
+ gint events = gtk_widget_get_events(GTK_WIDGET(sci));
+ events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
+ events &= ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
+ gtk_widget_set_events(GTK_WIDGET(sci), events);
+
+ g_signal_connect(sci, SCINTILLA_NOTIFY,
+ G_CALLBACK(teco_view_scintilla_notify), NULL);
+
+ return (teco_view_t *)sci;
+}
+
+static inline GtkWidget *
+teco_view_get_widget(teco_view_t *ctx)
+{
+ return GTK_WIDGET(ctx);
+}
+
+sptr_t
+teco_view_ssm(teco_view_t *ctx, unsigned int iMessage, uptr_t wParam, sptr_t lParam)
+{
+ return scintilla_send_message(SCINTILLA(ctx), iMessage, wParam, lParam);
+}
+
+static gboolean
+teco_view_free_idle_cb(gpointer user_data)
+{
+ /*
+ * This does NOT destroy the Scintilla object
+ * and GTK widget, if it is the current view
+ * (and therefore added to the vbox).
+ */
+ g_object_unref(user_data);
+ return G_SOURCE_REMOVE;
+}
+
+void
+teco_view_free(teco_view_t *ctx)
+{
+ /*
+ * FIXME: The widget is unreffed only in an idle watcher because
+ * Scintilla may have idle callbacks activated (see ScintillaGTK.cxx)
+ * and we must prevent use-after-frees.
+ * A simple g_idle_remove_by_data() does not suffice for some strange reason
+ * (perhaps it does not prevent the invocation of already activated watchers).
+ * This is a bug should better be fixed by reference counting in
+ * ScintillaGTK.cxx itself.
+ */
+ g_idle_add_full(G_PRIORITY_LOW, teco_view_free_idle_cb, SCINTILLA(ctx), NULL);
+}
+
+static struct {
+ GtkCssProvider *css_var_provider;
+
+ GtkWidget *window;
+
+ enum {
+ TECO_INFO_TYPE_BUFFER = 0,
+ TECO_INFO_TYPE_BUFFER_DIRTY,
+ TECO_INFO_TYPE_QREG
+ } info_type;
+ teco_string_t info_current;
+
+ gboolean no_csd;
+ GtkWidget *info_bar_widget;
+ GtkWidget *info_image;
+ GtkWidget *info_type_widget;
+ GtkWidget *info_name_widget;
+
+ GtkWidget *event_box_widget;
+
+ GtkWidget *message_bar_widget;
+ GtkWidget *message_widget;
+
+ teco_view_t *cmdline_view;
+
+ GtkWidget *popup_widget;
+
+ GtkWidget *current_view_widget;
+
+ GQueue *event_queue;
+} teco_interface;
+
+void
+teco_interface_init(void)
+{
+ /*
+ * gtk_init() is not necessary when using gtk_get_option_group(),
+ * but this will open the default display.
+ *
+ * FIXME: Perhaps it is possible to defer this until we initialize
+ * interactive mode!?
+ */
+ gtk_init(NULL, NULL);
+
+ /*
+ * Register clipboard registers.
+ * Unfortunately, we cannot find out which
+ * clipboards/selections are supported on this system,
+ * so we register only some default ones.
+ */
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("P"));
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("S"));
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("C"));
+
+ teco_interface.event_queue = g_queue_new();
+
+ teco_interface.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ g_signal_connect(teco_interface.window, "delete-event",
+ G_CALLBACK(teco_interface_window_delete_cb), NULL);
+
+ g_signal_connect(teco_interface.window, "key-press-event",
+ G_CALLBACK(teco_interface_key_pressed_cb), NULL);
+
+ GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+
+ /*
+ * The info bar is tried to be made the title bar of the
+ * window which also disables the default window decorations
+ * (client-side decorations) unless --no-csd was specified.
+ *
+ * NOTE: Client-side decoations could fail, leaving us with a
+ * standard title bar and the info bar with close buttons.
+ * Other window managers have undesirable side-effects.
+ */
+ teco_interface.info_bar_widget = gtk_header_bar_new();
+ gtk_widget_set_name(teco_interface.info_bar_widget, "sciteco-info-bar");
+ teco_interface.info_name_widget = teco_gtk_label_new(NULL, 0);
+ gtk_widget_set_valign(teco_interface.info_name_widget, GTK_ALIGN_CENTER);
+ /* eases writing portable fallback.css that avoids CSS element names */
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_name_widget),
+ "label");
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_name_widget),
+ "name-label");
+ gtk_label_set_selectable(GTK_LABEL(teco_interface.info_name_widget), TRUE);
+ /* NOTE: Header bar does not resize for multi-line labels */
+ //gtk_label_set_line_wrap(GTK_LABEL(teco_interface.info_name_widget), TRUE);
+ //gtk_label_set_lines(GTK_LABEL(teco_interface.info_name_widget), 2);
+ gtk_header_bar_set_custom_title(GTK_HEADER_BAR(teco_interface.info_bar_widget),
+ teco_interface.info_name_widget);
+ teco_interface.info_image = gtk_image_new();
+ gtk_header_bar_pack_start(GTK_HEADER_BAR(teco_interface.info_bar_widget),
+ teco_interface.info_image);
+ teco_interface.info_type_widget = gtk_label_new(NULL);
+ gtk_widget_set_valign(teco_interface.info_type_widget, GTK_ALIGN_CENTER);
+ /* eases writing portable fallback.css that avoids CSS element names */
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_type_widget),
+ "label");
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_type_widget),
+ "type-label");
+ gtk_header_bar_pack_start(GTK_HEADER_BAR(teco_interface.info_bar_widget),
+ teco_interface.info_type_widget);
+ if (teco_interface.no_csd) {
+ /* fall back to adding the info bar as an ordinary widget */
+ gtk_box_pack_start(GTK_BOX(vbox), teco_interface.info_bar_widget,
+ FALSE, FALSE, 0);
+ } else {
+ /* use client-side decorations */
+ gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(teco_interface.info_bar_widget), TRUE);
+ gtk_window_set_titlebar(GTK_WINDOW(teco_interface.window),
+ teco_interface.info_bar_widget);
+ }
+
+ /*
+ * Overlay widget will allow overlaying the Scintilla view
+ * and message widgets with the info popup.
+ * Therefore overlay_vbox (containing the view and popup)
+ * will be the main child of the overlay.
+ */
+ GtkWidget *overlay_widget = gtk_overlay_new();
+ GtkWidget *overlay_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+
+ /*
+ * The event box is the parent of all Scintilla views
+ * that should be displayed.
+ * This is handy when adding or removing current views,
+ * enabling and disabling GDK updates and in order to filter
+ * mouse and keyboard events going to Scintilla.
+ */
+ teco_interface.event_box_widget = gtk_event_box_new();
+ gtk_event_box_set_above_child(GTK_EVENT_BOX(teco_interface.event_box_widget), TRUE);
+ gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.event_box_widget,
+ TRUE, TRUE, 0);
+
+ teco_interface.message_bar_widget = gtk_info_bar_new();
+ gtk_widget_set_name(teco_interface.message_bar_widget, "sciteco-message-bar");
+ GtkWidget *message_bar_content =
+ gtk_info_bar_get_content_area(GTK_INFO_BAR(teco_interface.message_bar_widget));
+ /* NOTE: Messages are always pre-canonicalized */
+ teco_interface.message_widget = gtk_label_new(NULL);
+ /* eases writing portable fallback.css that avoids CSS element names */
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.message_widget),
+ "label");
+ gtk_label_set_selectable(GTK_LABEL(teco_interface.message_widget), TRUE);
+ gtk_label_set_line_wrap(GTK_LABEL(teco_interface.message_widget), TRUE);
+ gtk_container_add(GTK_CONTAINER(message_bar_content), teco_interface.message_widget);
+ gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.message_bar_widget,
+ FALSE, FALSE, 0);
+
+ gtk_container_add(GTK_CONTAINER(overlay_widget), overlay_vbox);
+ gtk_box_pack_start(GTK_BOX(vbox), overlay_widget, TRUE, TRUE, 0);
+
+ teco_interface.cmdline_view = teco_view_new();
+ teco_view_setup(teco_interface.cmdline_view);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETUNDOCOLLECTION, FALSE, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETVSCROLLBAR, FALSE, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETMARGINTYPEN, 1, SC_MARGIN_TEXT);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETSTYLE, 0, STYLE_ASTERISK);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETMARGINWIDTHN, 1,
+ teco_view_ssm(teco_interface.cmdline_view, SCI_TEXTWIDTH, STYLE_ASTERISK, (sptr_t)"*"));
+ teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETTEXT, 0, (sptr_t)"*");
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETSTYLE, INDIC_CONTROLCHAR, INDIC_ROUNDBOX);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETALPHA, INDIC_CONTROLCHAR, 128);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETSTYLE, INDIC_RUBBEDOUT, INDIC_STRIKE);
+
+ GtkWidget *cmdline_widget = teco_view_get_widget(teco_interface.cmdline_view);
+ gtk_widget_set_name(cmdline_widget, "sciteco-cmdline");
+ g_signal_connect(cmdline_widget, "size-allocate",
+ G_CALLBACK(teco_interface_cmdline_size_allocate_cb), NULL);
+ gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0);
+
+ gtk_container_add(GTK_CONTAINER(teco_interface.window), vbox);
+
+ /*
+ * Popup widget will be shown in the bottom
+ * of the overlay widget (i.e. the Scintilla views),
+ * filling the entire width.
+ */
+ teco_interface.popup_widget = teco_gtk_info_popup_new();
+ gtk_widget_set_name(teco_interface.popup_widget, "sciteco-info-popup");
+ gtk_overlay_add_overlay(GTK_OVERLAY(overlay_widget), teco_interface.popup_widget);
+ g_signal_connect(overlay_widget, "get-child-position",
+ G_CALLBACK(teco_gtk_info_popup_get_position_in_overlay), NULL);
+
+ /*
+ * FIXME: Nothing can really take the focus, so it will end up in the
+ * selectable labels unless we explicitly prevent it.
+ */
+ gtk_widget_set_can_focus(teco_interface.message_widget, FALSE);
+ gtk_widget_set_can_focus(teco_interface.info_name_widget, FALSE);
+
+ teco_cmdline_t empty_cmdline;
+ memset(&empty_cmdline, 0, sizeof(empty_cmdline));
+ teco_interface_cmdline_update(&empty_cmdline);
+}
+
+GOptionGroup *
+teco_interface_get_options(void)
+{
+ /*
+ * FIXME: On platforms where you want to disable CSD, you usually
+ * want to disable it always, so it should be configurable in the SciTECO
+ * profile.
+ * On the other hand, you could just install gtk3-nocsd.
+ */
+ static const GOptionEntry entries[] = {
+ {"no-csd", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, &teco_interface.no_csd,
+ "Disable client-side decorations.", NULL},
+ {NULL}
+ };
+
+ /*
+ * Parsing the option context with the Gtk option group
+ * will automatically initialize Gtk, but we do not yet
+ * open the default display.
+ */
+ GOptionGroup *group = gtk_get_option_group(FALSE);
+
+ g_option_group_add_entries(group, entries);
+
+ return group;
+}
+
+void teco_interface_init_color(guint color, guint32 rgb) {}
+
+void
+teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
+{
+ /*
+ * The message types are chosen such that there is a CSS class
+ * for every one of them. GTK_MESSAGE_OTHER does not have
+ * a CSS class.
+ */
+ static const GtkMessageType type2gtk[] = {
+ [TECO_MSG_USER] = GTK_MESSAGE_QUESTION,
+ [TECO_MSG_INFO] = GTK_MESSAGE_INFO,
+ [TECO_MSG_WARNING] = GTK_MESSAGE_WARNING,
+ [TECO_MSG_ERROR] = GTK_MESSAGE_ERROR
+ };
+
+ g_assert(type < G_N_ELEMENTS(type2gtk));
+
+ gchar buf[256];
+
+ /*
+ * stdio_vmsg() leaves `ap` undefined and we are expected
+ * to do the same and behave like vprintf().
+ */
+ va_list aq;
+ va_copy(aq, ap);
+ teco_interface_stdio_vmsg(type, fmt, ap);
+ g_vsnprintf(buf, sizeof(buf), fmt, aq);
+ va_end(aq);
+
+ gtk_info_bar_set_message_type(GTK_INFO_BAR(teco_interface.message_bar_widget),
+ type2gtk[type]);
+ gtk_label_set_text(GTK_LABEL(teco_interface.message_widget), buf);
+
+ if (type == TECO_MSG_ERROR)
+ gtk_widget_error_bell(teco_interface.window);
+}
+
+void
+teco_interface_msg_clear(void)
+{
+ gtk_info_bar_set_message_type(GTK_INFO_BAR(teco_interface.message_bar_widget),
+ GTK_MESSAGE_QUESTION);
+ gtk_label_set_text(GTK_LABEL(teco_interface.message_widget), "");
+}
+
+void
+teco_interface_show_view(teco_view_t *view)
+{
+ teco_interface_current_view = view;
+}
+
+static void
+teco_interface_refresh_info(void)
+{
+ GtkStyleContext *style = gtk_widget_get_style_context(teco_interface.info_bar_widget);
+
+ gtk_style_context_remove_class(style, "info-qregister");
+ gtk_style_context_remove_class(style, "info-buffer");
+ gtk_style_context_remove_class(style, "dirty");
+
+ g_auto(teco_string_t) info_current_temp;
+ teco_string_init(&info_current_temp,
+ teco_interface.info_current.data, teco_interface.info_current.len);
+ if (teco_interface.info_type == TECO_INFO_TYPE_BUFFER_DIRTY)
+ teco_string_append_c(&info_current_temp, '*');
+ teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.info_name_widget),
+ info_current_temp.data, info_current_temp.len);
+ g_autofree gchar *info_current_canon =
+ teco_string_echo(info_current_temp.data, info_current_temp.len);
+
+ const gchar *info_type_str = PACKAGE;
+ g_autoptr(GIcon) icon = NULL;
+
+ switch (teco_interface.info_type) {
+ case TECO_INFO_TYPE_QREG:
+ gtk_style_context_add_class(style, "info-qregister");
+
+ info_type_str = PACKAGE_NAME " - <QRegister> ";
+ gtk_label_set_text(GTK_LABEL(teco_interface.info_type_widget), "QRegister");
+ gtk_label_set_ellipsize(GTK_LABEL(teco_interface.info_name_widget),
+ PANGO_ELLIPSIZE_START);
+
+ /*
+ * FIXME: Perhaps we should use the SciTECO icon for Q-Registers.
+ */
+ icon = g_icon_new_for_string("emblem-generic", NULL);
+ break;
+
+ case TECO_INFO_TYPE_BUFFER_DIRTY:
+ gtk_style_context_add_class(style, "dirty");
+ /* fall through */
+ case TECO_INFO_TYPE_BUFFER:
+ gtk_style_context_add_class(style, "info-buffer");
+
+ info_type_str = PACKAGE_NAME " - <Buffer> ";
+ gtk_label_set_text(GTK_LABEL(teco_interface.info_type_widget), "Buffer");
+ gtk_label_set_ellipsize(GTK_LABEL(teco_interface.info_name_widget),
+ PANGO_ELLIPSIZE_MIDDLE);
+
+ icon = teco_gtk_info_popup_get_icon_for_path(teco_interface.info_current.data,
+ "text-x-generic");
+ break;
+ }
+
+ g_autofree gchar *title = g_strconcat(info_type_str, info_current_canon, NULL);
+ gtk_window_set_title(GTK_WINDOW(teco_interface.window), title);
+
+ if (icon) {
+ gint width, height;
+ gtk_icon_size_lookup(GTK_ICON_SIZE_LARGE_TOOLBAR, &width, &height);
+
+ gtk_image_set_from_gicon(GTK_IMAGE(teco_interface.info_image),
+ icon, GTK_ICON_SIZE_LARGE_TOOLBAR);
+ /* This is necessary so that oversized icons get scaled down. */
+ gtk_image_set_pixel_size(GTK_IMAGE(teco_interface.info_image), height);
+ }
+}
+
+void
+teco_interface_info_update_qreg(const teco_qreg_t *reg)
+{
+ teco_string_clear(&teco_interface.info_current);
+ teco_string_init(&teco_interface.info_current,
+ reg->head.name.data, reg->head.name.len);
+ teco_interface.info_type = TECO_INFO_TYPE_QREG;
+}
+
+void
+teco_interface_info_update_buffer(const teco_buffer_t *buffer)
+{
+ const gchar *filename = buffer->filename ? : UNNAMED_FILE;
+
+ teco_string_clear(&teco_interface.info_current);
+ teco_string_init(&teco_interface.info_current, filename, strlen(filename));
+ teco_interface.info_type = buffer->dirty ? TECO_INFO_TYPE_BUFFER_DIRTY
+ : TECO_INFO_TYPE_BUFFER;
+}
+
+/**
+ * Insert a single character into the command line.
+ *
+ * @fixme
+ * Control characters should be inserted verbatim since the Scintilla
+ * representations of them should be preferred.
+ * However, Scintilla would break the line on every CR/LF and there is
+ * currently no way to prevent this.
+ * Scintilla needs to be patched.
+ *
+ * @see teco_view_set_representations()
+ * @see teco_curses_format_str()
+ */
+static void
+teco_interface_cmdline_insert_c(gchar chr)
+{
+ gchar buffer[3+1] = "";
+
+ /*
+ * NOTE: This mapping is similar to teco_view_set_representations()
+ */
+ switch (chr) {
+ case '\e': strcpy(buffer, "$"); break;
+ case '\r': strcpy(buffer, "CR"); break;
+ case '\n': strcpy(buffer, "LF"); break;
+ case '\t': strcpy(buffer, "TAB"); break;
+ default:
+ if (TECO_IS_CTL(chr)) {
+ buffer[0] = '^';
+ buffer[1] = TECO_CTL_ECHO(chr);
+ buffer[2] = '\0';
+ }
+ }
+
+ if (*buffer) {
+ gsize len = strlen(buffer);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_APPENDTEXT, len, (sptr_t)buffer);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETINDICATORCURRENT, INDIC_CONTROLCHAR, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICATORFILLRANGE,
+ teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0) - len, len);
+ } else {
+ teco_view_ssm(teco_interface.cmdline_view, SCI_APPENDTEXT, 1, (sptr_t)&chr);
+ }
+}
+
+void
+teco_interface_cmdline_update(const teco_cmdline_t *cmdline)
+{
+ /*
+ * We don't know if the new command line is similar to
+ * the old one, so we can just as well rebuild it.
+ *
+ * NOTE: teco_view_ssm() already locks the GDK lock.
+ */
+ teco_view_ssm(teco_interface.cmdline_view, SCI_CLEARALL, 0, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SCROLLCARET, 0, 0);
+
+ /* format effective command line */
+ for (guint i = 0; i < cmdline->effective_len; i++)
+ teco_interface_cmdline_insert_c(cmdline->str.data[i]);
+
+ /* cursor should be after effective command line */
+ guint pos = teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_GOTOPOS, pos, 0);
+
+ /* format rubbed out command line */
+ for (guint i = cmdline->effective_len; i < cmdline->str.len; i++)
+ teco_interface_cmdline_insert_c(cmdline->str.data[i]);
+
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETINDICATORCURRENT, INDIC_RUBBEDOUT, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICATORFILLRANGE, pos,
+ teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0) - pos);
+
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SCROLLCARET, 0, 0);
+}
+
+static GdkAtom
+teco_interface_get_selection_by_name(const gchar *name)
+{
+ /*
+ * We can use gdk_atom_intern() to support arbitrary X11 selection
+ * names. However, since we cannot find out which selections are
+ * registered, we are only providing QRegisters for the three default
+ * selections.
+ * Checking them here avoids expensive X server roundtrips.
+ */
+ switch (*name) {
+ case '\0': return GDK_NONE;
+ case 'P': return GDK_SELECTION_PRIMARY;
+ case 'S': return GDK_SELECTION_SECONDARY;
+ case 'C': return GDK_SELECTION_CLIPBOARD;
+ default: break;
+ }
+
+ return gdk_atom_intern(name, FALSE);
+}
+
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, GError **error)
+{
+ GtkClipboard *clipboard = gtk_clipboard_get(teco_interface_get_selection_by_name(name));
+
+ /*
+ * NOTE: function has compatible semantics for str_len < 0.
+ */
+ gtk_clipboard_set_text(clipboard, str, str_len);
+
+ return TRUE;
+}
+
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
+{
+ GtkClipboard *clipboard = gtk_clipboard_get(teco_interface_get_selection_by_name(name));
+ /*
+ * Could return NULL for an empty clipboard.
+ *
+ * FIXME: This converts to UTF8 and we loose the ability
+ * to get clipboard with embedded nulls.
+ */
+ g_autofree gchar *contents = gtk_clipboard_wait_for_text(clipboard);
+
+ *len = contents ? strlen(contents) : 0;
+ if (str)
+ *str = g_steal_pointer(&contents);
+
+ return TRUE;
+}
+
+void
+teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len,
+ gboolean highlight)
+{
+ teco_gtk_info_popup_add(TECO_GTK_INFO_POPUP(teco_interface.popup_widget),
+ type, name, name_len, highlight);
+}
+
+void
+teco_interface_popup_show(void)
+{
+ if (gtk_widget_get_visible(teco_interface.popup_widget))
+ teco_gtk_info_popup_scroll_page(TECO_GTK_INFO_POPUP(teco_interface.popup_widget));
+ else
+ gtk_widget_show(teco_interface.popup_widget);
+}
+
+gboolean
+teco_interface_popup_is_shown(void)
+{
+ return gtk_widget_get_visible(teco_interface.popup_widget);
+}
+
+void
+teco_interface_popup_clear(void)
+{
+ if (gtk_widget_get_visible(teco_interface.popup_widget)) {
+ gtk_widget_hide(teco_interface.popup_widget);
+ teco_gtk_info_popup_clear(TECO_GTK_INFO_POPUP(teco_interface.popup_widget));
+ }
+}
+
+/**
+ * Whether the execution has been interrupted (CTRL+C).
+ *
+ * This is called regularily, so it is used to drive the
+ * main loop so that we can still process key presses.
+ *
+ * This approach is significantly slower in interactive mode
+ * than executing in a separate thread probably due to the
+ * system call overhead.
+ * But the GDK lock that would be necessary for synchronization
+ * has been deprecated.
+ */
+gboolean
+teco_interface_is_interrupted(void)
+{
+ if (gtk_main_level() > 0)
+ gtk_main_iteration_do(FALSE);
+
+ return teco_sigint_occurred != FALSE;
+}
+
+static void
+teco_interface_set_css_variables(teco_view_t *view)
+{
+ guint32 default_fg_color = teco_view_ssm(view, SCI_STYLEGETFORE, STYLE_DEFAULT, 0);
+ guint32 default_bg_color = teco_view_ssm(view, SCI_STYLEGETBACK, STYLE_DEFAULT, 0);
+ guint32 calltip_fg_color = teco_view_ssm(view, SCI_STYLEGETFORE, STYLE_CALLTIP, 0);
+ guint32 calltip_bg_color = teco_view_ssm(view, SCI_STYLEGETBACK, STYLE_CALLTIP, 0);
+
+ /*
+ * FIXME: Font and colors of Scintilla views cannot be set via CSS.
+ * But some day, there will be a way to send messages to the commandline view
+ * from SciTECO code via ES.
+ * Configuration will then be in the hands of color schemes.
+ *
+ * NOTE: We don't actually know apriori how large the font_size buffer should be,
+ * but luckily SCI_STYLEGETFONT with a sptr==0 will return only the size.
+ * This is undocumented in the Scintilla docs.
+ */
+ g_autofree gchar *font_name = g_malloc(teco_view_ssm(view, SCI_STYLEGETFONT, STYLE_DEFAULT, 0) + 1);
+ teco_view_ssm(view, SCI_STYLEGETFONT, STYLE_DEFAULT, (sptr_t)font_name);
+
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFORE, STYLE_DEFAULT, default_fg_color);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBACK, STYLE_DEFAULT, default_bg_color);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)font_name);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETSIZE, STYLE_DEFAULT,
+ teco_view_ssm(view, SCI_STYLEGETSIZE, STYLE_DEFAULT, 0));
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLECLEARALL, 0, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFORE, STYLE_CALLTIP, calltip_fg_color);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBACK, STYLE_CALLTIP, calltip_bg_color);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETCARETFORE, default_fg_color, 0);
+ /* used for the asterisk at the beginning of the command line */
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBOLD, STYLE_ASTERISK, TRUE);
+ /* used for character representations */
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETFORE, INDIC_CONTROLCHAR, default_fg_color);
+ /* used for the rubbed out command line */
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETFORE, INDIC_RUBBEDOUT, default_fg_color);
+ /* this somehow gets reset */
+ teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETTEXT, 0, (sptr_t)"*");
+
+ guint text_height = teco_view_ssm(teco_interface.cmdline_view, SCI_TEXTHEIGHT, 0, 0);
+
+ /*
+ * Generates a CSS that sets some predefined color variables.
+ * This effectively "exports" Scintilla styles into the CSS
+ * world.
+ * Those colors are used by the fallback.css shipping with SciTECO
+ * in order to apply the SciTECO-controlled color scheme to all the
+ * predefined UI elements.
+ * They can also be used in user-customizations.
+ */
+ gchar css[256];
+ g_snprintf(css, sizeof(css),
+ "@define-color sciteco_default_fg_color " CSS_COLOR_FORMAT ";"
+ "@define-color sciteco_default_bg_color " CSS_COLOR_FORMAT ";"
+ "@define-color sciteco_calltip_fg_color " CSS_COLOR_FORMAT ";"
+ "@define-color sciteco_calltip_bg_color " CSS_COLOR_FORMAT ";",
+ teco_bgr2rgb(default_fg_color), teco_bgr2rgb(default_bg_color),
+ teco_bgr2rgb(calltip_fg_color), teco_bgr2rgb(calltip_bg_color));
+
+ /*
+ * The GError and return value has been deprecated.
+ * A CSS parsing error would point to a programming
+ * error anyway.
+ */
+ gtk_css_provider_load_from_data(teco_interface.css_var_provider, css, -1, NULL);
+
+ /*
+ * The font and size of the commandline view might have changed,
+ * so we resize it.
+ * This cannot be done via CSS or Scintilla messages.
+ * Currently, it is always exactly one line high in order to mimic the Curses UI.
+ */
+ gtk_widget_set_size_request(teco_view_get_widget(teco_interface.cmdline_view), -1, text_height);
+}
+
+static gboolean
+teco_interface_handle_key_press(guint keyval, guint state, GError **error)
+{
+ teco_view_t *last_view = teco_interface_current_view;
+
+ switch (keyval) {
+ case GDK_KEY_Escape:
+ if (!teco_cmdline_keypress_c('\e', error))
+ return FALSE;
+ break;
+ case GDK_KEY_BackSpace:
+ if (!teco_cmdline_keypress_c(TECO_CTL_KEY('H'), error))
+ return FALSE;
+ break;
+ case GDK_KEY_Tab:
+ if (!teco_cmdline_keypress_c('\t', error))
+ return FALSE;
+ break;
+ case GDK_KEY_Return:
+ if (!teco_cmdline_keypress_c('\n', error))
+ return FALSE;
+ break;
+
+ /*
+ * Function key macros
+ */
+#define FN(KEY, MACRO) \
+ case GDK_KEY_##KEY: \
+ if (!teco_cmdline_fnmacro(#MACRO, error)) \
+ return FALSE; \
+ break
+#define FNS(KEY, MACRO) \
+ case GDK_KEY_##KEY: \
+ if (!teco_cmdline_fnmacro(state & GDK_SHIFT_MASK ? "S" #MACRO : #MACRO, error)) \
+ return FALSE; \
+ break
+ FN(Down, DOWN); FN(Up, UP);
+ FNS(Left, LEFT); FNS(Right, RIGHT);
+ FN(KP_Down, DOWN); FN(KP_Up, UP);
+ FNS(KP_Left, LEFT); FNS(KP_Right, RIGHT);
+ FNS(Home, HOME);
+ case GDK_KEY_F1...GDK_KEY_F35: {
+ gchar macro_name[3+1];
+
+ g_snprintf(macro_name, sizeof(macro_name),
+ "F%d", keyval - GDK_KEY_F1 + 1);
+ if (!teco_cmdline_fnmacro(macro_name, error))
+ return FALSE;
+ break;
+ }
+ FNS(Delete, DC);
+ FNS(Insert, IC);
+ FN(Page_Down, NPAGE); FN(Page_Up, PPAGE);
+ FNS(Print, PRINT);
+ FN(KP_Home, A1); FN(KP_Prior, A3);
+ FN(KP_Begin, B2);
+ FN(KP_End, C1); FN(KP_Next, C3);
+ FNS(End, END);
+ FNS(Help, HELP);
+ FN(Close, CLOSE);
+#undef FNS
+#undef FN
+
+ /*
+ * Control keys and keys with printable representation
+ */
+ default: {
+ gunichar u = gdk_keyval_to_unicode(keyval);
+
+ if (!u || g_unichar_to_utf8(u, NULL) != 1)
+ break;
+
+ gchar key;
+
+ g_unichar_to_utf8(u, &key);
+ if (key > 0x7F)
+ break;
+ if (state & GDK_CONTROL_MASK)
+ key = TECO_CTL_KEY(g_ascii_toupper(key));
+
+ if (!teco_cmdline_keypress_c(key, error))
+ return FALSE;
+ }
+ }
+
+ /*
+ * The styles configured via Scintilla might change
+ * with every keypress.
+ */
+ teco_interface_set_css_variables(teco_interface_current_view);
+
+ /*
+ * The info area is updated very often and setting the
+ * window title each time it is updated is VERY costly.
+ * So we set it here once after every keypress even if the
+ * info line did not change.
+ * View changes are also only applied here to the GTK
+ * window even though GDK updates have been frozen since
+ * the size reallocations are very costly.
+ */
+ teco_interface_refresh_info();
+
+ if (teco_interface_current_view != last_view) {
+ /*
+ * The last view's object is not guaranteed to
+ * still exist.
+ * However its widget is, due to reference counting.
+ */
+ if (teco_interface.current_view_widget)
+ gtk_container_remove(GTK_CONTAINER(teco_interface.event_box_widget),
+ teco_interface.current_view_widget);
+
+ teco_interface.current_view_widget = teco_view_get_widget(teco_interface_current_view);
+
+ gtk_container_add(GTK_CONTAINER(teco_interface.event_box_widget),
+ teco_interface.current_view_widget);
+ gtk_widget_show(teco_interface.current_view_widget);
+ }
+
+ return TRUE;
+}
+
+gboolean
+teco_interface_event_loop(GError **error)
+{
+ static const gchar *icon_files[] = {
+ SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-48.png",
+ SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-32.png",
+ SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-16.png",
+ NULL
+ };
+
+ /*
+ * Assign an icon to the window.
+ *
+ * FIXME: On Windows, it may be better to load the icon compiled
+ * as a resource into the binary.
+ */
+ GList *icon_list = NULL;
+
+ for (const gchar **file = icon_files; *file; file++) {
+ GdkPixbuf *icon_pixbuf = gdk_pixbuf_new_from_file(*file, NULL);
+
+ /* fail silently if there's a problem with one of the icons */
+ if (icon_pixbuf)
+ icon_list = g_list_append(icon_list, icon_pixbuf);
+ }
+
+ gtk_window_set_default_icon_list(icon_list);
+
+ g_list_free_full(icon_list, g_object_unref);
+
+ teco_interface_refresh_info();
+
+ /*
+ * Initialize the CSS variable provider and the CSS provider
+ * for the included fallback.css.
+ */
+ teco_interface.css_var_provider = gtk_css_provider_new();
+ if (teco_interface_current_view)
+ /* set CSS variables initially */
+ teco_interface_set_css_variables(teco_interface_current_view);
+ GdkScreen *default_screen = gdk_screen_get_default();
+ gtk_style_context_add_provider_for_screen(default_screen,
+ GTK_STYLE_PROVIDER(teco_interface.css_var_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ /* get path of $SCITECOCONFIG/.teco_css */
+ teco_qreg_t *config_path_reg = teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECOCONFIG", 14);
+ g_assert(config_path_reg != NULL);
+ g_auto(teco_string_t) config_path = {NULL, 0};
+ if (!config_path_reg->vtable->get_string(config_path_reg, &config_path.data, &config_path.len, error))
+ return FALSE;
+ if (teco_string_contains(&config_path, '\0')) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Null-character not allowed in filenames");
+ return FALSE;
+ }
+ g_autofree gchar *user_css_file = g_build_filename(config_path.data, USER_CSS_FILE, NULL);
+
+ GtkCssProvider *user_css_provider = gtk_css_provider_new();
+ /*
+ * NOTE: The return value of gtk_css_provider_load() is deprecated.
+ * Instead we could register for the "parsing-error" signal.
+ * For the time being we just silently ignore parsing errors.
+ * They will be printed to stderr by Gtk anyway.
+ */
+ if (g_file_test(user_css_file, G_FILE_TEST_IS_REGULAR))
+ /* open user CSS */
+ gtk_css_provider_load_from_path(user_css_provider, user_css_file, NULL);
+ else
+ /* use fallback CSS */
+ gtk_css_provider_load_from_path(user_css_provider,
+ SCITECODATADIR G_DIR_SEPARATOR_S "fallback.css",
+ NULL);
+ gtk_style_context_add_provider_for_screen(default_screen,
+ GTK_STYLE_PROVIDER(user_css_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+
+ /*
+ * When changing views, the new widget is not
+ * added immediately to avoid flickering in the GUI.
+ * It is only updated once per key press and only
+ * if it really changed.
+ * Therefore we must add the current view to the
+ * window initially.
+ * For the same reason, window title updates are
+ * deferred to once after every key press, so we must
+ * set the window title initially.
+ */
+ if (teco_interface_current_view) {
+ teco_interface.current_view_widget = teco_view_get_widget(teco_interface_current_view);
+ gtk_container_add(GTK_CONTAINER(teco_interface.event_box_widget),
+ teco_interface.current_view_widget);
+ }
+
+ gtk_widget_show_all(teco_interface.window);
+ /* don't show popup by default */
+ gtk_widget_hide(teco_interface.popup_widget);
+
+ /*
+ * SIGTERM emulates the "Close" key just like when
+ * closing the window if supported by this version of glib.
+ * Note that this replaces SciTECO's default SIGTERM handler
+ * so it will additionally raise(SIGINT).
+ *
+ * FIXME: On ^Z, we do not suspend properly. The window is still shown.
+ * Perhaps we should try to catch SIGTSTP?
+ * This does not work with g_unix_signal_add(), though, so any
+ * workaround would be tricky.
+ * We could create a pipe via g_unix_open_pipe() which we
+ * write to using write() in a normal signal handler.
+ * We can then add a watcher using g_unix_fd_add() which will
+ * hide the main window.
+ */
+#ifdef G_OS_UNIX
+ g_unix_signal_add(SIGTERM, teco_interface_sigterm_handler, NULL);
+#endif
+
+ gtk_main();
+
+ /*
+ * Make sure the window is hidden
+ * now already, as there may be code that has to be
+ * executed in batch mode.
+ */
+ gtk_widget_hide(teco_interface.window);
+
+ return TRUE;
+}
+
+void
+teco_interface_cleanup(void)
+{
+ teco_string_clear(&teco_interface.info_current);
+
+ if (teco_interface.window)
+ gtk_widget_destroy(teco_interface.window);
+
+ scintilla_release_resources();
+
+ if (teco_interface.event_queue)
+ g_queue_free_full(teco_interface.event_queue,
+ (GDestroyNotify)gdk_event_free);
+
+ if (teco_interface.css_var_provider)
+ g_object_unref(teco_interface.css_var_provider);
+}
+
+/*
+ * GTK+ callbacks
+ */
+
+/**
+ * Called when the commandline widget is resized.
+ * This should ensure that the caret jumps to the middle of the command line,
+ * imitating the behaviour of the current Curses command line.
+ */
+static void
+teco_interface_cmdline_size_allocate_cb(GtkWidget *widget,
+ GdkRectangle *allocation, gpointer user_data)
+{
+ /*
+ * The GDK lock is already held, so we avoid using teco_view_ssm().
+ */
+ scintilla_send_message(SCINTILLA(widget), SCI_SETXCARETPOLICY,
+ CARET_SLOP, allocation->width/2);
+}
+
+static gboolean
+teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
+{
+ g_autoptr(GError) error = NULL;
+
+#ifdef DEBUG
+ g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n",
+ event->string, *event->string,
+ event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK);
+#endif
+
+ if (teco_cmdline.pc < teco_cmdline.effective_len) {
+ /*
+ * We're already executing, so this event is processed
+ * from gtk_main_iteration_do().
+ * Unfortunately, gtk_main_level() is still 1 in this case.
+ *
+ * We might also completely replace the watchers
+ * during execution, but the current implementation is
+ * probably easier.
+ */
+ if (event->state & GDK_CONTROL_MASK &&
+ gdk_keyval_to_upper(event->keyval) == GDK_KEY_C)
+ /*
+ * Handle asynchronous interruptions if CTRL+C is pressed.
+ * This will usually send SIGINT to the entire process
+ * group and set `teco_sigint_occurred`.
+ * If the execution thread is currently blocking,
+ * the key is delivered like an ordinary key press.
+ */
+ teco_interrupt();
+ else
+ g_queue_push_tail(teco_interface.event_queue,
+ gdk_event_copy((GdkEvent *)event));
+
+ return TRUE;
+ }
+
+ g_queue_push_tail(teco_interface.event_queue, gdk_event_copy((GdkEvent *)event));
+
+ /*
+ * Avoid redraws of the current view by freezing updates
+ * on the view's GDK window (we're running in parallel
+ * to the main loop so there could be frequent redraws).
+ * By freezing updates, the behaviour is similar to
+ * the Curses UI.
+ */
+ GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window));
+ /*
+ * FIXME: A simple freeze will not suffice to prevent updates in code like <Sx$;>.
+ * gdk_window_freeze_toplevel_updates_libgtk_only() is deprecated, though.
+ * Perhaps this hack is no longer required after upgrading Scintilla.
+ *
+ * For the time being, we just live with the expected deprecation warnings,
+ * although they could theoretically be suppressed using
+ * `#pragma GCC diagnostic ignored`.
+ */
+ //gdk_window_freeze_updates(top_window);
+ gdk_window_freeze_toplevel_updates_libgtk_only(top_window);
+
+ /*
+ * The event queue might be filled when pressing keys when SciTECO
+ * is busy executing code.
+ */
+ do {
+ g_autoptr(GdkEvent) event = g_queue_pop_head(teco_interface.event_queue);
+
+ teco_sigint_occurred = FALSE;
+ teco_interface_handle_key_press(event->key.keyval, event->key.state, &error);
+ teco_sigint_occurred = FALSE;
+
+ if (g_error_matches(error, TECO_ERROR, TECO_ERROR_QUIT)) {
+ gtk_main_quit();
+ break;
+ }
+ } while (!g_queue_is_empty(teco_interface.event_queue));
+
+ gdk_window_thaw_toplevel_updates_libgtk_only(top_window);
+ //gdk_window_thaw_updates(top_window);
+
+ return TRUE;
+}
+
+static gboolean
+teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer user_data)
+{
+ /*
+ * Emulate that the "close" key was pressed
+ * which may then be handled by the execution thread
+ * which invokes the appropriate "function key macro"
+ * if it exists. Its default action will ensure that
+ * the execution thread shuts down and the main loop
+ * will eventually terminate.
+ */
+ g_autoptr(GdkEvent) close_event = gdk_event_new(GDK_KEY_PRESS);
+ close_event->key.window = gtk_widget_get_parent_window(widget);
+ close_event->key.keyval = GDK_KEY_Close;
+
+ return teco_interface_key_pressed_cb(widget, &close_event->key, NULL);
+}
+
+static gboolean
+teco_interface_sigterm_handler(gpointer user_data)
+{
+ /*
+ * Since this handler replaces the default signal handler,
+ * we also have to make sure it interrupts.
+ */
+ teco_interrupt();
+
+ /*
+ * Similar to window deletion - emulate "close" key press.
+ */
+ g_autoptr(GdkEvent) close_event = gdk_event_new(GDK_KEY_PRESS);
+ close_event->key.keyval = GDK_KEY_Close;
+
+ return teco_interface_key_pressed_cb(teco_interface.window, &close_event->key, NULL);
+}