diff options
Diffstat (limited to 'src/nvim/tui/tui.c')
-rw-r--r-- | src/nvim/tui/tui.c | 194 |
1 files changed, 145 insertions, 49 deletions
diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 62bc81ba64..8cb8719cfe 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -1,3 +1,5 @@ +// Terminal UI functions. Invoked (by ui_bridge.c) on the TUI thread. + #include <assert.h> #include <stdbool.h> #include <stdio.h> @@ -5,30 +7,38 @@ #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/map.h" +#include "nvim/main.h" #include "nvim/memory.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/strings.h" -#include "nvim/ugrid.h" #include "nvim/ui_bridge.h" +#include "nvim/ugrid.h" +#include "nvim/tui/input.h" +#include "nvim/tui/tui.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. #define CNORM_COMMAND_MAX_SIZE 32 #define OUTBUF_SIZE 0xffff +#define TOO_MANY_EVENTS 1000000 + typedef struct { int top, bot, left, right; } Rect; @@ -64,7 +74,7 @@ typedef struct { struct { int enable_mouse, disable_mouse; int enable_bracketed_paste, disable_bracketed_paste; - int enter_insert_mode, enter_replace_mode, exit_insert_mode; + int set_cursor_shape_bar, set_cursor_shape_ul, set_cursor_shape_block; int set_rgb_foreground, set_rgb_background; int enable_focus_reporting, disable_focus_reporting; } unibi_ext; @@ -82,6 +92,7 @@ UI *tui_start(void) UI *ui = xcalloc(1, sizeof(UI)); ui->stop = tui_stop; ui->rgb = p_tgc; + ui->pum_external = false; ui->resize = tui_resize; ui->clear = tui_clear; ui->eol_clear = tui_eol_clear; @@ -105,6 +116,7 @@ UI *tui_start(void) ui->suspend = tui_suspend; ui->set_title = tui_set_title; ui->set_icon = tui_set_icon; + ui->event = tui_event; return ui_bridge_attach(ui, tui_main, tui_scheduler); } @@ -119,9 +131,9 @@ static void terminfo_start(UI *ui) data->unibi_ext.disable_mouse = -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.set_cursor_shape_bar = -1; + data->unibi_ext.set_cursor_shape_ul = -1; + data->unibi_ext.set_cursor_shape_block = -1; data->unibi_ext.enable_focus_reporting = -1; data->unibi_ext.disable_focus_reporting = -1; data->out_fd = 1; @@ -134,6 +146,10 @@ static void terminfo_start(UI *ui) data->ut = unibi_dummy(); } fix_terminfo(data); + // Initialize the cursor shape. + unibi_out(ui, data->unibi_ext.set_cursor_shape_block); + // 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 unibi_out(ui, unibi_enter_ca_mode); unibi_out(ui, unibi_clear_screen); @@ -165,7 +181,7 @@ static void terminfo_stop(UI *ui) unibi_out(ui, data->unibi_ext.disable_bracketed_paste); // Disable focus reporting unibi_out(ui, data->unibi_ext.disable_focus_reporting); - flush_buf(ui); + flush_buf(ui, true); uv_tty_reset_mode(); uv_close((uv_handle_t *)&data->output_handle, NULL); uv_run(&data->write_loop, UV_RUN_DEFAULT); @@ -183,6 +199,11 @@ static void tui_terminal_start(UI *ui) terminfo_start(ui); update_size(ui); signal_watcher_start(&data->winch_handle, sigwinch_cb, SIGWINCH); + +#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, data->loop); term_input_start(&data->input); } @@ -215,8 +236,6 @@ static void tui_main(UIBridgeData *bridge, UI *ui) signal_watcher_init(data->loop, &data->winch_handle, ui); signal_watcher_init(data->loop, &data->cont_handle, data); signal_watcher_start(&data->cont_handle, sigcont_cb, SIGCONT); - // initialize input reading structures - 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 @@ -232,7 +251,7 @@ 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); xfree(data); xfree(ui); @@ -245,11 +264,6 @@ static void tui_scheduler(Event event, void *d) loop_schedule(data->loop, event); } -static void refresh_event(void **argv) -{ - ui_refresh(); -} - static void sigcont_cb(SignalWatcher *watcher, int signum, void *data) { ((TUIData *)data)->cont_received = true; @@ -260,8 +274,7 @@ static void sigwinch_cb(SignalWatcher *watcher, int signum, void *data) got_winch = true; UI *ui = data; update_size(ui); - // run refresh_event in nvim main loop - loop_schedule(&loop, event_create(1, refresh_event, 0)); + ui_schedule_refresh(); } static bool attrs_differ(HlAttrs a1, HlAttrs a2) @@ -456,16 +469,20 @@ static void tui_mode_change(UI *ui, int mode) if (mode == INSERT) { if (data->showing_mode != INSERT) { - unibi_out(ui, data->unibi_ext.enter_insert_mode); + unibi_out(ui, data->unibi_ext.set_cursor_shape_bar); + } + } else if (mode == CMDLINE) { + if (data->showing_mode != CMDLINE) { + unibi_out(ui, data->unibi_ext.set_cursor_shape_bar); } } else if (mode == REPLACE) { if (data->showing_mode != REPLACE) { - unibi_out(ui, data->unibi_ext.enter_replace_mode); + unibi_out(ui, data->unibi_ext.set_cursor_shape_ul); } } else { assert(mode == NORMAL); if (data->showing_mode != NORMAL) { - unibi_out(ui, data->unibi_ext.exit_insert_mode); + unibi_out(ui, data->unibi_ext.set_cursor_shape_block); } } data->showing_mode = mode; @@ -584,6 +601,18 @@ 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) { + ILOG("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; @@ -598,7 +627,7 @@ static void tui_flush(UI *ui) unibi_goto(ui, grid->row, grid->col); - flush_buf(ui); + flush_buf(ui, true); } static void suspend_event(void **argv) @@ -608,6 +637,7 @@ 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 @@ -617,6 +647,7 @@ static void suspend_event(void **argv) if (enable_mouse) { tui_mouse_on(ui); } + stream_set_blocking(input_global_fd(), false); // libuv expects this // resume the main thread CONTINUE(data->bridge); } @@ -627,8 +658,8 @@ static void tui_suspend(UI *ui) // 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(1, suspend_event, 1, ui)); } static void tui_set_title(UI *ui, char *title) @@ -647,6 +678,12 @@ static void tui_set_icon(UI *ui, char *icon) { } +// NB: if we start to use this, the ui_bridge must be updated +// to make a copy for the tui thread +static void tui_event(UI *ui, char *name, Array args, bool *args_consumed) +{ +} + static void invalidate(UI *ui, int top, int bot, int left, int right) { TUIData *data = ui->data; @@ -681,7 +718,7 @@ static void invalidate(UI *ui, int top, int bot, int left, int right) intersects->right = MAX(right, intersects->right); } else { // Else just add a new entry; - kv_push(Rect, data->invalid_regions, ((Rect){top, bot, left, right})); + kv_push(data->invalid_regions, ((Rect) { top, bot, left, right })); } } @@ -726,8 +763,8 @@ end: height = DFLT_ROWS; } - data->bridge->bridge.width = ui->width = width; - data->bridge->bridge.height = ui->height = height; + data->bridge->bridge.width = width; + data->bridge->bridge.height = height; } static void unibi_goto(UI *ui, int row, int col) @@ -765,7 +802,7 @@ static void out(void *ctx, const char *str, size_t len) size_t available = data->bufsize - data->bufpos; if (len > available) { - flush_buf(ui); + flush_buf(ui, false); } memcpy(data->buf + data->bufpos, str, len); @@ -785,6 +822,7 @@ static void fix_terminfo(TUIData *data) unibi_term *ut = data->ut; const char *term = os_getenv("TERM"); + const char *colorterm = os_getenv("COLORTERM"); if (!term) { goto end; } @@ -806,7 +844,16 @@ static void fix_terminfo(TUIData *data) } if (STARTS_WITH(term, "xterm") || STARTS_WITH(term, "rxvt")) { - unibi_set_if_empty(ut, unibi_cursor_normal, "\x1b[?12l\x1b[?25h"); + const char *normal = unibi_get_str(ut, unibi_cursor_normal); + if (!normal) { + unibi_set_str(ut, unibi_cursor_normal, "\x1b[?25h"); + } else if (STARTS_WITH(normal, "\x1b[?12l")) { + // terminfo typically includes DECRST 12 as part of setting up the normal + // cursor, which interferes with the user's control via + // NVIM_TUI_ENABLE_CURSOR_SHAPE. When DECRST 12 is present, skip over + // it, but honor the rest of the TI setting. + unibi_set_str(ut, unibi_cursor_normal, normal + strlen("\x1b[?12l")); + } 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"); @@ -830,18 +877,20 @@ static void fix_terminfo(TUIData *data) #define XTERM_SETAB \ "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m" - if (os_getenv("COLORTERM") != NULL - && (!strcmp(term, "xterm") || !strcmp(term, "screen"))) { - // probably every modern terminal that sets TERM=xterm supports 256 - // colors(eg: gnome-terminal). Also do it when TERM=screen. + 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); } - if (os_getenv("NVIM_TUI_ENABLE_CURSOR_SHAPE") == NULL) { + const char * env_cusr_shape = os_getenv("NVIM_TUI_ENABLE_CURSOR_SHAPE"); + if (env_cusr_shape && strncmp(env_cusr_shape, "0", 1) == 0) { goto end; } + bool cusr_blink = env_cusr_shape && strncmp(env_cusr_shape, "2", 1) == 0; #define TMUX_WRAP(seq) (inside_tmux ? "\x1bPtmux;\x1b" seq "\x1b\\" : seq) // Support changing cursor shape on some popular terminals. @@ -852,23 +901,23 @@ static void fix_terminfo(TUIData *data) || 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")); + data->unibi_ext.set_cursor_shape_bar = (int)unibi_add_ext_str(ut, NULL, + TMUX_WRAP("\x1b]50;CursorShape=1\x07")); + data->unibi_ext.set_cursor_shape_ul = (int)unibi_add_ext_str(ut, NULL, + TMUX_WRAP("\x1b]50;CursorShape=2\x07")); + data->unibi_ext.set_cursor_shape_block = (int)unibi_add_ext_str(ut, NULL, + TMUX_WRAP("\x1b]50;CursorShape=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"); + data->unibi_ext.set_cursor_shape_bar = + (int)unibi_add_ext_str(ut, NULL, cusr_blink ? "\x1b[5 q" : "\x1b[6 q"); + data->unibi_ext.set_cursor_shape_ul = + (int)unibi_add_ext_str(ut, NULL, cusr_blink ? "\x1b[3 q" : "\x1b[4 q"); + data->unibi_ext.set_cursor_shape_block = + (int)unibi_add_ext_str(ut, NULL, cusr_blink ? "\x1b[1 q" : "\x1b[2 q"); } end: @@ -900,13 +949,13 @@ end: unibi_set_if_empty(ut, unibi_clr_eos, "\x1b[J"); } -static void flush_buf(UI *ui) +static void flush_buf(UI *ui, bool toggle_cursor) { uv_write_t req; uv_buf_t buf; TUIData *data = ui->data; - if (!data->busy) { + if (toggle_cursor && !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; @@ -920,9 +969,56 @@ static void flush_buf(UI *ui) uv_run(&data->write_loop, UV_RUN_DEFAULT); data->bufpos = 0; - if (!data->busy) { + if (toggle_cursor && !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(ECHOE) && defined(ICANON) && 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'; + ILOG("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 (strcmp(name, "key_backspace") == 0) { + ILOG("libtermkey:kbs=%s", value); + if (stty_erase != NULL && stty_erase[0] != 0) { + return stty_erase; + } + } else if (strcmp(name, "key_dc") == 0) { + ILOG("libtermkey:kdch1=%s", value); + // Vim: "If <BS> and <DEL> are now the same, redefine <DEL>." + if (stty_erase != NULL && strcmp(stty_erase, value) == 0) { + return stty_erase[0] == DEL ? (char *)CTRL_H_STR : (char *)DEL_STR; + } + } + + return value; +} +#endif |