diff options
Diffstat (limited to 'src/interface-curses/interface.c')
| -rw-r--r-- | src/interface-curses/interface.c | 824 |
1 files changed, 455 insertions, 369 deletions
diff --git a/src/interface-curses/interface.c b/src/interface-curses/interface.c index a71ca20..b1c806f 100644 --- a/src/interface-curses/interface.c +++ b/src/interface-curses/interface.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 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 @@ -22,7 +22,6 @@ #include <string.h> #include <stdio.h> #include <stdlib.h> -#include <stdarg.h> #include <unistd.h> #include <errno.h> @@ -108,6 +107,8 @@ #define CURSES_TTY #endif +//#define DEBUG + #ifdef G_OS_WIN32 /** @@ -135,7 +136,7 @@ teco_console_ctrl_handler(DWORD type) static gint teco_xterm_version(void) G_GNUC_UNUSED; -#define UNNAMED_FILE "(Unnamed)" +static gint teco_interface_blocking_getch(void); /** * Get bright variant of one of the 8 standard @@ -163,19 +164,98 @@ static gint teco_xterm_version(void) G_GNUC_UNUSED; #define COLOR_LCYAN COLOR_LIGHT(COLOR_CYAN) #define COLOR_LWHITE COLOR_LIGHT(COLOR_WHITE) +static struct { + /** + * Mapping of foreground and background curses color tuples + * (encoded into a pointer) to a color pair number. + */ + GHashTable *pair_table; + + /** + * 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 stdin_orig, stdout_orig, stderr_orig; + SCREEN *screen; + FILE *screen_tty; + + WINDOW *info_window; + enum { + TECO_INFO_TYPE_BUFFER = 0, + TECO_INFO_TYPE_QREG + } info_type; + /* current document's name or empty string for "(Unnamed)" buffer */ + teco_string_t info_current; + gboolean info_dirty; + + /** timer to track the recovery interval */ + GTimer *recovery_timer; + + WINDOW *msg_window; + + /** + * Pad used exclusively for wgetch() as it will not + * result in unwanted wrefresh(). + */ + WINDOW *input_pad; + GQueue *input_queue; + + teco_curses_info_popup_t popup; + gsize popup_prefix_len; + + /** + * GError "thrown" by teco_interface_event_loop_iter(). + * Having this in a variable avoids problems with EMScripten. + */ + GError *event_loop_error; +} teco_interface; + /** - * Returns the curses `COLOR_PAIR` for the given curses foreground and background `COLOR`s. - * This is used simply to enumerate every possible color combination. - * Note: only 256 combinations are possible due to curses portability. + * Returns the curses color pair for the given curses foreground and background colors. + * Initializes a new pair if necessary. + * + * Scinterm no longer initializes all color pairs for all combinations of + * the builtin foreground and background colors. + * Since curses guarantees only 256 color pairs, we cannot do that either. + * Instead we allocate color pairs beginnig at 128 on demand + * (similar to what Scinterm does). * - * @param fg The curses foreground `COLOR`. - * @param bg The curses background `COLOR`. - * @return number for defining a curses `COLOR_PAIR`. + * @note Scinterm now also has scintilla_set_color_offsets(), + * so we could use the lower 127 color pairs as well. + * + * @param fg curses foreground color + * @param bg curses background color + * @return curses color pair number */ -static inline gshort +static gshort teco_color_pair(gshort fg, gshort bg) { - return bg * (COLORS < 16 ? 8 : 16) + fg + 1; + static gshort last_pair = 127; + + G_STATIC_ASSERT(sizeof(gshort)*2 <= sizeof(guint)); + gpointer key = GUINT_TO_POINTER(((guint)fg << 16) | bg); + gpointer value = g_hash_table_lookup(teco_interface.pair_table, key); + if (G_LIKELY(value != NULL)) + return GPOINTER_TO_UINT(value); + init_pair(++last_pair, fg, bg); + g_hash_table_insert(teco_interface.pair_table, key, GUINT_TO_POINTER(last_pair)); + return last_pair; } /** @@ -333,61 +413,6 @@ teco_view_free(teco_view_t *ctx) scintilla_delete(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; - gboolean info_dirty; - - WINDOW *msg_window; - - WINDOW *cmdline_window, *cmdline_pad; - guint cmdline_len, cmdline_rubout_len; - - /** - * Pad used exclusively for wgetch() as it will not - * result in unwanted wrefresh(). - */ - WINDOW *input_pad; - GQueue *input_queue; - - teco_curses_info_popup_t popup; - gsize popup_prefix_len; - - /** - * 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); @@ -401,7 +426,6 @@ 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) @@ -411,15 +435,16 @@ teco_interface_init(void) 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_interface.stdin_orig = teco_interface.stdout_orig = teco_interface.stderr_orig = -1; teco_curses_info_popup_init(&teco_interface.popup); + teco_cmdline_init(); /* - * Make sure we have a string for the info line - * even if teco_interface_info_update() is never called. + * The default INDIC_STRIKE wouldn't be visible. + * Instead we use INDIC_SQUIGGLE, which is rendered as A_UNDERLINE. */ - teco_string_init(&teco_interface.info_current, PACKAGE_NAME, strlen(PACKAGE_NAME)); + teco_cmdline_ssm(SCI_INDICSETSTYLE, INDICATOR_RUBBEDOUT, INDIC_SQUIGGLE); /* * On all platforms except Curses/XTerm, it's @@ -558,7 +583,7 @@ teco_interface_init_color(guint color, guint32 rgb) ((color & 0x1) << 2) | ((color & 0x4) >> 2); #endif - if (teco_interface.cmdline_window) { + if (teco_interface.input_pad) { /* interactive mode */ if (!can_change_color()) return; @@ -580,22 +605,46 @@ teco_interface_init_color(guint color, guint32 rgb) static void teco_interface_init_screen(void) { - teco_interface.screen_tty = g_fopen("/dev/tty", "r+"); + teco_interface.screen_tty = g_fopen("/dev/tty", "a"); /* should never fail */ g_assert(teco_interface.screen_tty != NULL); - teco_interface.screen = newterm(NULL, teco_interface.screen_tty, teco_interface.screen_tty); + /* + * At least on NetBSD we loose keypresses when passing in a + * handle for /dev/tty. + * We therefore redirect stdin in interactive mode. + * This works always if stdin was already redirected or not (isatty(0)) + * since we are guaranteed not to read from stdin outside of curses. + * When returning to batch mode, we can restore the original stdin. + */ + teco_interface.stdin_orig = dup(0); + g_assert(teco_interface.stdin_orig >= 0); + G_GNUC_UNUSED FILE *stdin_new = g_freopen("/dev/tty", "r", stdin); + g_assert(stdin_new != NULL); + + teco_interface.screen = newterm(NULL, teco_interface.screen_tty, stdin); if (G_UNLIKELY(!teco_interface.screen)) { g_fprintf(stderr, "Error initializing interactive mode. " "$TERM may be incorrect.\n"); exit(EXIT_FAILURE); } + /* initscr() does that in ncurses */ + def_prog_mode(); + /* * 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. + * + * This cannot of course preserve all messages written to stdout/stderr. + * Only those messages written before flushing will be preserved and + * be visible after program termination since they are still in a user- + * space stdio-buffer. + * All messages could only be preserved if we redirected to a temporary + * file and replayed it afterwards. It wouldn't preserve the order of + * stdout vs. stderr messages. */ if (isatty(1)) { teco_interface.stdout_orig = dup(1); @@ -690,6 +739,9 @@ teco_interface_init_interactive(GError **error) teco_interface_init_screen(); + teco_interface.pair_table = g_hash_table_new(g_direct_hash, g_direct_equal); + start_color(); + /* * On UNIX terminals, the escape key is usually * delivered as the escape character even though function @@ -716,12 +768,8 @@ teco_interface_init_interactive(GError **error) * Disables click-detection. * If we'd want to discern PRESSED and CLICKED events, * we'd have to emulate the same feature on GTK. - * - * On PDCurses/Wincon we currently rely on click detection - * since it does not report BUTTONX_RELEASED unless also - * moving the mouse cursor. */ -#if NCURSES_MOUSE_VERSION >= 2 && !defined(PDCURSES_WINCON) +#if NCURSES_MOUSE_VERSION >= 2 mouseinterval(0); #endif @@ -745,8 +793,12 @@ teco_interface_init_interactive(GError **error) leaveok(stdscr, TRUE); 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); + teco_interface.msg_window = newwin(1, 0, LINES - teco_cmdline.height - 1, 0); + + WINDOW *cmdline_win = teco_view_get_window(teco_cmdline.view); + wresize(cmdline_win, teco_cmdline.height, COLS); + mvwin(cmdline_win, LINES - teco_cmdline.height, 0); + teco_cmdline_resized(COLS); teco_interface.input_pad = newpad(1, 1); /* @@ -819,10 +871,14 @@ teco_interface_restore_batch(void) teco_interface_restore_colors(); /* - * Restore stdout and stderr, so output goes to + * Restore stdin, stdout and stderr, so output goes to * the terminal again in case we "muted" them. */ #ifdef CURSES_TTY + if (teco_interface.stdin_orig >= 0) { + G_GNUC_UNUSED int fd = dup2(teco_interface.stdin_orig, 0); + g_assert(fd == 0); + } if (teco_interface.stdout_orig >= 0) { G_GNUC_UNUSED int fd = dup2(teco_interface.stdout_orig, 1); g_assert(fd == 1); @@ -834,40 +890,38 @@ teco_interface_restore_batch(void) #endif /* - * cmdline_window determines whether we're in batch mode. + * input_pad determines whether we're in batch mode. */ - if (teco_interface.cmdline_window) { - delwin(teco_interface.cmdline_window); - teco_interface.cmdline_window = NULL; + if (teco_interface.input_pad) { + delwin(teco_interface.input_pad); + teco_interface.input_pad = 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_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); + LINES - 2 - teco_cmdline.height, COLS); + wresize(teco_interface.msg_window, 1, COLS); + mvwin(teco_interface.msg_window, LINES - 1 - teco_cmdline.height, 0); + + WINDOW *cmdline_win = teco_view_get_window(teco_cmdline.view); + wresize(cmdline_win, teco_cmdline.height, COLS); + mvwin(cmdline_win, LINES - teco_cmdline.height, 0); + teco_cmdline_resized(COLS); 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) +teco_interface_msg_literal(teco_msg_t type, const gchar *str, gsize len) { - if (!teco_interface.cmdline_window) { /* batch mode */ - teco_interface_stdio_vmsg(type, fmt, ap); + if (!teco_interface.input_pad) { /* batch mode */ + teco_interface_stdio_msg(type, str, len); return; } @@ -876,10 +930,7 @@ teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) * even in interactive mode. */ #if defined(PDCURSES_GUI) || defined(CURSES_TTY) || defined(NCURSES_WIN32) - va_list aq; - va_copy(aq, ap); - teco_interface_stdio_vmsg(type, fmt, aq); - va_end(aq); + teco_interface_stdio_msg(type, str, len); #endif short fg, bg; @@ -903,27 +954,68 @@ teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) break; } - /* - * NOTE: This is safe since we don't have to cancel out any A_REVERSE, - * that could be set in the background attributes. - */ wmove(teco_interface.msg_window, 0, 0); - wbkgdset(teco_interface.msg_window, teco_color_attr(fg, bg)); - vw_printw(teco_interface.msg_window, fmt, ap); - wclrtoeol(teco_interface.msg_window); + wattrset(teco_interface.msg_window, teco_color_attr(fg, bg)); + teco_curses_format_str(teco_interface.msg_window, str, len, -1); + teco_curses_clrtobot(teco_interface.msg_window); } void teco_interface_msg_clear(void) { - if (!teco_interface.cmdline_window) /* batch mode */ + if (!teco_interface.input_pad) /* 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, teco_color_attr(fg, bg)); - werase(teco_interface.msg_window); + wmove(teco_interface.msg_window, 0, 0); + wattrset(teco_interface.msg_window, teco_color_attr(fg, bg)); + teco_curses_clrtobot(teco_interface.msg_window); +} + +teco_int_t +teco_interface_getch(gboolean widechar) +{ + if (!teco_interface.input_pad) /* batch mode */ + return teco_interface_stdio_getch(widechar); + + teco_interface_refresh(FALSE); + + /* + * Signal that we accept input by drawing a real cursor in the message bar. + */ + wmove(teco_interface.msg_window, 0, 0); + curs_set(1); + wrefresh(teco_interface.msg_window); + + gchar buf[4]; + gint i = 0; + gint32 cp; + + do { + cp = teco_interface_blocking_getch(); + if (cp == TECO_CTL_KEY('C')) + teco_interrupted = TRUE; + if (cp == TECO_CTL_KEY('C') || cp == TECO_CTL_KEY('D')) { + cp = -1; + break; + } + if (cp < 0 || cp > 0xFF) + continue; + + if (!widechar || !cp) + break; + + /* doesn't work as expected when passed a null byte */ + buf[i] = cp; + cp = g_utf8_get_char_validated(buf, ++i); + if (i >= sizeof(buf) || cp != -2) + i = 0; + } while (cp < 0); + + curs_set(0); + return cp; } void @@ -931,7 +1023,7 @@ teco_interface_show_view(teco_view_t *view) { teco_interface_current_view = view; - if (!teco_interface.cmdline_window) /* batch mode */ + if (!teco_interface.input_pad) /* batch mode */ return; WINDOW *current_view_win = teco_view_get_window(teco_interface_current_view); @@ -940,9 +1032,7 @@ teco_interface_show_view(teco_view_t *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); + wresize(current_view_win, LINES - 2 - teco_cmdline.height, COLS); /* Set up window position: never changes */ mvwin(current_view_win, 1, 0); } @@ -972,40 +1062,64 @@ teco_interface_set_window_title(const gchar *title) #elif defined(CURSES_TTY) && defined(HAVE_TIGETSTR) +/* + * Many 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, rxvt and many pseudo-xterms) + * support setting the window title via custom escape + * sequences and via the status line but their + * terminfo entry does not say so. + * Real XTerm can also save and restore window titles but + * there is not even a terminfo capability defined for this. + * Currently, SciTECO just leaves the title set after we quit. + * + * TODO: Once we support customizing the UI, + * there could be a special status line that's sent + * to the terminal that may be set up in the profile + * depending on $TERM. + */ static void teco_interface_set_window_title(const gchar *title) { - if (!has_status_line || !to_status_line || !from_status_line) + static const gchar *term = NULL; + static const gchar *title_start = NULL; + static const gchar *title_end = NULL; + + if (G_UNLIKELY(!term)) { + term = g_getenv("TERM"); + + title_start = to_status_line; + title_end = from_status_line; + + if ((!title_start || !title_end) && term && + (g_str_has_prefix(term, "xterm") || g_str_has_prefix(term, "rxvt"))) { + /* + * Just assume that any whitelisted $TERM has the OSC-0 + * escape sequence or at least ignores it. + * This might also set the window's icon, but it's more widely + * used than OSC-2. + */ + title_start = "\e]0;"; + title_end = "\a"; + } + } + + if (!title_start || !title_end) 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_start, teco_interface.screen_tty); fputs(title, teco_interface.screen_tty); - fputs(from_status_line, teco_interface.screen_tty); + fputs(title_end, teco_interface.screen_tty); fflush(teco_interface.screen_tty); } @@ -1040,26 +1154,30 @@ teco_interface_draw_info(void) waddstr(teco_interface.info_window, PACKAGE_NAME " "); + teco_string_t info_current = teco_interface.info_current; + if (!info_current.len) { + info_current.data = TECO_UNNAMED_FILE; + info_current.len = strlen(info_current.data); + } + switch (teco_interface.info_type) { case TECO_INFO_TYPE_QREG: info_type_str = PACKAGE_NAME " - <QRegister> "; teco_curses_add_wc(teco_interface.info_window, teco_ed & TECO_ED_ICONS ? TECO_CURSES_ICONS_QREG : '-'); waddstr(teco_interface.info_window, " <QRegister> "); - /* 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); + info_current.data, info_current.len, -1); break; case TECO_INFO_TYPE_BUFFER: info_type_str = PACKAGE_NAME " - <Buffer> "; - g_assert(!teco_string_contains(&teco_interface.info_current, '\0')); + g_assert(!teco_string_contains(info_current, '\0')); + /* "(Unnamed)" buffer has to be looked up as "" */ teco_curses_add_wc(teco_interface.info_window, teco_ed & TECO_ED_ICONS ? teco_curses_icons_lookup_file(teco_interface.info_current.data) : '-'); waddstr(teco_interface.info_window, " <Buffer> "); - teco_curses_format_filename(teco_interface.info_window, - teco_interface.info_current.data, + teco_curses_format_filename(teco_interface.info_window, info_current.data, getmaxx(teco_interface.info_window) - getcurx(teco_interface.info_window) - 1); waddch(teco_interface.info_window, teco_interface.info_dirty ? '*' : ' '); @@ -1075,8 +1193,7 @@ teco_interface_draw_info(void) * 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); + info_current_printable = teco_string_echo(info_current.data, info_current.len); g_autofree gchar *title = g_strconcat(info_type_str, info_current_printable, teco_interface.info_dirty ? "*" : "", NULL); teco_interface_set_window_title(title); @@ -1096,123 +1213,14 @@ teco_interface_info_update_qreg(const teco_qreg_t *reg) 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_interface.info_dirty = buffer->dirty; + teco_string_init(&teco_interface.info_current, buffer->filename, + buffer->filename ? strlen(buffer->filename) : 0); + teco_interface.info_dirty = buffer->state > TECO_BUFFER_CLEAN; 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) -{ - /* - * Especially important on PDCurses, which can crash - * in newpad() when run with --fake-cmdline. - */ - if (!teco_interface.cmdline_window) /* batch mode */ - return; - - /* - * 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 and - * multi-byte Unicode sequences. - */ - 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)); - wattrset(teco_interface.cmdline_pad, teco_color_attr(fg, bg)); - - /* 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. - * The attributes, supported by the terminal can theoretically - * be queried with term_attrs(). - */ - 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 - * FIXME: This should use SCI_GETCARETFORE(). - */ - attr_t attr = A_NORMAL; - short pair = 0; - if (teco_interface.cmdline_rubout_len) { - 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)) ^ A_REVERSE, pair, NULL); - } else { - teco_interface.cmdline_len++; - wattr_get(teco_interface.cmdline_pad, &attr, &pair, NULL); - wattr_set(teco_interface.cmdline_pad, (attr & ~(A_UNDERLINE | A_BOLD)) ^ A_REVERSE, pair, NULL); - waddch(teco_interface.cmdline_pad, ' '); - } - - 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)); - - wattrset(teco_interface.cmdline_window, teco_color_attr(fg, bg)); - mvwaddch(teco_interface.cmdline_window, 0, 0, '*' | A_BOLD); - teco_curses_clrtobot(teco_interface.cmdline_window); - copywin(teco_interface.cmdline_pad, teco_interface.cmdline_window, - 0, disp_offset, 0, 1, 0, disp_len, FALSE); -} - #if PDCURSES /* @@ -1243,7 +1251,7 @@ teco_interface_init_clipboard(void) if (rc == PDC_CLIP_SUCCESS) PDC_freeclipboard(contents); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); } gboolean @@ -1307,8 +1315,10 @@ get_selection_by_name(const gchar *name) * (everything gets passed down), but currently we * only register the three standard registers * "~", "~P", "~S" and "~C". + * (We are never called with "~", though.) */ - return g_ascii_tolower(*name) ? : 'c'; + g_assert(*name != '\0'); + return g_ascii_tolower(*name); } /* @@ -1507,10 +1517,10 @@ teco_interface_init_clipboard(void) !teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECO_CLIPBOARD_GET", 22))) 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")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("P")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("S")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("C")); } gboolean @@ -1520,7 +1530,7 @@ teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, if (teco_interface_osc52_is_enabled()) return teco_interface_osc52_set_clipboard(name, str, str_len, error); - static const gchar *reg_name = "$SCITECO_CLIPBOARD_SET"; + static const gchar reg_name[] = "$SCITECO_CLIPBOARD_SET"; teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, reg_name, strlen(reg_name)); if (!reg) { @@ -1533,7 +1543,7 @@ teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, g_auto(teco_string_t) command; if (!reg->vtable->get_string(reg, &command.data, &command.len, NULL, error)) return FALSE; - if (teco_string_contains(&command, '\0')) { + if (teco_string_contains(command, '\0')) { teco_error_qregcontainsnull_set(error, reg_name, strlen(reg_name), FALSE); return FALSE; } @@ -1581,7 +1591,7 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError if (teco_interface_osc52_is_enabled()) return teco_interface_osc52_get_clipboard(name, str, len, error); - static const gchar *reg_name = "$SCITECO_CLIPBOARD_GET"; + static const gchar reg_name[] = "$SCITECO_CLIPBOARD_GET"; teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, reg_name, strlen(reg_name)); if (!reg) { @@ -1594,7 +1604,7 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError g_auto(teco_string_t) command; if (!reg->vtable->get_string(reg, &command.data, &command.len, NULL, error)) return FALSE; - if (teco_string_contains(&command, '\0')) { + if (teco_string_contains(command, '\0')) { teco_error_qregcontainsnull_set(error, reg_name, strlen(reg_name), FALSE); return FALSE; } @@ -1680,7 +1690,7 @@ void teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len, gboolean highlight) { - if (teco_interface.cmdline_window) + if (teco_interface.input_pad) /* interactive mode */ teco_curses_info_popup_add(&teco_interface.popup, type, name, name_len, highlight); } @@ -1688,7 +1698,7 @@ teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize void teco_interface_popup_show(gsize prefix_len) { - if (!teco_interface.cmdline_window) + if (!teco_interface.input_pad) /* batch mode */ return; @@ -1702,7 +1712,7 @@ teco_interface_popup_show(gsize prefix_len) void teco_interface_popup_scroll(void) { - if (!teco_interface.cmdline_window) + if (!teco_interface.input_pad) /* batch mode */ return; @@ -1757,8 +1767,8 @@ teco_interface_is_interrupted(void) * filtering out CTRL+C. * It's currently necessary as a fallback e.g. for PDCURSES_GUI or XCurses. * - * NOTE: Theoretically, this can be optimized by doing wgetch() only every X - * microseconds like on Gtk+. + * NOTE: Theoretically, this can be optimized by doing wgetch() only every + * TECO_POLL_INTERVAL microseconds like on Gtk+. * But this turned out to slow things down, at least on PDCurses/WinGUI. */ gboolean @@ -1786,9 +1796,22 @@ teco_interface_is_interrupted(void) #endif -static void -teco_interface_refresh(void) +void +teco_interface_refresh(gboolean force) { + if (!teco_interface.input_pad) + /* batch mode */ + return; + +#ifdef NETBSD_CURSES + /* works around crashes in doupdate() */ + if (G_UNLIKELY(COLS <= 1 || LINES <= 1)) + return; +#endif + + if (G_UNLIKELY(force)) + clearok(curscr, TRUE); + /* * Info window is updated very often which is very * costly, especially when using PDC_set_title(), @@ -1799,7 +1822,7 @@ teco_interface_refresh(void) wnoutrefresh(teco_interface.info_window); teco_view_noutrefresh(teco_interface_current_view); wnoutrefresh(teco_interface.msg_window); - wnoutrefresh(teco_interface.cmdline_window); + teco_view_noutrefresh(teco_cmdline.view); teco_curses_info_popup_noutrefresh(&teco_interface.popup); doupdate(); } @@ -1813,42 +1836,50 @@ teco_interface_refresh(void) (BUTTON1_##X | BUTTON2_##X | BUTTON3_##X | BUTTON4_##X | BUTTON5_##X) static gboolean -teco_interface_getmouse(GError **error) +teco_interface_process_mevent(MEVENT *event, GError **error) { - MEVENT event; - - if (getmouse(&event) != OK) - return TRUE; +#ifdef DEBUG + g_printf("EVENT: 0x%016X -> bit %02d [%c%c%c%c%c]\n", + event->bstate, ffs(event->bstate)-1, + event->bstate & BUTTON_NUM(4) ? 'U' : ' ', + event->bstate & BUTTON_NUM(5) ? 'D' : ' ', + event->bstate & BUTTON_EVENT(PRESSED) ? 'P' : ' ', + event->bstate & BUTTON_EVENT(RELEASED) ? 'R' : ' ', + event->bstate & REPORT_MOUSE_POSITION ? 'M' : ' '); +#endif if (teco_curses_info_popup_is_shown(&teco_interface.popup) && - wmouse_trafo(teco_interface.popup.window, &event.y, &event.x, FALSE)) { + wmouse_trafo(teco_interface.popup.window, &event->y, &event->x, FALSE)) { /* * NOTE: Not all curses variants report the RELEASED event, * but may also return REPORT_MOUSE_POSITION. * So we might react to all button presses as well. - * Others will still report CLICKED. */ - if (event.bstate & (BUTTON1_RELEASED | BUTTON1_CLICKED | REPORT_MOUSE_POSITION)) { + if (event->bstate & (BUTTON1_RELEASED | REPORT_MOUSE_POSITION)) { teco_machine_t *machine = &teco_cmdline.machine.parent; - const teco_string_t *insert = teco_curses_info_popup_getentry(&teco_interface.popup, event.y, event.x); + const teco_string_t *insert = teco_curses_info_popup_getentry(&teco_interface.popup, + event->y, event->x); if (insert && machine->current->insert_completion_cb) { - /* successfully clicked popup item */ + /* + * Successfully clicked popup item. + * `insert` is the empty string for the "(Unnamed)" buffer. + */ const teco_string_t insert_suffix = {insert->data + teco_interface.popup_prefix_len, insert->len - teco_interface.popup_prefix_len}; - if (!machine->current->insert_completion_cb(machine, &insert_suffix, error)) + if (!machine->current->insert_completion_cb(machine, insert_suffix, error)) return FALSE; teco_interface_popup_clear(); teco_interface_msg_clear(); - teco_interface_cmdline_update(&teco_cmdline); + teco_cmdline_update(); } return TRUE; } - if (event.bstate & BUTTON_NUM(4)) + if (event->bstate & BUTTON_NUM(4)) teco_curses_info_popup_scroll(&teco_interface.popup, -2); - else if (event.bstate & BUTTON_NUM(5)) + else if (event->bstate & BUTTON_NUM(5)) teco_curses_info_popup_scroll(&teco_interface.popup, +2); short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_CALLTIP, 0)); @@ -1859,106 +1890,144 @@ teco_interface_getmouse(GError **error) } /* - * Return mouse coordinates relative to the view. - * They will be in characters, but that's what SCI_POSITIONFROMPOINT - * expects on Scinterm anyway. - */ - WINDOW *current = teco_view_get_window(teco_interface_current_view); - if (!wmouse_trafo(current, &event.y, &event.x, FALSE)) - /* no event inside of current view */ - return TRUE; - - /* * NOTE: There will only be one of the button bits * set in bstate, so we don't loose information translating * them to enums. * - * At least on ncurses, we don't always get a RELEASED event. - * It instead sends only REPORT_MOUSE_POSITION, - * so make sure not to overwrite teco_mouse.button in this case. + * At least on ncurses, this enables the "Normal tracking mode" + * which only reports PRESSEND and RELEASED events, but no mouse + * position tracing. */ - if (event.bstate & BUTTON_NUM(4)) + if (event->bstate & BUTTON_NUM(4)) /* scroll up - there will be no RELEASED event */ teco_mouse.type = TECO_MOUSE_SCROLLUP; - else if (event.bstate & BUTTON_NUM(5)) + else if (event->bstate & BUTTON_NUM(5)) /* scroll down - there will be no RELEASED event */ teco_mouse.type = TECO_MOUSE_SCROLLDOWN; - else if (event.bstate & BUTTON_EVENT(RELEASED)) + else if (event->bstate & BUTTON_EVENT(RELEASED)) teco_mouse.type = TECO_MOUSE_RELEASED; - else if (event.bstate & BUTTON_EVENT(PRESSED)) + else if (event->bstate & BUTTON_EVENT(PRESSED)) teco_mouse.type = TECO_MOUSE_PRESSED; else - /* can also be REPORT_MOUSE_POSITION */ - teco_mouse.type = TECO_MOUSE_RELEASED; + return TRUE; - teco_mouse.x = event.x; - teco_mouse.y = event.y; + /* + * Return mouse coordinates relative to the view. + * They will be in characters, but that's what SCI_POSITIONFROMPOINT + * expects on Scinterm anyway. + */ + WINDOW *current = teco_view_get_window(teco_interface_current_view); + if (!wmouse_trafo(current, &event->y, &event->x, FALSE)) + /* no event inside of current view */ + return TRUE; - if (event.bstate & BUTTON_NUM(1)) + teco_mouse.x = event->x; + teco_mouse.y = event->y; + + if (event->bstate & BUTTON_NUM(1)) teco_mouse.button = 1; - else if (event.bstate & BUTTON_NUM(2)) + else if (event->bstate & BUTTON_NUM(2)) teco_mouse.button = 2; - else if (event.bstate & BUTTON_NUM(3)) + else if (event->bstate & BUTTON_NUM(3)) teco_mouse.button = 3; - else if (!(event.bstate & REPORT_MOUSE_POSITION)) + else if (!(event->bstate & REPORT_MOUSE_POSITION)) teco_mouse.button = -1; teco_mouse.mods = 0; - if (event.bstate & BUTTON_SHIFT) + if (event->bstate & BUTTON_SHIFT) teco_mouse.mods |= TECO_MOUSE_SHIFT; - if (event.bstate & BUTTON_CTRL) + if (event->bstate & BUTTON_CTRL) teco_mouse.mods |= TECO_MOUSE_CTRL; - if (event.bstate & BUTTON_ALT) + if (event->bstate & BUTTON_ALT) teco_mouse.mods |= TECO_MOUSE_ALT; - if (event.bstate & BUTTON_EVENT(CLICKED)) { - /* - * Click detection __should__ be disabled, - * but some Curses implementations report them anyway. - * This has been observed on PDCurses/WinGUI. - * On PDCurses/Wincon we especially did not disable - * click detection since it doesn't report - * BUTTONX_RELEASED at all. - * We emulate separate PRESSED/RELEASE events on those - * platforms. - */ - teco_mouse.type = TECO_MOUSE_PRESSED; - if (!teco_cmdline_keymacro("MOUSE", -1, error)) - return FALSE; - teco_mouse.type = TECO_MOUSE_RELEASED; +#if defined(NCURSES_UNIX) && NCURSES_VERSION_PATCH < 20250913 + /* + * FIXME: Some terminal emulators do not send separate + * middle click PRESSED and RELEASED buttons + * (both are sent when releasing the button). + * Furthermore due to ncurses bugs the order + * of events is arbitrary. + * Therefore we ignore BUTTON2_PRESSED and synthesize + * PRESSED and RELEASED evnts on BUTTON2_RELEASED: + */ + if (teco_mouse.button == 2) { + if (teco_mouse.type == TECO_MOUSE_PRESSED) + /* ignore BUTTON2_PRESSED events */ + return TRUE; + if (teco_mouse.type == TECO_MOUSE_RELEASED) { + teco_mouse.type = TECO_MOUSE_PRESSED; + if (!teco_cmdline_keymacro("MOUSE", -1, error)) + return FALSE; + teco_mouse.type = TECO_MOUSE_RELEASED; + } } +#endif /* NCURSES_UNIX && NCURSES_VERSION_PATCH < 20250913 */ + return teco_cmdline_keymacro("MOUSE", -1, error); } +static gboolean +teco_interface_getmouse(GError **error) +{ + MEVENT event; + + while (getmouse(&event) == OK) + if (!teco_interface_process_mevent(&event, error)) + return FALSE; + + return TRUE; +} + #endif /* NCURSES_MOUSE_VERSION >= 2 */ static gint teco_interface_blocking_getch(void) { + if (!g_queue_is_empty(teco_interface.input_queue)) + return GPOINTER_TO_INT(g_queue_pop_head(teco_interface.input_queue)); + #if NCURSES_MOUSE_VERSION >= 2 -#if defined(NCURSES_VERSION) || defined(PDCURSES_WINCON) /* - * REPORT_MOUSE_POSITION is necessary at least on - * ncurses, so that BUTTONX_RELEASED events are reported. - * At least we interpret REPORT_MOUSE_POSITION - * like BUTTONX_RELEASED. - * It does NOT report every cursor movement, though. - * - * FIXME: On PDCurses/Wincon we enable it, so we at least - * receive something that will be interpreted as - * BUTTONX_RELEASED, although it really just reports - * cursor movements. - */ - static const mmask_t mmask = ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION; -#else - static const mmask_t mmask = ALL_MOUSE_EVENTS; + * FIXME: Due to an ncurses bug, we can receive bogus BUTTON3_PRESSED events after + * left scrolling. + * If we would reset the mouse mask with every wgetch(), which resets + * the internal button state, we would receive bogus BUTTON3_PRESSED events + * repeatedly. + * An upstream ncurses patch will probably be merged soon. + */ + static gboolean old_mousekey = FALSE; + gboolean new_mousekey = (teco_ed & TECO_ED_MOUSEKEY) != 0; + if (new_mousekey != old_mousekey) { + old_mousekey = new_mousekey; + mmask_t mmask = BUTTON_EVENT(PRESSED) | BUTTON_EVENT(RELEASED); +#ifdef __PDCURSES__ + /* + * On PDCurses it's crucial NOT to mask for BUTTONX_CLICKED. + * Also, scroll events are not reported without the non-standard + * MOUSE_WHEEL_SCROLL. + */ + mmask |= MOUSE_WHEEL_SCROLL; #endif - mousemask(teco_ed & TECO_ED_MOUSEKEY ? mmask : 0, NULL); + mousemask(new_mousekey ? mmask : 0, NULL); + } #endif /* NCURSES_MOUSE_VERSION >= 2 */ /* no special <CTRL/C> handling */ raw(); nodelay(teco_interface.input_pad, FALSE); + + /* + * Make sure we return when it's time to create recovery dumps. + */ + if (teco_ring_recovery_interval != 0) { + if (G_UNLIKELY(!teco_interface.recovery_timer)) + teco_interface.recovery_timer = g_timer_new(); + gdouble elapsed = g_timer_elapsed(teco_interface.recovery_timer, NULL); + wtimeout(teco_interface.input_pad, + MAX((gdouble)teco_ring_recovery_interval - elapsed, 0)*1000); + } + /* * Memory limiting is stopped temporarily, since it might otherwise * constantly place 100% load on the CPU. @@ -1974,6 +2043,12 @@ teco_interface_blocking_getch(void) cbreak(); #endif + if (key == ERR && teco_ring_recovery_interval != 0 && + g_timer_elapsed(teco_interface.recovery_timer, NULL) >= teco_ring_recovery_interval) { + teco_ring_dump_recovery(); + g_timer_start(teco_interface.recovery_timer); + } + return key; } @@ -1995,12 +2070,11 @@ teco_interface_event_loop_iter(void) GError **error = &teco_interface.event_loop_error; - gint key = g_queue_is_empty(teco_interface.input_queue) - ? teco_interface_blocking_getch() - : GPOINTER_TO_INT(g_queue_pop_head(teco_interface.input_queue)); + gint key = teco_interface_blocking_getch(); const teco_view_t *last_view = teco_interface_current_view; sptr_t last_vpos = teco_interface_ssm(SCI_GETFIRSTVISIBLELINE, 0, 0); + guint last_cmdline_height = teco_cmdline.height; switch (key) { case ERR: @@ -2008,10 +2082,11 @@ teco_interface_event_loop_iter(void) return; #ifdef KEY_RESIZE case KEY_RESIZE: -#ifdef __PDCURSES__ - /* NOTE: No longer necessary since PDCursesMod v4.3.3. */ - resize_term(0, 0); -#endif + /* + * At least on PDCurses/Wincon, the hardware cursor is sometimes + * reactivated. + */ + curs_set(0); teco_interface_resize_all_windows(); break; #endif @@ -2078,9 +2153,10 @@ teco_interface_event_loop_iter(void) * Do not auto-scroll on mouse events, so you can scroll the view manually * in the ^KMOUSE macro, allowing dot to be outside of the view. */ - teco_interface_refresh(); + teco_interface_unfold(); + teco_interface_refresh(FALSE); return; -#endif +#endif /* NCURSES_MOUSE_VERSION >= 2 */ /* * Control keys and keys with printable representation @@ -2125,6 +2201,10 @@ teco_interface_event_loop_iter(void) } } + if (G_UNLIKELY(teco_cmdline.height != last_cmdline_height)) + /* command line height was changed with h,5EJ */ + teco_interface_resize_all_windows(); + /* * Scintilla has been patched to avoid any automatic scrolling since that * has been benchmarked to be a very costly operation. @@ -2134,9 +2214,10 @@ teco_interface_event_loop_iter(void) */ if (teco_interface_current_view == last_view) teco_interface_ssm(SCI_SETFIRSTVISIBLELINE, last_vpos, 0); + teco_interface_unfold(); teco_interface_ssm(SCI_SCROLLCARET, 0, 0); - teco_interface_refresh(); + teco_interface_refresh(FALSE); } gboolean @@ -2148,11 +2229,14 @@ teco_interface_event_loop(GError **error) if (!teco_interface_init_interactive(error)) return FALSE; - static const teco_cmdline_t empty_cmdline; // FIXME - teco_interface_cmdline_update(&empty_cmdline); teco_interface_msg_clear(); teco_interface_ssm(SCI_SCROLLCARET, 0, 0); - teco_interface_refresh(); + /* + * NetBSD's Curses needs the hard refresh as it would + * otherwise draw the info window in the wrong row. + * Shouldn't cause any slowdown on ncurses. + */ + teco_interface_refresh(TRUE); #ifdef EMCURSES PDC_emscripten_set_handler(teco_interface_event_loop_iter, TRUE); @@ -2199,10 +2283,6 @@ teco_interface_cleanup(void) teco_string_clear(&teco_interface.info_current); if (teco_interface.input_queue) g_queue_free(teco_interface.input_queue); - 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); if (teco_interface.input_pad) @@ -2227,4 +2307,10 @@ teco_interface_cleanup(void) close(teco_interface.stderr_orig); if (teco_interface.stdout_orig >= 0) close(teco_interface.stdout_orig); + + if (teco_interface.pair_table) + g_hash_table_destroy(teco_interface.pair_table); + + if (teco_interface.recovery_timer) + g_timer_destroy(teco_interface.recovery_timer); } |
