diff options
Diffstat (limited to 'src/interface-curses/interface.c')
-rw-r--r-- | src/interface-curses/interface.c | 1723 |
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); +} |