/* * Copyright (C) 2012-2015 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 #include #ifdef EMSCRIPTEN #include #endif #include "sciteco.h" #include "string-utils.h" #include "cmdline.h" #include "qregisters.h" #include "ring.h" #include "interface.h" #include "interface-curses.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 #endif /** * Whether we're on ncurses/win32 console */ #if defined(NCURSES_VERSION) && defined(G_OS_WIN32) #define NCURSES_WIN32 #endif namespace SciTECO { extern "C" { static void scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data); } #define UNNAMED_FILE "(Unnamed)" #define SCI_COLOR_ATTR(f, b) \ ((attr_t)COLOR_PAIR(SCI_COLOR_PAIR(f, b))) void ViewCurses::initialize_impl(void) { WINDOW *window; /* NOTE: Scintilla initializes color pairs */ sci = scintilla_new(scintilla_notify); window = get_window(); /* * Window must have dimension before it can be * positioned. * Perhaps it's better to leave the window * unitialized and set the position in * InterfaceCurses::show_view(). */ wresize(window, 1, 1); /* Set up window position: never changes */ mvwin(window, 1, 0); setup(); } void InterfaceCurses::main_impl(int &argc, char **&argv) { init_batch(); /* * We're in prog mode, so we must set it up * now, even though we're also in SciTECO batch mode. * This is because endwin() saves the prog mode * and Curses restores it automatically. */ cbreak(); noecho(); /* Scintilla draws its own cursor */ curs_set(0); setlocale(LC_CTYPE, ""); /* for displaying UTF-8 characters properly */ info_window = newwin(1, 0, 0, 0); info_current = g_strdup(PACKAGE_NAME); msg_window = newwin(1, 0, LINES - 2, 0); cmdline_window = newwin(0, 0, LINES - 1, 0); #ifdef EMSCRIPTEN nodelay(cmdline_window, TRUE); #elif !defined(PDCURSES_WIN32A) /* workaround: endwin() is somewhat broken in the win32a port */ endwin(); #endif } #if defined(__PDCURSES__) || !defined(G_OS_UNIX) void InterfaceCurses::init_batch(void) { #ifdef PDCURSES_WIN32A /* enables window resizing on Win32a port */ PDC_set_resize_limits(25, 0xFFFF, 80, 0xFFFF); #endif #ifdef NCURSES_WIN32 /* $TERM must be unset for the win32 driver to load */ g_unsetenv("TERM"); #endif /* * PDCurses cannot support terminal redirection * into files, nor can it support multiple terminals. * So we do a classic Curses initialization here. * Unfortunately, this clears the screen in * PDCurses/win32, so that batch mode is somewhat * broken there. */ initscr(); } void InterfaceCurses::init_interactive(void) { /* * Nothing to do, we are already controlling the * terminal. */ } #else /* UNIX, no PDCurses */ void InterfaceCurses::init_batch(void) { const gchar *term = g_getenv("TERM"); /* * In headless or broken environments, * $TERM may be unset or empty. * Still batch-mode operation is supposed * to work. * Therefore we initialize Curses with * a terminal type that ensures it will at least * start up ("ansi" is in ncurses-base). * We do not always use "ansi" since * it is also not guaranteed to work and we cannot * change it later on. */ if (!term || !*term) term = "ansi"; /* * This sets stdscr to a new screen associated * with /dev/null. * This way we get ncurses to leave the current * controlling tty alone, while still initializing * Curses (required by Scintilla and thus for * batch processing). */ screen_tty = g_fopen("/dev/null", "r+"); /* should never fail */ g_assert(screen_tty != NULL); setvbuf(screen_tty, NULL, _IOFBF, 0); screen = newterm(term, screen_tty, screen_tty); if (!screen) { /* $TERM may be set but to a wrong value */ g_fprintf(stderr, "Error initializing batch mode. " "$TERM may be incorrent.\n" "Try unsetting it if you do not need " "the interactive mode.\n"); exit(EXIT_FAILURE); } def_shell_mode(); } void InterfaceCurses::init_interactive(void) { const gchar *term = g_getenv("TERM"); /* at least try to report a broken $TERM */ if (!term || !*term) { g_fprintf(stderr, "Error initializing interactive mode: " "$TERM is unset or empty.\n"); exit(EXIT_FAILURE); } /* * Reopen screen_tty on the real terminal * device. * NOTE: It would be better to create a new * terminal with newterm() since the current * terminal might still be configured as * "ansi" to get the batch mode working. * If this is the case because we are in a head-less * or broken environment NOW would be the time * to tell the user. * However, I cannot get ncurses to switch * to a new terminal. Perhaps existing windows are * somehow "bound" to the current screen. * Perhaps this only works if we delete and recreate * ALL windows... */ if (!g_freopen("/dev/tty", "r+", screen_tty)) { /* no controlling terminal? */ g_fprintf(stderr, "Error initializing interactice mode: %s\n", g_strerror(errno)); exit(EXIT_FAILURE); } def_shell_mode(); } #endif 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) { static const attr_t type2attr[] = { SCI_COLOR_ATTR(COLOR_BLACK, COLOR_WHITE), /* MSG_USER */ SCI_COLOR_ATTR(COLOR_BLACK, COLOR_GREEN), /* MSG_INFO */ SCI_COLOR_ATTR(COLOR_BLACK, COLOR_YELLOW), /* MSG_WARNING */ SCI_COLOR_ATTR(COLOR_BLACK, COLOR_RED) /* MSG_ERROR */ }; #ifdef PDCURSES_WIN32A stdio_vmsg(type, fmt, ap); if (isendwin()) /* batch mode */ return; #else if (isendwin()) { /* batch mode */ stdio_vmsg(type, fmt, ap); return; } #endif wmove(msg_window, 0, 0); wbkgdset(msg_window, ' ' | type2attr[type]); vw_printw(msg_window, fmt, ap); wclrtoeol(msg_window); if (type == MSG_ERROR) beep(); } void InterfaceCurses::msg_clear(void) { if (isendwin()) /* batch mode */ return; wmove(msg_window, 0, 0); wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(COLOR_BLACK, COLOR_WHITE)); wclrtoeol(msg_window); } void InterfaceCurses::show_view_impl(ViewCurses *view) { int lines, cols; /* screen dimensions */ current_view = view; /* * screen size might have changed since * this view's WINDOW was last active */ getmaxyx(stdscr, lines, cols); wresize(current_view->get_window(), lines - 3, cols); } #if PDCURSES void InterfaceCurses::set_window_title(const gchar *title) { PDC_set_title(title); } #elif defined(HAVE_TIGETSTR) && defined(G_OS_UNIX) void InterfaceCurses::set_window_title(const gchar *title) { /* * NOTE: terminfo variables in term.h interfere with * the rest of our code */ const char *tsl = tigetstr((char *)"tsl"); const char *fsl = tigetstr((char *)"fsl"); if (!tsl || !fsl) 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. * NOTE: The terminfo manpage advises us to use putp(), * but I don't feel comfortable with writing to stdout. * NOTE: This leaves the title set after we quit. * xterm has escape sequences to save/restore a window title, * but there do not seem to be terminfo capabilities for that. * NOTE: Resetting the title does not always work ;-) */ fputs(tsl, screen_tty); fputs(info_current, screen_tty); fputs(fsl, 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) { if (isendwin()) /* batch mode */ return; wmove(info_window, 0, 0); wbkgdset(info_window, ' ' | SCI_COLOR_ATTR(COLOR_BLACK, COLOR_WHITE)); waddstr(info_window, info_current); wclrtoeol(info_window); set_window_title(info_current); } void InterfaceCurses::info_update_impl(const QRegister *reg) { /* * We cannot rely on Curses' control character drawing * and we need the info_current string for other purposes * (like PDC_set_title()), so we "canonicalize" the * register name here: */ gchar *name = String::canonicalize_ctl(reg->name); g_free(info_current); info_current = g_strconcat(PACKAGE_NAME " - ", name, NIL); g_free(name); /* NOTE: drawn in event_loop_iter() */ } void InterfaceCurses::info_update_impl(const Buffer *buffer) { g_free(info_current); info_current = g_strconcat(PACKAGE_NAME " - ", buffer->filename ? : UNNAMED_FILE, buffer->dirty ? "*" : "", NIL); /* NOTE: drawn in event_loop_iter() */ } void InterfaceCurses::format_chr(chtype *&target, gchar chr, attr_t attr) { /* * NOTE: This mapping is similar to * View::set_representations() */ switch (chr) { case CTL_KEY_ESC: *target++ = '$' | attr | A_REVERSE; break; case '\r': *target++ = 'C' | attr | A_REVERSE; *target++ = 'R' | attr | A_REVERSE; break; case '\n': *target++ = 'L' | attr | A_REVERSE; *target++ = 'F' | attr | A_REVERSE; break; case '\t': *target++ = 'T' | attr | A_REVERSE; *target++ = 'A' | attr | A_REVERSE; *target++ = 'B' | attr | A_REVERSE; break; default: if (IS_CTL(chr)) { *target++ = '^' | attr | A_REVERSE; *target++ = CTL_ECHO(chr) | attr | A_REVERSE; } else { *target++ = chr | attr; } } } void InterfaceCurses::cmdline_update_impl(const Cmdline *cmdline) { gsize alloc_len = 1; chtype *p; /* * AFAIK bold black should be rendered grey by any * common terminal. * If not, this problem will be gone once we support * a Scintilla view command line. * Also A_UNDERLINE is not supported by PDCurses/win32 * and causes weird colors, so we better leave it away. */ static const attr_t rubout_attr = #ifndef PDCURSES_WIN32 A_UNDERLINE | #endif A_BOLD | SCI_COLOR_ATTR(COLOR_BLACK, COLOR_BLACK); /* * Replace entire pre-formatted command-line. * We don't know if it is similar to the last one, * so realloc makes no sense. * We approximate the size of the new formatted command-line, * wasting a few bytes for control characters. */ delete[] cmdline_current; for (guint i = 0; i < cmdline->len+cmdline->rubout_len; i++) alloc_len += IS_CTL((*cmdline)[i]) ? 3 : 1; p = cmdline_current = new chtype[alloc_len]; /* format effective command line */ for (guint i = 0; i < cmdline->len; i++) format_chr(p, (*cmdline)[i]); cmdline_len = p - cmdline_current; /* Format rubbed-out command line. */ for (guint i = cmdline->len; i < cmdline->len+cmdline->rubout_len; i++) format_chr(p, (*cmdline)[i], rubout_attr); cmdline_rubout_len = p - cmdline_current - cmdline_len; /* highlight cursor after effective command line */ if (cmdline_rubout_len) { cmdline_current[cmdline_len] &= A_CHARTEXT | A_UNDERLINE; cmdline_current[cmdline_len] |= A_REVERSE; } else { cmdline_current[cmdline_len++] = ' ' | A_REVERSE; } draw_cmdline(); } void InterfaceCurses::draw_cmdline(void) { /* total width available for command line */ guint total_width = getmaxx(stdscr) - 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 % (total_width/2)); disp_len = MIN(total_width, cmdline_len+cmdline_rubout_len - disp_offset); werase(cmdline_window); mvwaddch(cmdline_window, 0, 0, '*' | A_BOLD); waddchnstr(cmdline_window, cmdline_current+disp_offset, disp_len); } void InterfaceCurses::popup_add_impl(PopupEntryType type, const gchar *name, bool highlight) { gchar *entry; if (isendwin()) /* batch mode */ return; entry = g_strconcat(highlight ? "*" : " ", name, NIL); popup.longest = MAX(popup.longest, (gint)strlen(name)); popup.length++; popup.list = g_slist_prepend(popup.list, entry); } void InterfaceCurses::popup_show_impl(void) { int lines, cols; /* screen dimensions */ int popup_lines; gint popup_cols; gint popup_colwidth; gint cur_col; if (isendwin() || !popup.length) /* batch mode or nothing to display */ return; getmaxyx(stdscr, lines, cols); if (popup.window) delwin(popup.window); else /* reverse list only once */ popup.list = g_slist_reverse(popup.list); if (!popup.cur_list) { /* start from beginning of list */ popup.cur_list = popup.list; popup.cur_entry = 0; } /* reserve 2 spaces between columns */ popup_colwidth = popup.longest + 2; /* popup_cols = floor(cols / popup_colwidth) */ popup_cols = MAX(cols / popup_colwidth, 1); /* popup_lines = ceil((popup.length-popup.cur_entry) / popup_cols) */ popup_lines = (popup.length-popup.cur_entry+popup_cols-1) / popup_cols; /* * Popup window can cover all but one screen row. * If it does not fit, the list of tokens is truncated * and "..." is displayed. */ popup_lines = MIN(popup_lines, lines - 1); /* window covers message, scintilla and info windows */ popup.window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0); wbkgd(popup.window, ' ' | SCI_COLOR_ATTR(COLOR_BLACK, COLOR_BLUE)); /* * 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; while (popup.cur_list) { gchar *entry = (gchar *)popup.cur_list->data; gint cur_line = cur_col/popup_cols + 1; wmove(popup.window, cur_line-1, (cur_col % popup_cols)*popup_colwidth); cur_col++; if (cur_line == popup_lines && !(cur_col % popup_cols) && g_slist_next(popup.cur_list) != NULL) { /* truncate entries in the popup's very last column */ (void)wattrset(popup.window, A_BOLD); waddstr(popup.window, "..."); break; } (void)wattrset(popup.window, *entry == '*' ? A_BOLD : A_NORMAL); waddstr(popup.window, entry + 1); popup.cur_list = g_slist_next(popup.cur_list); popup.cur_entry++; } redrawwin(info_window); /* scintilla window is redrawn by ViewCurses::refresh() */ redrawwin(msg_window); } void InterfaceCurses::popup_clear_impl(void) { g_slist_free_full(popup.list, g_free); popup.list = NULL; popup.length = 0; /* reserve at least 3 characters for "..." */ popup.longest = 3; popup.cur_list = NULL; popup.cur_entry = 0; if (!popup.window) return; redrawwin(info_window); /* scintilla window is redrawn by ViewCurses::refresh() */ redrawwin(msg_window); delwin(popup.window); popup.window = NULL; } /** * 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; /* * 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. */ keypad(interface.cmdline_window, Flags::ed & Flags::ED_FNKEYS); /* no special handling */ raw(); key = wgetch(interface.cmdline_window); /* allow asynchronous interruptions on */ cbreak(); if (key == ERR) return; switch (key) { #ifdef KEY_RESIZE case KEY_RESIZE: #ifdef __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); #undef FNS #undef FN /* * Control keys and keys with printable representation */ default: if (key <= 0xFF) cmdline.keypress((gchar)key); } sigint_occurred = FALSE; /* * 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); /* FIXME: this does wrefresh() internally */ interface.current_view->refresh(); wnoutrefresh(interface.msg_window); wnoutrefresh(interface.cmdline_window); if (interface.popup.window) wnoutrefresh(interface.popup.window); doupdate(); } void InterfaceCurses::event_loop_impl(void) { static const Cmdline empty_cmdline; /* * Initialize Curses for interactive mode */ init_interactive(); /* initial refresh */ /* FIXME: this does wrefresh() internally */ current_view->refresh(); draw_info(); wnoutrefresh(info_window); 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$$) */ } /* * Set window title to a reasonable default, * in case it is not reset immediately by the * shell. */ #if !PDCURSES && defined(HAVE_TIGETSTR) set_window_title(g_getenv("TERM") ? : ""); #endif /* * Restore ordinary terminal behaviour * (i.e. return to batch mode) */ endwin(); #endif } InterfaceCurses::Popup::~Popup() { if (window) delwin(window); if (list) g_slist_free_full(list, g_free); } InterfaceCurses::~InterfaceCurses() { if (info_window) delwin(info_window); g_free(info_current); if (cmdline_window) delwin(cmdline_window); delete[] cmdline_current; if (msg_window) delwin(msg_window); /* PDCurses (win32) crashes if initscr() wasn't called */ if (info_window && !isendwin()) endwin(); if (screen) delscreen(screen); if (screen_tty) fclose(screen_tty); } /* * Callbacks */ static void scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data) { interface.process_notify((SCNotification *)notify); } } /* namespace SciTECO */