diff options
Diffstat (limited to 'src/nvim/tui/tui.c')
-rw-r--r-- | src/nvim/tui/tui.c | 1859 |
1 files changed, 1468 insertions, 391 deletions
diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index c4efa69c4c..dbffabd707 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -1,3 +1,8 @@ +// This is an open source non-commercial project. Dear PVS-Studio, please check +// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + +// Terminal UI functions. Invoked (by ui_bridge.c) on the TUI thread. + #include <assert.h> #include <stdbool.h> #include <stdio.h> @@ -5,31 +10,60 @@ #include <uv.h> #include <unibilium.h> +#if defined(HAVE_TERMIOS_H) +# include <termios.h> +#endif #include "nvim/lib/kvec.h" +#include "nvim/ascii.h" #include "nvim/vim.h" +#include "nvim/log.h" #include "nvim/ui.h" +#include "nvim/highlight.h" #include "nvim/map.h" #include "nvim/main.h" #include "nvim/memory.h" +#include "nvim/option.h" #include "nvim/api/vim.h" #include "nvim/api/private/helpers.h" #include "nvim/event/loop.h" #include "nvim/event/signal.h" -#include "nvim/tui/tui.h" -#include "nvim/tui/input.h" #include "nvim/os/input.h" #include "nvim/os/os.h" +#include "nvim/os/tty.h" #include "nvim/strings.h" -#include "nvim/ugrid.h" +#include "nvim/syntax.h" #include "nvim/ui_bridge.h" +#include "nvim/ugrid.h" +#include "nvim/tui/input.h" +#include "nvim/tui/tui.h" +#include "nvim/tui/terminfo.h" +#include "nvim/cursor_shape.h" +#include "nvim/macros.h" -// Space reserved in the output buffer to restore the cursor to normal when -// flushing. No existing terminal will require 32 bytes to do that. +// Space reserved in two output buffers to make the cursor normal or invisible +// when flushing. No existing terminal will require 32 bytes to do that. #define CNORM_COMMAND_MAX_SIZE 32 #define OUTBUF_SIZE 0xffff +#define TOO_MANY_EVENTS 1000000 +#define STARTS_WITH(str, prefix) (strlen(str) >= (sizeof(prefix) - 1) \ + && 0 == memcmp((str), (prefix), sizeof(prefix) - 1)) +#define TMUX_WRAP(is_tmux, seq) ((is_tmux) \ + ? "\x1bPtmux;\x1b" seq "\x1b\\" : seq) +#define LINUXSET0C "\x1b[?0c" +#define LINUXSET1C "\x1b[?1c" + +#ifdef NVIM_UNIBI_HAS_VAR_FROM +#define UNIBI_SET_NUM_VAR(var, num) \ + do { \ + (var) = unibi_var_from_num((num)); \ + } while (0) +#else +#define UNIBI_SET_NUM_VAR(var, num) (var).i = (num); +#endif + typedef struct { int top, bot, left, right; } Rect; @@ -37,10 +71,12 @@ typedef struct { typedef struct { UIBridgeData *bridge; Loop *loop; - bool stop; unibi_var_t params[9]; char buf[OUTBUF_SIZE]; - size_t bufpos, bufsize; + size_t bufpos; + char norm[CNORM_COMMAND_MAX_SIZE]; + char invis[CNORM_COMMAND_MAX_SIZE]; + size_t normlen, invislen; TermInput input; uv_loop_t write_loop; unibi_term *ut; @@ -51,28 +87,49 @@ typedef struct { bool out_isatty; SignalWatcher winch_handle, cont_handle; bool cont_received; - // Event scheduled by the ui bridge. Since the main thread suspends until - // the event is handled, it is fine to use a single field instead of a queue - Event scheduled_event; UGrid grid; kvec_t(Rect) invalid_regions; + int row, col; int out_fd; - bool can_use_terminal_scroll; + bool scroll_region_is_full_screen; + bool can_change_scroll_region; + bool can_set_lr_margin; + bool can_set_left_right_margin; + bool can_scroll; + bool can_erase_chars; + bool immediate_wrap_after_last_column; + bool bce; bool mouse_enabled; - bool busy; - HlAttrs print_attrs; - int showing_mode; + bool busy, is_invisible; + bool cork, overflow; + bool cursor_color_changed; + cursorentry_T cursor_shapes[SHAPE_IDX_COUNT]; + HlAttrs clear_attrs; + kvec_t(HlAttrs) attrs; + int print_attr_id; + bool default_attr; + bool can_clear_attr; + ModeShape showing_mode; struct { int enable_mouse, disable_mouse; int enable_bracketed_paste, disable_bracketed_paste; - int enter_insert_mode, enter_replace_mode, exit_insert_mode; + int enable_lr_margin, disable_lr_margin; int set_rgb_foreground, set_rgb_background; + int set_cursor_color; + int reset_cursor_color; int enable_focus_reporting, disable_focus_reporting; + int resize_screen; + int reset_scroll_region; + int set_cursor_style, reset_cursor_style; + int save_title, restore_title; + int enter_undercurl_mode, exit_undercurl_mode, set_underline_color; int get_bg; } unibi_ext; + char *space_buf; } TUIData; static bool volatile got_winch = false; +static bool cursor_style_enabled = false; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "tui/tui.c.generated.h" @@ -81,77 +138,162 @@ static bool volatile got_winch = false; UI *tui_start(void) { - UI *ui = xcalloc(1, sizeof(UI)); + UI *ui = xcalloc(1, sizeof(UI)); // Freed by ui_bridge_stop(). ui->stop = tui_stop; - ui->rgb = p_tgc; - ui->resize = tui_resize; - ui->clear = tui_clear; - ui->eol_clear = tui_eol_clear; - ui->cursor_goto = tui_cursor_goto; + ui->grid_resize = tui_grid_resize; + ui->grid_clear = tui_grid_clear; + ui->grid_cursor_goto = tui_grid_cursor_goto; + ui->mode_info_set = tui_mode_info_set; ui->update_menu = tui_update_menu; ui->busy_start = tui_busy_start; ui->busy_stop = tui_busy_stop; ui->mouse_on = tui_mouse_on; ui->mouse_off = tui_mouse_off; ui->mode_change = tui_mode_change; - ui->set_scroll_region = tui_set_scroll_region; - ui->scroll = tui_scroll; - ui->highlight_set = tui_highlight_set; - ui->put = tui_put; + ui->grid_scroll = tui_grid_scroll; + ui->hl_attr_define = tui_hl_attr_define; ui->bell = tui_bell; ui->visual_bell = tui_visual_bell; - ui->update_fg = tui_update_fg; - ui->update_bg = tui_update_bg; - ui->update_sp = tui_update_sp; + ui->default_colors_set = tui_default_colors_set; ui->flush = tui_flush; ui->suspend = tui_suspend; ui->set_title = tui_set_title; ui->set_icon = tui_set_icon; + ui->option_set= tui_option_set; + ui->raw_line = tui_raw_line; + + memset(ui->ui_ext, 0, sizeof(ui->ui_ext)); + ui->ui_ext[kUILinegrid] = true; + return ui_bridge_attach(ui, tui_main, tui_scheduler); } +static size_t unibi_pre_fmt_str(TUIData *data, unsigned int unibi_index, + char * buf, size_t len) +{ + const char *str = unibi_get_str(data->ut, unibi_index); + if (!str) { + return 0U; + } + return unibi_run(str, data->params, buf, len); +} + +static void termname_set_event(void **argv) +{ + char *termname = argv[0]; + set_tty_option("term", termname); + // Do not free termname, it is freed by set_tty_option. +} + static void terminfo_start(UI *ui) { TUIData *data = ui->data; - data->can_use_terminal_scroll = true; + data->scroll_region_is_full_screen = true; data->bufpos = 0; - data->bufsize = sizeof(data->buf) - CNORM_COMMAND_MAX_SIZE; - data->showing_mode = 0; + data->default_attr = false; + data->can_clear_attr = false; + data->is_invisible = true; + data->busy = false; + data->cork = false; + data->overflow = false; + data->cursor_color_changed = false; + data->showing_mode = SHAPE_IDX_N; data->unibi_ext.enable_mouse = -1; data->unibi_ext.disable_mouse = -1; + data->unibi_ext.set_cursor_color = -1; + data->unibi_ext.reset_cursor_color = -1; data->unibi_ext.enable_bracketed_paste = -1; data->unibi_ext.disable_bracketed_paste = -1; - data->unibi_ext.enter_insert_mode = -1; - data->unibi_ext.enter_replace_mode = -1; - data->unibi_ext.exit_insert_mode = -1; + data->unibi_ext.enable_lr_margin = -1; + data->unibi_ext.disable_lr_margin = -1; data->unibi_ext.enable_focus_reporting = -1; data->unibi_ext.disable_focus_reporting = -1; + data->unibi_ext.resize_screen = -1; + data->unibi_ext.reset_scroll_region = -1; + data->unibi_ext.set_cursor_style = -1; + data->unibi_ext.reset_cursor_style = -1; data->unibi_ext.get_bg = -1; data->out_fd = 1; data->out_isatty = os_isatty(data->out_fd); - // setup unibilium + + const char *term = os_getenv("TERM"); +#ifdef WIN32 + os_tty_guess_term(&term, data->out_fd); + os_setenv("TERM", term, 1); +#endif + + // Set up unibilium/terminfo. data->ut = unibi_from_env(); - if (!data->ut) { - // For some reason could not read terminfo file, use a dummy entry that - // will be populated with common values by fix_terminfo below - data->ut = unibi_dummy(); + char *termname = NULL; + if (!term || !data->ut) { + data->ut = terminfo_from_builtin(term, &termname); + } else { + termname = xstrdup(term); } - fix_terminfo(data); + // Update 'term' option. + loop_schedule_deferred(&main_loop, + event_create(termname_set_event, 1, termname)); + + // None of the following work over SSH; see :help TERM . + const char *colorterm = os_getenv("COLORTERM"); + const char *termprg = os_getenv("TERM_PROGRAM"); + const char *vte_version_env = os_getenv("VTE_VERSION"); + long vtev = vte_version_env ? strtol(vte_version_env, NULL, 10) : 0; + bool iterm_env = termprg && strstr(termprg, "iTerm.app"); + bool konsole = terminfo_is_term_family(term, "konsole") + || os_getenv("KONSOLE_PROFILE_NAME") + || os_getenv("KONSOLE_DBUS_SESSION"); + const char *konsolev_env = os_getenv("KONSOLE_VERSION"); + long konsolev = konsolev_env ? strtol(konsolev_env, NULL, 10) + : (konsole ? 1 : 0); + + patch_terminfo_bugs(data, term, colorterm, vtev, konsolev, iterm_env); + augment_terminfo(data, term, colorterm, vtev, konsolev, iterm_env); + data->can_change_scroll_region = + !!unibi_get_str(data->ut, unibi_change_scroll_region); + data->can_set_lr_margin = + !!unibi_get_str(data->ut, unibi_set_lr_margin); + data->can_set_left_right_margin = + !!unibi_get_str(data->ut, unibi_set_left_margin_parm) + && !!unibi_get_str(data->ut, unibi_set_right_margin_parm); + data->can_scroll = + !!unibi_get_str(data->ut, unibi_delete_line) + && !!unibi_get_str(data->ut, unibi_parm_delete_line) + && !!unibi_get_str(data->ut, unibi_insert_line) + && !!unibi_get_str(data->ut, unibi_parm_insert_line); + data->can_erase_chars = !!unibi_get_str(data->ut, unibi_erase_chars); + data->immediate_wrap_after_last_column = + terminfo_is_term_family(term, "conemu") + || terminfo_is_term_family(term, "cygwin") + || terminfo_is_term_family(term, "win32con") + || terminfo_is_term_family(term, "interix"); + data->bce = unibi_get_bool(data->ut, unibi_back_color_erase); + data->normlen = unibi_pre_fmt_str(data, unibi_cursor_normal, + data->norm, sizeof data->norm); + data->invislen = unibi_pre_fmt_str(data, unibi_cursor_invisible, + data->invis, sizeof data->invis); // Set 't_Co' from the result of unibilium & fix_terminfo. t_colors = unibi_get_num(data->ut, unibi_max_colors); - // Enter alternate screen and clear + // Enter alternate screen, save title, and clear. + // NOTE: Do this *before* changing terminal settings. #6433 unibi_out(ui, unibi_enter_ca_mode); + // Save title/icon to the "stack". #4063 + unibi_out_ext(ui, data->unibi_ext.save_title); + unibi_out(ui, unibi_keypad_xmit); unibi_out(ui, unibi_clear_screen); - // Ask the terminal to send us the background color - unibi_out(ui, data->unibi_ext.get_bg); + // Ask the terminal to send us the background color. + unibi_out_ext(ui, data->unibi_ext.get_bg); // Enable bracketed paste - unibi_out(ui, data->unibi_ext.enable_bracketed_paste); - // Enable focus reporting - unibi_out(ui, data->unibi_ext.enable_focus_reporting); + unibi_out_ext(ui, data->unibi_ext.enable_bracketed_paste); + uv_loop_init(&data->write_loop); if (data->out_isatty) { uv_tty_init(&data->write_loop, &data->output_handle.tty, data->out_fd, 0); +#ifdef WIN32 uv_tty_set_mode(&data->output_handle.tty, UV_TTY_MODE_RAW); +#else + uv_tty_set_mode(&data->output_handle.tty, UV_TTY_MODE_IO); +#endif } else { uv_pipe_init(&data->write_loop, &data->output_handle.pipe, 0); uv_pipe_open(&data->output_handle.pipe, data->out_fd); @@ -162,16 +304,22 @@ static void terminfo_stop(UI *ui) { TUIData *data = ui->data; // Destroy output stuff - tui_mode_change(ui, NORMAL); + tui_mode_change(ui, (String)STRING_INIT, SHAPE_IDX_N); tui_mouse_off(ui); unibi_out(ui, unibi_exit_attribute_mode); - // cursor should be set to normal before exiting alternate screen + // Reset cursor to normal before exiting alternate screen. unibi_out(ui, unibi_cursor_normal); + unibi_out(ui, unibi_keypad_local); unibi_out(ui, unibi_exit_ca_mode); + // Restore title/icon from the "stack". #4063 + unibi_out_ext(ui, data->unibi_ext.restore_title); + if (data->cursor_color_changed) { + unibi_out_ext(ui, data->unibi_ext.reset_cursor_color); + } // Disable bracketed paste - unibi_out(ui, data->unibi_ext.disable_bracketed_paste); + unibi_out_ext(ui, data->unibi_ext.disable_bracketed_paste); // Disable focus reporting - unibi_out(ui, data->unibi_ext.disable_focus_reporting); + unibi_out_ext(ui, data->unibi_ext.disable_focus_reporting); flush_buf(ui); uv_tty_reset_mode(); uv_close((uv_handle_t *)&data->output_handle, NULL); @@ -185,7 +333,7 @@ static void terminfo_stop(UI *ui) static void tui_terminal_start(UI *ui) { TUIData *data = ui->data; - data->print_attrs = EMPTY_ATTRS; + data->print_attr_id = -1; ugrid_init(&data->grid); terminfo_start(ui); update_size(ui); @@ -193,9 +341,25 @@ static void tui_terminal_start(UI *ui) term_input_start(&data->input); } +static void tui_terminal_after_startup(UI *ui) + FUNC_ATTR_NONNULL_ALL +{ + TUIData *data = ui->data; + + // Emit this after Nvim startup, not during. This works around a tmux + // 2.3 bug(?) which caused slow drawing during startup. #7649 + unibi_out_ext(ui, data->unibi_ext.enable_focus_reporting); +} + static void tui_terminal_stop(UI *ui) { TUIData *data = ui->data; + if (uv_is_closing(STRUCT_CAST(uv_handle_t, &data->output_handle))) { + // Race between SIGCONT (tui.c) and SIGHUP (os/signal.c)? #8075 + ELOG("TUI already stopped (race?)"); + ui->data = NULL; // Flag UI as "stopped". + return; + } term_input_stop(&data->input); signal_watcher_stop(&data->winch_handle); terminfo_stop(ui); @@ -205,11 +369,16 @@ static void tui_terminal_stop(UI *ui) static void tui_stop(UI *ui) { tui_terminal_stop(ui); - TUIData *data = ui->data; - data->stop = true; + ui->data = NULL; // Flag UI as "stopped". } -// Main function of the TUI thread +/// Returns true if UI `ui` is stopped. +static bool tui_is_stopped(UI *ui) +{ + return ui->data == NULL; +} + +/// Main function of the TUI thread. static void tui_main(UIBridgeData *bridge, UI *ui) { Loop tui_loop; @@ -221,17 +390,38 @@ static void tui_main(UIBridgeData *bridge, UI *ui) kv_init(data->invalid_regions); signal_watcher_init(data->loop, &data->winch_handle, ui); signal_watcher_init(data->loop, &data->cont_handle, data); +#ifdef UNIX signal_watcher_start(&data->cont_handle, sigcont_cb, SIGCONT); - // initialize input reading structures +#endif + + // TODO(bfredl): zero hl is empty, send this explicitly? + kv_push(data->attrs, HLATTRS_INIT); + +#if TERMKEY_VERSION_MAJOR > 0 || TERMKEY_VERSION_MINOR > 18 + data->input.tk_ti_hook_fn = tui_tk_ti_getstr; +#endif term_input_init(&data->input, &tui_loop); tui_terminal_start(ui); - data->stop = false; - // allow the main thread to continue, we are ready to start handling UI - // callbacks + + // Allow main thread to continue, we are ready to handle UI callbacks. CONTINUE(bridge); - while (!data->stop) { - loop_poll_events(&tui_loop, -1); + loop_schedule_deferred(&main_loop, + event_create(show_termcap_event, 1, data->ut)); + + // "Active" loop: first ~100 ms of startup. + for (size_t ms = 0; ms < 100 && !tui_is_stopped(ui);) { + ms += (loop_poll_events(&tui_loop, 20) ? 20 : 1); + } + if (!tui_is_stopped(ui)) { + tui_terminal_after_startup(ui); + // Tickle `main_loop` with a dummy event, else the initial "focus-gained" + // terminal response may not get processed until user hits a key. + loop_schedule_deferred(&main_loop, event_create(tui_dummy_event, 0)); + } + // "Passive" (I/O-driven) loop: TUI thread "main loop". + while (!tui_is_stopped(ui)) { + loop_poll_events(&tui_loop, -1); // tui_loop.events is never processed } ui_bridge_stopped(bridge); @@ -239,193 +429,551 @@ static void tui_main(UIBridgeData *bridge, UI *ui) signal_watcher_stop(&data->cont_handle); signal_watcher_close(&data->cont_handle, NULL); signal_watcher_close(&data->winch_handle, NULL); - loop_close(&tui_loop); + loop_close(&tui_loop, false); kv_destroy(data->invalid_regions); + kv_destroy(data->attrs); + xfree(data->space_buf); xfree(data); - xfree(ui); } -static void tui_scheduler(Event event, void *d) +static void tui_dummy_event(void **argv) { - UI *ui = d; - TUIData *data = ui->data; - loop_schedule(data->loop, event); } -static void refresh_event(void **argv) +/// Handoff point between the main (ui_bridge) thread and the TUI thread. +static void tui_scheduler(Event event, void *d) { - ui_refresh(); + UI *ui = d; + TUIData *data = ui->data; + loop_schedule(data->loop, event); // `tui_loop` local to tui_main(). } +#ifdef UNIX static void sigcont_cb(SignalWatcher *watcher, int signum, void *data) { ((TUIData *)data)->cont_received = true; } +#endif static void sigwinch_cb(SignalWatcher *watcher, int signum, void *data) { got_winch = true; UI *ui = data; + if (tui_is_stopped(ui)) { + return; + } + update_size(ui); - // run refresh_event in nvim main loop - loop_schedule(&main_loop, event_create(1, refresh_event, 0)); + ui_schedule_refresh(); } -static bool attrs_differ(HlAttrs a1, HlAttrs a2) +static bool attrs_differ(UI *ui, int id1, int id2, bool rgb) { - return a1.foreground != a2.foreground || a1.background != a2.background - || a1.bold != a2.bold || a1.italic != a2.italic - || a1.undercurl != a2.undercurl || a1.underline != a2.underline - || a1.reverse != a2.reverse; + TUIData *data = ui->data; + if (id1 == id2) { + return false; + } else if (id1 < 0 || id2 < 0) { + return true; + } + HlAttrs a1 = kv_A(data->attrs, (size_t)id1); + HlAttrs a2 = kv_A(data->attrs, (size_t)id2); + + if (rgb) { + return a1.rgb_fg_color != a2.rgb_fg_color + || a1.rgb_bg_color != a2.rgb_bg_color + || a1.rgb_ae_attr != a2.rgb_ae_attr + || a1.rgb_sp_color != a2.rgb_sp_color; + } else { + return a1.cterm_fg_color != a2.cterm_fg_color + || a1.cterm_bg_color != a2.cterm_bg_color + || a1.cterm_ae_attr != a2.cterm_ae_attr + || (a1.cterm_ae_attr & (HL_UNDERLINE|HL_UNDERCURL) + && a1.rgb_sp_color != a2.rgb_sp_color); + } } -static void update_attrs(UI *ui, HlAttrs attrs) +static void update_attrs(UI *ui, int attr_id) { TUIData *data = ui->data; - if (!attrs_differ(attrs, data->print_attrs)) { + if (!attrs_differ(ui, attr_id, data->print_attr_id, ui->rgb)) { + data->print_attr_id = attr_id; return; } + data->print_attr_id = attr_id; + HlAttrs attrs = kv_A(data->attrs, (size_t)attr_id); - data->print_attrs = attrs; - unibi_out(ui, unibi_exit_attribute_mode); - UGrid *grid = &data->grid; + int fg = ui->rgb ? attrs.rgb_fg_color : (attrs.cterm_fg_color - 1); + if (fg == -1) { + fg = ui->rgb ? data->clear_attrs.rgb_fg_color + : (data->clear_attrs.cterm_fg_color - 1); + } - int fg = attrs.foreground != -1 ? attrs.foreground : grid->fg; - int bg = attrs.background != -1 ? attrs.background : grid->bg; + int bg = ui->rgb ? attrs.rgb_bg_color : (attrs.cterm_bg_color - 1); + if (bg == -1) { + bg = ui->rgb ? data->clear_attrs.rgb_bg_color + : (data->clear_attrs.cterm_bg_color - 1); + } + + int attr = ui->rgb ? attrs.rgb_ae_attr : attrs.cterm_ae_attr; + bool bold = attr & HL_BOLD; + bool italic = attr & HL_ITALIC; + bool reverse = attr & HL_INVERSE; + bool standout = attr & HL_STANDOUT; + + bool underline; + bool undercurl; + if (data->unibi_ext.enter_undercurl_mode) { + underline = attr & HL_UNDERLINE; + undercurl = attr & HL_UNDERCURL; + } else { + underline = (attr & HL_UNDERLINE) || (attr & HL_UNDERCURL); + undercurl = false; + } + if (unibi_get_str(data->ut, unibi_set_attributes)) { + if (bold || reverse || underline || standout) { + UNIBI_SET_NUM_VAR(data->params[0], standout); + UNIBI_SET_NUM_VAR(data->params[1], underline); + UNIBI_SET_NUM_VAR(data->params[2], reverse); + UNIBI_SET_NUM_VAR(data->params[3], 0); // blink + UNIBI_SET_NUM_VAR(data->params[4], 0); // dim + UNIBI_SET_NUM_VAR(data->params[5], bold); + UNIBI_SET_NUM_VAR(data->params[6], 0); // blank + UNIBI_SET_NUM_VAR(data->params[7], 0); // protect + UNIBI_SET_NUM_VAR(data->params[8], 0); // alternate character set + unibi_out(ui, unibi_set_attributes); + } else if (!data->default_attr) { + unibi_out(ui, unibi_exit_attribute_mode); + } + } else { + if (!data->default_attr) { + unibi_out(ui, unibi_exit_attribute_mode); + } + if (bold) { + unibi_out(ui, unibi_enter_bold_mode); + } + if (underline) { + unibi_out(ui, unibi_enter_underline_mode); + } + if (standout) { + unibi_out(ui, unibi_enter_standout_mode); + } + if (reverse) { + unibi_out(ui, unibi_enter_reverse_mode); + } + } + if (italic) { + unibi_out(ui, unibi_enter_italics_mode); + } + if (undercurl && data->unibi_ext.enter_undercurl_mode) { + unibi_out_ext(ui, data->unibi_ext.enter_undercurl_mode); + } + if ((undercurl || underline) && data->unibi_ext.set_underline_color) { + int color = attrs.rgb_sp_color; + if (color != -1) { + UNIBI_SET_NUM_VAR(data->params[0], (color >> 16) & 0xff); // red + UNIBI_SET_NUM_VAR(data->params[1], (color >> 8) & 0xff); // green + UNIBI_SET_NUM_VAR(data->params[2], color & 0xff); // blue + unibi_out_ext(ui, data->unibi_ext.set_underline_color); + } + } if (ui->rgb) { if (fg != -1) { - data->params[0].i = (fg >> 16) & 0xff; // red - data->params[1].i = (fg >> 8) & 0xff; // green - data->params[2].i = fg & 0xff; // blue - unibi_out(ui, data->unibi_ext.set_rgb_foreground); + UNIBI_SET_NUM_VAR(data->params[0], (fg >> 16) & 0xff); // red + UNIBI_SET_NUM_VAR(data->params[1], (fg >> 8) & 0xff); // green + UNIBI_SET_NUM_VAR(data->params[2], fg & 0xff); // blue + unibi_out_ext(ui, data->unibi_ext.set_rgb_foreground); } if (bg != -1) { - data->params[0].i = (bg >> 16) & 0xff; // red - data->params[1].i = (bg >> 8) & 0xff; // green - data->params[2].i = bg & 0xff; // blue - unibi_out(ui, data->unibi_ext.set_rgb_background); + UNIBI_SET_NUM_VAR(data->params[0], (bg >> 16) & 0xff); // red + UNIBI_SET_NUM_VAR(data->params[1], (bg >> 8) & 0xff); // green + UNIBI_SET_NUM_VAR(data->params[2], bg & 0xff); // blue + unibi_out_ext(ui, data->unibi_ext.set_rgb_background); } } else { if (fg != -1) { - data->params[0].i = fg; + UNIBI_SET_NUM_VAR(data->params[0], fg); unibi_out(ui, unibi_set_a_foreground); } if (bg != -1) { - data->params[0].i = bg; + UNIBI_SET_NUM_VAR(data->params[0], bg); unibi_out(ui, unibi_set_a_background); } } - if (attrs.bold) { - unibi_out(ui, unibi_enter_bold_mode); - } - if (attrs.italic) { - unibi_out(ui, unibi_enter_italics_mode); - } - if (attrs.underline || attrs.undercurl) { - unibi_out(ui, unibi_enter_underline_mode); - } - if (attrs.reverse) { - unibi_out(ui, unibi_enter_reverse_mode); + data->default_attr = fg == -1 && bg == -1 + && !bold && !italic && !underline && !undercurl && !reverse && !standout; + + // Non-BCE terminals can't clear with non-default background color. Some BCE + // terminals don't support attributes either, so don't rely on it. But assume + // italic and bold has no effect if there is no text. + data->can_clear_attr = !reverse && !standout && !underline && !undercurl + && (data->bce || bg == -1); +} + +static void final_column_wrap(UI *ui) +{ + TUIData *data = ui->data; + UGrid *grid = &data->grid; + if (grid->row != -1 && grid->col == ui->width) { + grid->col = 0; + if (grid->row < MIN(ui->height, grid->height - 1)) { + grid->row++; + } } } +/// It is undocumented, but in the majority of terminals and terminal emulators +/// printing at the right margin does not cause an automatic wrap until the +/// next character is printed, holding the cursor in place until then. static void print_cell(UI *ui, UCell *ptr) { - update_attrs(ui, ptr->attrs); + TUIData *data = ui->data; + UGrid *grid = &data->grid; + if (!data->immediate_wrap_after_last_column) { + // Printing the next character finally advances the cursor. + final_column_wrap(ui); + } + update_attrs(ui, ptr->attr); out(ui, ptr->data, strlen(ptr->data)); + grid->col++; + if (data->immediate_wrap_after_last_column) { + // Printing at the right margin immediately advances the cursor. + final_column_wrap(ui); + } } -static void clear_region(UI *ui, int top, int bot, int left, int right) +static bool cheap_to_print(UI *ui, int row, int col, int next) { TUIData *data = ui->data; UGrid *grid = &data->grid; + UCell *cell = grid->cells[row] + col; + while (next) { + next--; + if (attrs_differ(ui, cell->attr, + data->print_attr_id, ui->rgb)) { + if (data->default_attr) { + return false; + } + } + if (strlen(cell->data) > 1) { + return false; + } + cell++; + } + return true; +} - bool cleared = false; - if (grid->bg == -1 && right == ui->width -1) { - // Background is set to the default color and the right edge matches the - // screen end, try to use terminal codes for clearing the requested area. - HlAttrs clear_attrs = EMPTY_ATTRS; - clear_attrs.foreground = grid->fg; - clear_attrs.background = grid->bg; - update_attrs(ui, clear_attrs); - if (left == 0) { - if (bot == ui->height - 1) { - if (top == 0) { - unibi_out(ui, unibi_clear_screen); - } else { - unibi_goto(ui, top, 0); - unibi_out(ui, unibi_clr_eos); +/// This optimizes several cases where it is cheaper to do something other +/// than send a full cursor positioning control sequence. However, there are +/// some further optimizations that may seem obvious but that will not work. +/// +/// We cannot use VT (ASCII 0/11) for moving the cursor up, because VT means +/// move the cursor down on a DEC terminal. Similarly, on a DEC terminal FF +/// (ASCII 0/12) means the same thing and does not mean home. VT, CVT, and +/// TAB also stop at software-defined tabulation stops, not at a fixed set +/// of row/column positions. +static void cursor_goto(UI *ui, int row, int col) +{ + TUIData *data = ui->data; + UGrid *grid = &data->grid; + if (row == grid->row && col == grid->col) { + return; + } + if (0 == row && 0 == col) { + unibi_out(ui, unibi_cursor_home); + ugrid_goto(grid, row, col); + return; + } + if (grid->row == -1) { + goto safe_move; + } + if (0 == col ? col != grid->col : + row != grid->row ? false : + 1 == col ? 2 < grid->col && cheap_to_print(ui, grid->row, 0, col) : + 2 == col ? 5 < grid->col && cheap_to_print(ui, grid->row, 0, col) : + false) { + // Motion to left margin from anywhere else, or CR + printing chars is + // even less expensive than using BSes or CUB. + unibi_out(ui, unibi_carriage_return); + ugrid_goto(grid, grid->row, 0); + } else if (col > grid->col) { + int n = col - grid->col; + if (n <= (row == grid->row ? 4 : 2) + && cheap_to_print(ui, grid->row, grid->col, n)) { + UGRID_FOREACH_CELL(grid, grid->row, grid->col, col, { + print_cell(ui, cell); + }); + } + } + if (row == grid->row) { + if (col < grid->col + // Deferred right margin wrap terminals have inconsistent ideas about + // where the cursor actually is during a deferred wrap. Relative + // motion calculations have OBOEs that cannot be compensated for, + // because two terminals that claim to be the same will implement + // different cursor positioning rules. + && (data->immediate_wrap_after_last_column || grid->col < ui->width)) { + int n = grid->col - col; + if (n <= 4) { // This might be just BS, so it is considered really cheap. + while (n--) { + unibi_out(ui, unibi_cursor_left); } - cleared = true; + } else { + UNIBI_SET_NUM_VAR(data->params[0], n); + unibi_out(ui, unibi_parm_left_cursor); } + ugrid_goto(grid, row, col); + return; + } else if (col > grid->col) { + int n = col - grid->col; + if (n <= 2) { + while (n--) { + unibi_out(ui, unibi_cursor_right); + } + } else { + UNIBI_SET_NUM_VAR(data->params[0], n); + unibi_out(ui, unibi_parm_right_cursor); + } + ugrid_goto(grid, row, col); + return; + } + } + if (col == grid->col) { + if (row > grid->row) { + int n = row - grid->row; + if (n <= 4) { // This might be just LF, so it is considered really cheap. + while (n--) { + unibi_out(ui, unibi_cursor_down); + } + } else { + UNIBI_SET_NUM_VAR(data->params[0], n); + unibi_out(ui, unibi_parm_down_cursor); + } + ugrid_goto(grid, row, col); + return; + } else if (row < grid->row) { + int n = grid->row - row; + if (n <= 2) { + while (n--) { + unibi_out(ui, unibi_cursor_up); + } + } else { + UNIBI_SET_NUM_VAR(data->params[0], n); + unibi_out(ui, unibi_parm_up_cursor); + } + ugrid_goto(grid, row, col); + return; + } + } + +safe_move: + unibi_goto(ui, row, col); + ugrid_goto(grid, row, col); +} + +static void clear_region(UI *ui, int top, int bot, int left, int right, + int attr_id) +{ + TUIData *data = ui->data; + UGrid *grid = &data->grid; + + update_attrs(ui, attr_id); + + // Background is set to the default color and the right edge matches the + // screen end, try to use terminal codes for clearing the requested area. + if (data->can_clear_attr + && left == 0 && right == ui->width && bot == ui->height) { + if (top == 0) { + unibi_out(ui, unibi_clear_screen); + ugrid_goto(&data->grid, top, left); + } else { + cursor_goto(ui, top, 0); + unibi_out(ui, unibi_clr_eos); } + } else { + int width = right-left; - if (!cleared) { - // iterate through each line and clear with clr_eol - for (int row = top; row <= bot; ++row) { - unibi_goto(ui, row, left); + // iterate through each line and clear + for (int row = top; row < bot; row++) { + cursor_goto(ui, row, left); + if (data->can_clear_attr && right == ui->width) { unibi_out(ui, unibi_clr_eol); + } else if (data->can_erase_chars && data->can_clear_attr && width >= 5) { + UNIBI_SET_NUM_VAR(data->params[0], width); + unibi_out(ui, unibi_erase_chars); + } else { + out(ui, data->space_buf, (size_t)width); + grid->col += width; + if (data->immediate_wrap_after_last_column) { + // Printing at the right margin immediately advances the cursor. + final_column_wrap(ui); + } } - cleared = true; } } +} - if (!cleared) { - // could not clear using faster terminal codes, refresh the whole region - int currow = -1; - UGRID_FOREACH_CELL(grid, top, bot, left, right, { - if (currow != row) { - unibi_goto(ui, row, col); - currow = row; - } - print_cell(ui, cell); - }); +static void set_scroll_region(UI *ui, int top, int bot, int left, int right) +{ + TUIData *data = ui->data; + UGrid *grid = &data->grid; + + UNIBI_SET_NUM_VAR(data->params[0], top); + UNIBI_SET_NUM_VAR(data->params[1], bot); + unibi_out(ui, unibi_change_scroll_region); + if (left != 0 || right != ui->width - 1) { + unibi_out_ext(ui, data->unibi_ext.enable_lr_margin); + if (data->can_set_lr_margin) { + UNIBI_SET_NUM_VAR(data->params[0], left); + UNIBI_SET_NUM_VAR(data->params[1], right); + unibi_out(ui, unibi_set_lr_margin); + } else { + UNIBI_SET_NUM_VAR(data->params[0], left); + unibi_out(ui, unibi_set_left_margin_parm); + UNIBI_SET_NUM_VAR(data->params[0], right); + unibi_out(ui, unibi_set_right_margin_parm); + } } + grid->row = -1; +} - // restore cursor - unibi_goto(ui, grid->row, grid->col); +static void reset_scroll_region(UI *ui, bool fullwidth) +{ + TUIData *data = ui->data; + UGrid *grid = &data->grid; + + if (0 <= data->unibi_ext.reset_scroll_region) { + unibi_out_ext(ui, data->unibi_ext.reset_scroll_region); + } else { + UNIBI_SET_NUM_VAR(data->params[0], 0); + UNIBI_SET_NUM_VAR(data->params[1], ui->height - 1); + unibi_out(ui, unibi_change_scroll_region); + } + if (!fullwidth) { + if (data->can_set_lr_margin) { + UNIBI_SET_NUM_VAR(data->params[0], 0); + UNIBI_SET_NUM_VAR(data->params[1], ui->width - 1); + unibi_out(ui, unibi_set_lr_margin); + } else { + UNIBI_SET_NUM_VAR(data->params[0], 0); + unibi_out(ui, unibi_set_left_margin_parm); + UNIBI_SET_NUM_VAR(data->params[0], ui->width - 1); + unibi_out(ui, unibi_set_right_margin_parm); + } + unibi_out_ext(ui, data->unibi_ext.disable_lr_margin); + } + grid->row = -1; } -static void tui_resize(UI *ui, int width, int height) +static void tui_grid_resize(UI *ui, Integer g, Integer width, Integer height) { TUIData *data = ui->data; - ugrid_resize(&data->grid, width, height); + UGrid *grid = &data->grid; + ugrid_resize(grid, (int)width, (int)height); + + xfree(data->space_buf); + data->space_buf = xmalloc((size_t)width * sizeof(*data->space_buf)); + memset(data->space_buf, ' ', (size_t)width); + + // resize might not always be followed by a clear before flush + // so clip the invalid region + for (size_t i = 0; i < kv_size(data->invalid_regions); i++) { + Rect *r = &kv_A(data->invalid_regions, i); + r->bot = MIN(r->bot, grid->height); + r->right = MIN(r->right, grid->width); + } if (!got_winch) { // Try to resize the terminal window. - char r[16]; // enough for 9999x9999 - snprintf(r, sizeof(r), "\x1b[8;%d;%dt", height, width); - out(ui, r, strlen(r)); + UNIBI_SET_NUM_VAR(data->params[0], (int)height); + UNIBI_SET_NUM_VAR(data->params[1], (int)width); + unibi_out_ext(ui, data->unibi_ext.resize_screen); + // DECSLPP does not reset the scroll region. + if (data->scroll_region_is_full_screen) { + reset_scroll_region(ui, ui->width == grid->width); + } } else { // Already handled the SIGWINCH signal; avoid double-resize. got_winch = false; + grid->row = -1; } } -static void tui_clear(UI *ui) +static void tui_grid_clear(UI *ui, Integer g) { TUIData *data = ui->data; UGrid *grid = &data->grid; ugrid_clear(grid); - clear_region(ui, grid->top, grid->bot, grid->left, grid->right); + kv_size(data->invalid_regions) = 0; + clear_region(ui, 0, grid->height, 0, grid->width, 0); } -static void tui_eol_clear(UI *ui) +static void tui_grid_cursor_goto(UI *ui, Integer grid, Integer row, Integer col) { TUIData *data = ui->data; - UGrid *grid = &data->grid; - ugrid_eol_clear(grid); - clear_region(ui, grid->row, grid->row, grid->col, grid->right); + + // cursor position is validated in tui_flush + data->row = (int)row; + data->col = (int)col; +} + +CursorShape tui_cursor_decode_shape(const char *shape_str) +{ + CursorShape shape; + if (strequal(shape_str, "block")) { + shape = SHAPE_BLOCK; + } else if (strequal(shape_str, "vertical")) { + shape = SHAPE_VER; + } else if (strequal(shape_str, "horizontal")) { + shape = SHAPE_HOR; + } else { + WLOG("Unknown shape value '%s'", shape_str); + shape = SHAPE_BLOCK; + } + return shape; } -static void tui_cursor_goto(UI *ui, int row, int col) +static cursorentry_T decode_cursor_entry(Dictionary args) { + cursorentry_T r = shape_table[0]; + + for (size_t i = 0; i < args.size; i++) { + char *key = args.items[i].key.data; + Object value = args.items[i].value; + + if (strequal(key, "cursor_shape")) { + r.shape = tui_cursor_decode_shape(args.items[i].value.data.string.data); + } else if (strequal(key, "blinkon")) { + r.blinkon = (int)value.data.integer; + } else if (strequal(key, "blinkoff")) { + r.blinkoff = (int)value.data.integer; + } else if (strequal(key, "attr_id")) { + r.id = (int)value.data.integer; + } + } + return r; +} + +static void tui_mode_info_set(UI *ui, bool guicursor_enabled, Array args) +{ + cursor_style_enabled = guicursor_enabled; + if (!guicursor_enabled) { + return; // Do not send cursor style control codes. + } TUIData *data = ui->data; - ugrid_goto(&data->grid, row, col); - unibi_goto(ui, row, col); + + assert(args.size); + + // cursor style entries as defined by `shape_table`. + for (size_t i = 0; i < args.size; i++) { + assert(args.items[i].type == kObjectTypeDictionary); + cursorentry_T r = decode_cursor_entry(args.items[i].data.dictionary); + data->cursor_shapes[i] = r; + } + + tui_set_mode(ui, data->showing_mode); } static void tui_update_menu(UI *ui) @@ -446,119 +994,130 @@ static void tui_busy_stop(UI *ui) static void tui_mouse_on(UI *ui) { TUIData *data = ui->data; - unibi_out(ui, data->unibi_ext.enable_mouse); - data->mouse_enabled = true; + if (!data->mouse_enabled) { + unibi_out_ext(ui, data->unibi_ext.enable_mouse); + data->mouse_enabled = true; + } } static void tui_mouse_off(UI *ui) { TUIData *data = ui->data; - unibi_out(ui, data->unibi_ext.disable_mouse); - data->mouse_enabled = false; + if (data->mouse_enabled) { + unibi_out_ext(ui, data->unibi_ext.disable_mouse); + data->mouse_enabled = false; + } } -static void tui_mode_change(UI *ui, int mode) +static void tui_set_mode(UI *ui, ModeShape mode) { + if (!cursor_style_enabled) { + return; + } TUIData *data = ui->data; - - if (mode == INSERT) { - if (data->showing_mode != INSERT) { - unibi_out(ui, data->unibi_ext.enter_insert_mode); - } - } else if (mode == REPLACE) { - if (data->showing_mode != REPLACE) { - unibi_out(ui, data->unibi_ext.enter_replace_mode); - } - } else { - assert(mode == NORMAL); - if (data->showing_mode != NORMAL) { - unibi_out(ui, data->unibi_ext.exit_insert_mode); + cursorentry_T c = data->cursor_shapes[mode]; + + if (c.id != 0 && c.id < (int)kv_size(data->attrs) && ui->rgb) { + HlAttrs aep = kv_A(data->attrs, c.id); + if (aep.rgb_ae_attr & HL_INVERSE) { + // We interpret "inverse" as "default" (no termcode for "inverse"...). + // Hopefully the user's default cursor color is inverse. + unibi_out_ext(ui, data->unibi_ext.reset_cursor_color); + } else { + UNIBI_SET_NUM_VAR(data->params[0], aep.rgb_bg_color); + unibi_out_ext(ui, data->unibi_ext.set_cursor_color); + data->cursor_color_changed = true; } + } else if (c.id == 0) { + // No cursor color for this mode; reset to default. + unibi_out_ext(ui, data->unibi_ext.reset_cursor_color); + } + + int shape; + switch (c.shape) { + default: abort(); break; + case SHAPE_BLOCK: shape = 1; break; + case SHAPE_HOR: shape = 3; break; + case SHAPE_VER: shape = 5; break; } - data->showing_mode = mode; + UNIBI_SET_NUM_VAR(data->params[0], shape + (int)(c.blinkon == 0)); + unibi_out_ext(ui, data->unibi_ext.set_cursor_style); } -static void tui_set_scroll_region(UI *ui, int top, int bot, int left, - int right) +/// @param mode editor mode +static void tui_mode_change(UI *ui, String mode, Integer mode_idx) { TUIData *data = ui->data; - ugrid_set_scroll_region(&data->grid, top, bot, left, right); - data->can_use_terminal_scroll = - left == 0 && right == ui->width - 1 - && ((top == 0 && bot == ui->height - 1) - || unibi_get_str(data->ut, unibi_change_scroll_region)); + tui_set_mode(ui, (ModeShape)mode_idx); + data->showing_mode = (ModeShape)mode_idx; } -static void tui_scroll(UI *ui, int count) +static void tui_grid_scroll(UI *ui, Integer g, Integer startrow, Integer endrow, + Integer startcol, Integer endcol, + Integer rows, Integer cols) { + (void)cols; // unused TUIData *data = ui->data; UGrid *grid = &data->grid; - int clear_top, clear_bot; - ugrid_scroll(grid, count, &clear_top, &clear_bot); + int top = (int)startrow, bot = (int)endrow-1; + int left = (int)startcol, right = (int)endcol-1; - if (data->can_use_terminal_scroll) { + bool fullwidth = left == 0 && right == ui->width-1; + data->scroll_region_is_full_screen = fullwidth + && top == 0 && bot == ui->height-1; + + ugrid_scroll(grid, top, bot, left, right, (int)rows); + + bool can_scroll = data->can_scroll + && (data->scroll_region_is_full_screen + || (data->can_change_scroll_region + && ((left == 0 && right == ui->width - 1) + || data->can_set_lr_margin + || data->can_set_left_right_margin))); + + if (can_scroll) { // Change terminal scroll region and move cursor to the top - data->params[0].i = grid->top; - data->params[1].i = grid->bot; - unibi_out(ui, unibi_change_scroll_region); - unibi_goto(ui, grid->top, grid->left); - // also set default color attributes or some terminals can become funny - HlAttrs clear_attrs = EMPTY_ATTRS; - clear_attrs.foreground = grid->fg; - clear_attrs.background = grid->bg; - update_attrs(ui, clear_attrs); - } + if (!data->scroll_region_is_full_screen) { + set_scroll_region(ui, top, bot, left, right); + } + cursor_goto(ui, top, left); - if (count > 0) { - if (data->can_use_terminal_scroll) { - if (count == 1) { + if (rows > 0) { + if (rows == 1) { unibi_out(ui, unibi_delete_line); } else { - data->params[0].i = count; + UNIBI_SET_NUM_VAR(data->params[0], (int)rows); unibi_out(ui, unibi_parm_delete_line); } - } - - } else { - if (data->can_use_terminal_scroll) { - if (count == -1) { + } else { + if (rows == -1) { unibi_out(ui, unibi_insert_line); } else { - data->params[0].i = -count; + UNIBI_SET_NUM_VAR(data->params[0], -(int)rows); unibi_out(ui, unibi_parm_insert_line); } } - } - if (data->can_use_terminal_scroll) { // Restore terminal scroll region and cursor - data->params[0].i = 0; - data->params[1].i = ui->height - 1; - unibi_out(ui, unibi_change_scroll_region); - unibi_goto(ui, grid->row, grid->col); - - if (grid->bg != -1) { - // Update the cleared area of the terminal if its builtin scrolling - // facility was used and the background color is not the default. This is - // required because scrolling may leave wrong background in the cleared - // area. - clear_region(ui, clear_top, clear_bot, grid->left, grid->right); + if (!data->scroll_region_is_full_screen) { + reset_scroll_region(ui, fullwidth); } } else { - // Mark the entire scroll region as invalid for redrawing later - invalidate(ui, grid->top, grid->bot, grid->left, grid->right); + // Mark the moved region as invalid for redrawing later + if (rows > 0) { + endrow = endrow - rows; + } else { + startrow = startrow - rows; + } + invalidate(ui, (int)startrow, (int)endrow, (int)startcol, (int)endcol); } } -static void tui_highlight_set(UI *ui, HlAttrs attrs) -{ - ((TUIData *)ui->data)->grid.attrs = attrs; -} - -static void tui_put(UI *ui, uint8_t *text, size_t size) +static void tui_hl_attr_define(UI *ui, Integer id, HlAttrs attrs, + HlAttrs cterm_attrs, Array info) { TUIData *data = ui->data; - print_cell(ui, ugrid_put(&data->grid, text, size)); + kv_a(data->attrs, (size_t)id) = attrs; } static void tui_bell(UI *ui) @@ -571,19 +1130,20 @@ static void tui_visual_bell(UI *ui) unibi_out(ui, unibi_flash_screen); } -static void tui_update_fg(UI *ui, int fg) +static void tui_default_colors_set(UI *ui, Integer rgb_fg, Integer rgb_bg, + Integer rgb_sp, + Integer cterm_fg, Integer cterm_bg) { - ((TUIData *)ui->data)->grid.fg = fg; -} + TUIData *data = ui->data; -static void tui_update_bg(UI *ui, int bg) -{ - ((TUIData *)ui->data)->grid.bg = bg; -} + data->clear_attrs.rgb_fg_color = (int)rgb_fg; + data->clear_attrs.rgb_bg_color = (int)rgb_bg; + data->clear_attrs.rgb_sp_color = (int)rgb_sp; + data->clear_attrs.cterm_fg_color = (int)cterm_fg; + data->clear_attrs.cterm_bg_color = (int)cterm_bg; -static void tui_update_sp(UI *ui, int sp) -{ - // Do nothing; 'special' color is for GUI only + data->print_attr_id = -1; + invalidate(ui, 0, data->grid.height, 0, data->grid.width); } static void tui_flush(UI *ui) @@ -591,23 +1151,67 @@ static void tui_flush(UI *ui) TUIData *data = ui->data; UGrid *grid = &data->grid; + size_t nrevents = loop_size(data->loop); + if (nrevents > TOO_MANY_EVENTS) { + WLOG("TUI event-queue flooded (thread_events=%zu); purging", nrevents); + // Back-pressure: UI events may accumulate much faster than the terminal + // device can serve them. Even if SIGINT/CTRL-C is received, user must still + // wait for the TUI event-queue to drain, and if there are ~millions of + // events in the queue, it could take hours. Clearing the queue allows the + // UI to recover. #1234 #5396 + loop_purge(data->loop); + tui_busy_stop(ui); // avoid hidden cursor + } + while (kv_size(data->invalid_regions)) { Rect r = kv_pop(data->invalid_regions); - int currow = -1; - UGRID_FOREACH_CELL(grid, r.top, r.bot, r.left, r.right, { - if (currow != row) { - unibi_goto(ui, row, col); - currow = row; + assert(r.bot <= grid->height && r.right <= grid->width); + + for (int row = r.top; row < r.bot; row++) { + int clear_attr = grid->cells[row][r.right-1].attr; + int clear_col; + for (clear_col = r.right; clear_col > 0; clear_col--) { + UCell *cell = &grid->cells[row][clear_col-1]; + if (!(cell->data[0] == ' ' && cell->data[1] == NUL + && cell->attr == clear_attr)) { + break; + } + } + + UGRID_FOREACH_CELL(grid, row, r.left, clear_col, { + cursor_goto(ui, row, curcol); + print_cell(ui, cell); + }); + if (clear_col < r.right) { + clear_region(ui, row, row+1, clear_col, r.right, clear_attr); } - print_cell(ui, cell); - }); + } } - unibi_goto(ui, grid->row, grid->col); + cursor_goto(ui, data->row, data->col); flush_buf(ui); } +/// Dumps termcap info to the messages area, if 'verbose' >= 3. +static void show_termcap_event(void **argv) +{ + if (p_verbose < 3) { + return; + } + const unibi_term *const ut = argv[0]; + if (!ut) { + abort(); + } + verbose_enter(); + // XXX: (future) if unibi_term is modified (e.g. after a terminal + // query-response) this is a race condition. + terminfo_info_msg(ut); + verbose_leave(); + verbose_stop(); // flush now +} + +#ifdef UNIX static void suspend_event(void **argv) { UI *ui = argv[0]; @@ -615,70 +1219,118 @@ static void suspend_event(void **argv) bool enable_mouse = data->mouse_enabled; tui_terminal_stop(ui); data->cont_received = false; + stream_set_blocking(input_global_fd(), true); // normalize stream (#2598) kill(0, SIGTSTP); while (!data->cont_received) { // poll the event loop until SIGCONT is received loop_poll_events(data->loop, -1); } tui_terminal_start(ui); + tui_terminal_after_startup(ui); if (enable_mouse) { tui_mouse_on(ui); } + stream_set_blocking(input_global_fd(), false); // libuv expects this // resume the main thread CONTINUE(data->bridge); } +#endif static void tui_suspend(UI *ui) { +#ifdef UNIX TUIData *data = ui->data; // kill(0, SIGTSTP) won't stop the UI thread, so we must poll for SIGCONT // before continuing. This is done in another callback to avoid // loop_poll_events recursion - queue_put_event(data->loop->fast_events, - event_create(1, suspend_event, 1, ui)); + multiqueue_put_event(data->loop->fast_events, + event_create(suspend_event, 1, ui)); +#endif } -static void tui_set_title(UI *ui, char *title) +static void tui_set_title(UI *ui, String title) { TUIData *data = ui->data; - if (!(title && unibi_get_str(data->ut, unibi_to_status_line) + if (!(title.data && unibi_get_str(data->ut, unibi_to_status_line) && unibi_get_str(data->ut, unibi_from_status_line))) { return; } unibi_out(ui, unibi_to_status_line); - out(ui, title, strlen(title)); + out(ui, title.data, title.size); unibi_out(ui, unibi_from_status_line); } -static void tui_set_icon(UI *ui, char *icon) +static void tui_set_icon(UI *ui, String icon) { } +static void tui_option_set(UI *ui, String name, Object value) +{ + TUIData *data = ui->data; + if (strequal(name.data, "termguicolors")) { + ui->rgb = value.data.boolean; + + data->print_attr_id = -1; + invalidate(ui, 0, data->grid.height, 0, data->grid.width); + } +} + +static void tui_raw_line(UI *ui, Integer g, Integer linerow, Integer startcol, + Integer endcol, Integer clearcol, Integer clearattr, + Boolean wrap, const schar_T *chunk, + const sattr_T *attrs) +{ + TUIData *data = ui->data; + UGrid *grid = &data->grid; + for (Integer c = startcol; c < endcol; c++) { + memcpy(grid->cells[linerow][c].data, chunk[c-startcol], sizeof(schar_T)); + assert((size_t)attrs[c-startcol] < kv_size(data->attrs)); + grid->cells[linerow][c].attr = attrs[c-startcol]; + } + UGRID_FOREACH_CELL(grid, (int)linerow, (int)startcol, (int)endcol, { + cursor_goto(ui, (int)linerow, curcol); + print_cell(ui, cell); + }); + + if (clearcol > endcol) { + ugrid_clear_chunk(grid, (int)linerow, (int)endcol, (int)clearcol, + (sattr_T)clearattr); + clear_region(ui, (int)linerow, (int)linerow+1, (int)endcol, (int)clearcol, + (int)clearattr); + } + + if (wrap && ui->width == grid->width && linerow + 1 < grid->height) { + // Only do line wrapping if the grid width is equal to the terminal + // width and the line continuation is within the grid. + + if (endcol != grid->width) { + // Print the last char of the row, if we haven't already done so. + int size = grid->cells[linerow][grid->width - 1].data[0] == NUL ? 2 : 1; + cursor_goto(ui, (int)linerow, grid->width - size); + print_cell(ui, &grid->cells[linerow][grid->width - size]); + } + + // Wrap the cursor over to the next line. The next line will be + // printed immediately without an intervening newline. + final_column_wrap(ui); + } +} + static void invalidate(UI *ui, int top, int bot, int left, int right) { TUIData *data = ui->data; Rect *intersects = NULL; - // Increase dimensions before comparing to ensure adjacent regions are - // treated as intersecting - --top; - ++bot; - --left; - ++right; for (size_t i = 0; i < kv_size(data->invalid_regions); i++) { Rect *r = &kv_A(data->invalid_regions, i); - if (!(top > r->bot || bot < r->top - || left > r->right || right < r->left)) { + // adjacent regions are treated as overlapping + if (!(top > r->bot || bot < r->top) + && !(left > r->right || right < r->left)) { intersects = r; break; } } - ++top; - --bot; - ++left; - --right; - if (intersects) { // If top/bot/left/right intersects with a invalid rect, we replace it // by the union @@ -740,39 +1392,61 @@ end: static void unibi_goto(UI *ui, int row, int col) { TUIData *data = ui->data; - data->params[0].i = row; - data->params[1].i = col; + UNIBI_SET_NUM_VAR(data->params[0], row); + UNIBI_SET_NUM_VAR(data->params[1], col); unibi_out(ui, unibi_cursor_address); } +#define UNIBI_OUT(fn) \ + do { \ + TUIData *data = ui->data; \ + const char *str = NULL; \ + if (unibi_index >= 0) { \ + str = fn(data->ut, (unsigned)unibi_index); \ + } \ + if (str) { \ + unibi_var_t vars[26 + 26]; \ + size_t orig_pos = data->bufpos; \ + \ + memset(&vars, 0, sizeof(vars)); \ + data->cork = true; \ +retry: \ + unibi_format(vars, vars + 26, str, data->params, out, ui, NULL, NULL); \ + if (data->overflow) { \ + data->bufpos = orig_pos; \ + flush_buf(ui); \ + goto retry; \ + } \ + data->cork = false; \ + } \ + } while (0) static void unibi_out(UI *ui, int unibi_index) { - TUIData *data = ui->data; - - const char *str = NULL; - - if (unibi_index >= 0) { - if (unibi_index < unibi_string_begin_) { - str = unibi_get_ext_str(data->ut, (unsigned)unibi_index); - } else { - str = unibi_get_str(data->ut, (unsigned)unibi_index); - } - } - - if (str) { - unibi_var_t vars[26 + 26] = {{0}}; - unibi_format(vars, vars + 26, str, data->params, out, ui, NULL, NULL); - } + UNIBI_OUT(unibi_get_str); } +static void unibi_out_ext(UI *ui, int unibi_index) +{ + UNIBI_OUT(unibi_get_ext_str); +} +#undef UNIBI_OUT static void out(void *ctx, const char *str, size_t len) { UI *ui = ctx; TUIData *data = ui->data; - size_t available = data->bufsize - data->bufpos; + size_t available = sizeof(data->buf) - data->bufpos; + + if (data->cork && data->overflow) { + return; + } if (len > available) { - flush_buf(ui); + if (data->cork) { + data->overflow = true; + return; + } else { + flush_buf(ui); + } } memcpy(data->buf + data->bufpos, str, len); @@ -787,152 +1461,555 @@ static void unibi_set_if_empty(unibi_term *ut, enum unibi_string str, } } -static void fix_terminfo(TUIData *data) +static int unibi_find_ext_str(unibi_term *ut, const char *name) +{ + size_t max = unibi_count_ext_str(ut); + for (size_t i = 0; i < max; i++) { + const char * n = unibi_get_ext_str_name(ut, i); + if (n && 0 == strcmp(n, name)) { + return (int)i; + } + } + return -1; +} + +static int unibi_find_ext_bool(unibi_term *ut, const char *name) +{ + size_t max = unibi_count_ext_bool(ut); + for (size_t i = 0; i < max; i++) { + const char * n = unibi_get_ext_bool_name(ut, i); + if (n && 0 == strcmp(n, name)) { + return (int)i; + } + } + return -1; +} + +/// Patches the terminfo records after loading from system or built-in db. +/// Several entries in terminfo are known to be deficient or outright wrong; +/// and several terminal emulators falsely announce incorrect terminal types. +static void patch_terminfo_bugs(TUIData *data, const char *term, + const char *colorterm, long vte_version, + long konsolev, bool iterm_env) { unibi_term *ut = data->ut; + const char *xterm_version = os_getenv("XTERM_VERSION"); +#if 0 // We don't need to identify this specifically, for now. + bool roxterm = !!os_getenv("ROXTERM_ID"); +#endif + bool xterm = terminfo_is_term_family(term, "xterm") + // Treat Terminal.app as generic xterm-like, for now. + || terminfo_is_term_family(term, "nsterm"); + bool kitty = terminfo_is_term_family(term, "xterm-kitty"); + bool linuxvt = terminfo_is_term_family(term, "linux"); + bool bsdvt = terminfo_is_bsd_console(term); + bool rxvt = terminfo_is_term_family(term, "rxvt"); + bool teraterm = terminfo_is_term_family(term, "teraterm"); + bool putty = terminfo_is_term_family(term, "putty"); + bool screen = terminfo_is_term_family(term, "screen"); + bool tmux = terminfo_is_term_family(term, "tmux") || !!os_getenv("TMUX"); + bool st = terminfo_is_term_family(term, "st"); + bool gnome = terminfo_is_term_family(term, "gnome") + || terminfo_is_term_family(term, "vte"); + bool iterm = terminfo_is_term_family(term, "iterm") + || terminfo_is_term_family(term, "iterm2") + || terminfo_is_term_family(term, "iTerm.app") + || terminfo_is_term_family(term, "iTerm2.app"); + bool alacritty = terminfo_is_term_family(term, "alacritty"); + // None of the following work over SSH; see :help TERM . + bool iterm_pretending_xterm = xterm && iterm_env; + bool konsole_pretending_xterm = xterm && konsolev; + bool gnome_pretending_xterm = xterm && colorterm + && strstr(colorterm, "gnome-terminal"); + bool mate_pretending_xterm = xterm && colorterm + && strstr(colorterm, "mate-terminal"); + bool true_xterm = xterm && !!xterm_version && !bsdvt; + bool cygwin = terminfo_is_term_family(term, "cygwin"); + + char *fix_normal = (char *)unibi_get_str(ut, unibi_cursor_normal); + if (fix_normal) { + if (STARTS_WITH(fix_normal, "\x1b[?12l")) { + // terminfo typically includes DECRST 12 as part of setting up the + // normal cursor, which interferes with the user's control via + // set_cursor_style. When DECRST 12 is present, skip over it, but honor + // the rest of the cnorm setting. + fix_normal += sizeof "\x1b[?12l" - 1; + unibi_set_str(ut, unibi_cursor_normal, fix_normal); + } + if (linuxvt + && strlen(fix_normal) >= (sizeof LINUXSET0C - 1) + && !memcmp(strchr(fix_normal, 0) - (sizeof LINUXSET0C - 1), + LINUXSET0C, sizeof LINUXSET0C - 1)) { + // The Linux terminfo entry similarly includes a Linux-idiosyncractic + // cursor shape reset in cnorm, which similarly interferes with + // set_cursor_style. + fix_normal[strlen(fix_normal) - (sizeof LINUXSET0C - 1)] = 0; + } + } + char *fix_invisible = (char *)unibi_get_str(ut, unibi_cursor_invisible); + if (fix_invisible) { + if (linuxvt + && strlen(fix_invisible) >= (sizeof LINUXSET1C - 1) + && !memcmp(strchr(fix_invisible, 0) - (sizeof LINUXSET1C - 1), + LINUXSET1C, sizeof LINUXSET1C - 1)) { + // The Linux terminfo entry similarly includes a Linux-idiosyncractic + // cursor shape reset in cinvis, which similarly interferes with + // set_cursor_style. + fix_invisible[strlen(fix_invisible) - (sizeof LINUXSET1C - 1)] = 0; + } + } - const char *term = os_getenv("TERM"); - const char *colorterm = os_getenv("COLORTERM"); - if (!term) { - goto end; + if (tmux || screen || kitty) { + // Disable BCE in some cases we know it is not working. #8806 + unibi_set_bool(ut, unibi_back_color_erase, false); } - bool inside_tmux = os_getenv("TMUX") != NULL; + if (xterm) { + // Termit, LXTerminal, GTKTerm2, GNOME Terminal, MATE Terminal, roxterm, + // and EvilVTE falsely claim to be xterm and do not support important xterm + // control sequences that we use. In an ideal world, these would have + // their own terminal types and terminfo entries, like PuTTY does, and not + // claim to be xterm. Or they would mimic xterm properly enough to be + // treatable as xterm. -#define STARTS_WITH(str, prefix) (!memcmp(str, prefix, sizeof(prefix) - 1)) + // 2017-04 terminfo.src lacks these. genuine Xterm has them, as have + // the false claimants. + unibi_set_if_empty(ut, unibi_to_status_line, "\x1b]0;"); + unibi_set_if_empty(ut, unibi_from_status_line, "\x07"); + unibi_set_if_empty(ut, unibi_set_tb_margin, "\x1b[%i%p1%d;%p2%dr"); - if (STARTS_WITH(term, "rxvt")) { - unibi_set_if_empty(ut, unibi_exit_attribute_mode, "\x1b[m\x1b(B"); - unibi_set_if_empty(ut, unibi_flash_screen, "\x1b[?5h$<20/>\x1b[?5l"); + if (true_xterm) { + // 2017-04 terminfo.src lacks these. genuine Xterm has them. + unibi_set_if_empty(ut, unibi_set_lr_margin, "\x1b[%i%p1%d;%p2%ds"); + unibi_set_if_empty(ut, unibi_set_left_margin_parm, "\x1b[%i%p1%ds"); + unibi_set_if_empty(ut, unibi_set_right_margin_parm, "\x1b[%i;%p2%ds"); + } + if (true_xterm + || iterm_pretending_xterm + || gnome_pretending_xterm + || konsole_pretending_xterm) { + // Apple's outdated copy of terminfo.src for MacOS lacks these. + // genuine Xterm and three false claimants have them. + unibi_set_if_empty(ut, unibi_enter_italics_mode, "\x1b[3m"); + unibi_set_if_empty(ut, unibi_exit_italics_mode, "\x1b[23m"); + } + } else if (rxvt) { + // 2017-04 terminfo.src lacks these. Unicode rxvt has them. unibi_set_if_empty(ut, unibi_enter_italics_mode, "\x1b[3m"); + unibi_set_if_empty(ut, unibi_exit_italics_mode, "\x1b[23m"); unibi_set_if_empty(ut, unibi_to_status_line, "\x1b]2"); - } else if (STARTS_WITH(term, "xterm")) { - unibi_set_if_empty(ut, unibi_to_status_line, "\x1b]0;"); - } else if (STARTS_WITH(term, "screen") || STARTS_WITH(term, "tmux")) { + unibi_set_if_empty(ut, unibi_from_status_line, "\x07"); + // 2017-04 terminfo.src has older control sequences. + unibi_set_str(ut, unibi_enter_ca_mode, "\x1b[?1049h"); + unibi_set_str(ut, unibi_exit_ca_mode, "\x1b[?1049l"); + } else if (screen) { + // per the screen manual; 2017-04 terminfo.src lacks these. unibi_set_if_empty(ut, unibi_to_status_line, "\x1b_"); unibi_set_if_empty(ut, unibi_from_status_line, "\x1b\\"); + } else if (tmux) { + unibi_set_if_empty(ut, unibi_to_status_line, "\x1b_"); + unibi_set_if_empty(ut, unibi_from_status_line, "\x1b\\"); + } else if (terminfo_is_term_family(term, "interix")) { + // 2017-04 terminfo.src lacks this. + unibi_set_if_empty(ut, unibi_carriage_return, "\x0d"); + } else if (linuxvt) { + // Apple's outdated copy of terminfo.src for MacOS lacks these. + unibi_set_if_empty(ut, unibi_parm_up_cursor, "\x1b[%p1%dA"); + unibi_set_if_empty(ut, unibi_parm_down_cursor, "\x1b[%p1%dB"); + unibi_set_if_empty(ut, unibi_parm_right_cursor, "\x1b[%p1%dC"); + unibi_set_if_empty(ut, unibi_parm_left_cursor, "\x1b[%p1%dD"); + } else if (putty) { + // No bugs in the vanilla terminfo for our purposes. + } else if (iterm) { + // 2017-04 terminfo.src has older control sequences. + unibi_set_str(ut, unibi_enter_ca_mode, "\x1b[?1049h"); + unibi_set_str(ut, unibi_exit_ca_mode, "\x1b[?1049l"); + // 2017-04 terminfo.src lacks these. + unibi_set_if_empty(ut, unibi_set_tb_margin, "\x1b[%i%p1%d;%p2%dr"); + unibi_set_if_empty(ut, unibi_orig_pair, "\x1b[39;49m"); + unibi_set_if_empty(ut, unibi_enter_dim_mode, "\x1b[2m"); + unibi_set_if_empty(ut, unibi_enter_italics_mode, "\x1b[3m"); + unibi_set_if_empty(ut, unibi_exit_italics_mode, "\x1b[23m"); + unibi_set_if_empty(ut, unibi_exit_underline_mode, "\x1b[24m"); + unibi_set_if_empty(ut, unibi_exit_standout_mode, "\x1b[27m"); + } else if (st) { + // No bugs in the vanilla terminfo for our purposes. } - if (STARTS_WITH(term, "xterm") || STARTS_WITH(term, "rxvt")) { - unibi_set_if_empty(ut, unibi_cursor_normal, "\x1b[?12l\x1b[?25h"); - unibi_set_if_empty(ut, unibi_cursor_invisible, "\x1b[?25l"); - unibi_set_if_empty(ut, unibi_flash_screen, "\x1b[?5h$<100/>\x1b[?5l"); - unibi_set_if_empty(ut, unibi_exit_attribute_mode, "\x1b(B\x1b[m"); - unibi_set_if_empty(ut, unibi_change_scroll_region, "\x1b[%i%p1%d;%p2%dr"); - unibi_set_if_empty(ut, unibi_clear_screen, "\x1b[H\x1b[2J"); - unibi_set_if_empty(ut, unibi_from_status_line, "\x07"); - } - - data->unibi_ext.enable_bracketed_paste = (int)unibi_add_ext_str(ut, NULL, - "\x1b[?2004h"); - data->unibi_ext.disable_bracketed_paste = (int)unibi_add_ext_str(ut, NULL, - "\x1b[?2004l"); +// At this time (2017-07-12) it seems like all terminals that support 256 +// color codes can use semicolons in the terminal code and be fine. +// However, this is not correct according to the spec. So to reward those +// terminals that also support colons, we output the code that way on these +// specific ones. - data->unibi_ext.enable_focus_reporting = (int)unibi_add_ext_str(ut, NULL, - "\x1b[?1004h"); - data->unibi_ext.disable_focus_reporting = (int)unibi_add_ext_str(ut, NULL, - "\x1b[?1004l"); +// using colons like ISO 8613-6:1994/ITU T.416:1993 says. +#define XTERM_SETAF_256_COLON \ + "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38:5:%p1%d%;m" +#define XTERM_SETAB_256_COLON \ + "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48:5:%p1%d%;m" - data->unibi_ext.get_bg = (int)unibi_add_ext_str(ut, NULL, "\x1b]11;?\x07"); - -#define XTERM_SETAF \ +#define XTERM_SETAF_256 \ "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m" -#define XTERM_SETAB \ +#define XTERM_SETAB_256 \ "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m" +#define XTERM_SETAF_16 \ + "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e39%;m" +#define XTERM_SETAB_16 \ + "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e39%;m" + + data->unibi_ext.get_bg = (int)unibi_add_ext_str(ut, "ext.get_bg", + "\x1b]11;?\x07"); + + // Terminals with 256-colour SGR support despite what terminfo says. + if (unibi_get_num(ut, unibi_max_colors) < 256) { + // See http://fedoraproject.org/wiki/Features/256_Color_Terminals + if (true_xterm || iterm || iterm_pretending_xterm) { + unibi_set_num(ut, unibi_max_colors, 256); + unibi_set_str(ut, unibi_set_a_foreground, XTERM_SETAF_256_COLON); + unibi_set_str(ut, unibi_set_a_background, XTERM_SETAB_256_COLON); + } else if (konsolev || xterm || gnome || rxvt || st || putty + || linuxvt // Linux 4.8+ supports 256-colour SGR. + || mate_pretending_xterm || gnome_pretending_xterm + || tmux + || (colorterm && strstr(colorterm, "256")) + || (term && strstr(term, "256"))) { + unibi_set_num(ut, unibi_max_colors, 256); + unibi_set_str(ut, unibi_set_a_foreground, XTERM_SETAF_256); + unibi_set_str(ut, unibi_set_a_background, XTERM_SETAB_256); + } + } + // Terminals with 16-colour SGR support despite what terminfo says. + if (unibi_get_num(ut, unibi_max_colors) < 16) { + if (colorterm) { + unibi_set_num(ut, unibi_max_colors, 16); + unibi_set_if_empty(ut, unibi_set_a_foreground, XTERM_SETAF_16); + unibi_set_if_empty(ut, unibi_set_a_background, XTERM_SETAB_16); + } + } - if ((colorterm && strstr(colorterm, "256")) - || strstr(term, "256") - || strstr(term, "xterm")) { - // Assume TERM~=xterm or COLORTERM~=256 supports 256 colors. - unibi_set_num(ut, unibi_max_colors, 256); - unibi_set_str(ut, unibi_set_a_foreground, XTERM_SETAF); - unibi_set_str(ut, unibi_set_a_background, XTERM_SETAB); + // Blacklist of terminals that cannot be trusted to report DECSCUSR support. + if (!(st || (vte_version != 0 && vte_version < 3900) || konsolev)) { + data->unibi_ext.reset_cursor_style = unibi_find_ext_str(ut, "Se"); + data->unibi_ext.set_cursor_style = unibi_find_ext_str(ut, "Ss"); } - if (os_getenv("NVIM_TUI_ENABLE_CURSOR_SHAPE") == NULL) { - goto end; + // Dickey ncurses terminfo includes Ss/Se capabilities since 2011-07-14. So + // adding them to terminal types, that have such control sequences but lack + // the correct terminfo entries, is a fixup, not an augmentation. + if (-1 == data->unibi_ext.set_cursor_style) { + // DECSCUSR (cursor shape) is widely supported. + // https://github.com/gnachman/iTerm2/pull/92 + if ((!bsdvt && (!konsolev || konsolev >= 180770)) + && ((xterm && !vte_version) // anything claiming xterm compat + // per MinTTY 0.4.3-1 release notes from 2009 + || putty + // per https://bugzilla.gnome.org/show_bug.cgi?id=720821 + || (vte_version >= 3900) + || (konsolev >= 180770) // #9364 + || tmux // per tmux manual page + // https://lists.gnu.org/archive/html/screen-devel/2013-03/msg00000.html + || screen + || st // #7641 + || rxvt // per command.C + // per analysis of VT100Terminal.m + || iterm || iterm_pretending_xterm + || teraterm // per TeraTerm "Supported Control Functions" doco + || alacritty // https://github.com/jwilm/alacritty/pull/608 + || cygwin + // Some linux-type terminals implement the xterm extension. + // Example: console-terminal-emulator from the nosh toolset. + || (linuxvt + && (xterm_version || (vte_version > 0) || colorterm)))) { + data->unibi_ext.set_cursor_style = + (int)unibi_add_ext_str(ut, "Ss", "\x1b[%p1%d q"); + if (-1 == data->unibi_ext.reset_cursor_style) { + data->unibi_ext.reset_cursor_style = (int)unibi_add_ext_str(ut, "Se", + ""); + } + unibi_set_ext_str(ut, (size_t)data->unibi_ext.reset_cursor_style, + "\x1b[ q"); + } else if (linuxvt) { + // Linux uses an idiosyncratic escape code to set the cursor shape and + // does not support DECSCUSR. + // See http://linuxgazette.net/137/anonymous.html for more info + data->unibi_ext.set_cursor_style = (int)unibi_add_ext_str(ut, "Ss", + "\x1b[?" + "%?" + // The parameter passed to Ss is the DECSCUSR parameter, so the + // terminal capability has to translate into the Linux idiosyncratic + // parameter. + // + // linuxvt only supports block and underline. It is also only + // possible to have a steady block (no steady underline) + "%p1%{2}%<" "%t%{8}" // blink block + "%e%p1%{2}%=" "%t%{112}" // steady block + "%e%p1%{3}%=" "%t%{4}" // blink underline (set to half block) + "%e%p1%{4}%=" "%t%{4}" // steady underline + "%e%p1%{5}%=" "%t%{2}" // blink bar (set to underline) + "%e%p1%{6}%=" "%t%{2}" // steady bar + "%e%{0}" // anything else + "%;" "%dc"); + if (-1 == data->unibi_ext.reset_cursor_style) { + data->unibi_ext.reset_cursor_style = (int)unibi_add_ext_str(ut, "Se", + ""); + } + unibi_set_ext_str(ut, (size_t)data->unibi_ext.reset_cursor_style, + "\x1b[?c"); + } else if (konsolev > 0 && konsolev < 180770) { + // Konsole before version 18.07.70: set up a nonce profile. This has + // side-effects on temporary font resizing. #6798 + data->unibi_ext.set_cursor_style = (int)unibi_add_ext_str(ut, "Ss", + TMUX_WRAP(tmux, "\x1b]50;CursorShape=%?" + "%p1%{3}%<" "%t%{0}" // block + "%e%p1%{5}%<" "%t%{2}" // underline + "%e%{1}" // everything else is bar + "%;%d;BlinkingCursorEnabled=%?" + "%p1%{1}%<" "%t%{1}" // Fortunately if we exclude zero as special, + "%e%p1%{1}%&" // in all other cases we can treat bit #0 as a flag. + "%;%d\x07")); + if (-1 == data->unibi_ext.reset_cursor_style) { + data->unibi_ext.reset_cursor_style = (int)unibi_add_ext_str(ut, "Se", + ""); + } + unibi_set_ext_str(ut, (size_t)data->unibi_ext.reset_cursor_style, + "\x1b]50;\x07"); + } } +} -#define TMUX_WRAP(seq) (inside_tmux ? "\x1bPtmux;\x1b" seq "\x1b\\" : seq) - // Support changing cursor shape on some popular terminals. - const char *term_prog = os_getenv("TERM_PROGRAM"); - const char *vte_version = os_getenv("VTE_VERSION"); - - if ((term_prog && !strcmp(term_prog, "Konsole")) - || os_getenv("KONSOLE_DBUS_SESSION") != NULL) { - // Konsole uses a proprietary escape code to set the cursor shape - // and does not support DECSCUSR. - data->unibi_ext.enter_insert_mode = (int)unibi_add_ext_str(ut, NULL, - TMUX_WRAP("\x1b]50;CursorShape=1;BlinkingCursorEnabled=1\x07")); - data->unibi_ext.enter_replace_mode = (int)unibi_add_ext_str(ut, NULL, - TMUX_WRAP("\x1b]50;CursorShape=2;BlinkingCursorEnabled=1\x07")); - data->unibi_ext.exit_insert_mode = (int)unibi_add_ext_str(ut, NULL, - TMUX_WRAP("\x1b]50;CursorShape=0;BlinkingCursorEnabled=0\x07")); - } else if (!vte_version || atoi(vte_version) >= 3900) { - // Assume that the terminal supports DECSCUSR unless it is an - // old VTE based terminal. This should not get wrapped for tmux, - // which will handle it via its Ss/Se terminfo extension - usually - // according to its terminal-overrides. - data->unibi_ext.enter_insert_mode = (int)unibi_add_ext_str(ut, NULL, - "\x1b[5 q"); - data->unibi_ext.enter_replace_mode = (int)unibi_add_ext_str(ut, NULL, - "\x1b[3 q"); - data->unibi_ext.exit_insert_mode = (int)unibi_add_ext_str(ut, NULL, - "\x1b[2 q"); +/// This adds stuff that is not in standard terminfo as extended unibilium +/// capabilities. +static void augment_terminfo(TUIData *data, const char *term, + const char *colorterm, long vte_version, + long konsolev, bool iterm_env) +{ + unibi_term *ut = data->ut; + bool xterm = terminfo_is_term_family(term, "xterm") + // Treat Terminal.app as generic xterm-like, for now. + || terminfo_is_term_family(term, "nsterm"); + bool bsdvt = terminfo_is_bsd_console(term); + bool dtterm = terminfo_is_term_family(term, "dtterm"); + bool rxvt = terminfo_is_term_family(term, "rxvt"); + bool teraterm = terminfo_is_term_family(term, "teraterm"); + bool putty = terminfo_is_term_family(term, "putty"); + bool screen = terminfo_is_term_family(term, "screen"); + bool tmux = terminfo_is_term_family(term, "tmux") || !!os_getenv("TMUX"); + bool iterm = terminfo_is_term_family(term, "iterm") + || terminfo_is_term_family(term, "iterm2") + || terminfo_is_term_family(term, "iTerm.app") + || terminfo_is_term_family(term, "iTerm2.app"); + bool alacritty = terminfo_is_term_family(term, "alacritty"); + // None of the following work over SSH; see :help TERM . + bool iterm_pretending_xterm = xterm && iterm_env; + + const char *xterm_version = os_getenv("XTERM_VERSION"); + bool true_xterm = xterm && !!xterm_version && !bsdvt; + + // Only define this capability for terminal types that we know understand it. + if (dtterm // originated this extension + || xterm // per xterm ctlseqs doco + || konsolev // per commentary in VT102Emulation.cpp + || teraterm // per TeraTerm "Supported Control Functions" doco + || rxvt) { // per command.C + data->unibi_ext.resize_screen = (int)unibi_add_ext_str(ut, + "ext.resize_screen", + "\x1b[8;%p1%d;%p2%dt"); + } + if (putty || xterm || rxvt) { + data->unibi_ext.reset_scroll_region = (int)unibi_add_ext_str(ut, + "ext.reset_scroll_region", + "\x1b[r"); } -end: - // Fill some empty slots with common terminal strings - data->unibi_ext.enable_mouse = (int)unibi_add_ext_str(ut, NULL, - "\x1b[?1002h\x1b[?1006h"); - data->unibi_ext.disable_mouse = (int)unibi_add_ext_str(ut, NULL, - "\x1b[?1002l\x1b[?1006l"); - data->unibi_ext.set_rgb_foreground = (int)unibi_add_ext_str(ut, NULL, - "\x1b[38;2;%p1%d;%p2%d;%p3%dm"); - data->unibi_ext.set_rgb_background = (int)unibi_add_ext_str(ut, NULL, - "\x1b[48;2;%p1%d;%p2%d;%p3%dm"); - unibi_set_if_empty(ut, unibi_cursor_address, "\x1b[%i%p1%d;%p2%dH"); - unibi_set_if_empty(ut, unibi_exit_attribute_mode, "\x1b[0;10m"); - unibi_set_if_empty(ut, unibi_set_a_foreground, XTERM_SETAF); - unibi_set_if_empty(ut, unibi_set_a_background, XTERM_SETAB); - unibi_set_if_empty(ut, unibi_enter_bold_mode, "\x1b[1m"); - unibi_set_if_empty(ut, unibi_enter_underline_mode, "\x1b[4m"); - unibi_set_if_empty(ut, unibi_enter_reverse_mode, "\x1b[7m"); - unibi_set_if_empty(ut, unibi_bell, "\x07"); - unibi_set_if_empty(data->ut, unibi_enter_ca_mode, "\x1b[?1049h"); - unibi_set_if_empty(data->ut, unibi_exit_ca_mode, "\x1b[?1049l"); - unibi_set_if_empty(ut, unibi_delete_line, "\x1b[M"); - unibi_set_if_empty(ut, unibi_parm_delete_line, "\x1b[%p1%dM"); - unibi_set_if_empty(ut, unibi_insert_line, "\x1b[L"); - unibi_set_if_empty(ut, unibi_parm_insert_line, "\x1b[%p1%dL"); - unibi_set_if_empty(ut, unibi_clear_screen, "\x1b[H\x1b[J"); - unibi_set_if_empty(ut, unibi_clr_eol, "\x1b[K"); - unibi_set_if_empty(ut, unibi_clr_eos, "\x1b[J"); + // Dickey ncurses terminfo does not include the setrgbf and setrgbb + // capabilities, proposed by RĂ¼diger Sonderfeld on 2013-10-15. Adding + // them here when terminfo lacks them is an augmentation, not a fixup. + // https://gist.github.com/XVilka/8346728 + + // At this time (2017-07-12) it seems like all terminals that support rgb + // color codes can use semicolons in the terminal code and be fine. + // However, this is not correct according to the spec. So to reward those + // terminals that also support colons, we output the code that way on these + // specific ones. + + // can use colons like ISO 8613-6:1994/ITU T.416:1993 says. + bool has_colon_rgb = !tmux && !screen + && !vte_version // VTE colon-support has a big memory leak. #7573 + && (iterm || iterm_pretending_xterm // per VT100Terminal.m + // per http://invisible-island.net/xterm/xterm.log.html#xterm_282 + || true_xterm); + + data->unibi_ext.set_rgb_foreground = unibi_find_ext_str(ut, "setrgbf"); + if (-1 == data->unibi_ext.set_rgb_foreground) { + if (has_colon_rgb) { + data->unibi_ext.set_rgb_foreground = (int)unibi_add_ext_str(ut, "setrgbf", + "\x1b[38:2:%p1%d:%p2%d:%p3%dm"); + } else { + data->unibi_ext.set_rgb_foreground = (int)unibi_add_ext_str(ut, "setrgbf", + "\x1b[38;2;%p1%d;%p2%d;%p3%dm"); + } + } + data->unibi_ext.set_rgb_background = unibi_find_ext_str(ut, "setrgbb"); + if (-1 == data->unibi_ext.set_rgb_background) { + if (has_colon_rgb) { + data->unibi_ext.set_rgb_background = (int)unibi_add_ext_str(ut, "setrgbb", + "\x1b[48:2:%p1%d:%p2%d:%p3%dm"); + } else { + data->unibi_ext.set_rgb_background = (int)unibi_add_ext_str(ut, "setrgbb", + "\x1b[48;2;%p1%d;%p2%d;%p3%dm"); + } + } + + if (iterm || iterm_pretending_xterm) { + // FIXME: Bypassing tmux like this affects the cursor colour globally, in + // all panes, which is not particularly desirable. A better approach + // would use a tmux control sequence and an extra if(screen) test. + data->unibi_ext.set_cursor_color = (int)unibi_add_ext_str( + ut, NULL, TMUX_WRAP(tmux, "\033]Pl%p1%06x\033\\")); + } else if ((xterm || rxvt || alacritty) + && (vte_version == 0 || vte_version >= 3900)) { + // Supported in urxvt, newer VTE. + data->unibi_ext.set_cursor_color = (int)unibi_add_ext_str( + ut, "ext.set_cursor_color", "\033]12;#%p1%06x\007"); + } + + if (-1 != data->unibi_ext.set_cursor_color) { + data->unibi_ext.reset_cursor_color = (int)unibi_add_ext_str( + ut, "ext.reset_cursor_color", "\x1b]112\x07"); + } + + data->unibi_ext.save_title = (int)unibi_add_ext_str( + ut, "ext.save_title", "\x1b[22;0;0t"); + data->unibi_ext.restore_title = (int)unibi_add_ext_str( + ut, "ext.restore_title", "\x1b[23;0;0t"); + + /// Terminals usually ignore unrecognized private modes, and there is no + /// known ambiguity with these. So we just set them unconditionally. + data->unibi_ext.enable_lr_margin = (int)unibi_add_ext_str( + ut, "ext.enable_lr_margin", "\x1b[?69h"); + data->unibi_ext.disable_lr_margin = (int)unibi_add_ext_str( + ut, "ext.disable_lr_margin", "\x1b[?69l"); + data->unibi_ext.enable_bracketed_paste = (int)unibi_add_ext_str( + ut, "ext.enable_bpaste", "\x1b[?2004h"); + data->unibi_ext.disable_bracketed_paste = (int)unibi_add_ext_str( + ut, "ext.disable_bpaste", "\x1b[?2004l"); + // For urxvt send BOTH xterm and old urxvt sequences. #8695 + data->unibi_ext.enable_focus_reporting = (int)unibi_add_ext_str( + ut, "ext.enable_focus", + rxvt ? "\x1b[?1004h\x1b]777;focus;on\x7" : "\x1b[?1004h"); + data->unibi_ext.disable_focus_reporting = (int)unibi_add_ext_str( + ut, "ext.disable_focus", + rxvt ? "\x1b[?1004l\x1b]777;focus;off\x7" : "\x1b[?1004l"); + data->unibi_ext.enable_mouse = (int)unibi_add_ext_str( + ut, "ext.enable_mouse", "\x1b[?1002h\x1b[?1006h"); + data->unibi_ext.disable_mouse = (int)unibi_add_ext_str( + ut, "ext.disable_mouse", "\x1b[?1002l\x1b[?1006l"); + + int ext_bool_Su = unibi_find_ext_bool(ut, "Su"); // used by kitty + if (vte_version >= 5102 + || (ext_bool_Su != -1 && unibi_get_ext_bool(ut, (size_t)ext_bool_Su))) { + data->unibi_ext.enter_undercurl_mode = (int)unibi_add_ext_str( + ut, "ext.enter_undercurl_mode", "\x1b[4:3m"); + data->unibi_ext.exit_undercurl_mode = (int)unibi_add_ext_str( + ut, "ext.exit_undercurl_mode", "\x1b[4:0m"); + // Only support colon syntax. #9270 + data->unibi_ext.set_underline_color = (int)unibi_add_ext_str( + ut, "ext.set_underline_color", "\x1b[58:2::%p1%d:%p2%d:%p3%dm"); + } } static void flush_buf(UI *ui) { uv_write_t req; - uv_buf_t buf; + uv_buf_t bufs[3]; + uv_buf_t *bufp = &bufs[0]; TUIData *data = ui->data; + if (data->bufpos <= 0 && data->busy == data->is_invisible) { + return; + } + + if (!data->is_invisible) { + // cursor is visible. Write a "cursor invisible" command before writing the + // buffer. + bufp->base = data->invis; + bufp->len = UV_BUF_LEN(data->invislen); + bufp++; + data->is_invisible = true; + } + + if (data->bufpos > 0) { + bufp->base = data->buf; + bufp->len = UV_BUF_LEN(data->bufpos); + bufp++; + } + if (!data->busy) { - // not busy and the cursor is invisible(see below). Append a "cursor - // normal" command to the end of the buffer. - data->bufsize += CNORM_COMMAND_MAX_SIZE; - unibi_out(ui, unibi_cursor_normal); - data->bufsize -= CNORM_COMMAND_MAX_SIZE; + assert(data->is_invisible); + // not busy and the cursor is invisible. Write a "cursor normal" command + // after writing the buffer. + bufp->base = data->norm; + bufp->len = UV_BUF_LEN(data->normlen); + bufp++; + data->is_invisible = data->busy; } - buf.base = data->buf; - buf.len = data->bufpos; - uv_write(&req, (uv_stream_t *)&data->output_handle, &buf, 1, NULL); + uv_write(&req, STRUCT_CAST(uv_stream_t, &data->output_handle), + bufs, (unsigned)(bufp - bufs), NULL); uv_run(&data->write_loop, UV_RUN_DEFAULT); data->bufpos = 0; + data->overflow = false; +} - if (!data->busy) { - // not busy and cursor is visible(see above), append a "cursor invisible" - // command to the beginning of the buffer for the next flush - unibi_out(ui, unibi_cursor_invisible); +#if TERMKEY_VERSION_MAJOR > 0 || TERMKEY_VERSION_MINOR > 18 +/// Try to get "kbs" code from stty because "the terminfo kbs entry is extremely +/// unreliable." (Vim, Bash, and tmux also do this.) +/// +/// @see tmux/tty-keys.c fe4e9470bb504357d073320f5d305b22663ee3fd +/// @see https://bugzilla.redhat.com/show_bug.cgi?id=142659 +static const char *tui_get_stty_erase(void) +{ + static char stty_erase[2] = { 0 }; +#if defined(HAVE_TERMIOS_H) + struct termios t; + if (tcgetattr(input_global_fd(), &t) != -1) { + stty_erase[0] = (char)t.c_cc[VERASE]; + stty_erase[1] = '\0'; + DLOG("stty/termios:erase=%s", stty_erase); } +#endif + return stty_erase; } + +/// libtermkey hook to override terminfo entries. +/// @see TermInput.tk_ti_hook_fn +static const char *tui_tk_ti_getstr(const char *name, const char *value, + void *data) +{ + static const char *stty_erase = NULL; + if (stty_erase == NULL) { + stty_erase = tui_get_stty_erase(); + } + + if (strequal(name, "key_backspace")) { + DLOG("libtermkey:kbs=%s", value); + if (stty_erase[0] != 0) { + return stty_erase; + } + } else if (strequal(name, "key_dc")) { + DLOG("libtermkey:kdch1=%s", value); + // Vim: "If <BS> and <DEL> are now the same, redefine <DEL>." + if (value != NULL && strequal(stty_erase, value)) { + return stty_erase[0] == DEL ? CTRL_H_STR : DEL_STR; + } + } else if (strequal(name, "key_mouse")) { + DLOG("libtermkey:kmous=%s", value); + // If key_mouse is found, libtermkey uses its terminfo driver (driver-ti.c) + // for mouse input, which by accident only supports X10 protocol. + // Force libtermkey to fallback to its CSI driver (driver-csi.c). #7948 + return NULL; + } + + return value; +} +#endif |