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/Makefile.am | 11 +- src/interface-curses/curses-info-popup.c | 211 ++++ src/interface-curses/curses-info-popup.cpp | 219 ---- src/interface-curses/curses-info-popup.h | 96 +- src/interface-curses/curses-utils.c | 144 +++ src/interface-curses/curses-utils.cpp | 152 --- src/interface-curses/curses-utils.h | 22 +- src/interface-curses/interface-curses.cpp | 1613 -------------------------- src/interface-curses/interface-curses.h | 199 ---- src/interface-curses/interface.c | 1723 ++++++++++++++++++++++++++++ 10 files changed, 2123 insertions(+), 2267 deletions(-) create mode 100644 src/interface-curses/curses-info-popup.c delete mode 100644 src/interface-curses/curses-info-popup.cpp create mode 100644 src/interface-curses/curses-utils.c delete mode 100644 src/interface-curses/curses-utils.cpp delete mode 100644 src/interface-curses/interface-curses.cpp delete mode 100644 src/interface-curses/interface-curses.h create mode 100644 src/interface-curses/interface.c (limited to 'src/interface-curses') diff --git a/src/interface-curses/Makefile.am b/src/interface-curses/Makefile.am index 01e68ae..14fc920 100644 --- a/src/interface-curses/Makefile.am +++ b/src/interface-curses/Makefile.am @@ -1,8 +1,9 @@ -AM_CPPFLAGS += -I$(top_srcdir)/src +AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr \ + -I$(top_srcdir)/src -AM_CXXFLAGS = -Wall -Wno-char-subscripts +AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value noinst_LTLIBRARIES = libsciteco-interface.la -libsciteco_interface_la_SOURCES = interface-curses.cpp interface-curses.h \ - curses-utils.cpp curses-utils.h \ - curses-info-popup.cpp curses-info-popup.h +libsciteco_interface_la_SOURCES = interface.c \ + curses-utils.c curses-utils.h \ + curses-info-popup.c curses-info-popup.h diff --git a/src/interface-curses/curses-info-popup.c b/src/interface-curses/curses-info-popup.c new file mode 100644 index 0000000..7d661a2 --- /dev/null +++ b/src/interface-curses/curses-info-popup.c @@ -0,0 +1,211 @@ +/* + * 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 "list.h" +#include "string-utils.h" +#include "interface.h" +#include "curses-utils.h" +#include "curses-info-popup.h" + +/* + * FIXME: This is redundant with teco-gtk-info-popup.gob. + */ +typedef struct { + teco_stailq_entry_t entry; + + teco_popup_entry_type_t type; + teco_string_t name; + gboolean highlight; +} teco_popup_entry_t; + +void +teco_curses_info_popup_add(teco_curses_info_popup_t *ctx, teco_popup_entry_type_t type, + const gchar *name, gsize name_len, gboolean highlight) +{ + if (G_UNLIKELY(!ctx->chunk)) + ctx->chunk = g_string_chunk_new(32); + + /* + * FIXME: Test with g_slice_new()... + * It could however cause problems upon command-line termination + * and may not be measurably faster. + */ + teco_popup_entry_t *entry = g_new(teco_popup_entry_t, 1); + entry->type = type; + /* + * Popup entries aren't removed individually, so we can + * more efficiently store them via GStringChunk. + */ + teco_string_init_chunk(&entry->name, name, name_len, ctx->chunk); + entry->highlight = highlight; + + teco_stailq_insert_tail(&ctx->list, &entry->entry); + + ctx->longest = MAX(ctx->longest, (gint)name_len); + ctx->length++; +} + +static void +teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, 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 */ + + /* reserve 2 spaces between columns */ + pad_colwidth = MIN(ctx->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 = (ctx->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. + */ + ctx->pad = newpad(pad_lines, cols - 2); + + wbkgd(ctx->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) + */ + gint cur_col = 0; + for (teco_stailq_entry_t *cur = ctx->list.first; cur != NULL; cur = cur->next) { + teco_popup_entry_t *entry = (teco_popup_entry_t *)cur; + gint cur_line = cur_col/pad_cols + 1; + + wmove(ctx->pad, cur_line-1, + (cur_col % pad_cols)*pad_colwidth); + + wattrset(ctx->pad, entry->highlight ? A_BOLD : A_NORMAL); + + switch (entry->type) { + case TECO_POPUP_FILE: + case TECO_POPUP_DIRECTORY: + g_assert(!teco_string_contains(&entry->name, '\0')); + teco_curses_format_filename(ctx->pad, entry->name.data, -1); + break; + default: + teco_curses_format_str(ctx->pad, entry->name.data, entry->name.len, -1); + break; + } + + cur_col++; + } +} + +void +teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr) +{ + if (!ctx->length) + /* nothing to display */ + return; + + int lines, cols; /* screen dimensions */ + getmaxyx(stdscr, lines, cols); + + if (ctx->window) + delwin(ctx->window); + + if (!ctx->pad) + teco_curses_info_popup_init_pad(ctx, attr); + gint pad_lines = getmaxy(ctx->pad); + + /* + * Popup window can cover all but one screen row. + * Another row is reserved for the top border. + */ + gint popup_lines = MIN(pad_lines + 1, lines - 1); + + /* window covers message, scintilla and info windows */ + ctx->window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0); + + wbkgdset(ctx->window, ' ' | attr); + + wborder(ctx->window, + ACS_VLINE, + ACS_VLINE, /* may be overwritten with scrollbar */ + ACS_HLINE, + ' ', /* no bottom line */ + ACS_ULCORNER, ACS_URCORNER, + ACS_VLINE, ACS_VLINE); + + copywin(ctx->pad, ctx->window, + ctx->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)) */ + gint 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 */ + gint bar_y = ctx->pad_first_line*(popup_lines-2) / pad_lines + 1; + + mvwvline(ctx->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(ctx->window, bar_y, cols-1); + wattron(ctx->window, A_REVERSE); + wvline(ctx->window, ' ', bar_height); + + /* progress scroll position */ + ctx->pad_first_line += popup_lines - 1; + /* wrap on last shown page */ + ctx->pad_first_line %= pad_lines; + if (pad_lines - ctx->pad_first_line < popup_lines - 1) + /* show last page */ + ctx->pad_first_line = pad_lines - (popup_lines - 1); +} + +void +teco_curses_info_popup_clear(teco_curses_info_popup_t *ctx) +{ + if (ctx->window) + delwin(ctx->window); + if (ctx->pad) + delwin(ctx->pad); + if (ctx->chunk) + g_string_chunk_free(ctx->chunk); + + teco_stailq_entry_t *entry; + while ((entry = teco_stailq_remove_head(&ctx->list))) + g_free(entry); + + teco_curses_info_popup_init(ctx); +} diff --git a/src/interface-curses/curses-info-popup.cpp b/src/interface-curses/curses-info-popup.cpp deleted file mode 100644 index 487f1b7..0000000 --- a/src/interface-curses/curses-info-popup.cpp +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) 2012-2017 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 "curses-utils.h" -#include "curses-info-popup.h" - -namespace SciTECO { - -void -CursesInfoPopup::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 -CursesInfoPopup::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: - Curses::format_filename(pad, entry->name); - break; - default: - Curses::format_str(pad, entry->name); - break; - } - - cur_col++; - } -} - -void -CursesInfoPopup::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 -CursesInfoPopup::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; - } -} - -CursesInfoPopup::~CursesInfoPopup() -{ - if (window) - delwin(window); - if (pad) - delwin(pad); - if (list) - g_slist_free_full(list, g_free); -} - -} /* namespace SciTECO */ diff --git a/src/interface-curses/curses-info-popup.h b/src/interface-curses/curses-info-popup.h index af09cb4..d911182 100644 --- a/src/interface-curses/curses-info-popup.h +++ b/src/interface-curses/curses-info-popup.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * 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 @@ -14,78 +14,52 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#pragma once -#ifndef __CURSES_INFO_POPUP_H -#define __CURSES_INFO_POPUP_H +#include #include #include -#include "memory.h" +#include "list.h" +#include "interface.h" -namespace SciTECO { +typedef struct { + WINDOW *window; /**! window showing part of pad */ + WINDOW *pad; /**! full-height entry list */ -class CursesInfoPopup : public Object { -public: - /** - * @bug This is identical to the type defined in - * interface.h. But for the sake of abstraction - * we cannot access it here (or in gtk-info-popup - * for that matter). - */ - enum PopupEntryType { - POPUP_PLAIN, - POPUP_FILE, - POPUP_DIRECTORY - }; + teco_stailq_head_t list; /**! list of popup entries */ + gint longest; /**! size of longest entry */ + gint length; /**! total number of popup entries */ -private: - WINDOW *window; /**! window showing part of pad */ - WINDOW *pad; /**! full-height entry list */ + gint pad_first_line; /**! first line in pad to show */ - struct Entry { - PopupEntryType type; - bool highlight; - gchar name[]; - }; + GStringChunk *chunk; /**! string chunk for all popup entry names */ +} teco_curses_info_popup_t; - GSList *list; /**! list of popup entries */ - gint longest; /**! size of longest entry */ - gint length; /**! total number of popup entries */ +static inline void +teco_curses_info_popup_init(teco_curses_info_popup_t *ctx) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->list = TECO_STAILQ_HEAD_INITIALIZER(&ctx->list); +} - gint pad_first_line; /**! first line in pad to show */ +void teco_curses_info_popup_add(teco_curses_info_popup_t *ctx, teco_popup_entry_type_t type, + const gchar *name, gsize name_len, gboolean highlight); -public: - CursesInfoPopup() : window(NULL), pad(NULL), - list(NULL), longest(0), length(0), - pad_first_line(0) {} +void teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr); +static inline bool +teco_curses_info_popup_is_shown(teco_curses_info_popup_t *ctx) +{ + return ctx->window != NULL; +} - void add(PopupEntryType type, - const gchar *name, bool highlight = false); +static inline void +teco_curses_info_popup_noutrefresh(teco_curses_info_popup_t *ctx) +{ + if (ctx->window) + wnoutrefresh(ctx->window); +} - void show(attr_t attr); - inline bool - is_shown(void) - { - return window != NULL; - } - - void clear(void); - - inline void - noutrefresh(void) - { - if (window) - wnoutrefresh(window); - } - - ~CursesInfoPopup(); - -private: - void init_pad(attr_t attr); -}; - -} /* namespace SciTECO */ - -#endif +void teco_curses_info_popup_clear(teco_curses_info_popup_t *ctx); diff --git a/src/interface-curses/curses-utils.c b/src/interface-curses/curses-utils.c new file mode 100644 index 0000000..ace5795 --- /dev/null +++ b/src/interface-curses/curses-utils.c @@ -0,0 +1,144 @@ +/* + * 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 "sciteco.h" +#include "string-utils.h" +#include "curses-utils.h" + +gsize +teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width) +{ + int old_x, old_y; + gint chars_added = 0; + + getyx(win, old_y, old_x); + + 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 '\e': + 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 (TECO_IS_CTL(*str)) { + chars_added += 2; + if (chars_added > max_width) + goto truncate; + waddch(win, '^' | A_REVERSE); + waddch(win, TECO_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; +} + +gsize +teco_curses_format_filename(WINDOW *win, const gchar *filename, + gint max_width) +{ + int old_x = getcurx(win); + + g_autofree gchar *filename_printable = teco_string_echo(filename, strlen(filename)); + size_t filename_len = strlen(filename_printable); + + if (max_width < 0) + max_width = getmaxx(win) - old_x; + + if (filename_len <= (size_t)max_width) { + waddstr(win, filename_printable); + } else { + const gchar *keep_post = filename_printable + filename_len - + max_width + 3; + +#ifdef G_OS_WIN32 + const gchar *keep_pre = g_path_skip_root(filename_printable); + if (keep_pre) { + waddnstr(win, filename_printable, + keep_pre - filename_printable); + keep_post += keep_pre - filename_printable; + } +#endif + wattron(win, A_UNDERLINE | A_BOLD); + waddstr(win, "..."); + wattroff(win, A_UNDERLINE | A_BOLD); + waddstr(win, keep_post); + } + + return getcurx(win) - old_x; +} diff --git a/src/interface-curses/curses-utils.cpp b/src/interface-curses/curses-utils.cpp deleted file mode 100644 index f5d5c8c..0000000 --- a/src/interface-curses/curses-utils.cpp +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2012-2017 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 "sciteco.h" -#include "string-utils.h" -#include "curses-utils.h" - -namespace SciTECO { - -gsize -Curses::format_str(WINDOW *win, const gchar *str, - gssize len, gint max_width) -{ - 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; -} - -gsize -Curses::format_filename(WINDOW *win, const gchar *filename, - gint max_width) -{ - 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; -} - -} /* namespace SciTECO */ diff --git a/src/interface-curses/curses-utils.h b/src/interface-curses/curses-utils.h index 778f39e..3a681a4 100644 --- a/src/interface-curses/curses-utils.h +++ b/src/interface-curses/curses-utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * 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 @@ -14,26 +14,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -#ifndef __CURSES_UTILS_H -#define __CURSES_UTILS_H +#pragma once #include #include -namespace SciTECO { - -namespace Curses { - -gsize format_str(WINDOW *win, const gchar *str, - gssize len = -1, gint max_width = -1); - -gsize format_filename(WINDOW *win, const gchar *filename, - gint max_width = -1); - -} /* namespace Curses */ - -} /* namespace SciTECO */ +gsize teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width); -#endif +gsize teco_curses_format_filename(WINDOW *win, const gchar *filename, gint max_width); diff --git a/src/interface-curses/interface-curses.cpp b/src/interface-curses/interface-curses.cpp deleted file mode 100644 index a06fe30..0000000 --- a/src/interface-curses/interface-curses.cpp +++ /dev/null @@ -1,1613 +0,0 @@ -/* - * Copyright (C) 2012-2017 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 - -#include -#include -#include - -#include - -#ifdef HAVE_TIGETSTR -#include - -/* - * Some macros in term.h interfere with our code. - */ -#undef lines -#endif - -#include -#include - -#ifdef EMSCRIPTEN -#include -#endif - -#include "sciteco.h" -#include "string-utils.h" -#include "cmdline.h" -#include "qregisters.h" -#include "ring.h" -#include "error.h" -#include "interface.h" -#include "interface-curses.h" -#include "curses-utils.h" -#include "curses-info-popup.h" - -#ifdef HAVE_WINDOWS_H -/* here it shouldn't cause conflicts with other headers */ -#define WIN32_LEAN_AND_MEAN -#include -#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 - -/** - * 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 -#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 - -#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 - -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" */ - -static gint 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 -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 gint -xterm_version(void) -{ - static gint xterm_patch = -2; - - const gchar *term = g_getenv("TERM"); - const gchar *xterm_version; - - /* - * The XTerm patch level (version) is cached. - */ - if (xterm_patch != -2) - return xterm_patch; - xterm_patch = -1; - - 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. - */ - 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; -} - -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::init(void) -{ - /* - * 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); - - /* - * On all platforms except Curses/XTerm, it's - * safe to initialize the clipboards now. - */ -#ifndef CURSES_TTY - init_clipboard(); -#endif -} - -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(CURSES_TTY) - -/* - * 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\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. - */ -void -InterfaceCurses::restore_colors(void) -{ - if (xterm_version() < 0) - return; - - /* - * Looks like a real XTerm - */ - fputs(CTL_KEY_ESC_STR "]104\a", screen_tty); - fflush(screen_tty); -} - -#else /* !PDCURSES_WIN32 && !CURSES_TTY */ - -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 CURSES_TTY - -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 - * (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 - -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"). - * - * 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, ""); - - 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 EMCURSES - 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]); - } - } - - /* - * 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 - init_clipboard(); -#endif -} - -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(CURSES_TTY) && 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 CURSES_TTY - 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(CURSES_TTY) || 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(CURSES_TTY) && 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 " - "; - waddstr(info_window, info_type_str); - /* same formatting as in command lines */ - Curses::format_str(info_window, info_current); - break; - - case INFO_TYPE_BUFFER: - info_type_str = PACKAGE_NAME " - "; - waddstr(info_window, info_type_str); - Curses::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 = Curses::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 = Curses::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); -} - -#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. - */ -void -InterfaceCurses::init_clipboard(void) -{ - char *contents; - long length; - int rc; - - /* - * 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). - */ - rc = PDC_getclipboard(&contents, &length); - if (rc == PDC_CLIP_ACCESS_ERROR) - return; - if (rc == PDC_CLIP_SUCCESS) - PDC_freeclipboard(contents); - - QRegisters::globals.insert(new QRegisterClipboard()); -} - -void -InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len) -{ - int rc; - - if (str) { - if (str_len < 0) - str_len = strlen(str); - - rc = PDC_setclipboard(str, str_len); - } else { - rc = PDC_clearclipboard(); - } - - if (rc != PDC_CLIP_SUCCESS) - throw Error("Error %d copying to clipboard", rc); -} - -gchar * -InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len) -{ - char *contents; - long length = 0; - int rc; - gchar *str; - - /* - * NOTE: It is undefined whether we can pass in NULL for length. - */ - rc = PDC_getclipboard(&contents, &length); - if (str_len) - *str_len = length; - if (rc == PDC_CLIP_EMPTY) - return NULL; - if (rc != PDC_CLIP_SUCCESS) - throw Error("Error %d retrieving clipboard", rc); - - /* - * 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 we can null-terminate the return string in the - * process (PDCurses does not guarantee that either). - */ - str = (gchar *)g_malloc(length + 1); - memcpy(str, contents, length); - str[length] = '\0'; - - PDC_freeclipboard(contents); - return str; -} - -#elif defined(CURSES_TTY) - -void -InterfaceCurses::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 (!(Flags::ed & Flags::ED_XTERM_CLIPBOARD) || xterm_version() < 203) - return; - - QRegisters::globals.insert(new QRegisterClipboard()); - QRegisters::globals.insert(new QRegisterClipboard("P")); - QRegisters::globals.insert(new QRegisterClipboard("S")); - QRegisters::globals.insert(new QRegisterClipboard("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'; -} - -void -InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len) -{ - /* - * Enough space for 1024 Base64-encoded bytes. - */ - gchar buffer[(1024 / 3) * 4 + 4]; - gsize out_len; - - /* g_base64_encode_step() state: */ - gint state = 0; - gint save = 0; - - fputs(CTL_KEY_ESC_STR "]52;", screen_tty); - fputc(get_selection_by_name(name), screen_tty); - fputc(';', screen_tty); - - if (!str) - str_len = 0; - else if (str_len < 0) - str_len = strlen(str); - - 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, screen_tty); - - str_len -= step_len; - str += step_len; - } - - out_len = g_base64_encode_close(FALSE, buffer, &state, &save); - fwrite(buffer, 1, out_len, screen_tty); - - fputc('\a', screen_tty); - fflush(screen_tty); -} - -gchar * -InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len) -{ - /* - * Space for storing one group of decoded Base64 characters - * and the OSC-52 response. - */ - gchar buffer[MAX(3, 7)]; - GString *str_base64; - - /* g_base64_decode_step() state: */ - gint state = 0; - guint save = 0; - - /* - * Query the clipboard -- XTerm will reply with the - * OSC-52 command that would set the current selection. - */ - fputs(CTL_KEY_ESC_STR "]52;", screen_tty); - fputc(get_selection_by_name(name), screen_tty); - fputs(";?\a", screen_tty); - fflush(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(); - throw Error("Timed out reading XTerm clipboard"); - } - } - - str_base64 = g_string_new(""); - - for (;;) { - gchar c; - gsize out_len; - - c = (gchar)getch(); - if (c == ERR) { - /* timeout */ - cbreak(); - g_string_free(str_base64, TRUE); - throw Error("Timed out reading XTerm clipboard"); - } - 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 must should - * read character-wise using getch() anyway.) - */ - 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_len) - *str_len = str_base64->len; - - /* - * If the clipboard answer is empty, return NULL. - */ - return g_string_free(str_base64, str_base64->len == 0); -} - -#else - -void -InterfaceCurses::init_clipboard(void) -{ - /* - * No native clipboard support, so no clipboard Q-Regs are - * registered. - */ -} - -void -InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len) -{ - throw Error("Setting clipboard unsupported"); -} - -gchar * -InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len) -{ - throw Error("Getting clipboard unsupported"); -} - -#endif /* !__PDCURSES__ && !CURSES_TTY */ - -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. - * - * FIXME: NetBSD's curses could be handled like ncurses, - * but gets into an undefined state when SciTECO processes - * escape sequences. - */ -#ifdef NCURSES_UNIX - keypad(interface.cmdline_window, Flags::ed & Flags::ED_FNKEYS); -#endif - - /* no special handling */ - raw(); -#ifdef PDCURSES_WIN32 - SetConsoleMode(console_hnd, console_mode & ~ENABLE_PROCESSED_INPUT); -#endif - key = wgetch(interface.cmdline_window); - /* allow asynchronous interruptions on */ - 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 EMCURSES - 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 */ diff --git a/src/interface-curses/interface-curses.h b/src/interface-curses/interface-curses.h deleted file mode 100644 index 32fff1d..0000000 --- a/src/interface-curses/interface-curses.h +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (C) 2012-2017 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 . - */ - -#ifndef __INTERFACE_CURSES_H -#define __INTERFACE_CURSES_H - -#include - -#include - -#include - -#include -#include - -#include "interface.h" -#include "curses-info-popup.h" - -namespace SciTECO { - -typedef class ViewCurses : public View { - Scintilla *sci; - -public: - ViewCurses() : sci(NULL) {} - - /* implementation of View::initialize() */ - void initialize_impl(void); - - inline ~ViewCurses() - { - /* - * NOTE: This deletes/frees the view's - * curses WINDOW, despite of what old versions - * of the Scinterm documentation claim. - */ - if (sci) - scintilla_delete(sci); - } - - inline void - noutrefresh(void) - { - scintilla_noutrefresh(sci); - } - - inline void - refresh(void) - { - scintilla_refresh(sci); - } - - inline WINDOW * - get_window(void) - { - return scintilla_get_window(sci); - } - - /* implementation of View::ssm() */ - inline sptr_t - ssm_impl(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0) - { - return scintilla_send_message(sci, iMessage, wParam, lParam); - } -} ViewCurrent; - -typedef class InterfaceCurses : public Interface { - /** - * 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 { - short r, g, b; - } orig_color_table[16]; - - int stdout_orig, stderr_orig; - SCREEN *screen; - FILE *screen_tty; - - WINDOW *info_window; - enum { - INFO_TYPE_BUFFER = 0, - INFO_TYPE_QREGISTER - } info_type; - gchar *info_current; - - WINDOW *msg_window; - - WINDOW *cmdline_window, *cmdline_pad; - gsize cmdline_len, cmdline_rubout_len; - - CursesInfoPopup popup; - -public: - InterfaceCurses(); - ~InterfaceCurses(); - - /* override of Interface::init() */ - void init(void); - - /* override of Interface::init_color() */ - void init_color(guint color, guint32 rgb); - - /* implementation of Interface::vmsg() */ - void vmsg_impl(MessageType type, const gchar *fmt, va_list ap); - /* override of Interface::msg_clear() */ - void msg_clear(void); - - /* implementation of Interface::show_view() */ - void show_view_impl(ViewCurses *view); - - /* implementation of Interface::info_update() */ - void info_update_impl(const QRegister *reg); - void info_update_impl(const Buffer *buffer); - - /* implementation of Interface::cmdline_update() */ - void cmdline_update_impl(const Cmdline *cmdline); - - /* override of Interface::set_clipboard() */ - void set_clipboard(const gchar *name, - const gchar *str = NULL, gssize str_len = -1); - /* override of Interface::get_clipboard() */ - gchar *get_clipboard(const gchar *name, gsize *str_len = NULL); - - /* implementation of Interface::popup_add() */ - inline void - popup_add_impl(PopupEntryType type, - const gchar *name, bool highlight = false) - { - /* FIXME: The enum casting is dangerous */ - if (cmdline_window) - /* interactive mode */ - popup.add((CursesInfoPopup::PopupEntryType)type, - name, highlight); - } - - /* implementation of Interface::popup_show() */ - void popup_show_impl(void); - /* implementation of Interface::popup_is_shown() */ - inline bool - popup_is_shown_impl(void) - { - return popup.is_shown(); - } - - /* implementation of Interface::popup_clear() */ - void popup_clear_impl(void); - - /* main entry point (implementation) */ - void event_loop_impl(void); - -private: - void init_color_safe(guint color, guint32 rgb); - void restore_colors(void); - - void init_screen(void); - void init_interactive(void); - void restore_batch(void); - - void init_clipboard(void); - - void resize_all_windows(void); - - void set_window_title(const gchar *title); - void draw_info(void); - void draw_cmdline(void); - - friend void event_loop_iter(); -} InterfaceCurrent; - -} /* namespace SciTECO */ - -#endif 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