// Terminal UI functions. Invoked (by ui_bridge.c) on the TUI thread. #include #include #include #include #include #include #if defined(HAVE_TERMIOS_H) # include #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/os/input.h" #include "nvim/os/os.h" #include "nvim/strings.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; typedef struct { UIBridgeData *bridge; Loop *loop; bool stop; unibi_var_t params[9]; char buf[OUTBUF_SIZE]; size_t bufpos, bufsize; TermInput input; uv_loop_t write_loop; unibi_term *ut; union { uv_tty_t tty; uv_pipe_t pipe; } output_handle; 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 out_fd; bool can_use_terminal_scroll; bool mouse_enabled; bool busy; HlAttrs print_attrs; int showing_mode; struct { int enable_mouse, disable_mouse; int enable_bracketed_paste, disable_bracketed_paste; 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; } TUIData; static bool volatile got_winch = false; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "tui/tui.c.generated.h" #endif 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; ui->cursor_goto = tui_cursor_goto; 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->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->flush = tui_flush; 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); } static void terminfo_start(UI *ui) { TUIData *data = ui->data; data->can_use_terminal_scroll = true; data->bufpos = 0; data->bufsize = sizeof(data->buf) - CNORM_COMMAND_MAX_SIZE; data->showing_mode = 0; data->unibi_ext.enable_mouse = -1; data->unibi_ext.disable_mouse = -1; data->unibi_ext.enable_bracketed_paste = -1; data->unibi_ext.disable_bracketed_paste = -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; data->out_isatty = os_isatty(data->out_fd); // setup unibilium 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(); } 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); // Enable bracketed paste unibi_out(ui, data->unibi_ext.enable_bracketed_paste); // Enable focus reporting unibi_out(ui, data->unibi_ext.enable_focus_reporting); uv_loop_init(&data->write_loop); if (data->out_isatty) { uv_tty_init(&data->write_loop, &data->output_handle.tty, data->out_fd, 0); uv_tty_set_mode(&data->output_handle.tty, UV_TTY_MODE_RAW); } else { uv_pipe_init(&data->write_loop, &data->output_handle.pipe, 0); uv_pipe_open(&data->output_handle.pipe, data->out_fd); } } static void terminfo_stop(UI *ui) { TUIData *data = ui->data; // Destroy output stuff tui_mode_change(ui, NORMAL); tui_mouse_off(ui); unibi_out(ui, unibi_exit_attribute_mode); // cursor should be set to normal before exiting alternate screen unibi_out(ui, unibi_cursor_normal); unibi_out(ui, unibi_exit_ca_mode); // Disable bracketed paste unibi_out(ui, data->unibi_ext.disable_bracketed_paste); // Disable focus reporting unibi_out(ui, data->unibi_ext.disable_focus_reporting); 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); if (uv_loop_close(&data->write_loop)) { abort(); } unibi_destroy(data->ut); } static void tui_terminal_start(UI *ui) { TUIData *data = ui->data; data->print_attrs = EMPTY_ATTRS; ugrid_init(&data->grid); 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); } static void tui_terminal_stop(UI *ui) { TUIData *data = ui->data; term_input_stop(&data->input); signal_watcher_stop(&data->winch_handle); terminfo_stop(ui); ugrid_free(&data->grid); } static void tui_stop(UI *ui) { tui_terminal_stop(ui); TUIData *data = ui->data; data->stop = true; } // Main function of the TUI thread static void tui_main(UIBridgeData *bridge, UI *ui) { Loop tui_loop; loop_init(&tui_loop, NULL); TUIData *data = xcalloc(1, sizeof(TUIData)); ui->data = data; data->bridge = bridge; data->loop = &tui_loop; kv_init(data->invalid_regions); 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); tui_terminal_start(ui); data->stop = false; // allow the main thread to continue, we are ready to start handling UI // callbacks CONTINUE(bridge); while (!data->stop) { loop_poll_events(&tui_loop, -1); } ui_bridge_stopped(bridge); term_input_destroy(&data->input); signal_watcher_stop(&data->cont_handle); signal_watcher_close(&data->cont_handle, NULL); signal_watcher_close(&data->winch_handle, NULL); loop_close(&tui_loop, false); kv_destroy(data->invalid_regions); xfree(data); xfree(ui); } static void tui_scheduler(Event event, void *d) { UI *ui = d; TUIData *data = ui->data; loop_schedule(data->loop, event); } static void sigcont_cb(SignalWatcher *watcher, int signum, void *data) { ((TUIData *)data)->cont_received = true; } static void sigwinch_cb(SignalWatcher *watcher, int signum, void *data) { got_winch = true; UI *ui = data; update_size(ui); ui_schedule_refresh(); } static bool attrs_differ(HlAttrs a1, HlAttrs a2) { 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; } static void update_attrs(UI *ui, HlAttrs attrs) { TUIData *data = ui->data; if (!attrs_differ(attrs, data->print_attrs)) { return; } data->print_attrs = attrs; unibi_out(ui, unibi_exit_attribute_mode); UGrid *grid = &data->grid; int fg = attrs.foreground != -1 ? attrs.foreground : grid->fg; int bg = attrs.background != -1 ? attrs.background : grid->bg; 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); } 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); } } else { if (fg != -1) { data->params[0].i = fg; unibi_out(ui, unibi_set_a_foreground); } if (bg != -1) { data->params[0].i = 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); } } static void print_cell(UI *ui, UCell *ptr) { update_attrs(ui, ptr->attrs); out(ui, ptr->data, strlen(ptr->data)); } static void clear_region(UI *ui, int top, int bot, int left, int right) { TUIData *data = ui->data; UGrid *grid = &data->grid; 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); } cleared = true; } } if (!cleared) { // iterate through each line and clear with clr_eol for (int row = top; row <= bot; ++row) { unibi_goto(ui, row, left); unibi_out(ui, unibi_clr_eol); } 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); }); } // restore cursor unibi_goto(ui, grid->row, grid->col); } static void tui_resize(UI *ui, int width, int height) { TUIData *data = ui->data; ugrid_resize(&data->grid, width, height); 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)); } else { // Already handled the SIGWINCH signal; avoid double-resize. got_winch = false; } } static void tui_clear(UI *ui) { TUIData *data = ui->data; UGrid *grid = &data->grid; ugrid_clear(grid); clear_region(ui, grid->top, grid->bot, grid->left, grid->right); } static void tui_eol_clear(UI *ui) { TUIData *data = ui->data; UGrid *grid = &data->grid; ugrid_eol_clear(grid); clear_region(ui, grid->row, grid->row, grid->col, grid->right); } static void tui_cursor_goto(UI *ui, int row, int col) { TUIData *data = ui->data; ugrid_goto(&data->grid, row, col); unibi_goto(ui, row, col); } static void tui_update_menu(UI *ui) { // Do nothing; menus are for GUI only } static void tui_busy_start(UI *ui) { ((TUIData *)ui->data)->busy = true; } static void tui_busy_stop(UI *ui) { ((TUIData *)ui->data)->busy = false; } static void tui_mouse_on(UI *ui) { TUIData *data = ui->data; unibi_out(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; } static void tui_mode_change(UI *ui, int mode) { TUIData *data = ui->data; if (mode == INSERT) { if (data->showing_mode != INSERT) { 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.set_cursor_shape_ul); } } else { assert(mode == NORMAL); if (data->showing_mode != NORMAL) { unibi_out(ui, data->unibi_ext.set_cursor_shape_block); } } data->showing_mode = mode; } static void tui_set_scroll_region(UI *ui, int top, int bot, int left, int right) { 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)); } static void tui_scroll(UI *ui, int count) { TUIData *data = ui->data; UGrid *grid = &data->grid; int clear_top, clear_bot; ugrid_scroll(grid, count, &clear_top, &clear_bot); if (data->can_use_terminal_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 (count > 0) { if (data->can_use_terminal_scroll) { if (count == 1) { unibi_out(ui, unibi_delete_line); } else { data->params[0].i = count; unibi_out(ui, unibi_parm_delete_line); } } } else { if (data->can_use_terminal_scroll) { if (count == -1) { unibi_out(ui, unibi_insert_line); } else { data->params[0].i = -count; 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); } } else { // Mark the entire scroll region as invalid for redrawing later invalidate(ui, grid->top, grid->bot, grid->left, grid->right); } } 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) { TUIData *data = ui->data; print_cell(ui, ugrid_put(&data->grid, text, size)); } static void tui_bell(UI *ui) { unibi_out(ui, unibi_bell); } static void tui_visual_bell(UI *ui) { unibi_out(ui, unibi_flash_screen); } static void tui_update_fg(UI *ui, int fg) { ((TUIData *)ui->data)->grid.fg = fg; } static void tui_update_bg(UI *ui, int bg) { ((TUIData *)ui->data)->grid.bg = bg; } static void tui_update_sp(UI *ui, int sp) { // Do nothing; 'special' color is for GUI only } 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; UGRID_FOREACH_CELL(grid, r.top, r.bot, r.left, r.right, { if (currow != row) { unibi_goto(ui, row, col); currow = row; } print_cell(ui, cell); }); } unibi_goto(ui, grid->row, grid->col); flush_buf(ui, true); } static void suspend_event(void **argv) { UI *ui = argv[0]; TUIData *data = ui->data; 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); if (enable_mouse) { tui_mouse_on(ui); } stream_set_blocking(input_global_fd(), false); // libuv expects this // resume the main thread CONTINUE(data->bridge); } static void tui_suspend(UI *ui) { 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 multiqueue_put_event(data->loop->fast_events, event_create(1, suspend_event, 1, ui)); } static void tui_set_title(UI *ui, char *title) { TUIData *data = ui->data; if (!(title && 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)); unibi_out(ui, unibi_from_status_line); } 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; 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)) { 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 intersects->top = MIN(top, intersects->top); intersects->bot = MAX(bot, intersects->bot); intersects->left = MIN(left, intersects->left); intersects->right = MAX(right, intersects->right); } else { // Else just add a new entry; kv_push(data->invalid_regions, ((Rect) { top, bot, left, right })); } } static void update_size(UI *ui) { TUIData *data = ui->data; int width = 0, height = 0; // 1 - look for non-default 'columns' and 'lines' options during startup if (starting != 0 && (Columns != DFLT_COLS || Rows != DFLT_ROWS)) { assert(Columns >= INT_MIN && Columns <= INT_MAX); assert(Rows >= INT_MIN && Rows <= INT_MAX); width = (int)Columns; height = (int)Rows; goto end; } // 2 - try from a system call(ioctl/TIOCGWINSZ on unix) if (data->out_isatty && !uv_tty_get_winsize(&data->output_handle.tty, &width, &height)) { goto end; } // 3 - use $LINES/$COLUMNS if available const char *val; int advance; if ((val = os_getenv("LINES")) && sscanf(val, "%d%n", &height, &advance) != EOF && advance && (val = os_getenv("COLUMNS")) && sscanf(val, "%d%n", &width, &advance) != EOF && advance) { goto end; } // 4 - read from terminfo if available height = unibi_get_num(data->ut, unibi_lines); width = unibi_get_num(data->ut, unibi_columns); end: if (width <= 0 || height <= 0) { // use the defaults width = DFLT_COLS; height = DFLT_ROWS; } data->bridge->bridge.width = ui->width = width; data->bridge->bridge.height = ui->height = height; } 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_out(ui, unibi_cursor_address); } 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); } } 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; if (len > available) { flush_buf(ui, false); } memcpy(data->buf + data->bufpos, str, len); data->bufpos += len; } static void unibi_set_if_empty(unibi_term *ut, enum unibi_string str, const char *val) { if (!unibi_get_str(ut, str)) { unibi_set_str(ut, str, val); } } 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; } bool inside_tmux = os_getenv("TMUX") != NULL; #define STARTS_WITH(str, prefix) (!memcmp(str, prefix, sizeof(prefix) - 1)) 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"); unibi_set_if_empty(ut, unibi_enter_italics_mode, "\x1b[3m"); 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_to_status_line, "\x1b_"); unibi_set_if_empty(ut, unibi_from_status_line, "\x1b\\"); } 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"); 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"); #define XTERM_SETAF \ "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m" #define XTERM_SETAB \ "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m" 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) { goto end; } #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.set_cursor_shape_bar = (int)unibi_add_ext_str(ut, NULL, TMUX_WRAP("\x1b]50;CursorShape=1;BlinkingCursorEnabled=1\x07")); data->unibi_ext.set_cursor_shape_ul = (int)unibi_add_ext_str(ut, NULL, TMUX_WRAP("\x1b]50;CursorShape=2;BlinkingCursorEnabled=1\x07")); data->unibi_ext.set_cursor_shape_block = (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.set_cursor_shape_bar = (int)unibi_add_ext_str(ut, NULL, "\x1b[5 q"); data->unibi_ext.set_cursor_shape_ul = (int)unibi_add_ext_str(ut, NULL, "\x1b[3 q"); data->unibi_ext.set_cursor_shape_block = (int)unibi_add_ext_str(ut, NULL, "\x1b[2 q"); } 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"); } static void flush_buf(UI *ui, bool toggle_cursor) { uv_write_t req; uv_buf_t buf; TUIData *data = ui->data; 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; unibi_out(ui, unibi_cursor_normal); data->bufsize -= CNORM_COMMAND_MAX_SIZE; } buf.base = data->buf; buf.len = data->bufpos; uv_write(&req, (uv_stream_t *)&data->output_handle, &buf, 1, NULL); uv_run(&data->write_loop, UV_RUN_DEFAULT); data->bufpos = 0; 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 and are now the same, redefine ." if (stty_erase != NULL && strcmp(stty_erase, value) == 0) { return stty_erase[0] == DEL ? (char *)CTRL_H_STR : (char *)DEL_STR; } } return value; } #endif