/*
* Copyright (C) 2012-2016 Robin Haberkorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#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
#ifdef NCURSES_VERSION
#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
/**
* Whether we're on ncurses/UNIX.
* Haiku has a UNIX-like terminal and is largely
* POSIX compliant, so we can handle it like a
* UNIX ncurses.
*/
#define NCURSES_UNIX
#elif defined(G_OS_WIN32)
/**
* Whether we're on ncurses/win32 console
*/
#define NCURSES_WIN32
#endif
#endif
namespace SciTECO {
extern "C" {
/*
* PDCurses/win32a by default assigns functions to certain
* keys like CTRL+V, CTRL++, CTRL+- and CTRL+=.
* This conflicts with SciTECO that must remain in control
* of keyboard processing.
* Unfortunately, the default mapping can only be disabled
* or changed via the internal PDC_set_function_key() in
* pdcwin.h. Therefore we declare it manually here.
*/
#ifdef PDCURSES_WIN32A
int PDC_set_function_key(const unsigned function, const int new_key);
#define N_FUNCTION_KEYS 5
#define FUNCTION_KEY_SHUT_DOWN 0
#define FUNCTION_KEY_PASTE 1
#define FUNCTION_KEY_ENLARGE_FONT 2
#define FUNCTION_KEY_SHRINK_FONT 3
#define FUNCTION_KEY_CHOOSE_FONT 4
#endif
static void scintilla_notify(Scintilla *sci, int idFrom,
void *notify, void *user_data);
#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32)
/**
* This handler is the Windows-analogue of a signal
* handler. MinGW provides signal(), but it's not
* reliable.
* This may also be used to handle CTRL_CLOSE_EVENTs.
* NOTE: Unlike signal handlers, this is executed in a
* separate thread.
*/
static BOOL WINAPI
console_ctrl_handler(DWORD type)
{
switch (type) {
case CTRL_C_EVENT:
sigint_occurred = TRUE;
return TRUE;
}
return FALSE;
}
#endif
} /* extern "C" */
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 NCurses/XTerm, it's
* safe to initialize the clipboards now.
*/
#ifndef NCURSES_UNIX
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(NCURSES_UNIX)
/*
* FIXME: On UNIX/ncurses init_color_safe() __may__ change the
* terminal's palette permanently and there does not appear to be
* any portable way of restoring the original one.
* Curses has color_content(), but there is actually no terminal
* that allows querying the current palette and so color_content()
* will return bogus "default" values and only for the first 8 colors.
* It would do more damage to restore the palette returned by
* color_content() than it helps.
* xterm has the escape sequence "\e]104\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("\e]104\a", screen_tty);
fflush(screen_tty);
}
#else /* !PDCURSES_WIN32 && !NCURSES_UNIX */
void
InterfaceCurses::restore_colors(void)
{
/*
* No way to restore the palette, or it's
* unnecessary (e.g. XCurses)
*/
}
#endif
void
InterfaceCurses::init_color(guint color, guint32 rgb)
{
if (color >= G_N_ELEMENTS(color_table))
return;
#if defined(__PDCURSES__) && !defined(PDC_RGB)
/*
* PDCurses will usually number color codes differently
* (least significant bit is the blue component) while
* SciTECO macros will assume a standard terminal color
* code numbering with red as the LSB.
* Therefore we have to swap the bit order of the least
* significant 3 bits here.
*/
color = (color & ~0x5) |
((color & 0x1) << 2) | ((color & 0x4) >> 2);
#endif
if (cmdline_window) {
/* interactive mode */
if (!can_change_color())
return;
init_color_safe(color, rgb);
} else {
/*
* batch mode: store colors,
* they can only be initialized after start_color()
* which is called by Scinterm when interactive
* mode is initialized
*/
color_table[color] = (gint32)rgb;
}
}
#ifdef NCURSES_UNIX
void
InterfaceCurses::init_screen(void)
{
screen_tty = g_fopen("/dev/tty", "r+");
/* should never fail */
g_assert(screen_tty != NULL);
screen = newterm(NULL, screen_tty, screen_tty);
if (!screen) {
g_fprintf(stderr, "Error initializing interactive mode. "
"$TERM may be incorrect.\n");
exit(EXIT_FAILURE);
}
/*
* If stdout or stderr would go to the terminal,
* redirect it. Otherwise, they are already redirected
* (e.g. to a file) and writing to them does not
* interrupt terminal interaction.
*/
if (isatty(1)) {
FILE *stdout_new;
stdout_orig = dup(1);
g_assert(stdout_orig >= 0);
stdout_new = g_freopen("/dev/null", "a+", stdout);
g_assert(stdout_new != NULL);
}
if (isatty(2)) {
FILE *stderr_new;
stderr_orig = dup(2);
g_assert(stderr_orig >= 0);
stderr_new = g_freopen("/dev/null", "a+", stderr);
g_assert(stderr_new != NULL);
}
}
#elif defined(XCURSES)
void
InterfaceCurses::init_screen(void)
{
const char *argv[] = {PACKAGE_NAME, NULL};
/*
* This sets the program name to "SciTECO"
* which may then also be used as the X11 class name
* for overwriting X11 resources in .Xdefaults
*
* FIXME: We could support passing in resource
* overrides via the SciTECO command line.
* But unfortunately, Xinitscr() is called too
* late to modify argc/argv for command-line parsing
* (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").
*/
#ifdef NCURSES_UNIX
if (!g_getenv("ESCDELAY"))
set_escdelay(25);
#endif
/*
* $TERM must be unset or "#win32con" for the win32
* driver to load.
* So we always ignore any $TERM changes by the user.
*/
#ifdef NCURSES_WIN32
g_setenv("TERM", "#win32con", TRUE);
#endif
#ifdef PDCURSES_WIN32A
/*
* Necessary to enable window resizing in Win32a port
*/
PDC_set_resize_limits(25, 0xFFFF, 80, 0xFFFF);
/*
* Disable all magic function keys.
*/
for (int i = 0; i < N_FUNCTION_KEYS; i++)
PDC_set_function_key(i, 0);
/*
* Register the special shutdown function with the
* CLOSE key, so closing the window behaves similar as on
* GTK+.
*/
PDC_set_function_key(FUNCTION_KEY_SHUT_DOWN, KEY_CLOSE);
#endif
/* for displaying UTF-8 characters properly */
setlocale(LC_CTYPE, "");
init_screen();
cbreak();
noecho();
/* Scintilla draws its own cursor */
curs_set(0);
info_window = newwin(1, 0, 0, 0);
msg_window = newwin(1, 0, LINES - 2, 0);
cmdline_window = newwin(0, 0, LINES - 1, 0);
keypad(cmdline_window, TRUE);
#ifdef EMSCRIPTEN
nodelay(cmdline_window, TRUE);
#endif
/*
* Will also initialize Scinterm, Curses color pairs
* and resizes the current view.
*/
if (current_view)
show_view(current_view);
/*
* Only now it's safe to redefine the 16 default colors.
*/
if (can_change_color()) {
for (guint i = 0; i < G_N_ELEMENTS(color_table); i++) {
/*
* init_color() may still fail if COLORS < 16
*/
if (color_table[i] >= 0)
init_color_safe(i, (guint32)color_table[i]);
}
}
/*
* 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 NCURSES_UNIX
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(NCURSES_UNIX) && defined(HAVE_TIGETSTR)
set_window_title(g_getenv("TERM") ? : "");
#endif
/*
* Restore ordinary terminal behaviour
* (i.e. return to batch mode)
*/
endwin();
restore_colors();
/*
* Restore stdout and stderr, so output goes to
* the terminal again in case we "muted" them.
*/
#ifdef NCURSES_UNIX
if (stdout_orig >= 0) {
int fd = dup2(stdout_orig, 1);
g_assert(fd == 1);
}
if (stderr_orig >= 0) {
int fd = dup2(stderr_orig, 2);
g_assert(fd == 2);
}
#endif
/*
* See vmsg_impl(): It looks at msg_win to determine
* whether we're in batch mode.
*/
if (msg_window) {
delwin(msg_window);
msg_window = NULL;
}
}
void
InterfaceCurses::resize_all_windows(void)
{
int lines, cols; /* screen dimensions */
getmaxyx(stdscr, lines, cols);
wresize(info_window, 1, cols);
wresize(current_view->get_window(),
lines - 3, cols);
wresize(msg_window, 1, cols);
mvwin(msg_window, lines - 2, 0);
wresize(cmdline_window, 1, cols);
mvwin(cmdline_window, lines - 1, 0);
draw_info();
msg_clear(); /* FIXME: use saved message */
popup_clear();
draw_cmdline();
}
void
InterfaceCurses::vmsg_impl(MessageType type, const gchar *fmt, va_list ap)
{
short fg, bg;
if (!msg_window) { /* batch mode */
stdio_vmsg(type, fmt, ap);
return;
}
/*
* On most platforms we can write to stdout/stderr
* even in interactive mode.
*/
#if defined(XCURSES) || defined(PDCURSES_WIN32A) || \
defined(NCURSES_UNIX) || defined(NCURSES_WIN32)
va_list aq;
va_copy(aq, ap);
stdio_vmsg(type, fmt, aq);
va_end(aq);
#endif
fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
switch (type) {
default:
case MSG_USER:
bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
break;
case MSG_INFO:
bg = COLOR_GREEN;
break;
case MSG_WARNING:
bg = COLOR_YELLOW;
break;
case MSG_ERROR:
bg = COLOR_RED;
beep();
break;
}
wmove(msg_window, 0, 0);
wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
vw_printw(msg_window, fmt, ap);
wclrtoeol(msg_window);
}
void
InterfaceCurses::msg_clear(void)
{
short fg, bg;
if (!msg_window) /* batch mode */
return;
fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
werase(msg_window);
}
void
InterfaceCurses::show_view_impl(ViewCurses *view)
{
int lines, cols; /* screen dimensions */
WINDOW *current_view_win;
current_view = view;
if (!cmdline_window) /* batch mode */
return;
current_view_win = current_view->get_window();
/*
* screen size might have changed since
* this view's WINDOW was last active
*/
getmaxyx(stdscr, lines, cols);
wresize(current_view_win, lines - 3, cols);
/* Set up window position: never changes */
mvwin(current_view_win, 1, 0);
}
#if PDCURSES
void
InterfaceCurses::set_window_title(const gchar *title)
{
static gchar *last_title = NULL;
/*
* PDC_set_title() can result in flickering
* even when executed only once per pressed key,
* so we check whether it is really necessary to change
* the title.
* This is an issue at least with PDCurses/win32.
*/
if (!g_strcmp0(title, last_title))
return;
PDC_set_title(title);
g_free(last_title);
last_title = g_strdup(title);
}
#elif defined(NCURSES_UNIX) && defined(HAVE_TIGETSTR)
void
InterfaceCurses::set_window_title(const gchar *title)
{
if (!has_status_line || !to_status_line || !from_status_line)
return;
/*
* Modern terminal emulators map the window title to
* the historic status line.
* This feature is not standardized in ncurses,
* so we query the terminfo database.
* This feature may make problems with terminal emulators
* that do support a status line but do not map them
* to the window title. Some emulators (like xterm)
* support setting the window title via custom escape
* sequences and via the status line but their
* terminfo entry does not say so. (xterm can also
* save and restore window titles but there is not
* even a terminfo capability defined for this.)
* Taken the different emulator incompatibilites
* it may be best to make this configurable.
* Once we support configurable status lines,
* there could be a special status line that's sent
* to the terminal that may be set up in the profile
* depending on $TERM.
*
* NOTE: The terminfo manpage advises us to use putp()
* but on ncurses/UNIX (where terminfo is available),
* we do not let curses write to stdout.
* NOTE: This leaves the title set after we quit.
*/
fputs(to_status_line, screen_tty);
fputs(title, screen_tty);
fputs(from_status_line, screen_tty);
fflush(screen_tty);
}
#else
void
InterfaceCurses::set_window_title(const gchar *title)
{
/* no way to set window title */
}
#endif
void
InterfaceCurses::draw_info(void)
{
short fg, bg;
const gchar *info_type_str;
gchar *info_current_canon, *title;
if (!info_window) /* batch mode */
return;
/*
* The info line is printed in reverse colors of
* the current buffer's STYLE_DEFAULT.
* The same style is used for MSG_USER messages.
*/
fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
wmove(info_window, 0, 0);
wbkgdset(info_window, ' ' | SCI_COLOR_ATTR(fg, bg));
switch (info_type) {
case INFO_TYPE_QREGISTER:
info_type_str = PACKAGE_NAME " - ";
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(NCURSES_UNIX)
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("\e]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("\e]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__ && !NCURSES_UNIX */
void
InterfaceCurses::popup_show_impl(void)
{
short fg, bg;
if (!cmdline_window)
/* batch mode */
return;
fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_CALLTIP));
bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_CALLTIP));
popup.show(SCI_COLOR_ATTR(fg, bg));
}
void
InterfaceCurses::popup_clear_impl(void)
{
#ifdef __PDCURSES__
/*
* PDCurses will not redraw all windows that may be
* overlapped by the popup window correctly - at least
* not the info window.
* The Scintilla window is apparently always touched by
* scintilla_noutrefresh().
* Actually we would expect this to be necessary on any curses,
* but ncurses doesn't require this.
*/
if (popup.is_shown()) {
touchwin(info_window);
touchwin(msg_window);
}
#endif
popup.clear();
}
/**
* One iteration of the event loop.
*
* This is a global function, so it may
* be used as an Emscripten callback.
*
* @bug
* Can probably be defined as a static method,
* so we can avoid declaring it a fried function of
* InterfaceCurses.
*/
void
event_loop_iter()
{
int key;
/*
* On PDCurses/win32, raw() and cbreak() does
* not disable and enable CTRL+C handling properly.
* Since I don't want to patch PDCurses/win32,
* we do this manually here.
* NOTE: This exploits the fact that PDCurses uses
* STD_INPUT_HANDLE internally!
*/
#ifdef PDCURSES_WIN32
HANDLE console_hnd = GetStdHandle(STD_INPUT_HANDLE);
DWORD console_mode;
GetConsoleMode(console_hnd, &console_mode);
#endif
/*
* Setting function key processing is important
* on Unix Curses, as ESCAPE is handled as the beginning
* of a escape sequence when terminal emulators are
* involved.
* On some Curses variants (XCurses) however, keypad
* must always be TRUE so we receive KEY_RESIZE.
*/
#ifdef NCURSES_UNIX
keypad(interface.cmdline_window, Flags::ed & Flags::ED_FNKEYS);
#endif
/* no special 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 EMSCRIPTEN
PDC_emscripten_set_handler(event_loop_iter, TRUE);
/*
* We must not block emscripten's main loop,
* instead event_loop_iter() is called asynchronously.
* We also must not exit the event_loop() method, since
* SciTECO would assume ordinary program termination.
* We also must not call exit() since that would run
* the global destructors.
* The following exits the main() function immediately
* while keeping the "runtime" alive.
*/
emscripten_exit_with_live_runtime();
#else
try {
for (;;)
event_loop_iter();
} catch (Quit) {
/* SciTECO termination (e.g. EX$$) */
}
restore_batch();
#endif
}
InterfaceCurses::~InterfaceCurses()
{
if (info_window)
delwin(info_window);
g_free(info_current);
if (cmdline_window)
delwin(cmdline_window);
if (cmdline_pad)
delwin(cmdline_pad);
if (msg_window)
delwin(msg_window);
/*
* PDCurses (win32) crashes if initscr() wasn't called.
* Others (XCurses) crash if we try to use isendwin() here.
* Perhaps Curses cleanup should be in restore_batch()
* instead.
*/
#ifndef XCURSES
if (info_window && !isendwin())
endwin();
#endif
if (screen)
delscreen(screen);
if (screen_tty)
fclose(screen_tty);
if (stderr_orig >= 0)
close(stderr_orig);
if (stdout_orig >= 0)
close(stdout_orig);
}
/*
* Callbacks
*/
static void
scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data)
{
interface.process_notify((SCNotification *)notify);
}
} /* namespace SciTECO */