diff options
Diffstat (limited to 'src/interface-curses/interface-curses.cpp')
-rw-r--r-- | src/interface-curses/interface-curses.cpp | 1548 |
1 files changed, 1548 insertions, 0 deletions
diff --git a/src/interface-curses/interface-curses.cpp b/src/interface-curses/interface-curses.cpp new file mode 100644 index 0000000..49339d4 --- /dev/null +++ b/src/interface-curses/interface-curses.cpp @@ -0,0 +1,1548 @@ +/* + * Copyright (C) 2012-2016 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> + +#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> + +#ifdef EMSCRIPTEN +#include <emscripten.h> +#endif + +#include "sciteco.h" +#include "string-utils.h" +#include "cmdline.h" +#include "qregisters.h" +#include "ring.h" +#include "interface.h" +#include "interface-curses.h" + +#ifdef HAVE_WINDOWS_H +/* here it shouldn't cause conflicts with other headers */ +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#endif + +/** + * 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 + +#ifdef NCURSES_VERSION +#if defined(G_OS_UNIX) || defined(G_OS_HAIKU) +/** + * Whether we're on ncurses/UNIX. + * Haiku has a UNIX-like terminal and is largely + * POSIX compliant, so we can handle it like a + * UNIX ncurses. + */ +#define NCURSES_UNIX +#elif defined(G_OS_WIN32) +/** + * Whether we're on ncurses/win32 console + */ +#define NCURSES_WIN32 +#endif +#endif + +namespace SciTECO { + +extern "C" { + +/* + * 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 + +static void scintilla_notify(Scintilla *sci, int idFrom, + void *notify, void *user_data); + +#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 +console_ctrl_handler(DWORD type) +{ + switch (type) { + case CTRL_C_EVENT: + sigint_occurred = TRUE; + return TRUE; + } + + return FALSE; +} + +#endif + +} /* extern "C" */ + +#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 +rgb2curses(guint32 rgb, short &r, short &g, short &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 short +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 gsize +format_str(WINDOW *win, const gchar *str, + gssize len = -1, gint max_width = -1) +{ + int old_x, old_y; + gint chars_added = 0; + + getyx(win, old_y, old_x); + + if (len < 0) + len = strlen(str); + if (max_width < 0) + max_width = getmaxx(win) - old_x; + + while (len > 0) { + /* + * NOTE: This mapping is similar to + * View::set_representations() + */ + switch (*str) { + case CTL_KEY_ESC: + chars_added++; + if (chars_added > max_width) + goto truncate; + waddch(win, '$' | A_REVERSE); + break; + case '\r': + chars_added += 2; + if (chars_added > max_width) + goto truncate; + waddch(win, 'C' | A_REVERSE); + waddch(win, 'R' | A_REVERSE); + break; + case '\n': + chars_added += 2; + if (chars_added > max_width) + goto truncate; + waddch(win, 'L' | A_REVERSE); + waddch(win, 'F' | A_REVERSE); + break; + case '\t': + chars_added += 3; + if (chars_added > max_width) + goto truncate; + waddch(win, 'T' | A_REVERSE); + waddch(win, 'A' | A_REVERSE); + waddch(win, 'B' | A_REVERSE); + break; + default: + if (IS_CTL(*str)) { + chars_added += 2; + if (chars_added > max_width) + goto truncate; + waddch(win, '^' | A_REVERSE); + waddch(win, CTL_ECHO(*str) | A_REVERSE); + } else { + chars_added++; + if (chars_added > max_width) + goto truncate; + waddch(win, *str); + } + } + + str++; + len--; + } + + return getcurx(win) - old_x; + +truncate: + if (max_width >= 3) { + /* + * Truncate string + */ + wattron(win, A_UNDERLINE | A_BOLD); + mvwaddstr(win, old_y, old_x + max_width - 3, "..."); + wattroff(win, A_UNDERLINE | A_BOLD); + } + + return getcurx(win) - old_x; +} + +static gsize +format_filename(WINDOW *win, const gchar *filename, + gint max_width = -1) +{ + int old_x = getcurx(win); + + gchar *filename_canon = String::canonicalize_ctl(filename); + size_t filename_len = strlen(filename_canon); + + if (max_width < 0) + max_width = getmaxx(win) - old_x; + + if (filename_len <= (size_t)max_width) { + waddstr(win, filename_canon); + } else { + const gchar *keep_post = filename_canon + filename_len - + max_width + 3; + +#ifdef G_OS_WIN32 + const gchar *keep_pre = g_path_skip_root(filename_canon); + if (keep_pre) { + waddnstr(win, filename_canon, + keep_pre - filename_canon); + keep_post += keep_pre - filename_canon; + } +#endif + wattron(win, A_UNDERLINE | A_BOLD); + waddstr(win, "..."); + wattroff(win, A_UNDERLINE | A_BOLD); + waddstr(win, keep_post); + } + + g_free(filename_canon); + return getcurx(win) - old_x; +} + +void +ViewCurses::initialize_impl(void) +{ + sci = scintilla_new(scintilla_notify); + setup(); +} + +InterfaceCurses::InterfaceCurses() : stdout_orig(-1), stderr_orig(-1), + screen(NULL), + screen_tty(NULL), + info_window(NULL), + info_type(INFO_TYPE_BUFFER), + info_current(NULL), + msg_window(NULL), + cmdline_window(NULL), cmdline_pad(NULL), + cmdline_len(0), cmdline_rubout_len(0) +{ + for (guint i = 0; i < G_N_ELEMENTS(color_table); i++) + color_table[i] = -1; + for (guint i = 0; i < G_N_ELEMENTS(orig_color_table); i++) + orig_color_table[i].r = -1; +} + +void +InterfaceCurses::Popup::add(PopupEntryType type, + const gchar *name, bool highlight) +{ + size_t name_len = strlen(name); + Entry *entry = (Entry *)g_malloc(sizeof(Entry) + name_len + 1); + + entry->type = type; + entry->highlight = highlight; + strcpy(entry->name, name); + + longest = MAX(longest, (gint)name_len); + length++; + + /* + * Entries are added in reverse (constant time for GSList), + * so they will later have to be reversed. + */ + list = g_slist_prepend(list, entry); +} + +void +InterfaceCurses::Popup::init_pad(attr_t attr) +{ + int cols = getmaxx(stdscr); /* screen width */ + int pad_lines; /* pad height */ + gint pad_cols; /* entry columns */ + gint pad_colwidth; /* width per entry column */ + + gint cur_col; + + /* reserve 2 spaces between columns */ + pad_colwidth = MIN(longest + 2, cols - 2); + + /* pad_cols = floor((cols - 2) / pad_colwidth) */ + pad_cols = (cols - 2) / pad_colwidth; + /* pad_lines = ceil(length / pad_cols) */ + pad_lines = (length+pad_cols-1) / pad_cols; + + /* + * Render the entire autocompletion list into a pad + * which can be higher than the physical screen. + * The pad uses two columns less than the screen since + * it will be drawn into the popup window which has left + * and right borders. + */ + pad = newpad(pad_lines, cols - 2); + + wbkgd(pad, ' ' | attr); + + /* + * cur_col is the row currently written. + * It does not wrap but grows indefinitely. + * Therefore the real current row is (cur_col % popup_cols) + */ + cur_col = 0; + for (GSList *cur = list; cur != NULL; cur = g_slist_next(cur)) { + Entry *entry = (Entry *)cur->data; + gint cur_line = cur_col/pad_cols + 1; + + wmove(pad, cur_line-1, + (cur_col % pad_cols)*pad_colwidth); + + wattrset(pad, entry->highlight ? A_BOLD : A_NORMAL); + + switch (entry->type) { + case POPUP_FILE: + case POPUP_DIRECTORY: + format_filename(pad, entry->name); + break; + default: + format_str(pad, entry->name); + break; + } + + cur_col++; + } +} + +void +InterfaceCurses::Popup::show(attr_t attr) +{ + int lines, cols; /* screen dimensions */ + gint pad_lines; + gint popup_lines; + gint bar_height, bar_y; + + if (!length) + /* nothing to display */ + return; + + getmaxyx(stdscr, lines, cols); + + if (window) + delwin(window); + else + /* reverse list only once */ + list = g_slist_reverse(list); + + if (!pad) + init_pad(attr); + pad_lines = getmaxy(pad); + + /* + * Popup window can cover all but one screen row. + * Another row is reserved for the top border. + */ + popup_lines = MIN(pad_lines + 1, lines - 1); + + /* window covers message, scintilla and info windows */ + window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0); + + wbkgdset(window, ' ' | attr); + + wborder(window, + ACS_VLINE, + ACS_VLINE, /* may be overwritten with scrollbar */ + ACS_HLINE, + ' ', /* no bottom line */ + ACS_ULCORNER, ACS_URCORNER, + ACS_VLINE, ACS_VLINE); + + copywin(pad, window, + pad_first_line, 0, + 1, 1, popup_lines - 1, cols - 2, FALSE); + + if (pad_lines <= popup_lines - 1) + /* no need for scrollbar */ + return; + + /* bar_height = ceil((popup_lines-1)/pad_lines * (popup_lines-2)) */ + bar_height = ((popup_lines-1)*(popup_lines-2) + pad_lines-1) / + pad_lines; + /* bar_y = floor(pad_first_line/pad_lines * (popup_lines-2)) + 1 */ + bar_y = pad_first_line*(popup_lines-2) / pad_lines + 1; + + mvwvline(window, 1, cols-1, ACS_CKBOARD, popup_lines-2); + /* + * We do not use ACS_BLOCK here since it will not + * always be drawn as a solid block (e.g. xterm). + * Instead, simply draw reverse blanks. + */ + wmove(window, bar_y, cols-1); + wattron(window, A_REVERSE); + wvline(window, ' ', bar_height); + + /* progress scroll position */ + pad_first_line += popup_lines - 1; + /* wrap on last shown page */ + pad_first_line %= pad_lines; + if (pad_lines - pad_first_line < popup_lines - 1) + /* show last page */ + pad_first_line = pad_lines - (popup_lines - 1); +} + +void +InterfaceCurses::Popup::clear(void) +{ + g_slist_free_full(list, g_free); + list = NULL; + length = 0; + longest = 0; + + pad_first_line = 0; + + if (window) { + delwin(window); + window = NULL; + } + + if (pad) { + delwin(pad); + pad = NULL; + } +} + +InterfaceCurses::Popup::~Popup() +{ + if (window) + delwin(window); + if (pad) + delwin(pad); + if (list) + g_slist_free_full(list, g_free); +} + +void +InterfaceCurses::main_impl(int &argc, char **&argv) +{ + /* + * 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(console_ctrl_handler, TRUE); +#endif + + /* + * Make sure we have a string for the info line + * even if info_update() is never called. + */ + info_current = g_strdup(PACKAGE_NAME); +} + +void +InterfaceCurses::init_color_safe(guint color, guint32 rgb) +{ + short r, g, b; + +#ifdef PDCURSES_WIN32 + if (orig_color_table[color].r < 0) { + color_content((short)color, + &orig_color_table[color].r, + &orig_color_table[color].g, + &orig_color_table[color].b); + } +#endif + + rgb2curses(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. + */ +void +InterfaceCurses::restore_colors(void) +{ + if (!can_change_color()) + return; + + for (guint i = 0; i < G_N_ELEMENTS(orig_color_table); i++) { + if (orig_color_table[i].r < 0) + continue; + + ::init_color((short)i, + orig_color_table[i].r, + orig_color_table[i].g, + orig_color_table[i].b); + } +} + +#elif defined(NCURSES_UNIX) + +/* + * FIXME: On UNIX/ncurses 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\x07" 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. + */ +void +InterfaceCurses::restore_colors(void) +{ + if (g_str_has_prefix(g_getenv("TERM") ? : "", "xterm") && + g_getenv("XTERM_VERSION")) { + /* + * Looks like a real xterm. $TERM alone is not + * sufficient to tell. + */ + fputs("\e]104\x07", screen_tty); + fflush(screen_tty); + } +} + +#else /* !PDCURSES_WIN32 && !NCURSES_UNIX */ + +void +InterfaceCurses::restore_colors(void) +{ + /* + * No way to restore the palette, or it's + * unnecessary (e.g. XCurses) + */ +} + +#endif + +void +InterfaceCurses::init_color(guint color, guint32 rgb) +{ + if (color >= G_N_ELEMENTS(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 (cmdline_window) { + /* interactive mode */ + if (!can_change_color()) + return; + + 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 + */ + color_table[color] = (gint32)rgb; + } +} + +#ifdef NCURSES_UNIX + +void +InterfaceCurses::init_screen(void) +{ + screen_tty = g_fopen("/dev/tty", "r+"); + /* should never fail */ + g_assert(screen_tty != NULL); + + screen = newterm(NULL, screen_tty, screen_tty); + if (!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)) { + FILE *stdout_new; + stdout_orig = dup(1); + g_assert(stdout_orig >= 0); + stdout_new = g_freopen("/dev/null", "a+", stdout); + g_assert(stdout_new != NULL); + } + if (isatty(2)) { + FILE *stderr_new; + stderr_orig = dup(2); + g_assert(stderr_orig >= 0); + stderr_new = g_freopen("/dev/null", "a+", stderr); + g_assert(stderr_new != NULL); + } +} + +#elif defined(XCURSES) + +void +InterfaceCurses::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. + * Therefore this could only be supported by + * adding a special option like --resource. + */ + Xinitscr(1, (char **)argv); +} + +#else + +void +InterfaceCurses::init_screen(void) +{ + initscr(); +} + +#endif + +void +InterfaceCurses::init_interactive(void) +{ + /* + * 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. + */ + QRegisters::globals.update_environ(); + + /* + * 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"). + */ +#ifdef NCURSES_UNIX + 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. + * NOTE: This could also be used to assign + * a "shutdown" key when program termination is requested. + */ + 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, ""); + + init_screen(); + + cbreak(); + noecho(); + /* Scintilla draws its own cursor */ + curs_set(0); + + info_window = newwin(1, 0, 0, 0); + + msg_window = newwin(1, 0, LINES - 2, 0); + + cmdline_window = newwin(0, 0, LINES - 1, 0); + keypad(cmdline_window, TRUE); + +#ifdef EMSCRIPTEN + nodelay(cmdline_window, TRUE); +#endif + + /* + * Will also initialize Scinterm, Curses color pairs + * and resizes the current view. + */ + if (current_view) + show_view(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(color_table); i++) { + /* + * init_color() may still fail if COLORS < 16 + */ + if (color_table[i] >= 0) + init_color_safe(i, (guint32)color_table[i]); + } + } +} + +void +InterfaceCurses::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(NCURSES_UNIX) && defined(HAVE_TIGETSTR) + set_window_title(g_getenv("TERM") ? : ""); +#endif + + /* + * Restore ordinary terminal behaviour + * (i.e. return to batch mode) + */ + endwin(); + restore_colors(); + + /* + * Restore stdout and stderr, so output goes to + * the terminal again in case we "muted" them. + */ +#ifdef NCURSES_UNIX + if (stdout_orig >= 0) { + int fd = dup2(stdout_orig, 1); + g_assert(fd == 1); + } + if (stderr_orig >= 0) { + int fd = dup2(stderr_orig, 2); + g_assert(fd == 2); + } +#endif + + /* + * See vmsg_impl(): It looks at msg_win to determine + * whether we're in batch mode. + */ + if (msg_window) { + delwin(msg_window); + msg_window = NULL; + } +} + +void +InterfaceCurses::resize_all_windows(void) +{ + int lines, cols; /* screen dimensions */ + + getmaxyx(stdscr, lines, cols); + + wresize(info_window, 1, cols); + wresize(current_view->get_window(), + lines - 3, cols); + wresize(msg_window, 1, cols); + mvwin(msg_window, lines - 2, 0); + wresize(cmdline_window, 1, cols); + mvwin(cmdline_window, lines - 1, 0); + + draw_info(); + msg_clear(); /* FIXME: use saved message */ + popup_clear(); + draw_cmdline(); +} + +void +InterfaceCurses::vmsg_impl(MessageType type, const gchar *fmt, va_list ap) +{ + short fg, bg; + + if (!msg_window) { /* batch mode */ + 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(NCURSES_UNIX) || defined(NCURSES_WIN32) + va_list aq; + va_copy(aq, ap); + stdio_vmsg(type, fmt, aq); + va_end(aq); +#endif + + fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); + + switch (type) { + default: + case MSG_USER: + bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); + break; + case MSG_INFO: + bg = COLOR_GREEN; + break; + case MSG_WARNING: + bg = COLOR_YELLOW; + break; + case MSG_ERROR: + bg = COLOR_RED; + beep(); + break; + } + + wmove(msg_window, 0, 0); + wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg)); + vw_printw(msg_window, fmt, ap); + wclrtoeol(msg_window); +} + +void +InterfaceCurses::msg_clear(void) +{ + short fg, bg; + + if (!msg_window) /* batch mode */ + return; + + fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); + bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); + + wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg)); + werase(msg_window); +} + +void +InterfaceCurses::show_view_impl(ViewCurses *view) +{ + int lines, cols; /* screen dimensions */ + WINDOW *current_view_win; + + current_view = view; + + if (!cmdline_window) /* batch mode */ + return; + + current_view_win = current_view->get_window(); + + /* + * screen size might have changed since + * this view's WINDOW was last active + */ + 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 + +void +InterfaceCurses::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(NCURSES_UNIX) && defined(HAVE_TIGETSTR) + +void +InterfaceCurses::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, screen_tty); + fputs(title, screen_tty); + fputs(from_status_line, screen_tty); + fflush(screen_tty); +} + +#else + +void +InterfaceCurses::set_window_title(const gchar *title) +{ + /* no way to set window title */ +} + +#endif + +void +InterfaceCurses::draw_info(void) +{ + short fg, bg; + const gchar *info_type_str; + gchar *info_current_canon, *title; + + if (!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. + */ + fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); + bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); + + wmove(info_window, 0, 0); + wbkgdset(info_window, ' ' | SCI_COLOR_ATTR(fg, bg)); + + switch (info_type) { + case INFO_TYPE_QREGISTER: + info_type_str = PACKAGE_NAME " - <QRegister> "; + waddstr(info_window, info_type_str); + /* same formatting as in command lines */ + format_str(info_window, info_current); + break; + + case INFO_TYPE_BUFFER: + info_type_str = PACKAGE_NAME " - <Buffer> "; + waddstr(info_window, info_type_str); + format_filename(info_window, info_current); + break; + + default: + g_assert_not_reached(); + } + + wclrtoeol(info_window); + + /* + * Make sure the title will consist only of printable + * characters + */ + info_current_canon = String::canonicalize_ctl(info_current); + title = g_strconcat(info_type_str, info_current_canon, NIL); + g_free(info_current_canon); + set_window_title(title); + g_free(title); +} + +void +InterfaceCurses::info_update_impl(const QRegister *reg) +{ + g_free(info_current); + /* NOTE: will contain control characters */ + info_type = INFO_TYPE_QREGISTER; + info_current = g_strdup(reg->name); + /* NOTE: drawn in event_loop_iter() */ +} + +void +InterfaceCurses::info_update_impl(const Buffer *buffer) +{ + g_free(info_current); + info_type = INFO_TYPE_BUFFER; + info_current = g_strconcat(buffer->filename ? : UNNAMED_FILE, + buffer->dirty ? "*" : " ", NIL); + /* NOTE: drawn in event_loop_iter() */ +} + +void +InterfaceCurses::cmdline_update_impl(const Cmdline *cmdline) +{ + short fg, bg; + int max_cols = 1; + + /* + * 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 (cmdline_pad) + delwin(cmdline_pad); + for (guint i = 0; i < cmdline->len+cmdline->rubout_len; i++) + max_cols += IS_CTL((*cmdline)[i]) ? 3 : 1; + cmdline_pad = newpad(1, max_cols); + + fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); + bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); + wcolor_set(cmdline_pad, SCI_COLOR_PAIR(fg, bg), NULL); + + /* format effective command line */ + cmdline_len = format_str(cmdline_pad, cmdline->str, cmdline->len); + + /* + * 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(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. + */ + cmdline_rubout_len = format_str(cmdline_pad, cmdline->str + cmdline->len, + cmdline->rubout_len); + + /* highlight cursor after effective command line */ + if (cmdline_rubout_len) { + attr_t attr; + short pair; + + wmove(cmdline_pad, 0, cmdline_len); + wattr_get(cmdline_pad, &attr, &pair, NULL); + wchgat(cmdline_pad, 1, + (attr & A_UNDERLINE) | A_REVERSE, pair, NULL); + } else { + cmdline_len++; + wattroff(cmdline_pad, A_UNDERLINE | A_BOLD); + waddch(cmdline_pad, ' ' | A_REVERSE); + } + + draw_cmdline(); +} + +void +InterfaceCurses::draw_cmdline(void) +{ + short fg, bg; + /* total width available for command line */ + guint total_width = getmaxx(cmdline_window) - 1; + /* beginning of command line to show */ + guint disp_offset; + /* length of command line to show */ + guint disp_len; + + disp_offset = cmdline_len - + MIN(cmdline_len, + total_width/2 + cmdline_len % MAX(total_width/2, 1)); + /* + * NOTE: we do not use getmaxx(cmdline_pad) here since it may be + * larger than the text the pad contains. + */ + disp_len = MIN(total_width, cmdline_len+cmdline_rubout_len - disp_offset); + + fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); + bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); + + wbkgdset(cmdline_window, ' ' | SCI_COLOR_ATTR(fg, bg)); + werase(cmdline_window); + mvwaddch(cmdline_window, 0, 0, '*' | A_BOLD); + copywin(cmdline_pad, cmdline_window, + 0, disp_offset, 0, 1, 0, disp_len, FALSE); +} + +void +InterfaceCurses::popup_show_impl(void) +{ + short fg, bg; + + if (!cmdline_window) + /* batch mode */ + return; + + fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_CALLTIP)); + bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_CALLTIP)); + + popup.show(SCI_COLOR_ATTR(fg, bg)); +} + +void +InterfaceCurses::popup_clear_impl(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 (popup.is_shown()) { + touchwin(info_window); + touchwin(msg_window); + } +#endif + + popup.clear(); +} + +/** + * One iteration of the event loop. + * + * This is a global function, so it may + * be used as an Emscripten callback. + * + * @bug + * Can probably be defined as a static method, + * so we can avoid declaring it a fried function of + * InterfaceCurses. + */ +void +event_loop_iter() +{ + int key; + + /* + * 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. + */ +#ifdef NCURSES_UNIX + keypad(interface.cmdline_window, Flags::ed & Flags::ED_FNKEYS); +#endif + + /* no special <CTRL/C> handling */ + raw(); +#ifdef PDCURSES_WIN32 + SetConsoleMode(console_hnd, console_mode & ~ENABLE_PROCESSED_INPUT); +#endif + key = wgetch(interface.cmdline_window); + /* allow asynchronous interruptions on <CTRL/C> */ + 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 + interface.resize_all_windows(); + break; +#endif + case 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. + */ + cmdline.keypress(CTL_KEY('H')); + break; + case KEY_ENTER: + case '\r': + case '\n': + cmdline.keypress('\n'); + break; + + /* + * Function key macros + */ +#define FN(KEY) case KEY_##KEY: cmdline.fnmacro(#KEY); 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); + cmdline.fnmacro(macro_name); + 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) + cmdline.keypress((gchar)key); + } + + /* + * 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. + */ + interface.draw_info(); + wnoutrefresh(interface.info_window); + interface.current_view->noutrefresh(); + wnoutrefresh(interface.msg_window); + wnoutrefresh(interface.cmdline_window); + interface.popup.noutrefresh(); + doupdate(); +} + +void +InterfaceCurses::event_loop_impl(void) +{ + static const Cmdline empty_cmdline; + + /* + * Initialize Curses for interactive mode + */ + init_interactive(); + + /* initial refresh */ + draw_info(); + wnoutrefresh(info_window); + current_view->noutrefresh(); + msg_clear(); + wnoutrefresh(msg_window); + cmdline_update(&empty_cmdline); + wnoutrefresh(cmdline_window); + doupdate(); + +#ifdef EMSCRIPTEN + PDC_emscripten_set_handler(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 + try { + for (;;) + event_loop_iter(); + } catch (Quit) { + /* SciTECO termination (e.g. EX$$) */ + } + + restore_batch(); +#endif +} + +InterfaceCurses::~InterfaceCurses() +{ + if (info_window) + delwin(info_window); + g_free(info_current); + if (cmdline_window) + delwin(cmdline_window); + if (cmdline_pad) + delwin(cmdline_pad); + if (msg_window) + delwin(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 (info_window && !isendwin()) + endwin(); +#endif + + if (screen) + delscreen(screen); + if (screen_tty) + fclose(screen_tty); + if (stderr_orig >= 0) + close(stderr_orig); + if (stdout_orig >= 0) + close(stdout_orig); +} + +/* + * Callbacks + */ + +static void +scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data) +{ + interface.process_notify((SCNotification *)notify); +} + +} /* namespace SciTECO */ |