aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/interface-curses/interface.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/interface-curses/interface.c')
-rw-r--r--src/interface-curses/interface.c1723
1 files changed, 1723 insertions, 0 deletions
diff --git a/src/interface-curses/interface.c b/src/interface-curses/interface.c
new file mode 100644
index 0000000..821581b
--- /dev/null
+++ b/src/interface-curses/interface.c
@@ -0,0 +1,1723 @@
+/*
+ * 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 <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <locale.h>
+#include <errno.h>
+
+#ifdef HAVE_WINDOWS_H
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+
+#ifdef EMSCRIPTEN
+#include <emscripten.h>
+#endif
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include <curses.h>
+
+#ifdef HAVE_TIGETSTR
+#include <term.h>
+
+/*
+ * Some macros in term.h interfere with our code.
+ */
+#undef lines
+#endif
+
+#include <Scintilla.h>
+#include <ScintillaTerm.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "cmdline.h"
+#include "qreg.h"
+#include "ring.h"
+#include "error.h"
+#include "curses-utils.h"
+#include "curses-info-popup.h"
+#include "view.h"
+#include "memory.h"
+#include "interface.h"
+
+/**
+ * Whether we have PDCurses-only routines:
+ * Could be 0, even on PDCurses
+ */
+#ifndef PDCURSES
+#define PDCURSES 0
+#endif
+
+/**
+ * Whether we're on PDCurses/win32
+ */
+#if defined(__PDCURSES__) && defined(G_OS_WIN32) && \
+ !defined(PDCURSES_WIN32A)
+#define PDCURSES_WIN32
+
+/*
+ * A_UNDERLINE is not supported by PDCurses/win32
+ * and causes weird colors, so we simply disable it globally.
+ */
+#undef A_UNDERLINE
+#define A_UNDERLINE 0
+#endif
+
+/**
+ * Whether we're on EMCurses.
+ * Could be replaced with a configure-time check for
+ * PDC_emscripten_set_handler().
+ */
+#if defined(__PDCURSES__) && defined(EMSCRIPTEN)
+#define EMCURSES
+#endif
+
+#ifdef NCURSES_VERSION
+#ifdef G_OS_UNIX
+/** Whether we're on ncurses/UNIX. */
+#define NCURSES_UNIX
+#elif defined(G_OS_WIN32)
+/** Whether we're on ncurses/win32 console */
+#define NCURSES_WIN32
+#endif
+#endif
+
+#if defined(NCURSES_UNIX) || defined(NETBSD_CURSES)
+/**
+ * Whether Curses works on a real or pseudo TTY
+ * (ie. classic use with terminal emulators on Unix)
+ */
+#define CURSES_TTY
+#endif
+
+/*
+ * PDCurses/win32a by default assigns functions to certain
+ * keys like CTRL+V, CTRL++, CTRL+- and CTRL+=.
+ * This conflicts with SciTECO that must remain in control
+ * of keyboard processing.
+ * Unfortunately, the default mapping can only be disabled
+ * or changed via the internal PDC_set_function_key() in
+ * pdcwin.h. Therefore we declare it manually here.
+ */
+#ifdef PDCURSES_WIN32A
+int PDC_set_function_key(const unsigned function, const int new_key);
+
+#define N_FUNCTION_KEYS 5
+#define FUNCTION_KEY_SHUT_DOWN 0
+#define FUNCTION_KEY_PASTE 1
+#define FUNCTION_KEY_ENLARGE_FONT 2
+#define FUNCTION_KEY_SHRINK_FONT 3
+#define FUNCTION_KEY_CHOOSE_FONT 4
+#endif
+
+#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32)
+
+/**
+ * This handler is the Windows-analogue of a signal
+ * handler. MinGW provides signal(), but it's not
+ * reliable.
+ * This may also be used to handle CTRL_CLOSE_EVENTs.
+ * NOTE: Unlike signal handlers, this is executed in a
+ * separate thread.
+ */
+static BOOL WINAPI
+teco_console_ctrl_handler(DWORD type)
+{
+ switch (type) {
+ case CTRL_C_EVENT:
+ teco_sigint_occurred = TRUE;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+#endif
+
+static gint teco_xterm_version(void) G_GNUC_UNUSED;
+
+#define UNNAMED_FILE "(Unnamed)"
+
+/**
+ * Get bright variant of one of the 8 standard
+ * curses colors.
+ * On 8 color terminals, this returns the non-bright
+ * color - but you __may__ get a bright version using
+ * the A_BOLD attribute.
+ * NOTE: This references `COLORS` and is thus not a
+ * constant expression.
+ */
+#define COLOR_LIGHT(C) \
+ (COLORS < 16 ? (C) : (C) + 8)
+
+/*
+ * The 8 bright colors (if terminal supports at
+ * least 16 colors), else they are identical to
+ * the non-bright colors (default curses colors).
+ */
+#define COLOR_LBLACK COLOR_LIGHT(COLOR_BLACK)
+#define COLOR_LRED COLOR_LIGHT(COLOR_RED)
+#define COLOR_LGREEN COLOR_LIGHT(COLOR_GREEN)
+#define COLOR_LYELLOW COLOR_LIGHT(COLOR_YELLOW)
+#define COLOR_LBLUE COLOR_LIGHT(COLOR_BLUE)
+#define COLOR_LMAGENTA COLOR_LIGHT(COLOR_MAGENTA)
+#define COLOR_LCYAN COLOR_LIGHT(COLOR_CYAN)
+#define COLOR_LWHITE COLOR_LIGHT(COLOR_WHITE)
+
+/**
+ * Curses attribute for the color combination
+ * `f` (foreground) and `b` (background)
+ * according to the color pairs initialized by
+ * Scinterm.
+ * NOTE: This depends on the global variable
+ * `COLORS` and is thus not a constant expression.
+ */
+#define SCI_COLOR_ATTR(f, b) \
+ ((attr_t)COLOR_PAIR(SCI_COLOR_PAIR(f, b)))
+
+/**
+ * Translate a Scintilla-compatible RGB color value
+ * (0xBBGGRR) to a Curses color triple (0 to 1000
+ * for each component).
+ */
+static inline void
+teco_rgb2curses_triple(guint32 rgb, gshort *r, gshort *g, gshort *b)
+{
+ /* NOTE: We could also use 200/51 */
+ *r = ((rgb & 0x0000FF) >> 0)*1000/0xFF;
+ *g = ((rgb & 0x00FF00) >> 8)*1000/0xFF;
+ *b = ((rgb & 0xFF0000) >> 16)*1000/0xFF;
+}
+
+/**
+ * Convert a Scintilla-compatible RGB color value
+ * (0xBBGGRR) to a Curses color code (e.g. COLOR_BLACK).
+ * This does not work with arbitrary RGB values but
+ * only the 16 RGB color values defined by Scinterm
+ * corresponding to the 16 terminal colors.
+ * It is equivalent to Scinterm's internal `term_color`
+ * function.
+ */
+static gshort
+teco_rgb2curses(guint32 rgb)
+{
+ switch (rgb) {
+ case 0x000000: return COLOR_BLACK;
+ case 0x000080: return COLOR_RED;
+ case 0x008000: return COLOR_GREEN;
+ case 0x008080: return COLOR_YELLOW;
+ case 0x800000: return COLOR_BLUE;
+ case 0x800080: return COLOR_MAGENTA;
+ case 0x808000: return COLOR_CYAN;
+ case 0xC0C0C0: return COLOR_WHITE;
+ case 0x404040: return COLOR_LBLACK;
+ case 0x0000FF: return COLOR_LRED;
+ case 0x00FF00: return COLOR_LGREEN;
+ case 0x00FFFF: return COLOR_LYELLOW;
+ case 0xFF0000: return COLOR_LBLUE;
+ case 0xFF00FF: return COLOR_LMAGENTA;
+ case 0xFFFF00: return COLOR_LCYAN;
+ case 0xFFFFFF: return COLOR_LWHITE;
+ }
+
+ return COLOR_WHITE;
+}
+
+static gint
+teco_xterm_version(void)
+{
+ static gint xterm_patch = -2;
+
+ /*
+ * The XTerm patch level (version) is cached.
+ */
+ if (G_LIKELY(xterm_patch != -2))
+ return xterm_patch;
+ xterm_patch = -1;
+
+ const gchar *term = g_getenv("TERM");
+
+ if (!term || !g_str_has_prefix(term, "xterm"))
+ /* no XTerm */
+ return -1;
+
+ /*
+ * Terminal might claim to be XTerm-compatible,
+ * but this only refers to the terminfo database.
+ * XTERM_VERSION however should be sufficient to tell
+ * whether we are running under a real XTerm.
+ */
+ const gchar *xterm_version = g_getenv("XTERM_VERSION");
+ if (!xterm_version)
+ /* no XTerm */
+ return -1;
+ xterm_patch = 0;
+
+ xterm_version = strrchr(xterm_version, '(');
+ if (!xterm_version)
+ /* Invalid XTERM_VERSION, assume some XTerm */
+ return 0;
+
+ xterm_patch = atoi(xterm_version+1);
+ return xterm_patch;
+}
+
+/*
+ * NOTE: The teco_view_t pointer is reused to directly
+ * point to the Scintilla object.
+ * This saves one heap object per view.
+ */
+
+static void
+teco_view_scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data)
+{
+ teco_interface_process_notify(notify);
+}
+
+teco_view_t *
+teco_view_new(void)
+{
+ return (teco_view_t *)scintilla_new(teco_view_scintilla_notify);
+}
+
+static inline void
+teco_view_noutrefresh(teco_view_t *ctx)
+{
+ scintilla_noutrefresh((Scintilla *)ctx);
+}
+
+static inline WINDOW *
+teco_view_get_window(teco_view_t *ctx)
+{
+ return scintilla_get_window((Scintilla *)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);
+}
+
+void
+teco_view_free(teco_view_t *ctx)
+{
+ scintilla_delete((Scintilla *)ctx);
+}
+
+static struct {
+ /**
+ * Mapping of the first 16 curses color codes (that may or may not
+ * correspond with the standard terminal color codes) to
+ * Scintilla-compatible RGB values (red is LSB) to initialize after
+ * Curses startup.
+ * Negative values mean no color redefinition (keep the original
+ * palette entry).
+ */
+ gint32 color_table[16];
+
+ /**
+ * Mapping of the first 16 curses color codes to their
+ * original values for restoring them on shutdown.
+ * Unfortunately, this may not be supported on all
+ * curses ports, so this array may be unused.
+ */
+ struct {
+ gshort r, g, b;
+ } orig_color_table[16];
+
+ int stdout_orig, stderr_orig;
+ SCREEN *screen;
+ FILE *screen_tty;
+
+ WINDOW *info_window;
+ enum {
+ TECO_INFO_TYPE_BUFFER = 0,
+ TECO_INFO_TYPE_QREG
+ } info_type;
+ teco_string_t info_current;
+
+ WINDOW *msg_window;
+
+ WINDOW *cmdline_window, *cmdline_pad;
+ gsize cmdline_len, cmdline_rubout_len;
+
+ teco_curses_info_popup_t popup;
+
+ /**
+ * GError "thrown" by teco_interface_event_loop_iter().
+ * Having this in a variable avoids problems with EMScripten.
+ */
+ GError *event_loop_error;
+} teco_interface;
+
+static void teco_interface_init_color_safe(guint color, guint32 rgb);
+static void teco_interface_restore_colors(void);
+
+static void teco_interface_init_screen(void);
+static gboolean teco_interface_init_interactive(GError **error);
+static void teco_interface_restore_batch(void);
+
+static void teco_interface_init_clipboard(void);
+
+static void teco_interface_resize_all_windows(void);
+
+static void teco_interface_set_window_title(const gchar *title);
+static void teco_interface_draw_info(void);
+static void teco_interface_draw_cmdline(void);
+
+void
+teco_interface_init(void)
+{
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.color_table); i++)
+ teco_interface.color_table[i] = -1;
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.orig_color_table); i++)
+ teco_interface.orig_color_table[i].r = -1;
+
+ teco_interface.stdout_orig = teco_interface.stderr_orig = -1;
+
+ teco_curses_info_popup_init(&teco_interface.popup);
+
+ /*
+ * We must register this handler to handle
+ * asynchronous interruptions via CTRL+C
+ * reliably. The signal handler we already
+ * have won't do.
+ */
+#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32)
+ SetConsoleCtrlHandler(teco_console_ctrl_handler, TRUE);
+#endif
+
+ /*
+ * Make sure we have a string for the info line
+ * even if teco_interface_info_update() is never called.
+ */
+ teco_string_init(&teco_interface.info_current, PACKAGE_NAME, strlen(PACKAGE_NAME));
+
+ /*
+ * On all platforms except Curses/XTerm, it's
+ * safe to initialize the clipboards now.
+ */
+#ifndef CURSES_TTY
+ teco_interface_init_clipboard();
+#endif
+}
+
+GOptionGroup *
+teco_interface_get_options(void)
+{
+ return NULL;
+}
+
+static void
+teco_interface_init_color_safe(guint color, guint32 rgb)
+{
+#ifdef PDCURSES_WIN32
+ if (teco_interface.orig_color_table[color].r < 0) {
+ color_content((short)color,
+ &teco_interface.orig_color_table[color].r,
+ &teco_interface.orig_color_table[color].g,
+ &teco_interface.orig_color_table[color].b);
+ }
+#endif
+
+ gshort r, g, b;
+ teco_rgb2curses_triple(rgb, &r, &g, &b);
+ init_color((short)color, r, g, b);
+}
+
+#ifdef PDCURSES_WIN32
+
+/*
+ * On PDCurses/win32, color_content() will actually return
+ * the real console color palette - or at least the default
+ * palette when the console started.
+ */
+static void
+teco_interface_restore_colors(void)
+{
+ if (!can_change_color())
+ return;
+
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.orig_color_table); i++) {
+ if (teco_interface.orig_color_table[i].r < 0)
+ continue;
+
+ init_color((short)i,
+ teco_interface.orig_color_table[i].r,
+ teco_interface.orig_color_table[i].g,
+ teco_interface.orig_color_table[i].b);
+ }
+}
+
+#elif defined(CURSES_TTY)
+
+/*
+ * FIXME: On UNIX/ncurses teco_interface_init_color_safe() __may__
+ * change the terminal's palette permanently and there does not
+ * appear to be any portable way of restoring the original one.
+ * Curses has color_content(), but there is actually no terminal
+ * that allows querying the current palette and so color_content()
+ * will return bogus "default" values and only for the first 8 colors.
+ * It would do more damage to restore the palette returned by
+ * color_content() than it helps.
+ * xterm has the escape sequence "\e]104\a" which restores
+ * the palette from Xdefaults but not all terminal emulators
+ * claiming to be "xterm" via $TERM support this escape sequence.
+ * lxterminal for instance will print gibberish instead.
+ * So we try to look whether $XTERM_VERSION is set.
+ * There are hardly any other terminal emulators that support palette
+ * resets.
+ * The only emulator I'm aware of which can be identified reliably
+ * by $TERM supporting a palette reset is the Linux console
+ * (see console_codes(4)). The escape sequence "\e]R" is already
+ * part of its terminfo description (orig_colors capability)
+ * which is apparently sent by endwin(), so the palette is
+ * already properly restored on endwin().
+ * Welcome in Curses hell.
+ */
+static void
+teco_interface_restore_colors(void)
+{
+ if (teco_xterm_version() < 0)
+ return;
+
+ /*
+ * Looks like a real XTerm
+ */
+ fputs("\e]104\a", teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
+}
+
+#else /* !PDCURSES_WIN32 && !CURSES_TTY */
+
+static void
+teco_interface_restore_colors(void)
+{
+ /*
+ * No way to restore the palette, or it's
+ * unnecessary (e.g. XCurses)
+ */
+}
+
+#endif
+
+void
+teco_interface_init_color(guint color, guint32 rgb)
+{
+ if (color >= G_N_ELEMENTS(teco_interface.color_table))
+ return;
+
+#if defined(__PDCURSES__) && !defined(PDC_RGB)
+ /*
+ * PDCurses will usually number color codes differently
+ * (least significant bit is the blue component) while
+ * SciTECO macros will assume a standard terminal color
+ * code numbering with red as the LSB.
+ * Therefore we have to swap the bit order of the least
+ * significant 3 bits here.
+ */
+ color = (color & ~0x5) |
+ ((color & 0x1) << 2) | ((color & 0x4) >> 2);
+#endif
+
+ if (teco_interface.cmdline_window) {
+ /* interactive mode */
+ if (!can_change_color())
+ return;
+
+ teco_interface_init_color_safe(color, rgb);
+ } else {
+ /*
+ * batch mode: store colors,
+ * they can only be initialized after start_color()
+ * which is called by Scinterm when interactive
+ * mode is initialized
+ */
+ teco_interface.color_table[color] = (gint32)rgb;
+ }
+}
+
+#ifdef CURSES_TTY
+
+static void
+teco_interface_init_screen(void)
+{
+ teco_interface.screen_tty = g_fopen("/dev/tty", "r+");
+ /* should never fail */
+ g_assert(teco_interface.screen_tty != NULL);
+
+ teco_interface.screen = newterm(NULL, teco_interface.screen_tty, teco_interface.screen_tty);
+ if (!teco_interface.screen) {
+ g_fprintf(stderr, "Error initializing interactive mode. "
+ "$TERM may be incorrect.\n");
+ exit(EXIT_FAILURE);
+ }
+
+ /*
+ * If stdout or stderr would go to the terminal,
+ * redirect it. Otherwise, they are already redirected
+ * (e.g. to a file) and writing to them does not
+ * interrupt terminal interaction.
+ */
+ if (isatty(1)) {
+ teco_interface.stdout_orig = dup(1);
+ g_assert(teco_interface.stdout_orig >= 0);
+ FILE *stdout_new = g_freopen("/dev/null", "a+", stdout);
+ g_assert(stdout_new != NULL);
+ }
+ if (isatty(2)) {
+ teco_interface.stderr_orig = dup(2);
+ g_assert(teco_interface.stderr_orig >= 0);
+ FILE *stderr_new = g_freopen("/dev/null", "a+", stderr);
+ g_assert(stderr_new != NULL);
+ }
+}
+
+#elif defined(XCURSES)
+
+static void
+teco_interface_init_screen(void)
+{
+ const char *argv[] = {PACKAGE_NAME, NULL};
+
+ /*
+ * This sets the program name to "SciTECO"
+ * which may then also be used as the X11 class name
+ * for overwriting X11 resources in .Xdefaults
+ *
+ * FIXME: We could support passing in resource
+ * overrides via the SciTECO command line.
+ * But unfortunately, Xinitscr() is called too
+ * late to modify argc/argv for command-line parsing
+ * (and GOption needs to know about the additional
+ * possible arguments since they are not passed through
+ * transparently).
+ * Therefore this could only be supported by
+ * adding a special option like --resource KEY=VAL.
+ */
+ Xinitscr(1, (char **)argv);
+}
+
+#else
+
+static void
+teco_interface_init_screen(void)
+{
+ initscr();
+}
+
+#endif
+
+static gboolean
+teco_interface_init_interactive(GError **error)
+{
+ /*
+ * Curses accesses many environment variables
+ * internally. In order to be able to modify them in
+ * the SciTECO profile, we must update the process
+ * environment before initscr()/newterm().
+ * This is safe to do here since there are no threads.
+ */
+ if (!teco_qreg_table_set_environ(&teco_qreg_table_globals, error))
+ return FALSE;
+
+ /*
+ * On UNIX terminals, the escape key is usually
+ * delivered as the escape character even though function
+ * keys are delivered as escape sequences as well.
+ * That's why there has to be a timeout for detecting
+ * escape presses if function key handling is enabled.
+ * This timeout can be controlled using $ESCDELAY on
+ * ncurses but its default is much too long.
+ * We set it to 25ms as Vim does. In the very rare cases
+ * this won't suffice, $ESCDELAY can still be set explicitly.
+ *
+ * NOTE: The only terminal emulator I'm aware of that lets
+ * us send an escape sequence for the escape key is Mintty
+ * (see "\e[?7727h").
+ *
+ * FIXME: This appears to be ineffective for netbsd-curses.
+ */
+#ifdef CURSES_TTY
+ if (!g_getenv("ESCDELAY"))
+ set_escdelay(25);
+#endif
+
+ /*
+ * $TERM must be unset or "#win32con" for the win32
+ * driver to load.
+ * So we always ignore any $TERM changes by the user.
+ */
+#ifdef NCURSES_WIN32
+ g_setenv("TERM", "#win32con", TRUE);
+#endif
+
+#ifdef PDCURSES_WIN32A
+ /*
+ * Necessary to enable window resizing in Win32a port
+ */
+ PDC_set_resize_limits(25, 0xFFFF, 80, 0xFFFF);
+
+ /*
+ * Disable all magic function keys.
+ */
+ for (int i = 0; i < N_FUNCTION_KEYS; i++)
+ PDC_set_function_key(i, 0);
+
+ /*
+ * Register the special shutdown function with the
+ * CLOSE key, so closing the window behaves similar as on
+ * GTK+.
+ */
+ PDC_set_function_key(FUNCTION_KEY_SHUT_DOWN, KEY_CLOSE);
+#endif
+
+ /* for displaying UTF-8 characters properly */
+ setlocale(LC_CTYPE, "");
+
+ teco_interface_init_screen();
+
+ cbreak();
+ noecho();
+ /* Scintilla draws its own cursor */
+ curs_set(0);
+
+ teco_interface.info_window = newwin(1, 0, 0, 0);
+
+ teco_interface.msg_window = newwin(1, 0, LINES - 2, 0);
+
+ teco_interface.cmdline_window = newwin(0, 0, LINES - 1, 0);
+ keypad(teco_interface.cmdline_window, TRUE);
+
+#ifdef EMCURSES
+ nodelay(teco_interface.cmdline_window, TRUE);
+#endif
+
+ /*
+ * Will also initialize Scinterm, Curses color pairs
+ * and resizes the current view.
+ */
+ if (teco_interface_current_view)
+ teco_interface_show_view(teco_interface_current_view);
+
+ /*
+ * Only now it's safe to redefine the 16 default colors.
+ */
+ if (can_change_color()) {
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.color_table); i++) {
+ /*
+ * init_color() may still fail if COLORS < 16
+ */
+ if (teco_interface.color_table[i] >= 0)
+ teco_interface_init_color_safe(i, (guint32)teco_interface.color_table[i]);
+ }
+ }
+
+ /*
+ * Only now (in interactive mode), it's safe to initialize
+ * the clipboard Q-Registers on ncurses with a compatible terminal
+ * emulator since clipboard operations will no longer interfer
+ * with stdout.
+ */
+#ifdef CURSES_TTY
+ teco_interface_init_clipboard();
+#endif
+
+ return TRUE;
+}
+
+static void
+teco_interface_restore_batch(void)
+{
+ /*
+ * Set window title to a reasonable default,
+ * in case it is not reset immediately by the
+ * shell.
+ * FIXME: See set_window_title() why this
+ * is necessary.
+ */
+#if defined(CURSES_TTY) && defined(HAVE_TIGETSTR)
+ teco_interface_set_window_title(g_getenv("TERM") ? : "");
+#endif
+
+ /*
+ * Restore ordinary terminal behaviour
+ * (i.e. return to batch mode)
+ */
+ endwin();
+ teco_interface_restore_colors();
+
+ /*
+ * Restore stdout and stderr, so output goes to
+ * the terminal again in case we "muted" them.
+ */
+#ifdef CURSES_TTY
+ if (teco_interface.stdout_orig >= 0) {
+ int fd = dup2(teco_interface.stdout_orig, 1);
+ g_assert(fd == 1);
+ }
+ if (teco_interface.stderr_orig >= 0) {
+ int fd = dup2(teco_interface.stderr_orig, 2);
+ g_assert(fd == 2);
+ }
+#endif
+
+ /*
+ * See teco_interface_vmsg(): It looks at msg_window to determine
+ * whether we're in batch mode.
+ */
+ if (teco_interface.msg_window) {
+ delwin(teco_interface.msg_window);
+ teco_interface.msg_window = NULL;
+ }
+}
+
+static void
+teco_interface_resize_all_windows(void)
+{
+ int lines, cols; /* screen dimensions */
+
+ getmaxyx(stdscr, lines, cols);
+
+ wresize(teco_interface.info_window, 1, cols);
+ wresize(teco_view_get_window(teco_interface_current_view),
+ lines - 3, cols);
+ wresize(teco_interface.msg_window, 1, cols);
+ mvwin(teco_interface.msg_window, lines - 2, 0);
+ wresize(teco_interface.cmdline_window, 1, cols);
+ mvwin(teco_interface.cmdline_window, lines - 1, 0);
+
+ teco_interface_draw_info();
+ teco_interface_msg_clear(); /* FIXME: use saved message */
+ teco_interface_popup_clear();
+ teco_interface_draw_cmdline();
+}
+
+void
+teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
+{
+ if (!teco_interface.msg_window) { /* batch mode */
+ teco_interface_stdio_vmsg(type, fmt, ap);
+ return;
+ }
+
+ /*
+ * On most platforms we can write to stdout/stderr
+ * even in interactive mode.
+ */
+#if defined(XCURSES) || defined(PDCURSES_WIN32A) || \
+ defined(CURSES_TTY) || defined(NCURSES_WIN32)
+ va_list aq;
+ va_copy(aq, ap);
+ teco_interface_stdio_vmsg(type, fmt, aq);
+ va_end(aq);
+#endif
+
+ short fg, bg;
+
+ fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
+
+ switch (type) {
+ default:
+ case TECO_MSG_USER:
+ bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+ break;
+ case TECO_MSG_INFO:
+ bg = COLOR_GREEN;
+ break;
+ case TECO_MSG_WARNING:
+ bg = COLOR_YELLOW;
+ break;
+ case TECO_MSG_ERROR:
+ bg = COLOR_RED;
+ beep();
+ break;
+ }
+
+ wmove(teco_interface.msg_window, 0, 0);
+ wbkgdset(teco_interface.msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ vw_printw(teco_interface.msg_window, fmt, ap);
+ wclrtoeol(teco_interface.msg_window);
+}
+
+void
+teco_interface_msg_clear(void)
+{
+ if (!teco_interface.msg_window) /* batch mode */
+ return;
+
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+
+ wbkgdset(teco_interface.msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ werase(teco_interface.msg_window);
+}
+
+void
+teco_interface_show_view(teco_view_t *view)
+{
+ teco_interface_current_view = view;
+
+ if (!teco_interface.cmdline_window) /* batch mode */
+ return;
+
+ WINDOW *current_view_win = teco_view_get_window(teco_interface_current_view);
+
+ /*
+ * screen size might have changed since
+ * this view's WINDOW was last active
+ */
+ int lines, cols; /* screen dimensions */
+ getmaxyx(stdscr, lines, cols);
+ wresize(current_view_win, lines - 3, cols);
+ /* Set up window position: never changes */
+ mvwin(current_view_win, 1, 0);
+}
+
+#if PDCURSES
+
+static void
+teco_interface_set_window_title(const gchar *title)
+{
+ static gchar *last_title = NULL;
+
+ /*
+ * PDC_set_title() can result in flickering
+ * even when executed only once per pressed key,
+ * so we check whether it is really necessary to change
+ * the title.
+ * This is an issue at least with PDCurses/win32.
+ */
+ if (!g_strcmp0(title, last_title))
+ return;
+
+ PDC_set_title(title);
+
+ g_free(last_title);
+ last_title = g_strdup(title);
+}
+
+#elif defined(CURSES_TTY) && defined(HAVE_TIGETSTR)
+
+static void
+teco_interface_set_window_title(const gchar *title)
+{
+ if (!has_status_line || !to_status_line || !from_status_line)
+ return;
+
+ /*
+ * Modern terminal emulators map the window title to
+ * the historic status line.
+ * This feature is not standardized in ncurses,
+ * so we query the terminfo database.
+ * This feature may make problems with terminal emulators
+ * that do support a status line but do not map them
+ * to the window title. Some emulators (like xterm)
+ * support setting the window title via custom escape
+ * sequences and via the status line but their
+ * terminfo entry does not say so. (xterm can also
+ * save and restore window titles but there is not
+ * even a terminfo capability defined for this.)
+ * Taken the different emulator incompatibilites
+ * it may be best to make this configurable.
+ * Once we support configurable status lines,
+ * there could be a special status line that's sent
+ * to the terminal that may be set up in the profile
+ * depending on $TERM.
+ *
+ * NOTE: The terminfo manpage advises us to use putp()
+ * but on ncurses/UNIX (where terminfo is available),
+ * we do not let curses write to stdout.
+ * NOTE: This leaves the title set after we quit.
+ */
+ fputs(to_status_line, teco_interface.screen_tty);
+ fputs(title, teco_interface.screen_tty);
+ fputs(from_status_line, teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
+}
+
+#else
+
+static void
+teco_interface_set_window_title(const gchar *title)
+{
+ /* no way to set window title */
+}
+
+#endif
+
+static void
+teco_interface_draw_info(void)
+{
+ if (!teco_interface.info_window) /* batch mode */
+ return;
+
+ /*
+ * The info line is printed in reverse colors of
+ * the current buffer's STYLE_DEFAULT.
+ * The same style is used for MSG_USER messages.
+ */
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+
+ wmove(teco_interface.info_window, 0, 0);
+ wbkgdset(teco_interface.info_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+
+ const gchar *info_type_str;
+
+ switch (teco_interface.info_type) {
+ case TECO_INFO_TYPE_QREG:
+ info_type_str = PACKAGE_NAME " - <QRegister> ";
+ waddstr(teco_interface.info_window, info_type_str);
+ /* same formatting as in command lines */
+ teco_curses_format_str(teco_interface.info_window,
+ teco_interface.info_current.data,
+ teco_interface.info_current.len, -1);
+ break;
+
+ case TECO_INFO_TYPE_BUFFER:
+ info_type_str = PACKAGE_NAME " - <Buffer> ";
+ waddstr(teco_interface.info_window, info_type_str);
+ g_assert(!teco_string_contains(&teco_interface.info_current, '\0'));
+ teco_curses_format_filename(teco_interface.info_window,
+ teco_interface.info_current.data, -1);
+ break;
+
+ default:
+ g_assert_not_reached();
+ }
+
+ wclrtoeol(teco_interface.info_window);
+
+ /*
+ * Make sure the title will consist only of printable
+ * characters
+ */
+ g_autofree gchar *info_current_printable;
+ info_current_printable = teco_string_echo(teco_interface.info_current.data,
+ teco_interface.info_current.len);
+ g_autofree gchar *title = g_strconcat(info_type_str, info_current_printable, NULL);
+ teco_interface_set_window_title(title);
+}
+
+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;
+ /* NOTE: drawn in teco_interface_event_loop_iter() */
+}
+
+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_string_append_c(&teco_interface.info_current,
+ buffer->dirty ? '*' : ' ');
+ teco_interface.info_type = TECO_INFO_TYPE_BUFFER;
+ /* NOTE: drawn in teco_interface_event_loop_iter() */
+}
+
+void
+teco_interface_cmdline_update(const teco_cmdline_t *cmdline)
+{
+ /*
+ * Replace entire pre-formatted command-line.
+ * We don't know if it is similar to the last one,
+ * so resizing makes no sense.
+ * We approximate the size of the new formatted command-line,
+ * wasting a few bytes for control characters.
+ */
+ if (teco_interface.cmdline_pad)
+ delwin(teco_interface.cmdline_pad);
+
+ int max_cols = 1;
+ for (guint i = 0; i < cmdline->str.len; i++)
+ max_cols += TECO_IS_CTL(cmdline->str.data[i]) ? 3 : 1;
+ teco_interface.cmdline_pad = newpad(1, max_cols);
+
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
+ wcolor_set(teco_interface.cmdline_pad, SCI_COLOR_PAIR(fg, bg), NULL);
+
+ /* format effective command line */
+ teco_interface.cmdline_len =
+ teco_curses_format_str(teco_interface.cmdline_pad,
+ cmdline->str.data, cmdline->effective_len, -1);
+
+ /*
+ * A_BOLD should result in either a bold font or a brighter
+ * color both on 8 and 16 color terminals.
+ * This is not quite color-scheme-agnostic, but works
+ * with both the `terminal` and `solarized` themes.
+ * This problem will be gone once we use a Scintilla view
+ * as command line, since we can then define a style
+ * for rubbed out parts of the command line which will
+ * be user-configurable.
+ */
+ wattron(teco_interface.cmdline_pad, A_UNDERLINE | A_BOLD);
+
+ /*
+ * Format rubbed-out command line.
+ * NOTE: This formatting will never be truncated since we're
+ * writing into the pad which is large enough.
+ */
+ teco_interface.cmdline_rubout_len =
+ teco_curses_format_str(teco_interface.cmdline_pad, cmdline->str.data + cmdline->effective_len,
+ cmdline->str.len - cmdline->effective_len, -1);
+
+ /* highlight cursor after effective command line */
+ if (teco_interface.cmdline_rubout_len) {
+ attr_t attr = 0;
+ short pair = 0;
+
+ wmove(teco_interface.cmdline_pad, 0, teco_interface.cmdline_len);
+ wattr_get(teco_interface.cmdline_pad, &attr, &pair, NULL);
+ wchgat(teco_interface.cmdline_pad, 1,
+ (attr & A_UNDERLINE) | A_REVERSE, pair, NULL);
+ } else {
+ teco_interface.cmdline_len++;
+ wattroff(teco_interface.cmdline_pad, A_UNDERLINE | A_BOLD);
+ waddch(teco_interface.cmdline_pad, ' ' | A_REVERSE);
+ }
+
+ teco_interface_draw_cmdline();
+}
+
+static void
+teco_interface_draw_cmdline(void)
+{
+ /* total width available for command line */
+ guint total_width = getmaxx(teco_interface.cmdline_window) - 1;
+
+ /* beginning of command line to show */
+ guint disp_offset = teco_interface.cmdline_len -
+ MIN(teco_interface.cmdline_len,
+ total_width/2 + teco_interface.cmdline_len % MAX(total_width/2, 1));
+ /*
+ * length of command line to show
+ *
+ * NOTE: we do not use getmaxx(cmdline_pad) here since it may be
+ * larger than the text the pad contains.
+ */
+ guint disp_len = MIN(total_width, teco_interface.cmdline_len +
+ teco_interface.cmdline_rubout_len - disp_offset);
+
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
+
+ wbkgdset(teco_interface.cmdline_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ werase(teco_interface.cmdline_window);
+ mvwaddch(teco_interface.cmdline_window, 0, 0, '*' | A_BOLD);
+ copywin(teco_interface.cmdline_pad, teco_interface.cmdline_window,
+ 0, disp_offset, 0, 1, 0, disp_len, FALSE);
+}
+
+#ifdef __PDCURSES__
+
+/*
+ * At least on PDCurses, a single clipboard
+ * can be supported. We register it as the
+ * default clipboard ("~") as we do not know whether
+ * it corresponds to the X11 PRIMARY, SECONDARY or
+ * CLIPBOARD selections.
+ */
+static void
+teco_interface_init_clipboard(void)
+{
+ char *contents;
+ long length;
+
+ /*
+ * Even on PDCurses, while the clipboard functions are
+ * available, the clipboard might not actually be supported.
+ * Since the existence of the QReg serves as an indication
+ * of clipboard support in SciTECO, we must first probe the
+ * usability of the clipboard.
+ * This could be done at compile time, but this way is more
+ * generic (albeit inefficient).
+ */
+ int rc = PDC_getclipboard(&contents, &length);
+ if (rc == PDC_CLIP_ACCESS_ERROR)
+ return;
+ if (rc == PDC_CLIP_SUCCESS)
+ PDC_freeclipboard(contents);
+
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
+}
+
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, GError **error)
+{
+ int rc = str ? PDC_setclipboard(str, str_len) : PDC_clearclipboard();
+ if (rc != PDC_CLIP_SUCCESS) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Error %d copying to clipboard", rc);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
+{
+ char *contents;
+ long length = 0;
+
+ /*
+ * NOTE: It is undefined whether we can pass in NULL for length.
+ */
+ int rc = PDC_getclipboard(&contents, &length);
+ *len = length;
+ if (rc == PDC_CLIP_EMPTY)
+ return TRUE;
+ if (rc != PDC_CLIP_SUCCESS) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Error %d retrieving clipboard", rc);
+ return FALSE;
+ }
+
+ /*
+ * PDCurses defines its own free function and there is no
+ * way to find out which allocator was used.
+ * We must therefore copy the memory to be on the safe side.
+ * At least, the result is guaranteed to be null-terminated
+ * and thus teco_string_t-compatible
+ * (PDCurses does not guarantee that either).
+ */
+ if (str) {
+ *str = memcpy(g_malloc(length + 1), contents, length);
+ (*str)[length] = '\0';
+ }
+
+ PDC_freeclipboard(contents);
+ return TRUE;
+}
+
+#elif defined(CURSES_TTY)
+
+static void
+teco_interface_init_clipboard(void)
+{
+ /*
+ * At least on XTerm, there are escape sequences
+ * for modifying the clipboard (OSC-52).
+ * This is not standardized in terminfo, so we add special
+ * XTerm support here. Unfortunately, it is pretty hard to find out
+ * whether clipboard operations will actually work.
+ * XTerm must be at least at v203 and the corresponding window operations
+ * must be enabled.
+ * There is no way to find out if they are but we must
+ * not register the clipboard registers if they aren't.
+ * Therefore, a special XTerm clipboard ED flag an be set by the user.
+ */
+ if (!(teco_ed & TECO_ED_XTERM_CLIPBOARD) || teco_xterm_version() < 203)
+ return;
+
+ 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"));
+}
+
+static inline gchar
+get_selection_by_name(const gchar *name)
+{
+ /*
+ * Only the first letter of name is significant.
+ * We allow to address the XTerm cut buffers as well
+ * (everything gets passed down), but currently we
+ * only register the three standard registers
+ * "~", "~P", "~S" and "~C".
+ */
+ return g_ascii_tolower(*name) ? : 'c';
+}
+
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len,
+ GError **error)
+{
+ fputs("\e]52;", teco_interface.screen_tty);
+ fputc(get_selection_by_name(name), teco_interface.screen_tty);
+ fputc(';', teco_interface.screen_tty);
+
+ /*
+ * Enough space for 1024 Base64-encoded bytes.
+ */
+ gchar buffer[(1024 / 3) * 4 + 4];
+ gsize out_len;
+ /* g_base64_encode_step() state: */
+ gint state = 0, save = 0;
+
+ while (str_len > 0) {
+ gsize step_len = MIN(1024, str_len);
+
+ /*
+ * This could be simplified using g_base64_encode().
+ * However, doing it step-wise avoids an allocation.
+ */
+ out_len = g_base64_encode_step((const guchar *)str,
+ step_len, FALSE,
+ buffer, &state, &save);
+ fwrite(buffer, 1, out_len, teco_interface.screen_tty);
+
+ str_len -= step_len;
+ str += step_len;
+ }
+
+ out_len = g_base64_encode_close(FALSE, buffer, &state, &save);
+ fwrite(buffer, 1, out_len, teco_interface.screen_tty);
+
+ fputc('\a', teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
+
+ return TRUE;
+}
+
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
+{
+ /*
+ * Query the clipboard -- XTerm will reply with the
+ * OSC-52 command that would set the current selection.
+ */
+ fputs("\e]52;", teco_interface.screen_tty);
+ fputc(get_selection_by_name(name), teco_interface.screen_tty);
+ fputs(";?\a", teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
+
+ /*
+ * It is very well possible that the XTerm clipboard
+ * is not working because it is disabled, so we
+ * must be prepared for timeouts when reading.
+ * That's why we're using the Curses API here, instead
+ * of accessing screen_tty directly. It gives us a relatively
+ * simple way to read with timeouts.
+ * We restore all changed Curses settings before returning
+ * to be on the safe side.
+ */
+ halfdelay(1); /* 100ms timeout */
+ keypad(stdscr, FALSE);
+
+ /*
+ * Skip "\e]52;x;" (7 characters).
+ */
+ for (gint i = 0; i < 7; i++) {
+ if (getch() == ERR) {
+ /* timeout */
+ cbreak();
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Timed out reading XTerm clipboard");
+ return FALSE;
+ }
+ }
+
+ GString *str_base64 = g_string_new("");
+ /* g_base64_decode_step() state: */
+ gint state = 0;
+ guint save = 0;
+
+ for (;;) {
+ /*
+ * Space for storing one group of decoded Base64 characters
+ * and the OSC-52 response.
+ */
+ gchar buffer[MAX(3, 7)];
+
+ gchar c = (gchar)getch();
+ if (c == ERR) {
+ /* timeout */
+ cbreak();
+ g_string_free(str_base64, TRUE);
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Timed out reading XTerm clipboard");
+ return FALSE;
+ }
+ if (c == '\a')
+ break;
+
+ /*
+ * This could be simplified using sscanf() and
+ * g_base64_decode(), but we avoid one allocation
+ * to get the entire Base64 string.
+ * (Also to allow for timeouts, we should
+ * read character-wise using getch() anyway.)
+ */
+ gsize out_len = g_base64_decode_step(&c, sizeof(c),
+ (guchar *)buffer,
+ &state, &save);
+ g_string_append_len(str_base64, buffer, out_len);
+ }
+
+ cbreak();
+
+ if (str)
+ *str = str_base64->str;
+ *len = str_base64->len;
+
+ g_string_free(str_base64, !str);
+ return TRUE;
+}
+
+#else
+
+static void
+teco_interface_init_clipboard(void)
+{
+ /*
+ * No native clipboard support, so no clipboard Q-Regs are
+ * registered.
+ */
+}
+
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len,
+ GError **error)
+{
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Setting clipboard unsupported");
+ return FALSE;
+}
+
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
+{
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Getting clipboard unsupported");
+ return FALSE;
+}
+
+#endif /* !__PDCURSES__ && !CURSES_TTY */
+
+void
+teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len,
+ gboolean highlight)
+{
+ if (teco_interface.cmdline_window)
+ /* interactive mode */
+ teco_curses_info_popup_add(&teco_interface.popup, type, name, name_len, highlight);
+}
+
+void
+teco_interface_popup_show(void)
+{
+ if (!teco_interface.cmdline_window)
+ /* batch mode */
+ return;
+
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_CALLTIP, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_CALLTIP, 0));
+
+ teco_curses_info_popup_show(&teco_interface.popup, SCI_COLOR_ATTR(fg, bg));
+}
+
+gboolean
+teco_interface_popup_is_shown(void)
+{
+ return teco_curses_info_popup_is_shown(&teco_interface.popup);
+}
+
+void
+teco_interface_popup_clear(void)
+{
+#ifdef __PDCURSES__
+ /*
+ * PDCurses will not redraw all windows that may be
+ * overlapped by the popup window correctly - at least
+ * not the info window.
+ * The Scintilla window is apparently always touched by
+ * scintilla_noutrefresh().
+ * Actually we would expect this to be necessary on any curses,
+ * but ncurses doesn't require this.
+ */
+ if (teco_curses_info_popup_is_shown(&teco_interface.popup)) {
+ touchwin(teco_interface.info_window);
+ touchwin(teco_interface.msg_window);
+ }
+#endif
+
+ teco_curses_info_popup_clear(&teco_interface.popup);
+ teco_curses_info_popup_init(&teco_interface.popup);
+}
+
+gboolean
+teco_interface_is_interrupted(void)
+{
+ return teco_sigint_occurred != FALSE;
+}
+
+/**
+ * One iteration of the event loop.
+ *
+ * This is a global function, so it may be used as an asynchronous Emscripten callback.
+ * While this function cannot directly throw GErrors,
+ * it can set teco_interface.event_loop_error.
+ *
+ * @fixme Thrown errors should be somehow caught when building for EMScripten as well.
+ * Perhaps in a goto-block.
+ */
+void
+teco_interface_event_loop_iter(void)
+{
+ /*
+ * On PDCurses/win32, raw() and cbreak() does
+ * not disable and enable CTRL+C handling properly.
+ * Since I don't want to patch PDCurses/win32,
+ * we do this manually here.
+ * NOTE: This exploits the fact that PDCurses uses
+ * STD_INPUT_HANDLE internally!
+ */
+#ifdef PDCURSES_WIN32
+ HANDLE console_hnd = GetStdHandle(STD_INPUT_HANDLE);
+ DWORD console_mode;
+ GetConsoleMode(console_hnd, &console_mode);
+#endif
+
+ /*
+ * Setting function key processing is important
+ * on Unix Curses, as ESCAPE is handled as the beginning
+ * of a escape sequence when terminal emulators are
+ * involved.
+ * On some Curses variants (XCurses) however, keypad
+ * must always be TRUE so we receive KEY_RESIZE.
+ *
+ * FIXME: NetBSD's curses could be handled like ncurses,
+ * but gets into an undefined state when SciTECO processes
+ * escape sequences.
+ */
+#ifdef NCURSES_UNIX
+ keypad(teco_interface.cmdline_window, teco_ed & TECO_ED_FNKEYS);
+#endif
+
+ /* no special <CTRL/C> handling */
+ raw();
+#ifdef PDCURSES_WIN32
+ SetConsoleMode(console_hnd, console_mode & ~ENABLE_PROCESSED_INPUT);
+#endif
+ /*
+ * Memory limiting is stopped temporarily, since it might otherwise
+ * constantly place 100% load on the CPU.
+ */
+ teco_memory_stop_limiting();
+ int key = wgetch(teco_interface.cmdline_window);
+ teco_memory_start_limiting();
+ /* allow asynchronous interruptions on <CTRL/C> */
+ teco_sigint_occurred = FALSE;
+ noraw(); /* FIXME: necessary because of NCURSES_WIN32 bug */
+ cbreak();
+#ifdef PDCURSES_WIN32
+ SetConsoleMode(console_hnd, console_mode | ENABLE_PROCESSED_INPUT);
+#endif
+ if (key == ERR)
+ return;
+
+ switch (key) {
+#ifdef KEY_RESIZE
+ case KEY_RESIZE:
+#if PDCURSES
+ resize_term(0, 0);
+#endif
+ teco_interface_resize_all_windows();
+ break;
+#endif
+ case TECO_CTL_KEY('H'):
+ case 0x7F: /* ^? */
+ case KEY_BACKSPACE:
+ /*
+ * For historic reasons terminals can send
+ * ASCII 8 (^H) or 127 (^?) for backspace.
+ * Curses also defines KEY_BACKSPACE, probably
+ * for terminals that send an escape sequence for
+ * backspace.
+ * In SciTECO backspace is normalized to ^H.
+ */
+ if (!teco_cmdline_keypress_c(TECO_CTL_KEY('H'),
+ &teco_interface.event_loop_error))
+ return;
+ break;
+ case KEY_ENTER:
+ case '\r':
+ case '\n':
+ if (!teco_cmdline_keypress_c('\n', &teco_interface.event_loop_error))
+ return;
+ break;
+
+ /*
+ * Function key macros
+ */
+#define FN(KEY) \
+ case KEY_##KEY: \
+ if (!teco_cmdline_fnmacro(#KEY, &teco_interface.event_loop_error)) \
+ return; \
+ break
+#define FNS(KEY) FN(KEY); FN(S##KEY)
+ FN(DOWN); FN(UP); FNS(LEFT); FNS(RIGHT);
+ FNS(HOME);
+ case KEY_F(0)...KEY_F(63): {
+ gchar macro_name[3+1];
+
+ g_snprintf(macro_name, sizeof(macro_name),
+ "F%d", key - KEY_F0);
+ if (!teco_cmdline_fnmacro(macro_name,
+ &teco_interface.event_loop_error))
+ return;
+ break;
+ }
+ FNS(DC);
+ FNS(IC);
+ FN(NPAGE); FN(PPAGE);
+ FNS(PRINT);
+ FN(A1); FN(A3); FN(B2); FN(C1); FN(C3);
+ FNS(END);
+ FNS(HELP);
+ FN(CLOSE);
+#undef FNS
+#undef FN
+
+ /*
+ * Control keys and keys with printable representation
+ */
+ default:
+ if (key <= 0xFF &&
+ !teco_cmdline_keypress_c(key, &teco_interface.event_loop_error))
+ return;
+ }
+
+ /*
+ * Info window is updated very often which is very
+ * costly, especially when using PDC_set_title(),
+ * so we redraw it here, where the overhead does
+ * not matter much.
+ */
+ teco_interface_draw_info();
+ wnoutrefresh(teco_interface.info_window);
+ teco_view_noutrefresh(teco_interface_current_view);
+ wnoutrefresh(teco_interface.msg_window);
+ wnoutrefresh(teco_interface.cmdline_window);
+ teco_curses_info_popup_noutrefresh(&teco_interface.popup);
+ doupdate();
+}
+
+gboolean
+teco_interface_event_loop(GError **error)
+{
+ static const teco_cmdline_t empty_cmdline; // FIXME
+
+ /*
+ * Initialize Curses for interactive mode
+ */
+ if (!teco_interface_init_interactive(error))
+ return FALSE;
+
+ /* initial refresh */
+ teco_interface_draw_info();
+ wnoutrefresh(teco_interface.info_window);
+ teco_view_noutrefresh(teco_interface_current_view);
+ teco_interface_msg_clear();
+ wnoutrefresh(teco_interface.msg_window);
+ teco_interface_cmdline_update(&empty_cmdline);
+ wnoutrefresh(teco_interface.cmdline_window);
+ doupdate();
+
+#ifdef EMCURSES
+ PDC_emscripten_set_handler(teco_interface_event_loop_iter, TRUE);
+ /*
+ * We must not block emscripten's main loop,
+ * instead event_loop_iter() is called asynchronously.
+ * We also must not exit the event_loop() method, since
+ * SciTECO would assume ordinary program termination.
+ * We also must not call exit() since that would run
+ * the global destructors.
+ * The following exits the main() function immediately
+ * while keeping the "runtime" alive.
+ */
+ emscripten_exit_with_live_runtime();
+#else
+ while (!teco_interface.event_loop_error)
+ teco_interface_event_loop_iter();
+
+ /*
+ * The error needs to be propagated only if this is
+ * NOT a SciTECO termination (e.g. EX$$)
+ */
+ if (!g_error_matches(teco_interface.event_loop_error,
+ TECO_ERROR, TECO_ERROR_QUIT)) {
+ g_propagate_error(error, g_steal_pointer(&teco_interface.event_loop_error));
+ return FALSE;
+ }
+ g_clear_error(&teco_interface.event_loop_error);
+
+ teco_interface_restore_batch();
+#endif
+
+ return TRUE;
+}
+
+void
+teco_interface_cleanup(void)
+{
+ if (teco_interface.event_loop_error)
+ g_error_free(teco_interface.event_loop_error);
+
+ if (teco_interface.info_window)
+ delwin(teco_interface.info_window);
+ teco_string_clear(&teco_interface.info_current);
+ if (teco_interface.cmdline_window)
+ delwin(teco_interface.cmdline_window);
+ if (teco_interface.cmdline_pad)
+ delwin(teco_interface.cmdline_pad);
+ if (teco_interface.msg_window)
+ delwin(teco_interface.msg_window);
+
+ /*
+ * PDCurses (win32) crashes if initscr() wasn't called.
+ * Others (XCurses) crash if we try to use isendwin() here.
+ * Perhaps Curses cleanup should be in restore_batch()
+ * instead.
+ */
+#ifndef XCURSES
+ if (teco_interface.info_window && !isendwin())
+ endwin();
+#endif
+
+ if (teco_interface.screen)
+ delscreen(teco_interface.screen);
+ if (teco_interface.screen_tty)
+ fclose(teco_interface.screen_tty);
+ if (teco_interface.stderr_orig >= 0)
+ close(teco_interface.stderr_orig);
+ if (teco_interface.stdout_orig >= 0)
+ close(teco_interface.stdout_orig);
+}