From 432ad24e382681f1c13b07e8486e91063dd96e2e Mon Sep 17 00:00:00 2001 From: Robin Haberkorn Date: Sun, 30 May 2021 02:38:43 +0200 Subject: THE GREAT CEEIFICATION EVENT This is a total conversion of SciTECO to plain C (GNU C11). The chance was taken to improve a lot of internal datastructures, fix fundamental bugs and lay the foundations of future features. The GTK user interface is now in an useable state! All changes have been squashed together. The language itself has almost not changed at all, except for: * Detection of string terminators (usually Escape) now takes the string building characters into account. A string is only terminated outside of string building characters. In other words, you can now for instance write I^EQ[Hello$world]$ This removes one of the last bits of shellisms which is out of place in SciTECO where no tokenization/lexing is performed. Consequently, the current termination character can also be escaped using ^Q/^R. This is used by auto completions to make sure that strings are inserted verbatim and without unwanted sideeffects. * All strings can now safely contain null-characters (see also: 8-bit cleanliness). The null-character itself (^@) is not (yet) a valid SciTECO command, though. An incomplete list of changes: * We got rid of the BSD headers for RB trees and lists/queues. The problem with them was that they used a form of metaprogramming only to gain a bit of type safety. It also resulted in less readble code. This was a C++ desease. The new code avoids metaprogramming only to gain type safety. The BSD tree.h has been replaced by rb3ptr by Jens Stimpfle (https://github.com/jstimpfle/rb3ptr). This implementation is also more memory efficient than BSD's. The BSD list.h and queue.h has been replaced with a custom src/list.h. * Fixed crashes, performance issues and compatibility issues with the Gtk 3 User Interface. It is now more or less ready for general use. The GDK lock is no longer used to avoid using deprecated functions. On the downside, the new implementation (driving the Gtk event loop stepwise) is even slower than the old one. A few glitches remain (see TODO), but it is hoped that they will be resolved by the Scintilla update which will be performed soon. * A lot of program units have been split up, so they are shorter and easier to maintain: core-commands.c, qreg-commands.c, goto-commands.c, file-utils.h. * Parser states are simply structs of callbacks now. They still use a kind of polymorphy using a preprocessor trick. TECO_DEFINE_STATE() takes an initializer list that will be merged with the default list of field initializers. To "subclass" states, you can simply define new macros that add initializers to existing macros. * Parsers no longer have a "transitions" table but the input_cb() may use switch-case statements. There are also teco_machine_main_transition_t now which can be used to implement simple transitions. Additionally, you can specify functions to execute during transitions. This largely avoids long switch-case-statements. * Parsers are embeddable/reusable now, at least in parse-only mode. This does not currently bring any advantages but may later be used to write a Scintilla lexer for TECO syntax highlighting. Once parsers are fully embeddable, it will also be possible to run TECO macros in a kind of coroutine which would allow them to process string arguments in real time. * undo.[ch] still uses metaprogramming extensively but via the C preprocessor of course. On the downside, most undo token generators must be initiated explicitly (theoretically we could have used embedded functions / trampolines to instantiate automatically but this has turned out to be dangereous). There is a TECO_DEFINE_UNDO_CALL() to generate closures for arbitrary functions now (ie. to call an arbitrary function at undo-time). This simplified a lot of code and is much shorter than manually pushing undo tokens in many cases. * Instead of the ridiculous C++ Curiously Recurring Template Pattern to achieve static polymorphy for user interface implementations, we now simply declare all functions to implement in interface.h and link in the implementations. This is possible since we no longer hace to define interface subclasses (all state is static variables in the interface's *.c files). * Headers are now significantly shorter than in C++ since we can often hide more of our "class" implementations. * Memory counting is based on dlmalloc for most platforms now. Unfortunately, there is no malloc implementation that provides an efficient constant-time memory counter that is guaranteed to decrease when freeing memory. But since we use a defined malloc implementation now, malloc_usable_size() can be used safely for tracking memory use. malloc() replacement is very tricky on Windows, so we use a poll thread on Windows. This can also be enabled on other supported platforms using --disable-malloc-replacement. All in all, I'm still not pleased with the state of memory limiting. It is a mess. * Error handling uses GError now. This has the advantage that the GError codes can be reused once we support error catching in the SciTECO language. * Added a few more test suite cases. * Haiku is no longer supported as builds are instable and I did not manage to debug them - quite possibly Haiku bugs were responsible. * Glib v2.44 or later are now required. The GTK UI requires Gtk+ v3.12 or later now. The GtkFlowBox fallback and sciteco-wrapper workaround are no longer required. * We now extensively use the GCC/Clang-specific g_auto feature (automatic deallocations when leaving the current code block). * Updated copyright to 2021. SciTECO has been in continuous development, even though there have been no commits since 2018. * Since these changes are so significant, the target release has been set to v2.0. It is planned that beginning with v3.0, the language will be kept stable. --- src/interface-curses/interface.c | 1723 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1723 insertions(+) create mode 100644 src/interface-curses/interface.c (limited to 'src/interface-curses/interface.c') 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 . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_WINDOWS_H +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#ifdef EMSCRIPTEN +#include +#endif + +#include +#include +#include + +#include + +#ifdef HAVE_TIGETSTR +#include + +/* + * Some macros in term.h interfere with our code. + */ +#undef lines +#endif + +#include +#include + +#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 " - "; + 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 " - "; + 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 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 */ + 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); +} -- cgit v1.2.3