neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

tui.c (86516B)


      1 // Terminal UI functions. Invoked by the UI process (ui_client.c), not the server.
      2 
      3 #include <assert.h>
      4 #include <inttypes.h>
      5 #include <signal.h>
      6 #include <stdbool.h>
      7 #include <stdio.h>
      8 #include <stdlib.h>
      9 #include <string.h>
     10 #include <uv.h>
     11 
     12 #include "auto/config.h"
     13 #include "klib/kvec.h"
     14 #include "nvim/api/private/defs.h"
     15 #include "nvim/api/private/helpers.h"
     16 #include "nvim/ascii_defs.h"
     17 #include "nvim/cursor_shape.h"
     18 #include "nvim/event/defs.h"
     19 #include "nvim/event/loop.h"
     20 #include "nvim/event/multiqueue.h"
     21 #include "nvim/event/signal.h"
     22 #include "nvim/event/stream.h"
     23 #include "nvim/globals.h"
     24 #include "nvim/grid.h"
     25 #include "nvim/highlight_defs.h"
     26 #include "nvim/log.h"
     27 #include "nvim/macros_defs.h"
     28 #include "nvim/main.h"
     29 #include "nvim/map_defs.h"
     30 #include "nvim/mbyte.h"
     31 #include "nvim/memory.h"
     32 #include "nvim/msgpack_rpc/channel.h"
     33 #include "nvim/os/input.h"
     34 #include "nvim/os/os.h"
     35 #include "nvim/os/os_defs.h"
     36 #include "nvim/strings.h"
     37 #include "nvim/tui/input.h"
     38 #include "nvim/tui/terminfo.h"
     39 #include "nvim/tui/tui.h"
     40 #include "nvim/types_defs.h"
     41 #include "nvim/ugrid.h"
     42 #include "nvim/ui_client.h"
     43 #include "nvim/ui_defs.h"
     44 #include "nvim/vim_defs.h"
     45 
     46 #ifdef MSWIN
     47 # include "nvim/os/os_win_console.h"
     48 #endif
     49 
     50 // Maximum amount of time (in ms) to wait to receive a Device Attributes
     51 // response before exiting.
     52 #define EXIT_TIMEOUT_MS 1000
     53 
     54 #define OUTBUF_SIZE 0xffff
     55 
     56 #define TOO_MANY_EVENTS 1000000
     57 #define STARTS_WITH(str, prefix) \
     58  (strlen(str) >= (sizeof(prefix) - 1) \
     59   && 0 == memcmp((str), (prefix), sizeof(prefix) - 1))
     60 #define TMUX_WRAP(is_tmux, seq) \
     61  ((is_tmux) ? "\x1bPtmux;\x1b" seq "\x1b\\" : seq)
     62 #define LINUXSET0C "\x1b[?0c"
     63 #define LINUXSET1C "\x1b[?1c"
     64 
     65 typedef struct {
     66  int top, bot, left, right;
     67 } Rect;
     68 
     69 struct TUIData {
     70  Loop *loop;
     71  char buf[OUTBUF_SIZE];
     72  char *buf_to_flush;  ///< If non-null, flush this instead of buf[].
     73  size_t bufpos;
     74  TermInput input;
     75  uv_loop_t write_loop;
     76  TerminfoEntry ti;
     77  char *term;  ///< value of $TERM
     78  union {
     79    uv_tty_t tty;
     80    uv_pipe_t pipe;
     81  } output_handle;
     82  bool out_isatty;
     83  SignalWatcher winch_handle;
     84  uv_timer_t startup_delay_timer;
     85  UGrid grid;
     86  kvec_t(Rect) invalid_regions;
     87  int row, col;
     88  int out_fd;
     89  int pending_resize_events;
     90  bool terminfo_found_in_db;
     91  bool can_change_scroll_region;
     92  bool has_left_and_right_margin_mode;
     93  bool has_sync_mode;
     94  bool can_set_lr_margin;  // smglr
     95  bool can_scroll;
     96  bool can_erase_chars;
     97  bool immediate_wrap_after_last_column;
     98  bool bce;
     99  bool mouse_enabled;
    100  bool mouse_move_enabled;
    101  bool mouse_enabled_save;
    102  bool title_enabled;
    103  bool sync_output;
    104  bool busy, is_invisible, want_invisible;
    105  bool set_cursor_color_as_str;
    106  bool cursor_has_color;
    107  bool is_starting;
    108  bool resize_events_enabled;
    109 
    110  // Terminal modes that Nvim enabled that it must disable on exit
    111  struct {
    112    bool grapheme_clusters : 1;
    113    bool theme_updates : 1;
    114    bool resize_events : 1;
    115  } modes;
    116 
    117  FILE *screenshot;
    118  cursorentry_T cursor_shapes[SHAPE_IDX_COUNT];
    119  HlAttrs clear_attrs;
    120  kvec_t(HlAttrs) attrs;
    121  int print_attr_id;
    122  bool default_attr;
    123  bool set_default_colors;
    124  bool can_clear_attr;
    125  ModeShape showing_mode;
    126  Integer verbose;
    127  struct {
    128    char *enable_focus_reporting;
    129    char *disable_focus_reporting;
    130    char *reset_scroll_region;
    131    char *enter_altfont_mode;
    132  } terminfo_ext;
    133  bool can_set_title;
    134  bool can_set_underline_color;
    135  bool can_resize_screen;
    136  bool stopped;
    137  int width;
    138  int height;
    139  bool rgb;
    140  bool screen_or_tmux;
    141  int url;  ///< Index of URL currently being printed, if any
    142  StringBuilder urlbuf;  ///< Re-usable buffer for writing OSC 8 control sequences
    143  Arena ti_arena;
    144 };
    145 
    146 static bool cursor_style_enabled = false;
    147 #include "tui/tui.c.generated.h"
    148 
    149 #define TERMINFO_SEQ_LIMIT 128
    150 
    151 #define terminfo_print_num1(tui, what, num) terminfo_print_num(tui, what, num, 0, 0)
    152 #define terminfo_print_num2(tui, what, num1, num2) terminfo_print_num(tui, what, num1, num2, 0)
    153 #define terminfo_print_num3 terminfo_print_num
    154 
    155 static Set(cstr_t) urls = SET_INIT;
    156 
    157 void tui_start(TUIData **tui_p, int *width, int *height, char **term, bool *rgb)
    158  FUNC_ATTR_NONNULL_ALL
    159 {
    160  TUIData *tui = xcalloc(1, sizeof(TUIData));
    161  tui->is_starting = true;
    162  tui->screenshot = NULL;
    163  tui->stopped = false;
    164  tui->loop = &main_loop;
    165  tui->url = -1;
    166 
    167  kv_init(tui->invalid_regions);
    168  kv_init(tui->urlbuf);
    169  signal_watcher_init(tui->loop, &tui->winch_handle, tui);
    170  signal_watcher_start(&tui->winch_handle, sigwinch_cb, SIGWINCH);
    171 
    172  // TODO(bfredl): zero hl is empty, send this explicitly?
    173  kv_push(tui->attrs, HLATTRS_INIT);
    174 
    175  tui->input.tk_ti_hook_fn = tui_tk_ti_getstr;
    176  ugrid_init(&tui->grid);
    177  tui_terminal_start(tui);
    178 
    179  uv_timer_init(&tui->loop->uv, &tui->startup_delay_timer);
    180  tui->startup_delay_timer.data = tui;
    181  uv_timer_start(&tui->startup_delay_timer, after_startup_cb, 100, 0);
    182 
    183  *tui_p = tui;
    184  loop_poll_events(&main_loop, 1);
    185  *width = tui->width;
    186  *height = tui->height;
    187  *term = tui->term;
    188  *rgb = tui->rgb;
    189 }
    190 
    191 /// Request the terminal's mode (DECRQM).
    192 ///
    193 /// @see handle_modereport
    194 static void tui_request_term_mode(TUIData *tui, TermMode mode)
    195  FUNC_ATTR_NONNULL_ALL
    196 {
    197  // 5 bytes for \x1b[?$p, 1 byte for null terminator, 6 bytes for mode digits (more than enough)
    198  char buf[12];
    199  int len = snprintf(buf, sizeof(buf), "\x1b[?%d$p", (int)mode);
    200  assert((len > 0) && (len < (int)sizeof(buf)));
    201  out(tui, buf, (size_t)len);
    202 }
    203 
    204 /// Set (DECSET) or reset (DECRST) a terminal mode.
    205 static void tui_set_term_mode(TUIData *tui, TermMode mode, bool set)
    206  FUNC_ATTR_NONNULL_ALL
    207 {
    208  char buf[12];
    209  int len = snprintf(buf, sizeof(buf), "\x1b[?%d%c", (int)mode, set ? 'h' : 'l');
    210  assert((len > 0) && (len < (int)sizeof(buf)));
    211  out(tui, buf, (size_t)len);
    212 }
    213 
    214 /// Handle a mode report (DECRPM) from the terminal.
    215 void tui_handle_term_mode(TUIData *tui, TermMode mode, TermModeState state)
    216  FUNC_ATTR_NONNULL_ALL
    217 {
    218  bool is_set = false;
    219  switch (state) {
    220  case kTermModeNotRecognized:
    221  case kTermModePermanentlyReset:
    222    // TODO(bfredl): This is really ILOG but we want it in all builds.
    223    // add to show_verbose_terminfo() without being too racy ????
    224    if (!nvim_testing) {
    225      // Very noisy in CI, don't log during tests. #33599
    226      WLOG("TUI: terminal mode %d unavailable, state %d", mode, state);
    227    }
    228    // If the mode is not recognized, or if the terminal emulator does not allow it to be changed,
    229    // then there is nothing to do
    230    break;
    231  case kTermModePermanentlySet:
    232  case kTermModeSet:
    233    is_set = true;
    234    FALLTHROUGH;
    235  case kTermModeReset:
    236    // The terminal supports changing the given mode
    237    if (!nvim_testing) {
    238      // Very noisy in CI, don't log during tests. #33599
    239      WLOG("TUI: terminal mode %d detected, state %d", mode, state);
    240    }
    241    switch (mode) {
    242    case kTermModeSynchronizedOutput:
    243      // Ref: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
    244      tui->has_sync_mode = true;
    245      break;
    246    case kTermModeGraphemeClusters:
    247      if (!is_set) {
    248        tui_set_term_mode(tui, mode, true);
    249        tui->modes.grapheme_clusters = true;
    250      }
    251      break;
    252    case kTermModeThemeUpdates:
    253      if (!is_set) {
    254        tui_set_term_mode(tui, mode, true);
    255        tui->modes.theme_updates = true;
    256      }
    257      break;
    258    case kTermModeResizeEvents:
    259      if (!is_set) {
    260        tui_set_term_mode(tui, mode, true);
    261        tui->modes.resize_events = true;
    262      }
    263 
    264      // We track both whether the mode is enabled AND if Nvim was the one to enable it
    265      tui->resize_events_enabled = true;
    266      break;
    267    case kTermModeLeftAndRightMargins:
    268      tui->has_left_and_right_margin_mode = true;
    269      break;
    270    default:
    271      break;
    272    }
    273  }
    274 }
    275 
    276 /// Query the terminal emulator to see if it supports extended underline.
    277 static void tui_query_extended_underline(TUIData *tui)
    278 {
    279  // Try to set an undercurl using an SGR sequence, followed by a DECRQSS SGR query.
    280  // Reset attributes first, as other code may have set attributes.
    281  out(tui, S_LEN("\x1b[0m\x1b[4:3m\x1bP$qm\x1b\\"));
    282  tui->print_attr_id = -1;
    283 }
    284 
    285 void tui_enable_extended_underline(TUIData *tui)
    286 {
    287  terminfo_set_if_empty(tui, kTerm_set_underline_style, "\x1b[4:%p1%dm");
    288  tui->can_set_underline_color = true;
    289 }
    290 
    291 /// Query the terminal emulator to see if it supports Kitty's keyboard protocol.
    292 ///
    293 /// Write CSI ? u followed by a primary device attributes request (CSI c). If
    294 /// a primary device attributes response is received without first receiving an
    295 /// answer to the progressive enhancement query (CSI u), then the terminal does
    296 /// not support the Kitty keyboard protocol.
    297 ///
    298 /// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#detection-of-support-for-this-protocol
    299 static void tui_query_kitty_keyboard(TUIData *tui)
    300  FUNC_ATTR_NONNULL_ALL
    301 {
    302  // Set the key encoding whenever the Device Attributes (DA1) response is received.
    303  tui->input.callbacks.primary_device_attr = tui_set_key_encoding;
    304  out(tui, S_LEN("\x1b[?u\x1b[c"));
    305 }
    306 
    307 void tui_set_key_encoding(TUIData *tui)
    308  FUNC_ATTR_NONNULL_ALL
    309 {
    310  switch (tui->input.key_encoding) {
    311  case kKeyEncodingKitty:
    312    // Progressive enhancement flags:
    313    //   0b01   (1) Disambiguate escape codes
    314    //   0b10   (2) Report event types
    315    out(tui, S_LEN("\x1b[>3u"));
    316    break;
    317  case kKeyEncodingXterm:
    318    out(tui, S_LEN("\x1b[>4;2m"));
    319    break;
    320  case kKeyEncodingLegacy:
    321    break;
    322  }
    323 }
    324 
    325 static void tui_reset_key_encoding(TUIData *tui)
    326  FUNC_ATTR_NONNULL_ALL
    327 {
    328  switch (tui->input.key_encoding) {
    329  case kKeyEncodingKitty:
    330    out(tui, S_LEN("\x1b[<u"));
    331    break;
    332  case kKeyEncodingXterm:
    333    out(tui, S_LEN("\x1b[>4;0m"));
    334    break;
    335  case kKeyEncodingLegacy:
    336    break;
    337  }
    338 }
    339 
    340 /// Write the OSC 11 + DSR sequence to the terminal emulator to query the current
    341 /// background color.
    342 ///
    343 /// Response will be handled by the TermResponse handler in _core/defaults.lua.
    344 void tui_query_bg_color(TUIData *tui)
    345  FUNC_ATTR_NONNULL_ALL
    346 {
    347  out(tui, S_LEN("\x1b]11;?\x07\x1b[5n"));
    348  flush_buf(tui);
    349 }
    350 
    351 /// Enable the alternate screen and emit other control sequences to start the TUI.
    352 ///
    353 /// This is also called when the TUI is resumed after being suspended. We reinitialize all state
    354 /// from terminfo just in case the controlling terminal has changed (#27177).
    355 static void terminfo_start(TUIData *tui)
    356 {
    357  tui->bufpos = 0;
    358  tui->default_attr = false;
    359  tui->can_clear_attr = false;
    360  tui->is_invisible = true;
    361  tui->want_invisible = false;
    362  tui->busy = false;
    363  tui->set_cursor_color_as_str = false;
    364  tui->cursor_has_color = false;
    365  tui->resize_events_enabled = false;
    366  tui->modes.grapheme_clusters = false;
    367  tui->modes.resize_events = false;
    368  tui->modes.theme_updates = false;
    369  tui->showing_mode = SHAPE_IDX_N;
    370  tui->terminfo_ext.enable_focus_reporting = NULL;
    371  tui->terminfo_ext.disable_focus_reporting = NULL;
    372 
    373  tui->out_fd = STDOUT_FILENO;
    374  tui->out_isatty = os_isatty(tui->out_fd);
    375  tui->input.tui_data = tui;
    376 
    377  tui->ti_arena = (Arena)ARENA_EMPTY;
    378  assert(tui->term == NULL);
    379 
    380  char *term = os_getenv("TERM");
    381 #ifdef MSWIN
    382  const char *guessed_term = NULL;
    383  os_tty_guess_term(&guessed_term, tui->out_fd);
    384  if (term == NULL && guessed_term != NULL) {
    385    // TODO(bfredl): should be arena_strdup, make os_getenv ready for the BIG STAGE?
    386    term = xstrdup(guessed_term);
    387    os_setenv("TERM", guessed_term, 1);
    388  }
    389 #endif
    390 
    391  // Set up terminfo.
    392  tui->terminfo_found_in_db = false;
    393  if (term) {
    394    if (terminfo_from_database(&tui->ti, term, &tui->ti_arena)) {
    395      tui->term = arena_strdup(&tui->ti_arena, term);
    396      tui->terminfo_found_in_db = true;
    397    }
    398  }
    399 
    400  if (!tui->terminfo_found_in_db) {
    401    const TerminfoEntry *new = terminfo_from_builtin(term, &tui->term);
    402    // we will patch it below, so make a copy
    403    memcpy(&tui->ti, new, sizeof tui->ti);
    404  }
    405 
    406  // None of the following work over SSH; see :help TERM .
    407  char *colorterm = os_getenv("COLORTERM");
    408  char *termprg = os_getenv("TERM_PROGRAM");
    409  char *vte_version_env = os_getenv("VTE_VERSION");
    410  char *konsolev_env = os_getenv("KONSOLE_VERSION");
    411  char *term_program_version_env = os_getenv("TERM_PROGRAM_VERSION");
    412 
    413  int vtev = vte_version_env ? (int)strtol(vte_version_env, NULL, 10) : 0;
    414  bool iterm_env = termprg && strstr(termprg, "iTerm.app");
    415  bool nsterm = (termprg && strstr(termprg, "Apple_Terminal"))
    416                || terminfo_is_term_family(term, "nsterm");
    417  bool konsole = terminfo_is_term_family(term, "konsole")
    418                 || os_env_exists("KONSOLE_PROFILE_NAME", true)
    419                 || os_env_exists("KONSOLE_DBUS_SESSION", true);
    420  int konsolev = konsolev_env ? (int)strtol(konsolev_env, NULL, 10)
    421                              : (konsole ? 1 : 0);
    422  bool wezterm = strequal(termprg, "WezTerm");
    423  const char *weztermv = wezterm ? term_program_version_env : NULL;
    424  bool screen = terminfo_is_term_family(term, "screen");
    425  bool tmux = terminfo_is_term_family(term, "tmux") || os_env_exists("TMUX", true);
    426  tui->screen_or_tmux = screen || tmux;
    427 
    428  // truecolor support must be checked before patching/augmenting terminfo
    429  tui->rgb = term_has_truecolor(tui, colorterm);
    430 
    431  patch_terminfo_bugs(tui, term, colorterm, vtev, konsolev, iterm_env, nsterm);
    432  augment_terminfo(tui, term, vtev, konsolev, weztermv, iterm_env, nsterm);
    433 
    434 #define TI_HAS(name) (tui->ti.defs[name] != NULL)
    435  tui->can_change_scroll_region = TI_HAS(kTerm_change_scroll_region);
    436  // note: also gated by tui->has_left_and_right_margin_mode
    437  tui->can_set_lr_margin = TI_HAS(kTerm_set_lr_margin);
    438  tui->can_scroll =
    439    TI_HAS(kTerm_delete_line)
    440    && TI_HAS(kTerm_parm_delete_line)
    441    && TI_HAS(kTerm_insert_line)
    442    && TI_HAS(kTerm_parm_insert_line);
    443  tui->can_erase_chars = TI_HAS(kTerm_erase_chars);
    444  tui->immediate_wrap_after_last_column =
    445    terminfo_is_term_family(term, "conemu")
    446    || terminfo_is_term_family(term, "cygwin")
    447    || terminfo_is_term_family(term, "win32con")
    448    || terminfo_is_term_family(term, "interix");
    449  tui->bce = tui->ti.bce;
    450  // Set 't_Co' from the result of terminfo & fix_terminfo.
    451  t_colors = tui->ti.max_colors;
    452  // Enter alternate screen, save title, and clear.
    453  // NOTE: Do this *before* changing terminal settings. #6433
    454  terminfo_out(tui, kTerm_enter_ca_mode);
    455  terminfo_out(tui, kTerm_keypad_xmit);
    456  terminfo_out(tui, kTerm_clear_screen);
    457 
    458  /// Terminals usually ignore unrecognized private modes, and there is no
    459  /// known ambiguity with these. So we just set them unconditionally.
    460  // Enable bracketed paste
    461  tui_set_term_mode(tui, kTermModeBracketedPaste, true);
    462 
    463  tui->has_left_and_right_margin_mode = false;
    464  tui->has_sync_mode = false;
    465 
    466  // Query support for private DEC modes that Nvim can take advantage of.
    467  // Some terminals (such as Terminal.app) do not support DECRQM, so skip the query.
    468  if (!nsterm) {
    469    tui_request_term_mode(tui, kTermModeLeftAndRightMargins);
    470    tui_request_term_mode(tui, kTermModeSynchronizedOutput);
    471    tui_request_term_mode(tui, kTermModeGraphemeClusters);
    472    tui_request_term_mode(tui, kTermModeThemeUpdates);
    473    tui_request_term_mode(tui, kTermModeResizeEvents);
    474  }
    475 
    476  // Don't use DECRQSS in screen or tmux, as they behave strangely when receiving it.
    477  // Terminal.app also doesn't support DECRQSS.
    478  if (!TI_HAS(kTerm_set_underline_style) && !(screen || tmux || nsterm)) {
    479    // Query the terminal to see if it supports extended underline.
    480    tui_query_extended_underline(tui);
    481  }
    482 
    483  // Query the terminal to see if it supports Kitty's keyboard protocol
    484  tui_query_kitty_keyboard(tui);
    485 
    486  int ret;
    487  uv_loop_init(&tui->write_loop);
    488  if (tui->out_isatty) {
    489    ret = uv_tty_init(&tui->write_loop, &tui->output_handle.tty, tui->out_fd, 0);
    490    if (ret) {
    491      ELOG("uv_tty_init failed: %s", uv_strerror(ret));
    492    }
    493 #ifndef MSWIN
    494    int retry_count = 10;
    495    // A signal may cause uv_tty_set_mode() to fail (e.g., SIGCONT). Retry a
    496    // few times. #12322
    497    while ((ret = uv_tty_set_mode(&tui->output_handle.tty, UV_TTY_MODE_IO)) == UV_EINTR
    498           && retry_count > 0) {
    499      retry_count--;
    500    }
    501    if (ret) {
    502      ELOG("uv_tty_set_mode failed: %s", uv_strerror(ret));
    503    }
    504 #endif
    505  } else {
    506    ret = uv_pipe_init(&tui->write_loop, &tui->output_handle.pipe, 0);
    507    if (ret) {
    508      ELOG("uv_pipe_init failed: %s", uv_strerror(ret));
    509    }
    510    ret = uv_pipe_open(&tui->output_handle.pipe, tui->out_fd);
    511    if (ret) {
    512      ELOG("uv_pipe_open failed: %s", uv_strerror(ret));
    513    }
    514  }
    515  flush_buf(tui);
    516 
    517  xfree(term);
    518  xfree(colorterm);
    519  xfree(termprg);
    520  xfree(vte_version_env);
    521  xfree(konsolev_env);
    522  xfree(term_program_version_env);
    523 #undef TI_HAS
    524 }
    525 
    526 /// Disable various terminal modes and other features.
    527 static void terminfo_disable(TUIData *tui)
    528 {
    529  // Disable theme update notifications. We do this first to avoid getting any
    530  // more notifications after we reset the cursor and any color palette changes.
    531  if (tui->modes.theme_updates) {
    532    tui_set_term_mode(tui, kTermModeThemeUpdates, false);
    533  }
    534 
    535  // Destroy output stuff
    536  tui_mode_change(tui, NULL_STRING, SHAPE_IDX_N);
    537  tui_mouse_off(tui);
    538  terminfo_out(tui, kTerm_exit_attribute_mode);
    539  // Reset cursor to normal before exiting alternate screen.
    540  terminfo_out(tui, kTerm_cursor_normal);
    541  terminfo_out(tui, kTerm_reset_cursor_style);
    542  terminfo_out(tui, kTerm_keypad_local);
    543 
    544  // Reset the key encoding
    545  tui_reset_key_encoding(tui);
    546 
    547  // Disable terminal modes that we enabled
    548  if (tui->modes.resize_events) {
    549    tui_set_term_mode(tui, kTermModeResizeEvents, false);
    550  }
    551 
    552  if (tui->modes.grapheme_clusters) {
    553    tui_set_term_mode(tui, kTermModeGraphemeClusters, false);
    554  }
    555 
    556  // May restore old title before exiting alternate screen.
    557  tui_set_title(tui, NULL_STRING);
    558  if (tui->cursor_has_color) {
    559    terminfo_out(tui, kTerm_reset_cursor_color);
    560  }
    561  // Disable bracketed paste
    562  tui_set_term_mode(tui, kTermModeBracketedPaste, false);
    563  // Disable focus reporting
    564  out_len(tui, tui->terminfo_ext.disable_focus_reporting);
    565 
    566  // Send a DA1 request. When the terminal responds we know that it has
    567  // processed all of our requests and won't be emitting anymore sequences.
    568  out(tui, S_LEN("\x1b[c"));
    569 
    570  // Immediately flush the buffer and wait for the DA1 response.
    571  flush_buf(tui);
    572 }
    573 
    574 /// Disable the alternate screen and prepare for the TUI to close.
    575 static void terminfo_stop(TUIData *tui)
    576 {
    577  if (ui_client_exit_status == 0 && ui_client_error_exit > 0) {
    578    ui_client_exit_status = ui_client_error_exit;
    579  }
    580 
    581  // If Nvim exited with nonzero status, without indicating this was an
    582  // intentional exit (like `:1cquit`), it likely was an internal failure.
    583  // Don't clobber the stderr error message in this case. #21608
    584  if (ui_client_exit_status == MAX(ui_client_error_exit, 0)) {
    585    // Position the cursor on the last screen line, below all the text
    586    cursor_goto(tui, tui->height - 1, 0);
    587    // Exit alternate screen.
    588    terminfo_out(tui, kTerm_exit_ca_mode);
    589  }
    590 
    591  flush_buf(tui);
    592  uv_tty_reset_mode();
    593  uv_close((uv_handle_t *)&tui->output_handle, NULL);
    594  uv_run(&tui->write_loop, UV_RUN_DEFAULT);
    595  if (uv_loop_close(&tui->write_loop)) {
    596    abort();
    597  }
    598  arena_mem_free(arena_finish(&tui->ti_arena));
    599  // Avoid using freed memory.
    600  memset(&tui->ti, 0, sizeof(tui->ti));
    601  tui->term = NULL;
    602 }
    603 
    604 static void tui_terminal_start(TUIData *tui)
    605 {
    606  tui->print_attr_id = -1;
    607  terminfo_start(tui);
    608  if (tui->input.loop == NULL) {
    609    tinput_init(&tui->input, &main_loop, &tui->ti);
    610  }
    611  tui_guess_size(tui);
    612  tinput_start(&tui->input);
    613 }
    614 
    615 static void after_startup_cb(uv_timer_t *handle)
    616 {
    617  TUIData *tui = handle->data;
    618  tui_terminal_after_startup(tui);
    619 }
    620 
    621 static void tui_terminal_after_startup(TUIData *tui)
    622  FUNC_ATTR_NONNULL_ALL
    623 {
    624  // Emit this after Nvim startup, not during.  This works around a tmux
    625  // 2.3 bug(?) which caused slow drawing during startup.  #7649
    626  out_len(tui, tui->terminfo_ext.enable_focus_reporting);
    627  flush_buf(tui);
    628 }
    629 
    630 void tui_stop(TUIData *tui)
    631  FUNC_ATTR_NONNULL_ALL
    632 {
    633  if (uv_is_closing((uv_handle_t *)&tui->output_handle)) {
    634    // Race between SIGCONT (tui.c) and SIGHUP (os/signal.c)? #8075
    635    ELOG("TUI already stopped (race?)");
    636    tui->stopped = true;
    637    return;
    638  }
    639 
    640  tui->input.callbacks.primary_device_attr = tui_stop_cb;
    641  terminfo_disable(tui);
    642 
    643  // Wait until DA1 response is received, or stdin is closed (#35744).
    644  LOOP_PROCESS_EVENTS_UNTIL(tui->loop, tui->loop->events, EXIT_TIMEOUT_MS,
    645                            tui->stopped || tui->input.read_stream.did_eof);
    646  if (!tui->stopped && !tui->input.read_stream.did_eof) {
    647    WLOG("TUI: timed out waiting for DA1 response");
    648  }
    649  tui->stopped = true;
    650 
    651  tui_terminal_stop(tui);
    652  stream_set_blocking(tui->input.in_fd, true);   // normalize stream (#2598)
    653  tinput_destroy(&tui->input);
    654  signal_watcher_stop(&tui->winch_handle);
    655  signal_watcher_close(&tui->winch_handle, NULL);
    656  uv_close((uv_handle_t *)&tui->startup_delay_timer, NULL);
    657 }
    658 
    659 /// Callback function called when the response to the Device Attributes (DA1)
    660 /// request is sent during shutdown.
    661 static void tui_stop_cb(TUIData *tui)
    662  FUNC_ATTR_NONNULL_ALL
    663 {
    664  tui->stopped = true;
    665 }
    666 
    667 /// Stop the terminal but allow it to restart later (like after suspend)
    668 ///
    669 /// This is called after we receive the response to the DA1 request sent from
    670 /// terminfo_disable.
    671 static void tui_terminal_stop(TUIData *tui)
    672  FUNC_ATTR_NONNULL_ALL
    673 {
    674  tinput_stop(&tui->input);
    675  terminfo_stop(tui);
    676 }
    677 
    678 /// Returns true if UI `ui` is stopped.
    679 bool tui_is_stopped(TUIData *tui)
    680 {
    681  return tui->stopped;
    682 }
    683 
    684 #ifdef EXITFREE
    685 void tui_free_all_mem(TUIData *tui)
    686 {
    687  ugrid_free(&tui->grid);
    688  kv_destroy(tui->invalid_regions);
    689 
    690  const char *url;
    691  set_foreach(&urls, url, {
    692    xfree((void *)url);
    693  });
    694  set_destroy(cstr_t, &urls);
    695 
    696  kv_destroy(tui->attrs);
    697  kv_destroy(tui->urlbuf);
    698  xfree(tui);
    699 }
    700 #endif
    701 
    702 static void sigwinch_cb(SignalWatcher *watcher, int signum, void *cbdata)
    703 {
    704  TUIData *tui = cbdata;
    705  if (tui_is_stopped(tui) || tui->resize_events_enabled) {
    706    return;
    707  }
    708 
    709  tui_guess_size(tui);
    710 }
    711 
    712 static bool attrs_differ(TUIData *tui, int id1, int id2, bool rgb)
    713 {
    714  if (id1 == id2) {
    715    return false;
    716  } else if (id1 < 0 || id2 < 0) {
    717    return true;
    718  }
    719  HlAttrs a1 = kv_A(tui->attrs, (size_t)id1);
    720  HlAttrs a2 = kv_A(tui->attrs, (size_t)id2);
    721 
    722  if (a1.url != a2.url) {
    723    return true;
    724  }
    725 
    726  if (rgb) {
    727    return a1.rgb_fg_color != a2.rgb_fg_color
    728           || a1.rgb_bg_color != a2.rgb_bg_color
    729           || a1.rgb_ae_attr != a2.rgb_ae_attr
    730           || a1.rgb_sp_color != a2.rgb_sp_color;
    731  } else {
    732    return a1.cterm_fg_color != a2.cterm_fg_color
    733           || a1.cterm_bg_color != a2.cterm_bg_color
    734           || a1.cterm_ae_attr != a2.cterm_ae_attr
    735           || (a1.cterm_ae_attr & HL_UNDERLINE_MASK
    736               && a1.rgb_sp_color != a2.rgb_sp_color);
    737  }
    738 }
    739 
    740 static void update_attrs(TUIData *tui, int attr_id)
    741 {
    742  if (!attrs_differ(tui, attr_id, tui->print_attr_id, tui->rgb)) {
    743    tui->print_attr_id = attr_id;
    744    return;
    745  }
    746  tui->print_attr_id = attr_id;
    747  HlAttrs attrs = kv_A(tui->attrs, (size_t)attr_id);
    748  int attr = tui->rgb ? attrs.rgb_ae_attr : attrs.cterm_ae_attr;
    749 
    750  bool bold = attr & HL_BOLD;
    751  bool italic = attr & HL_ITALIC;
    752  bool reverse = attr & HL_INVERSE;
    753  bool standout = attr & HL_STANDOUT;
    754  bool strikethrough = attr & HL_STRIKETHROUGH;
    755  bool altfont = attr & HL_ALTFONT;
    756  bool dim = attr & HL_DIM;
    757  bool blink = attr & HL_BLINK;
    758  bool conceal = attr & HL_CONCEALED;
    759  bool overline = attr & HL_OVERLINE;
    760 
    761  bool underline;
    762  bool undercurl;
    763  bool underdouble;
    764  bool underdotted;
    765  bool underdashed;
    766  if (tui->ti.defs[kTerm_set_underline_style]) {
    767    int ul = attr & HL_UNDERLINE_MASK;
    768    underline = ul == HL_UNDERLINE;
    769    undercurl = ul == HL_UNDERCURL;
    770    underdouble = ul == HL_UNDERDOUBLE;
    771    underdashed = ul == HL_UNDERDASHED;
    772    underdotted = ul == HL_UNDERDOTTED;
    773  } else {
    774    underline = attr & HL_UNDERLINE_MASK;
    775    undercurl = false;
    776    underdouble = false;
    777    underdotted = false;
    778    underdashed = false;
    779  }
    780 
    781  bool has_any_underline = undercurl || underline
    782                           || underdouble || underdotted || underdashed;
    783 
    784  if (tui->ti.defs[kTerm_set_attributes] != NULL) {
    785    if (bold || dim || blink || reverse || underline || standout) {
    786      TPVAR params[9] = { 0 };
    787      params[0].num = standout;
    788      params[1].num = underline;
    789      params[2].num = reverse;
    790      params[3].num = blink;
    791      params[4].num = dim;
    792      params[5].num = bold;
    793      params[6].num = 0;   // blank
    794      params[7].num = 0;   // protect
    795      params[8].num = 0;   // alternate character set
    796      terminfo_print(tui, kTerm_set_attributes, params);
    797    } else if (!tui->default_attr) {
    798      terminfo_out(tui, kTerm_exit_attribute_mode);
    799    }
    800  } else {
    801    if (!tui->default_attr) {
    802      terminfo_out(tui, kTerm_exit_attribute_mode);
    803    }
    804    if (bold) {
    805      terminfo_out(tui, kTerm_enter_bold_mode);
    806    }
    807    if (underline) {
    808      terminfo_out(tui, kTerm_enter_underline_mode);
    809    }
    810    if (standout) {
    811      terminfo_out(tui, kTerm_enter_standout_mode);
    812    }
    813    if (reverse) {
    814      terminfo_out(tui, kTerm_enter_reverse_mode);
    815    }
    816    if (dim) {
    817      terminfo_out(tui, kTerm_enter_dim_mode);
    818    }
    819    if (blink) {
    820      terminfo_out(tui, kTerm_enter_blink_mode);
    821    }
    822  }
    823  if (italic) {
    824    terminfo_out(tui, kTerm_enter_italics_mode);
    825  }
    826  if (altfont) {
    827    out_len(tui, tui->terminfo_ext.enter_altfont_mode);
    828  }
    829  if (strikethrough) {
    830    terminfo_out(tui, kTerm_enter_strikethrough_mode);
    831  }
    832  if (conceal) {
    833    terminfo_out(tui, kTerm_enter_secure_mode);
    834  }
    835  if (overline) {
    836    out(tui, S_LEN("\x1b[53m"));
    837  }
    838  if (tui->ti.defs[kTerm_set_underline_style]) {
    839    if (undercurl) {
    840      terminfo_print_num1(tui, kTerm_set_underline_style, 3);
    841    }
    842    if (underdouble) {
    843      terminfo_print_num1(tui, kTerm_set_underline_style, 2);
    844    }
    845    if (underdotted) {
    846      terminfo_print_num1(tui, kTerm_set_underline_style, 4);
    847    }
    848    if (underdashed) {
    849      terminfo_print_num1(tui, kTerm_set_underline_style, 5);
    850    }
    851  }
    852 
    853  if (has_any_underline && tui->can_set_underline_color) {
    854    int color = attrs.rgb_sp_color;
    855    if (color != -1) {
    856      // Only support colon syntax. #9270
    857      out_printf(tui, 128, "\x1b[58:2::%d:%d:%dm",
    858                 (color >> 16) & 0xff,  // red
    859                 (color >> 8) & 0xff,   // green
    860                 color & 0xff);         // blue
    861    }
    862  }
    863 
    864  int fg, bg;
    865  if (tui->rgb && !(attr & HL_FG_INDEXED)) {
    866    fg = ((attrs.rgb_fg_color != -1)
    867          ? attrs.rgb_fg_color : tui->clear_attrs.rgb_fg_color);
    868    if (fg != -1) {
    869      terminfo_print_num3(tui, kTerm_set_rgb_foreground,
    870                          (fg >> 16) & 0xff,  // red
    871                          (fg >> 8) & 0xff,   // green
    872                          fg & 0xff);         // blue
    873    }
    874  } else {
    875    fg = (attrs.cterm_fg_color
    876          ? attrs.cterm_fg_color - 1 : (tui->clear_attrs.cterm_fg_color - 1));
    877    if (fg != -1) {
    878      terminfo_print_num1(tui, kTerm_set_a_foreground, fg);
    879    }
    880  }
    881 
    882  if (tui->rgb && !(attr & HL_BG_INDEXED)) {
    883    bg = ((attrs.rgb_bg_color != -1)
    884          ? attrs.rgb_bg_color : tui->clear_attrs.rgb_bg_color);
    885    if (bg != -1) {
    886      terminfo_print_num3(tui, kTerm_set_rgb_background,
    887                          (bg >> 16) & 0xff,  // red
    888                          (bg >> 8) & 0xff,   // green
    889                          bg & 0xff);         // blue
    890    }
    891  } else {
    892    bg = (attrs.cterm_bg_color
    893          ? attrs.cterm_bg_color - 1 : (tui->clear_attrs.cterm_bg_color - 1));
    894    if (bg != -1) {
    895      terminfo_print_num1(tui, kTerm_set_a_background, bg);
    896    }
    897  }
    898 
    899  if (tui->url != attrs.url) {
    900    if (attrs.url >= 0) {
    901      const char *url = urls.keys[attrs.url];
    902      kv_size(tui->urlbuf) = 0;
    903 
    904      // Add some fixed offset to the URL ID to deconflict with other
    905      // applications which may set their own IDs
    906      const uint64_t id = 0xE1EA0000U + (uint32_t)attrs.url;
    907 
    908      kv_printf(tui->urlbuf, "\x1b]8;id=%" PRIu64 ";%s\x1b\\", id, url);
    909      out(tui, tui->urlbuf.items, kv_size(tui->urlbuf));
    910    } else {
    911      out(tui, S_LEN("\x1b]8;;\x1b\\"));
    912    }
    913 
    914    tui->url = attrs.url;
    915  }
    916 
    917  tui->default_attr = fg == -1 && bg == -1
    918                      && !bold && !dim && !blink && !conceal && !overline && !italic
    919                      && !has_any_underline && !reverse && !standout && !strikethrough;
    920 
    921  // Non-BCE terminals can't clear with non-default background color. Some BCE
    922  // terminals don't support attributes either, so don't rely on it. But assume
    923  // italic and bold has no effect if there is no text.
    924  tui->can_clear_attr = !reverse && !standout && !dim && !blink && !conceal && !overline
    925                        && !has_any_underline && !strikethrough && (tui->bce || bg == -1);
    926 }
    927 
    928 static void final_column_wrap(TUIData *tui)
    929 {
    930  UGrid *grid = &tui->grid;
    931  if (grid->row != -1 && grid->col == tui->width) {
    932    grid->col = 0;
    933    if (grid->row < MIN(tui->height, grid->height - 1)) {
    934      grid->row++;
    935    }
    936  }
    937 }
    938 
    939 /// It is undocumented, but in the majority of terminals and terminal emulators
    940 /// printing at the right margin does not cause an automatic wrap until the
    941 /// next character is printed, holding the cursor in place until then.
    942 static void print_cell(TUIData *tui, char *buf, sattr_T attr)
    943 {
    944  UGrid *grid = &tui->grid;
    945  if (!tui->immediate_wrap_after_last_column) {
    946    // Printing the next character finally advances the cursor.
    947    final_column_wrap(tui);
    948  }
    949  update_attrs(tui, attr);
    950  out(tui, buf, strlen(buf));
    951  grid->col++;
    952  if (tui->immediate_wrap_after_last_column) {
    953    // Printing at the right margin immediately advances the cursor.
    954    final_column_wrap(tui);
    955  }
    956 }
    957 
    958 static bool cheap_to_print(TUIData *tui, int row, int col, int next)
    959 {
    960  UGrid *grid = &tui->grid;
    961  UCell *cell = grid->cells[row] + col;
    962  while (next) {
    963    next--;
    964    if (attrs_differ(tui, cell->attr,
    965                     tui->print_attr_id, tui->rgb)) {
    966      if (tui->default_attr) {
    967        return false;
    968      }
    969    }
    970    if (schar_get_ascii(cell->data) == 0) {
    971      return false;  // not ascii
    972    }
    973    cell++;
    974  }
    975  return true;
    976 }
    977 
    978 /// This optimizes several cases where it is cheaper to do something other
    979 /// than send a full cursor positioning control sequence.  However, there are
    980 /// some further optimizations that may seem obvious but that will not work.
    981 ///
    982 /// We cannot use VT (ASCII 0/11) for moving the cursor up, because VT means
    983 /// move the cursor down on a DEC terminal.  Similarly, on a DEC terminal FF
    984 /// (ASCII 0/12) means the same thing and does not mean home.  VT, CVT, and
    985 /// TAB also stop at software-defined tabulation stops, not at a fixed set
    986 /// of row/column positions.
    987 static void cursor_goto(TUIData *tui, int row, int col)
    988 {
    989  UGrid *grid = &tui->grid;
    990  if (row == grid->row && col == grid->col) {
    991    return;
    992  }
    993 
    994  // If an OSC 8 sequence is active terminate it before moving the cursor
    995  if (tui->url >= 0) {
    996    out(tui, S_LEN("\x1b]8;;\x1b\\"));
    997    tui->url = -1;
    998    tui->print_attr_id = -1;
    999  }
   1000 
   1001  if (0 == row && 0 == col) {
   1002    terminfo_out(tui, kTerm_cursor_home);
   1003    ugrid_goto(grid, row, col);
   1004    return;
   1005  }
   1006  if (grid->row == -1) {
   1007    goto safe_move;
   1008  }
   1009  if (0 == col
   1010      ? col != grid->col
   1011      : (row != grid->row
   1012         ? false
   1013         : (1 == col
   1014            ? (2 < grid->col && cheap_to_print(tui, grid->row, 0, col))
   1015            : (2 == col
   1016               ? (5 < grid->col && cheap_to_print(tui, grid->row, 0, col))
   1017               : false)))) {
   1018    // Motion to left margin from anywhere else, or CR + printing chars is
   1019    // even less expensive than using BSes or CUB.
   1020    terminfo_out(tui, kTerm_carriage_return);
   1021    ugrid_goto(grid, grid->row, 0);
   1022  }
   1023  if (row == grid->row) {
   1024    if (col < grid->col
   1025        // Deferred right margin wrap terminals have inconsistent ideas about
   1026        // where the cursor actually is during a deferred wrap.  Relative
   1027        // motion calculations have OBOEs that cannot be compensated for,
   1028        // because two terminals that claim to be the same will implement
   1029        // different cursor positioning rules.
   1030        && (tui->immediate_wrap_after_last_column || grid->col < tui->width)) {
   1031      int n = grid->col - col;
   1032      if (n <= 4) {  // This might be just BS, so it is considered really cheap.
   1033        while (n--) {
   1034          terminfo_out(tui, kTerm_cursor_left);
   1035        }
   1036      } else {
   1037        terminfo_print_num1(tui, kTerm_parm_left_cursor, n);
   1038      }
   1039      ugrid_goto(grid, row, col);
   1040      return;
   1041    } else if (col > grid->col) {
   1042      int n = col - grid->col;
   1043      if (n <= 2) {
   1044        while (n--) {
   1045          terminfo_out(tui, kTerm_cursor_right);
   1046        }
   1047      } else {
   1048        terminfo_print_num1(tui, kTerm_parm_right_cursor, n);
   1049      }
   1050      ugrid_goto(grid, row, col);
   1051      return;
   1052    }
   1053  }
   1054  if (col == grid->col) {
   1055    if (row > grid->row) {
   1056      int n = row - grid->row;
   1057      if (n <= 4) {  // This might be just LF, so it is considered really cheap.
   1058        while (n--) {
   1059          terminfo_out(tui, kTerm_cursor_down);
   1060        }
   1061      } else {
   1062        terminfo_print_num1(tui, kTerm_parm_down_cursor, n);
   1063      }
   1064      ugrid_goto(grid, row, col);
   1065      return;
   1066    } else if (row < grid->row) {
   1067      int n = grid->row - row;
   1068      if (n <= 2) {
   1069        while (n--) {
   1070          terminfo_out(tui, kTerm_cursor_up);
   1071        }
   1072      } else {
   1073        terminfo_print_num1(tui, kTerm_parm_up_cursor, n);
   1074      }
   1075      ugrid_goto(grid, row, col);
   1076      return;
   1077    }
   1078  }
   1079 
   1080 safe_move:
   1081  terminfo_print_num2(tui, kTerm_cursor_address, row, col);
   1082  ugrid_goto(grid, row, col);
   1083 }
   1084 
   1085 static void print_spaces(TUIData *tui, int width)
   1086 {
   1087  UGrid *grid = &tui->grid;
   1088  size_t left = (size_t)width;
   1089 
   1090  // spaces are not a sequence, we can squeeze whatever's left of the buffer
   1091  while (true) {
   1092    size_t buf_fit = MIN(left, sizeof tui->buf - tui->bufpos);
   1093    memset(tui->buf + tui->bufpos, ' ', buf_fit);
   1094    tui->bufpos += buf_fit;
   1095    left -= buf_fit;
   1096 
   1097    if (left == 0) {
   1098      break;  // likely: didn't need to flush for sm0l spaces
   1099    }
   1100    flush_buf(tui);
   1101  }
   1102 
   1103  grid->col += width;
   1104  if (tui->immediate_wrap_after_last_column) {
   1105    // Printing at the right margin immediately advances the cursor.
   1106    final_column_wrap(tui);
   1107  }
   1108 }
   1109 
   1110 /// Move cursor to the position given by `row` and `col` and print the char in `cell`.
   1111 /// Allows grid and host terminal to assume different widths of ambiguous-width chars.
   1112 ///
   1113 /// @param is_doublewidth  whether the char is double-width on the grid.
   1114 ///                        If true and the char is ambiguous-width, clear two cells.
   1115 static void print_cell_at_pos(TUIData *tui, int row, int col, UCell *cell, bool is_doublewidth)
   1116 {
   1117  UGrid *grid = &tui->grid;
   1118 
   1119  if (grid->row == -1 && cell->data == NUL) {
   1120    // If cursor needs repositioning and there is nothing to print, don't move cursor.
   1121    return;
   1122  }
   1123 
   1124  cursor_goto(tui, row, col);
   1125 
   1126  char buf[MAX_SCHAR_SIZE];
   1127  schar_get(buf, cell->data);
   1128  int c = utf_ptr2char(buf);
   1129  bool is_ambiwidth = utf_ambiguous_width(buf);
   1130  if (is_doublewidth && (is_ambiwidth || utf_char2cells(c) == 1)) {
   1131    // If the server used setcellwidths() to treat a single-width char as double-width,
   1132    // it needs to be treated like an ambiguous-width char.
   1133    is_ambiwidth = true;
   1134    // Clear the two screen cells.
   1135    // If the char is single-width in host terminal it won't change the second cell.
   1136    update_attrs(tui, cell->attr);
   1137    print_spaces(tui, 2);
   1138    cursor_goto(tui, row, col);
   1139  }
   1140 
   1141  print_cell(tui, buf, cell->attr);
   1142 
   1143  if (is_ambiwidth) {
   1144    // Force repositioning cursor after printing an ambiguous-width char.
   1145    grid->row = -1;
   1146  }
   1147 }
   1148 
   1149 static void clear_region(TUIData *tui, int top, int bot, int left, int right, int attr_id)
   1150 {
   1151  UGrid *grid = &tui->grid;
   1152 
   1153  // Setting the default colors is delayed until after startup to avoid flickering
   1154  // with the default colorscheme background. Consequently, any flush that happens
   1155  // during startup would result in clearing invalidated regions with zeroed
   1156  // clear_attrs, perceived as a black flicker. Reset attributes to clear with
   1157  // current terminal background instead (#28667, #28668).
   1158  if (tui->set_default_colors) {
   1159    update_attrs(tui, attr_id);
   1160  } else {
   1161    terminfo_out(tui, kTerm_exit_attribute_mode);
   1162  }
   1163 
   1164  // Background is set to the default color and the right edge matches the
   1165  // screen end, try to use terminal codes for clearing the requested area.
   1166  if (tui->can_clear_attr
   1167      && left == 0 && right == tui->width && bot == tui->height) {
   1168    if (top == 0) {
   1169      terminfo_out(tui, kTerm_clear_screen);
   1170      ugrid_goto(grid, top, left);
   1171    } else {
   1172      cursor_goto(tui, top, 0);
   1173      terminfo_out(tui, kTerm_clr_eos);
   1174    }
   1175  } else {
   1176    int width = right - left;
   1177 
   1178    // iterate through each line and clear
   1179    for (int row = top; row < bot; row++) {
   1180      cursor_goto(tui, row, left);
   1181      if (tui->can_clear_attr && right == tui->width) {
   1182        terminfo_out(tui, kTerm_clr_eol);
   1183      } else if (tui->can_erase_chars && tui->can_clear_attr && width >= 5) {
   1184        terminfo_print_num1(tui, kTerm_erase_chars, width);
   1185      } else {
   1186        print_spaces(tui, width);
   1187      }
   1188    }
   1189  }
   1190 }
   1191 
   1192 static void set_scroll_region(TUIData *tui, int top, int bot, int left, int right)
   1193 {
   1194  UGrid *grid = &tui->grid;
   1195 
   1196  terminfo_print_num2(tui, kTerm_change_scroll_region, top, bot);
   1197  if (left != 0 || right != tui->width - 1) {
   1198    tui_set_term_mode(tui, kTermModeLeftAndRightMargins, true);
   1199    terminfo_print_num2(tui, kTerm_set_lr_margin, left, right);
   1200  }
   1201  grid->row = -1;
   1202 }
   1203 
   1204 static void reset_scroll_region(TUIData *tui, bool fullwidth)
   1205 {
   1206  UGrid *grid = &tui->grid;
   1207 
   1208  if (tui->terminfo_ext.reset_scroll_region) {
   1209    out_len(tui, tui->terminfo_ext.reset_scroll_region);
   1210  } else {
   1211    terminfo_print_num2(tui, kTerm_change_scroll_region, 0, tui->height - 1);
   1212  }
   1213  if (!fullwidth) {
   1214    terminfo_print_num2(tui, kTerm_set_lr_margin, 0, tui->width - 1);
   1215    tui_set_term_mode(tui, kTermModeLeftAndRightMargins, false);
   1216  }
   1217  grid->row = -1;
   1218 }
   1219 
   1220 void tui_grid_resize(TUIData *tui, Integer g, Integer width, Integer height)
   1221 {
   1222  UGrid *grid = &tui->grid;
   1223  ugrid_resize(grid, (int)width, (int)height);
   1224 
   1225  // resize might not always be followed by a clear before flush
   1226  // so clip the invalid region
   1227  for (size_t i = 0; i < kv_size(tui->invalid_regions); i++) {
   1228    Rect *r = &kv_A(tui->invalid_regions, i);
   1229    r->bot = MIN(r->bot, grid->height);
   1230    r->right = MIN(r->right, grid->width);
   1231  }
   1232 
   1233  if (tui->pending_resize_events == 0 && !tui->is_starting) {
   1234    // Resize the _host_ terminal.
   1235    out_printf(tui, 64, "\x1b[8;%d;%dt", (int)height, (int)width);
   1236  } else {  // Already handled the resize; avoid double-resize.
   1237    tui->pending_resize_events = tui->pending_resize_events >
   1238                                 0 ? tui->pending_resize_events - 1 : 0;
   1239    grid->row = -1;
   1240  }
   1241 }
   1242 
   1243 void tui_grid_clear(TUIData *tui, Integer g)
   1244 {
   1245  UGrid *grid = &tui->grid;
   1246  ugrid_clear(grid);
   1247  // safe to clear cache at this point
   1248  schar_cache_clear_if_full();
   1249  kv_size(tui->invalid_regions) = 0;
   1250  clear_region(tui, 0, tui->height, 0, tui->width, 0);
   1251 }
   1252 
   1253 void tui_grid_cursor_goto(TUIData *tui, Integer grid, Integer row, Integer col)
   1254 {
   1255  // cursor position is validated in tui_flush
   1256  tui->row = (int)row;
   1257  tui->col = (int)col;
   1258 }
   1259 
   1260 static CursorShape tui_cursor_decode_shape(const char *shape_str)
   1261 {
   1262  CursorShape shape;
   1263  if (strequal(shape_str, "block")) {
   1264    shape = SHAPE_BLOCK;
   1265  } else if (strequal(shape_str, "vertical")) {
   1266    shape = SHAPE_VER;
   1267  } else if (strequal(shape_str, "horizontal")) {
   1268    shape = SHAPE_HOR;
   1269  } else {
   1270    WLOG("Unknown shape value '%s'", shape_str);
   1271    shape = SHAPE_BLOCK;
   1272  }
   1273  return shape;
   1274 }
   1275 
   1276 static cursorentry_T decode_cursor_entry(Dict args)
   1277 {
   1278  cursorentry_T r = shape_table[0];
   1279 
   1280  for (size_t i = 0; i < args.size; i++) {
   1281    char *key = args.items[i].key.data;
   1282    Object value = args.items[i].value;
   1283 
   1284    if (strequal(key, "cursor_shape")) {
   1285      r.shape = tui_cursor_decode_shape(args.items[i].value.data.string.data);
   1286    } else if (strequal(key, "blinkon")) {
   1287      r.blinkon = (int)value.data.integer;
   1288    } else if (strequal(key, "blinkoff")) {
   1289      r.blinkoff = (int)value.data.integer;
   1290    } else if (strequal(key, "attr_id")) {
   1291      r.id = (int)value.data.integer;
   1292    }
   1293  }
   1294  return r;
   1295 }
   1296 
   1297 void tui_mode_info_set(TUIData *tui, bool guicursor_enabled, Array args)
   1298 {
   1299  cursor_style_enabled = guicursor_enabled;
   1300  if (!guicursor_enabled) {
   1301    return;  // Do not send cursor style control codes.
   1302  }
   1303 
   1304  assert(args.size);
   1305 
   1306  // cursor style entries as defined by `shape_table`.
   1307  for (size_t i = 0; i < args.size; i++) {
   1308    assert(args.items[i].type == kObjectTypeDict);
   1309    cursorentry_T r = decode_cursor_entry(args.items[i].data.dict);
   1310    tui->cursor_shapes[i] = r;
   1311  }
   1312 
   1313  tui_set_mode(tui, tui->showing_mode);
   1314 }
   1315 
   1316 void tui_update_menu(TUIData *tui)
   1317 {
   1318  // Do nothing; menus are for GUI only
   1319 }
   1320 
   1321 void tui_busy_start(TUIData *tui)
   1322 {
   1323  tui->busy = true;
   1324 }
   1325 
   1326 void tui_busy_stop(TUIData *tui)
   1327 {
   1328  tui->busy = false;
   1329 }
   1330 
   1331 void tui_mouse_on(TUIData *tui)
   1332 {
   1333  if (!tui->mouse_enabled) {
   1334    tui_set_term_mode(tui, kTermModeMouseButtonEvent, true);
   1335    tui_set_term_mode(tui, kTermModeMouseSGRExt, true);
   1336    if (tui->mouse_move_enabled) {
   1337      tui_set_term_mode(tui, kTermModeMouseAnyEvent, true);
   1338    }
   1339    tui->mouse_enabled = true;
   1340  }
   1341 }
   1342 
   1343 void tui_mouse_off(TUIData *tui)
   1344 {
   1345  if (tui->mouse_enabled) {
   1346    if (tui->mouse_move_enabled) {
   1347      tui_set_term_mode(tui, kTermModeMouseAnyEvent, false);
   1348    }
   1349    tui_set_term_mode(tui, kTermModeMouseButtonEvent, false);
   1350    tui_set_term_mode(tui, kTermModeMouseSGRExt, false);
   1351    tui->mouse_enabled = false;
   1352  }
   1353 }
   1354 
   1355 static void tui_set_mode(TUIData *tui, ModeShape mode)
   1356 {
   1357  if (!cursor_style_enabled) {
   1358    return;
   1359  }
   1360  cursorentry_T c = tui->cursor_shapes[mode];
   1361 
   1362  if (c.id != 0 && c.id < (int)kv_size(tui->attrs) && tui->rgb) {
   1363    HlAttrs aep = kv_A(tui->attrs, c.id);
   1364 
   1365    tui->want_invisible = aep.hl_blend == 100;
   1366    if (!tui->want_invisible && aep.rgb_ae_attr & HL_INVERSE) {
   1367      // We interpret "inverse" as "default" (no termcode for "inverse"...).
   1368      // Hopefully the user's default cursor color is inverse.
   1369      terminfo_out(tui, kTerm_reset_cursor_color);
   1370    } else if (!tui->want_invisible && aep.rgb_bg_color >= 0) {
   1371      TPVAR params[9] = { 0 };
   1372      char hexbuf[8];
   1373      if (tui->set_cursor_color_as_str) {
   1374        snprintf(hexbuf, 7 + 1, "#%06x", aep.rgb_bg_color);
   1375        params[0].string = hexbuf;
   1376      } else {
   1377        params[0].num = aep.rgb_bg_color;
   1378      }
   1379      terminfo_print(tui, kTerm_set_cursor_color, params);
   1380      tui->cursor_has_color = true;
   1381    }
   1382  } else if (c.id == 0 && (tui->want_invisible || tui->cursor_has_color)) {
   1383    // No cursor color for this mode; reset to default.
   1384    tui->want_invisible = false;
   1385    tui->cursor_has_color = false;
   1386    terminfo_out(tui, kTerm_reset_cursor_color);
   1387  }
   1388 
   1389  int shape;
   1390  switch (c.shape) {
   1391  case SHAPE_BLOCK:
   1392    shape = 1; break;
   1393  case SHAPE_HOR:
   1394    shape = 3; break;
   1395  case SHAPE_VER:
   1396    shape = 5; break;
   1397  }
   1398  terminfo_print_num1(tui, kTerm_set_cursor_style,
   1399                      shape + (int)(c.blinkon == 0 || c.blinkoff == 0));
   1400 }
   1401 
   1402 /// @param mode editor mode
   1403 void tui_mode_change(TUIData *tui, String mode, Integer mode_idx)
   1404 {
   1405 #ifdef UNIX
   1406  // If stdin is not a TTY, the LHS of pipe may change the state of the TTY
   1407  // after calling uv_tty_set_mode. So, set the mode of the TTY again here.
   1408  // #13073
   1409  if (tui->out_isatty && tui->is_starting && !stdin_isatty) {
   1410    int ret = uv_tty_set_mode(&tui->output_handle.tty, UV_TTY_MODE_NORMAL);
   1411    if (ret) {
   1412      ELOG("uv_tty_set_mode failed: %s", uv_strerror(ret));
   1413    }
   1414    ret = uv_tty_set_mode(&tui->output_handle.tty, UV_TTY_MODE_IO);
   1415    if (ret) {
   1416      ELOG("uv_tty_set_mode failed: %s", uv_strerror(ret));
   1417    }
   1418  }
   1419 #endif
   1420  tui_set_mode(tui, (ModeShape)mode_idx);
   1421  if (tui->is_starting) {
   1422    if (tui->verbose >= 3) {
   1423      show_verbose_terminfo(tui);
   1424    }
   1425  }
   1426  tui->is_starting = false;  // mode entered, no longer starting
   1427  tui->showing_mode = (ModeShape)mode_idx;
   1428 }
   1429 
   1430 void tui_grid_scroll(TUIData *tui, Integer g, Integer startrow, Integer endrow, Integer startcol,
   1431                     Integer endcol, Integer rows, Integer cols FUNC_ATTR_UNUSED)
   1432 {
   1433  UGrid *grid = &tui->grid;
   1434  int top = (int)startrow;
   1435  int bot = (int)endrow - 1;
   1436  int left = (int)startcol;
   1437  int right = (int)endcol - 1;
   1438 
   1439  bool fullwidth = left == 0 && right == tui->width - 1;
   1440  bool full_screen_scroll = fullwidth && top == 0 && bot == tui->height - 1;
   1441 
   1442  ugrid_scroll(grid, top, bot, left, right, (int)rows);
   1443 
   1444  bool has_lr_margins = tui->has_left_and_right_margin_mode && tui->can_set_lr_margin;
   1445 
   1446  bool can_scroll = tui->can_scroll
   1447                    && (full_screen_scroll
   1448                        || (tui->can_change_scroll_region
   1449                            && ((left == 0 && right == tui->width - 1) || has_lr_margins)));
   1450 
   1451  if (can_scroll) {
   1452    // Change terminal scroll region and move cursor to the top
   1453    if (!full_screen_scroll) {
   1454      set_scroll_region(tui, top, bot, left, right);
   1455    }
   1456    cursor_goto(tui, top, left);
   1457    update_attrs(tui, 0);
   1458 
   1459    if (rows > 0) {
   1460      if (rows == 1) {
   1461        terminfo_out(tui, kTerm_delete_line);
   1462      } else {
   1463        terminfo_print_num1(tui, kTerm_parm_delete_line, (int)rows);
   1464      }
   1465    } else {
   1466      if (rows == -1) {
   1467        terminfo_out(tui, kTerm_insert_line);
   1468      } else {
   1469        terminfo_print_num1(tui, kTerm_parm_insert_line, -(int)rows);
   1470      }
   1471    }
   1472 
   1473    // Restore terminal scroll region and cursor
   1474    if (!full_screen_scroll) {
   1475      reset_scroll_region(tui, fullwidth);
   1476    }
   1477  } else {
   1478    // Mark the moved region as invalid for redrawing later
   1479    if (rows > 0) {
   1480      endrow = endrow - rows;
   1481    } else {
   1482      startrow = startrow - rows;
   1483    }
   1484    invalidate(tui, (int)startrow, (int)endrow, (int)startcol, (int)endcol);
   1485  }
   1486 }
   1487 
   1488 /// Add a URL to be used in an OSC 8 hyperlink.
   1489 ///
   1490 /// @param tui TUIData
   1491 /// @param url URL to add
   1492 /// @return Index of new URL, or -1 if URL is invalid
   1493 int32_t tui_add_url(TUIData *tui, const char *url)
   1494  FUNC_ATTR_NONNULL_ARG(1)
   1495 {
   1496  if (url == NULL) {
   1497    return -1;
   1498  }
   1499 
   1500  MHPutStatus status;
   1501  uint32_t k = set_put_idx(cstr_t, &urls, url, &status);
   1502  if (status != kMHExisting) {
   1503    urls.keys[k] = xstrdup(url);
   1504  }
   1505  return (int32_t)k;
   1506 }
   1507 
   1508 void tui_hl_attr_define(TUIData *tui, Integer id, HlAttrs attrs, HlAttrs cterm_attrs, Array info)
   1509 {
   1510  attrs.cterm_ae_attr = cterm_attrs.cterm_ae_attr;
   1511  attrs.cterm_fg_color = cterm_attrs.cterm_fg_color;
   1512  attrs.cterm_bg_color = cterm_attrs.cterm_bg_color;
   1513 
   1514  kv_a(tui->attrs, (size_t)id) = attrs;
   1515 }
   1516 
   1517 void tui_bell(TUIData *tui)
   1518 {
   1519  out(tui, S_LEN("\a"));
   1520 }
   1521 
   1522 void tui_visual_bell(TUIData *tui)
   1523 {
   1524  if (tui->screen_or_tmux) {
   1525    out(tui, S_LEN("\x1bg"));
   1526  } else {
   1527    out(tui, S_LEN("\x1b[?5h"));
   1528 
   1529    flush_buf(tui);
   1530    uv_sleep(100);  // typically either 100 or 200 in terminfo. 100 seems enough
   1531 
   1532    out(tui, S_LEN("\x1b[?5l"));
   1533  }
   1534  flush_buf(tui);
   1535 }
   1536 
   1537 void tui_default_colors_set(TUIData *tui, Integer rgb_fg, Integer rgb_bg, Integer rgb_sp,
   1538                            Integer cterm_fg, Integer cterm_bg)
   1539 {
   1540  tui->clear_attrs.rgb_fg_color = (RgbValue)rgb_fg;
   1541  tui->clear_attrs.rgb_bg_color = (RgbValue)rgb_bg;
   1542  tui->clear_attrs.rgb_sp_color = (RgbValue)rgb_sp;
   1543  tui->clear_attrs.cterm_fg_color = (int16_t)cterm_fg;
   1544  tui->clear_attrs.cterm_bg_color = (int16_t)cterm_bg;
   1545 
   1546  tui->print_attr_id = -1;
   1547  tui->set_default_colors = true;
   1548  invalidate(tui, 0, tui->grid.height, 0, tui->grid.width);
   1549 }
   1550 
   1551 /// Writes directly to the TTY, bypassing the buffer.
   1552 void tui_ui_send(TUIData *tui, String content)
   1553  FUNC_ATTR_NONNULL_ALL
   1554 {
   1555  uv_write_t req;
   1556  uv_buf_t buf = { .base = content.data, .len = UV_BUF_LEN(content.size) };
   1557  int ret = uv_write(&req, (uv_stream_t *)&tui->output_handle, &buf, 1, NULL);
   1558  if (ret) {
   1559    ELOG("uv_write failed: %s", uv_strerror(ret));
   1560  }
   1561  uv_run(&tui->write_loop, UV_RUN_DEFAULT);
   1562 }
   1563 
   1564 /// Flushes TUI grid state to a buffer (which is later flushed to the TTY by `flush_buf`).
   1565 ///
   1566 /// @see flush_buf
   1567 void tui_flush(TUIData *tui)
   1568 {
   1569  UGrid *grid = &tui->grid;
   1570 
   1571  size_t nrevents = loop_size(tui->loop);
   1572  if (nrevents > TOO_MANY_EVENTS) {
   1573    WLOG("TUI event-queue flooded (thread_events=%zu); purging", nrevents);
   1574    // Back-pressure: UI events may accumulate much faster than the terminal
   1575    // device can serve them. Even if SIGINT/CTRL-C is received, user must still
   1576    // wait for the TUI event-queue to drain, and if there are ~millions of
   1577    // events in the queue, it could take hours. Clearing the queue allows the
   1578    // UI to recover. #1234 #5396
   1579    loop_purge(tui->loop);
   1580    tui_busy_stop(tui);  // avoid hidden cursor
   1581  }
   1582 
   1583  while (kv_size(tui->invalid_regions)) {
   1584    Rect r = kv_pop(tui->invalid_regions);
   1585    assert(r.bot <= grid->height && r.right <= grid->width);
   1586 
   1587    for (int row = r.top; row < r.bot; row++) {
   1588      int clear_attr = grid->cells[row][r.right - 1].attr;
   1589      int clear_col;
   1590      for (clear_col = r.right; clear_col > 0; clear_col--) {
   1591        UCell *cell = &grid->cells[row][clear_col - 1];
   1592        if (!(cell->data == schar_from_ascii(' ')
   1593              && cell->attr == clear_attr)) {
   1594          break;
   1595        }
   1596      }
   1597 
   1598      UGRID_FOREACH_CELL(grid, row, r.left, clear_col, {
   1599        print_cell_at_pos(tui, row, curcol, cell,
   1600                          curcol < clear_col - 1 && (cell + 1)->data == NUL);
   1601      });
   1602      if (clear_col < r.right) {
   1603        clear_region(tui, row, row + 1, clear_col, r.right, clear_attr);
   1604      }
   1605    }
   1606  }
   1607 
   1608  cursor_goto(tui, tui->row, tui->col);
   1609 
   1610  flush_buf(tui);
   1611 }
   1612 
   1613 /// Dumps termcap info to the messages area, if 'verbose' >= 3.
   1614 static void show_verbose_terminfo(TUIData *tui)
   1615 {
   1616  MAXSIZE_TEMP_ARRAY(chunks, 3);
   1617  MAXSIZE_TEMP_ARRAY(title, 2);
   1618  ADD_C(title, CSTR_AS_OBJ("\n\n--- Terminal info --- {{{\n"));
   1619  ADD_C(title, CSTR_AS_OBJ("Title"));
   1620  ADD_C(chunks, ARRAY_OBJ(title));
   1621  MAXSIZE_TEMP_ARRAY(info, 1);
   1622  String str = terminfo_info_msg(&tui->ti, tui->term, tui->terminfo_found_in_db);
   1623  ADD_C(info, STRING_OBJ(str));
   1624  ADD_C(chunks, ARRAY_OBJ(info));
   1625  MAXSIZE_TEMP_ARRAY(end_fold, 2);
   1626  ADD_C(end_fold, CSTR_AS_OBJ("}}}\n"));
   1627  ADD_C(end_fold, CSTR_AS_OBJ("Title"));
   1628  ADD_C(chunks, ARRAY_OBJ(end_fold));
   1629 
   1630  MAXSIZE_TEMP_ARRAY(args, 3);
   1631  ADD_C(args, ARRAY_OBJ(chunks));
   1632  ADD_C(args, BOOLEAN_OBJ(true));  // history
   1633  MAXSIZE_TEMP_DICT(opts, 1);
   1634  PUT_C(opts, "verbose", BOOLEAN_OBJ(true));
   1635  ADD_C(args, DICT_OBJ(opts));
   1636  rpc_send_event(ui_client_channel_id, "nvim_echo", args);
   1637  xfree(str.data);
   1638 }
   1639 
   1640 void tui_suspend(TUIData *tui)
   1641 {
   1642 // on a non-UNIX system, this is a no-op
   1643 #ifdef UNIX
   1644  ui_client_detach();
   1645  tui->mouse_enabled_save = tui->mouse_enabled;
   1646  tui->input.callbacks.primary_device_attr = tui_suspend_cb;
   1647  terminfo_disable(tui);
   1648 #endif
   1649 }
   1650 
   1651 #ifdef UNIX
   1652 static void tui_suspend_cb(TUIData *tui)
   1653  FUNC_ATTR_NONNULL_ALL
   1654 {
   1655  tui_terminal_stop(tui);
   1656  stream_set_blocking(tui->input.in_fd, true);   // normalize stream (#2598)
   1657 
   1658  // Avoid os/signal.c SIGTSTP handler. ex_stop calls auto_writeall. #33258
   1659  kill(0, SIGSTOP);
   1660 
   1661  tui_terminal_start(tui);
   1662  tui_terminal_after_startup(tui);
   1663  if (tui->mouse_enabled_save) {
   1664    tui_mouse_on(tui);
   1665  }
   1666  stream_set_blocking(tui->input.in_fd, false);  // libuv expects this
   1667  ui_client_attach(tui->width, tui->height, tui->term, tui->rgb);
   1668 }
   1669 #endif
   1670 
   1671 void tui_set_title(TUIData *tui, String title)
   1672 {
   1673  if (!tui->can_set_title) {
   1674    return;
   1675  }
   1676 
   1677  bool too_long = (title.size > 4096);  // should be enough
   1678  if (too_long) {
   1679    ELOG("set_title: title string too long!");
   1680  }
   1681  if (title.size > 0 && !too_long) {
   1682    if (!tui->title_enabled) {
   1683      // Save title/icon to the "stack". #4063
   1684      out(tui, S_LEN("\x1b[22;0t"));
   1685      tui->title_enabled = true;
   1686    }
   1687 
   1688    if ((sizeof tui->buf - tui->bufpos) < title.size + 2 * TERMINFO_SEQ_LIMIT) {
   1689      // The sequence to set title, is usually an OSC sequence that cannot be cut in half.
   1690      // flush buffer prior to printing to avoid this
   1691      flush_buf(tui);
   1692    }
   1693    terminfo_out(tui, kTerm_to_status_line);
   1694    out(tui, title.data, title.size);
   1695    terminfo_out(tui, kTerm_from_status_line);
   1696  } else if (tui->title_enabled) {
   1697    // Restore title/icon from the "stack". #4063
   1698    out(tui, S_LEN("\x1b[23;0t"));
   1699    tui->title_enabled = false;
   1700  }
   1701 }
   1702 
   1703 void tui_set_icon(TUIData *tui, String icon)
   1704 {
   1705 }
   1706 
   1707 void tui_screenshot(TUIData *tui, String path)
   1708 {
   1709  FILE *f = fopen(path.data, "w");
   1710  if (f == NULL) {
   1711    return;
   1712  }
   1713 
   1714  UGrid *grid = &tui->grid;
   1715  flush_buf(tui);
   1716  grid->row = 0;
   1717  grid->col = 0;
   1718 
   1719  tui->screenshot = f;
   1720  fprintf(f, "%d,%d\n", grid->height, grid->width);
   1721  terminfo_out(tui, kTerm_clear_screen);
   1722  for (int i = 0; i < grid->height; i++) {
   1723    cursor_goto(tui, i, 0);
   1724    for (int j = 0; j < grid->width; j++) {
   1725      UCell cell = grid->cells[i][j];
   1726      char buf[MAX_SCHAR_SIZE];
   1727      schar_get(buf, cell.data);
   1728      print_cell(tui, buf, cell.attr);
   1729    }
   1730  }
   1731  flush_buf(tui);
   1732  tui->screenshot = NULL;
   1733 
   1734  fclose(f);
   1735 }
   1736 
   1737 void tui_option_set(TUIData *tui, String name, Object value)
   1738 {
   1739  if (strequal(name.data, "mousemoveevent")) {
   1740    if (tui->mouse_move_enabled != value.data.boolean) {
   1741      if (tui->mouse_enabled) {
   1742        tui_mouse_off(tui);
   1743        tui->mouse_move_enabled = value.data.boolean;
   1744        tui_mouse_on(tui);
   1745      } else {
   1746        tui->mouse_move_enabled = value.data.boolean;
   1747      }
   1748    }
   1749  } else if (strequal(name.data, "termguicolors")) {
   1750    tui->rgb = value.data.boolean;
   1751    tui->print_attr_id = -1;
   1752    invalidate(tui, 0, tui->grid.height, 0, tui->grid.width);
   1753 
   1754    if (ui_client_channel_id) {
   1755      MAXSIZE_TEMP_ARRAY(args, 2);
   1756      ADD_C(args, CSTR_AS_OBJ("rgb"));
   1757      ADD_C(args, BOOLEAN_OBJ(value.data.boolean));
   1758      rpc_send_event(ui_client_channel_id, "nvim_ui_set_option", args);
   1759    }
   1760  } else if (strequal(name.data, "ttimeout")) {
   1761    tui->input.ttimeout = value.data.boolean;
   1762  } else if (strequal(name.data, "ttimeoutlen")) {
   1763    tui->input.ttimeoutlen = (OptInt)value.data.integer;
   1764  } else if (strequal(name.data, "verbose")) {
   1765    tui->verbose = value.data.integer;
   1766  } else if (strequal(name.data, "termsync")) {
   1767    tui->sync_output = value.data.boolean;
   1768  }
   1769 }
   1770 
   1771 void tui_chdir(TUIData *tui, String path)
   1772 {
   1773  int err = uv_chdir(path.data);
   1774  if (err != 0) {
   1775    ELOG("Failed to chdir to %s: %s", path.data, uv_strerror(err));
   1776  }
   1777 }
   1778 
   1779 void tui_raw_line(TUIData *tui, Integer g, Integer linerow, Integer startcol, Integer endcol,
   1780                  Integer clearcol, Integer clearattr, LineFlags flags, const schar_T *chunk,
   1781                  const sattr_T *attrs)
   1782 {
   1783  UGrid *grid = &tui->grid;
   1784  for (Integer c = startcol; c < endcol; c++) {
   1785    grid->cells[linerow][c].data = chunk[c - startcol];
   1786    assert((size_t)attrs[c - startcol] < kv_size(tui->attrs));
   1787    grid->cells[linerow][c].attr = attrs[c - startcol];
   1788  }
   1789  UGRID_FOREACH_CELL(grid, (int)linerow, (int)startcol, (int)endcol, {
   1790    print_cell_at_pos(tui, (int)linerow, curcol, cell,
   1791                      curcol < endcol - 1 && (cell + 1)->data == NUL);
   1792  });
   1793 
   1794  if (clearcol > endcol) {
   1795    ugrid_clear_chunk(grid, (int)linerow, (int)endcol, (int)clearcol,
   1796                      (sattr_T)clearattr);
   1797    clear_region(tui, (int)linerow, (int)linerow + 1, (int)endcol, (int)clearcol,
   1798                 (int)clearattr);
   1799  }
   1800 
   1801  if (flags & kLineFlagWrap && tui->width == grid->width
   1802      && linerow + 1 < grid->height) {
   1803    // Only do line wrapping if the grid width is equal to the terminal
   1804    // width and the line continuation is within the grid.
   1805 
   1806    if (endcol != grid->width) {
   1807      // Print the last char of the row, if we haven't already done so.
   1808      int size = grid->cells[linerow][grid->width - 1].data == NUL ? 2 : 1;
   1809      print_cell_at_pos(tui, (int)linerow, grid->width - size,
   1810                        &grid->cells[linerow][grid->width - size], size == 2);
   1811    }
   1812 
   1813    // Wrap the cursor over to the next line. The next line will be
   1814    // printed immediately without an intervening newline.
   1815    final_column_wrap(tui);
   1816  }
   1817 }
   1818 
   1819 static void invalidate(TUIData *tui, int top, int bot, int left, int right)
   1820 {
   1821  Rect *intersects = NULL;
   1822 
   1823  for (size_t i = 0; i < kv_size(tui->invalid_regions); i++) {
   1824    Rect *r = &kv_A(tui->invalid_regions, i);
   1825    // adjacent regions are treated as overlapping
   1826    if (!(top > r->bot || bot < r->top)
   1827        && !(left > r->right || right < r->left)) {
   1828      intersects = r;
   1829      break;
   1830    }
   1831  }
   1832 
   1833  if (intersects) {
   1834    // If top/bot/left/right intersects with a invalid rect, we replace it
   1835    // by the union
   1836    intersects->top = MIN(top, intersects->top);
   1837    intersects->bot = MAX(bot, intersects->bot);
   1838    intersects->left = MIN(left, intersects->left);
   1839    intersects->right = MAX(right, intersects->right);
   1840  } else {
   1841    // Else just add a new entry;
   1842    kv_push(tui->invalid_regions, ((Rect) { top, bot, left, right }));
   1843  }
   1844 }
   1845 
   1846 void tui_set_size(TUIData *tui, int width, int height)
   1847  FUNC_ATTR_NONNULL_ALL
   1848 {
   1849  tui->pending_resize_events++;
   1850  tui->width = width;
   1851  tui->height = height;
   1852  ui_client_set_size(width, height);
   1853 }
   1854 
   1855 /// Tries to get the user's wanted dimensions (columns and rows) for the entire
   1856 /// application (i.e., the host terminal).
   1857 void tui_guess_size(TUIData *tui)
   1858 {
   1859  int width = 0;
   1860  int height = 0;
   1861  char *lines = NULL;
   1862  char *columns = NULL;
   1863 
   1864  // 1 - try from a system call (ioctl/TIOCGWINSZ on unix)
   1865  if (tui->out_isatty
   1866      && !uv_tty_get_winsize(&tui->output_handle.tty, &width, &height)) {
   1867    goto end;
   1868  }
   1869 
   1870  // 2 - use $LINES/$COLUMNS if available
   1871  const char *val;
   1872  int advance;
   1873  if ((val = os_getenv_noalloc("LINES"))
   1874      && sscanf(val, "%d%n", &height, &advance) != EOF && advance
   1875      && (val = os_getenv_noalloc("COLUMNS"))
   1876      && sscanf(val, "%d%n", &width, &advance) != EOF && advance) {
   1877    goto end;
   1878  }
   1879 
   1880  // 3 - read from terminfo if available
   1881  height = tui->ti.lines;
   1882  width = tui->ti.columns;
   1883 
   1884  end:
   1885  if (width <= 0 || height <= 0) {
   1886    // use the defaults
   1887    width = DFLT_COLS;
   1888    height = DFLT_ROWS;
   1889  }
   1890 
   1891  tui_set_size(tui, width, height);
   1892 
   1893  xfree(lines);
   1894  xfree(columns);
   1895 }
   1896 
   1897 static void out(TUIData *tui, const char *str, size_t len)
   1898 {
   1899  size_t available = sizeof(tui->buf) - tui->bufpos;
   1900 
   1901  if (len > available) {
   1902    flush_buf(tui);
   1903    if (len > sizeof(tui->buf)) {
   1904      // Don't use tui->buf[] when the string to output is too long. #30794
   1905      tui->buf_to_flush = (char *)str;
   1906      tui->bufpos = len;
   1907      flush_buf(tui);
   1908      return;
   1909    }
   1910  }
   1911 
   1912  memcpy(tui->buf + tui->bufpos, str, len);
   1913  tui->bufpos += len;
   1914 }
   1915 
   1916 static void out_len(TUIData *tui, const char *str)
   1917 {
   1918  if (str != NULL) {
   1919    out(tui, str, strlen(str));
   1920  }
   1921 }
   1922 
   1923 /// drops the entire message if it doesn't fit in "limit"
   1924 void out_printf(TUIData *tui, size_t limit, const char *fmt, ...)
   1925  FUNC_ATTR_PRINTF(3, 4)
   1926 {
   1927  assert(limit <= sizeof(tui->buf));
   1928  size_t available = sizeof(tui->buf) - tui->bufpos;
   1929  if (available < limit) {
   1930    flush_buf(tui);
   1931  }
   1932 
   1933  va_list ap;
   1934  va_start(ap, fmt);
   1935  int printed = vsnprintf(tui->buf + tui->bufpos, limit, fmt, ap);
   1936  va_end(ap);
   1937 
   1938  if (printed > 0) {
   1939    tui->bufpos += (size_t)printed;
   1940  }
   1941 }
   1942 
   1943 static void terminfo_out(TUIData *tui, TerminfoDef what)
   1944 {
   1945  TPVAR null_params[9] = { 0 };
   1946  terminfo_print(tui, what, null_params);
   1947 }
   1948 
   1949 static void terminfo_print_num(TUIData *tui, TerminfoDef what, int num1, int num2, int num3)
   1950 {
   1951  TPVAR params[9] = { 0 };
   1952  params[0].num = num1;
   1953  params[1].num = num2;
   1954  params[2].num = num3;
   1955  terminfo_print(tui, what, params);
   1956 }
   1957 
   1958 static void terminfo_print(TUIData *tui, TerminfoDef what, TPVAR *params)
   1959 {
   1960  if (what >= kTermCount) {
   1961    abort();
   1962  }
   1963 
   1964  const char *str = tui->ti.defs[what];
   1965  if (str == NULL || *str == NUL) {
   1966    return;
   1967  }
   1968 
   1969  if (sizeof(tui->buf) - tui->bufpos > TERMINFO_SEQ_LIMIT) {
   1970    TPVAR copy_params[9];
   1971    memcpy(copy_params, params, sizeof copy_params);
   1972    size_t len = terminfo_fmt(tui->buf + tui->bufpos, tui->buf + sizeof(tui->buf), str,
   1973                              copy_params);
   1974    if (len > 0) {
   1975      tui->bufpos += len;
   1976      return;
   1977    }
   1978  }
   1979 
   1980  // try again with fresh buffer
   1981  flush_buf(tui);
   1982  size_t len = terminfo_fmt(tui->buf + tui->bufpos, tui->buf + sizeof(tui->buf), str, params);
   1983  if (len > 0) {
   1984    tui->bufpos += len;
   1985  }
   1986 }
   1987 static void terminfo_set_if_empty(TUIData *tui, TerminfoDef str, const char *val)
   1988 {
   1989  if (!tui->ti.defs[str]) {
   1990    tui->ti.defs[str] = val;
   1991  }
   1992 }
   1993 
   1994 static void terminfo_set_str(TUIData *tui, TerminfoDef str, const char *val)
   1995 {
   1996  tui->ti.defs[str] = val;
   1997 }
   1998 
   1999 /// Determine if the terminal supports truecolor or not.
   2000 ///
   2001 /// note: We get another chance at detecting these in the nvim server process, see
   2002 /// the use of vim.termcap in runtime/lua/vim/_core/defaults.lua
   2003 ///
   2004 /// If terminfo contains Tc, RGB, or both setrgbf and setrgbb capabilities, return true.
   2005 static bool term_has_truecolor(TUIData *tui, const char *colorterm)
   2006 {
   2007  // Check $COLORTERM
   2008  if (strequal(colorterm, "truecolor") || strequal(colorterm, "24bit")) {
   2009    return true;
   2010  }
   2011 
   2012  if (tui->ti.has_Tc_or_RGB) {
   2013    // terminfo had one of "Tc" or "RGB" extended boolean capabilities
   2014    return true;
   2015  }
   2016 
   2017  // Check for setrgbf and setrgbb
   2018  bool setrgbf = tui->ti.defs[kTerm_set_rgb_foreground];
   2019  bool setrgbb = tui->ti.defs[kTerm_set_rgb_background];
   2020 
   2021  return setrgbf && setrgbb;
   2022 }
   2023 
   2024 /// Patches the terminfo records after loading from system or built-in db.
   2025 /// Several entries in terminfo are known to be deficient or outright wrong;
   2026 /// and several terminal emulators falsely announce incorrect terminal types.
   2027 static void patch_terminfo_bugs(TUIData *tui, const char *term, const char *colorterm,
   2028                                int vte_version, int konsolev, bool iterm_env, bool nsterm)
   2029 {
   2030  char *xterm_version = os_getenv("XTERM_VERSION");
   2031  bool xterm = terminfo_is_term_family(term, "xterm")
   2032               // Treat Terminal.app as generic xterm-like, for now.
   2033               || nsterm;
   2034  bool hterm = terminfo_is_term_family(term, "hterm");
   2035  bool kitty = terminfo_is_term_family(term, "xterm-kitty");
   2036  bool linuxvt = terminfo_is_term_family(term, "linux");
   2037  bool bsdvt = terminfo_is_bsd_console(term);
   2038  bool rxvt = terminfo_is_term_family(term, "rxvt");
   2039  bool teraterm = terminfo_is_term_family(term, "teraterm");
   2040  bool putty = terminfo_is_term_family(term, "putty");
   2041  bool screen = terminfo_is_term_family(term, "screen");
   2042  bool tmux = terminfo_is_term_family(term, "tmux") || os_env_exists("TMUX", true);
   2043  bool st = terminfo_is_term_family(term, "st");
   2044  bool gnome = terminfo_is_term_family(term, "gnome")
   2045               || terminfo_is_term_family(term, "vte");
   2046  bool iterm = terminfo_is_term_family(term, "iterm")
   2047               || terminfo_is_term_family(term, "iterm2")
   2048               || terminfo_is_term_family(term, "iTerm.app")
   2049               || terminfo_is_term_family(term, "iTerm2.app");
   2050  bool alacritty = terminfo_is_term_family(term, "alacritty");
   2051  bool foot = terminfo_is_term_family(term, "foot");
   2052  // None of the following work over SSH; see :help TERM .
   2053  bool iterm_pretending_xterm = xterm && iterm_env;
   2054  bool gnome_pretending_xterm = xterm && colorterm
   2055                                && strstr(colorterm, "gnome-terminal");
   2056  bool mate_pretending_xterm = xterm && colorterm
   2057                               && strstr(colorterm, "mate-terminal");
   2058  bool true_xterm = xterm && !!xterm_version && !bsdvt;
   2059  bool cygwin = terminfo_is_term_family(term, "cygwin");
   2060 
   2061  const char *fix_normal = tui->ti.defs[kTerm_cursor_normal];
   2062  if (fix_normal) {
   2063    if (STARTS_WITH(fix_normal, "\x1b[?12l")) {
   2064      // terminfo typically includes DECRST 12 as part of setting up the
   2065      // normal cursor, which interferes with the user's control via
   2066      // set_cursor_style.  When DECRST 12 is present, skip over it, but honor
   2067      // the rest of the cnorm setting.
   2068      fix_normal += sizeof "\x1b[?12l" - 1;
   2069      terminfo_set_str(tui, kTerm_cursor_normal, fix_normal);
   2070    }
   2071    if (linuxvt
   2072        && strlen(fix_normal) >= (sizeof LINUXSET0C - 1)
   2073        && !memcmp(strchr(fix_normal, 0) - (sizeof LINUXSET0C - 1),
   2074                   LINUXSET0C, sizeof LINUXSET0C - 1)) {
   2075      // The Linux terminfo entry similarly includes a Linux-idiosyncractic
   2076      // cursor shape reset in cnorm, which similarly interferes with
   2077      // set_cursor_style.
   2078      char *new_normal = arena_memdupz(&tui->ti_arena, fix_normal,
   2079                                       strlen(fix_normal) - (sizeof LINUXSET0C - 1));
   2080      terminfo_set_str(tui, kTerm_cursor_normal, new_normal);
   2081    }
   2082  }
   2083  const char *fix_invisible = tui->ti.defs[kTerm_cursor_invisible];
   2084  if (fix_invisible) {
   2085    if (linuxvt
   2086        && strlen(fix_invisible) >= (sizeof LINUXSET1C - 1)
   2087        && !memcmp(strchr(fix_invisible, 0) - (sizeof LINUXSET1C - 1),
   2088                   LINUXSET1C, sizeof LINUXSET1C - 1)) {
   2089      // The Linux terminfo entry similarly includes a Linux-idiosyncractic
   2090      // cursor shape reset in cinvis, which similarly interferes with
   2091      // set_cursor_style.
   2092      char *new_invisible = arena_memdupz(&tui->ti_arena, fix_invisible,
   2093                                          strlen(fix_invisible) - (sizeof LINUXSET1C - 1));
   2094      terminfo_set_str(tui, kTerm_cursor_invisible, new_invisible);
   2095    }
   2096  }
   2097 
   2098  if (tmux || screen || kitty) {
   2099    // Disable BCE in some cases we know it is not working. #8806
   2100    tui->ti.bce = false;
   2101  }
   2102 
   2103  if (xterm || hterm) {
   2104    // Termit, LXTerminal, GTKTerm2, GNOME Terminal, MATE Terminal, roxterm,
   2105    // and EvilVTE falsely claim to be xterm and do not support important xterm
   2106    // control sequences that we use.  In an ideal world, these would have
   2107    // their own terminal types and terminfo entries, like PuTTY does, and not
   2108    // claim to be xterm.  Or they would mimic xterm properly enough to be
   2109    // treatable as xterm.
   2110 
   2111    // 2017-04 terminfo.src lacks these.  Xterm-likes have them.
   2112    if (!hterm) {
   2113      // hterm doesn't have a status line.
   2114      terminfo_set_if_empty(tui, kTerm_to_status_line, "\x1b]0;");
   2115      terminfo_set_if_empty(tui, kTerm_from_status_line, "\x07");
   2116    }
   2117    terminfo_set_if_empty(tui, kTerm_enter_italics_mode, "\x1b[3m");
   2118 
   2119    // 2025: This are not supported by all xterm-alikes, but it is only
   2120    // used when kTermModeLeftAndRightMargins is detected
   2121    terminfo_set_if_empty(tui, kTerm_set_lr_margin, "\x1b[%i%p1%d;%p2%ds");
   2122 
   2123 #ifdef MSWIN
   2124    // XXX: workaround libuv implicit LF => CRLF conversion. #10558
   2125    terminfo_set_str(tui, kTerm_cursor_down, "\x1b[B");
   2126 #endif
   2127  } else if (rxvt) {
   2128    // 2017-04 terminfo.src lacks these.  Unicode rxvt has them.
   2129    terminfo_set_if_empty(tui, kTerm_enter_italics_mode, "\x1b[3m");
   2130    terminfo_set_if_empty(tui, kTerm_to_status_line, "\x1b]2");
   2131    terminfo_set_if_empty(tui, kTerm_from_status_line, "\x07");
   2132    // 2017-04 terminfo.src has older control sequences.
   2133    terminfo_set_str(tui, kTerm_enter_ca_mode, "\x1b[?1049h");
   2134    terminfo_set_str(tui, kTerm_exit_ca_mode, "\x1b[?1049l");
   2135  } else if (screen) {
   2136    // per the screen manual; 2017-04 terminfo.src lacks these.
   2137    terminfo_set_if_empty(tui, kTerm_to_status_line, "\x1b_");
   2138    terminfo_set_if_empty(tui, kTerm_from_status_line, "\x1b\\");
   2139  } else if (tmux) {
   2140    terminfo_set_if_empty(tui, kTerm_to_status_line, "\x1b_");
   2141    terminfo_set_if_empty(tui, kTerm_from_status_line, "\x1b\\");
   2142    terminfo_set_if_empty(tui, kTerm_enter_italics_mode, "\x1b[3m");
   2143  } else if (terminfo_is_term_family(term, "interix")) {
   2144    // 2017-04 terminfo.src lacks this.
   2145    terminfo_set_if_empty(tui, kTerm_carriage_return, "\x0d");
   2146  } else if (linuxvt) {
   2147    terminfo_set_if_empty(tui, kTerm_parm_up_cursor, "\x1b[%p1%dA");
   2148    terminfo_set_if_empty(tui, kTerm_parm_down_cursor, "\x1b[%p1%dB");
   2149    terminfo_set_if_empty(tui, kTerm_parm_right_cursor, "\x1b[%p1%dC");
   2150    terminfo_set_if_empty(tui, kTerm_parm_left_cursor, "\x1b[%p1%dD");
   2151  } else if (putty) {
   2152    // No bugs in the vanilla terminfo for our purposes.
   2153  } else if (iterm) {
   2154    // 2017-04 terminfo.src has older control sequences.
   2155    terminfo_set_str(tui, kTerm_enter_ca_mode, "\x1b[?1049h");
   2156    terminfo_set_str(tui, kTerm_exit_ca_mode, "\x1b[?1049l");
   2157    // 2017-04 terminfo.src lacks these.
   2158    terminfo_set_if_empty(tui, kTerm_enter_italics_mode, "\x1b[3m");
   2159  } else if (st) {
   2160    // No bugs in the vanilla terminfo for our purposes.
   2161  }
   2162 
   2163 // At this time (2017-07-12) it seems like all terminals that support 256
   2164 // color codes can use semicolons in the terminal code and be fine.
   2165 // However, this is not correct according to the spec. So to reward those
   2166 // terminals that also support colons, we output the code that way on these
   2167 // specific ones.
   2168 
   2169 // using colons like ISO 8613-6:1994/ITU T.416:1993 says.
   2170 #define XTERM_SETAF_256_COLON \
   2171  "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38:5:%p1%d%;m"
   2172 #define XTERM_SETAB_256_COLON \
   2173  "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48:5:%p1%d%;m"
   2174 
   2175 #define XTERM_SETAF_256 \
   2176  "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"
   2177 #define XTERM_SETAB_256 \
   2178  "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m"
   2179 #define XTERM_SETAF_16 \
   2180  "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e39%;m"
   2181 #define XTERM_SETAB_16 \
   2182  "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e39%;m"
   2183 
   2184  // Terminals with 256-colour SGR support despite what terminfo says.
   2185  if (tui->ti.max_colors < 256) {
   2186    // See http://fedoraproject.org/wiki/Features/256_Color_Terminals
   2187    if (true_xterm || iterm || iterm_pretending_xterm) {
   2188      tui->ti.max_colors = 256;
   2189      terminfo_set_str(tui, kTerm_set_a_foreground, XTERM_SETAF_256_COLON);
   2190      terminfo_set_str(tui, kTerm_set_a_background, XTERM_SETAB_256_COLON);
   2191    } else if (konsolev || xterm || gnome || rxvt || st || putty
   2192               || linuxvt  // Linux 4.8+ supports 256-colour SGR.
   2193               || mate_pretending_xterm || gnome_pretending_xterm
   2194               || tmux
   2195               || (colorterm && strstr(colorterm, "256"))
   2196               || (term && strstr(term, "256"))) {
   2197      tui->ti.max_colors = 256;
   2198      terminfo_set_str(tui, kTerm_set_a_foreground, XTERM_SETAF_256);
   2199      terminfo_set_str(tui, kTerm_set_a_background, XTERM_SETAB_256);
   2200    }
   2201  }
   2202  // Terminals with 16-colour SGR support despite what terminfo says.
   2203  if (tui->ti.max_colors < 16) {
   2204    if (colorterm) {
   2205      tui->ti.max_colors = 16;
   2206      terminfo_set_if_empty(tui, kTerm_set_a_foreground, XTERM_SETAF_16);
   2207      terminfo_set_if_empty(tui, kTerm_set_a_background, XTERM_SETAB_16);
   2208    }
   2209  }
   2210 
   2211  // Blacklist of terminals that cannot be trusted to report DECSCUSR support.
   2212  if ((st || (vte_version != 0 && vte_version < 3900) || konsolev)) {
   2213    tui->ti.defs[kTerm_reset_cursor_style] = NULL;
   2214  }
   2215 
   2216  // Dickey ncurses terminfo includes Ss/Se capabilities since 2011-07-14. So
   2217  // adding them to terminal types, that have such control sequences but lack
   2218  // the correct terminfo entries, is a fixup, not an augmentation.
   2219  if (tui->ti.defs[kTerm_set_cursor_style] == NULL) {
   2220    // DECSCUSR (cursor shape) is widely supported.
   2221    // https://github.com/gnachman/iTerm2/pull/92
   2222    if ((!bsdvt && (!konsolev || konsolev >= 180770))
   2223        && ((xterm && !vte_version)  // anything claiming xterm compat
   2224            // per MinTTY 0.4.3-1 release notes from 2009
   2225            || putty
   2226            // per https://chromium.googlesource.com/apps/libapps/+/a5fb83c190aa9d74f4a9bca233dac6be2664e9e9/hterm/doc/ControlSequences.md
   2227            || hterm
   2228            // per https://bugzilla.gnome.org/show_bug.cgi?id=720821
   2229            || (vte_version >= 3900)
   2230            || (konsolev >= 180770)  // #9364
   2231            || tmux       // per tmux manual page
   2232            // https://lists.gnu.org/archive/html/screen-devel/2013-03/msg00000.html
   2233            || screen
   2234            || st         // #7641
   2235            || rxvt       // per command.C
   2236            // per analysis of VT100Terminal.m
   2237            || iterm || iterm_pretending_xterm
   2238            || teraterm   // per TeraTerm "Supported Control Functions" doco
   2239            || alacritty  // https://github.com/jwilm/alacritty/pull/608
   2240            || cygwin
   2241            || foot
   2242            // Some linux-type terminals implement the xterm extension.
   2243            // Example: console-terminal-emulator from the nosh toolset.
   2244            || (linuxvt
   2245                && (xterm_version || (vte_version > 0) || colorterm)))) {
   2246      terminfo_set_str(tui, kTerm_set_cursor_style, "\x1b[%p1%d q");
   2247      terminfo_set_str(tui, kTerm_reset_cursor_style, "\x1b[ q");
   2248    } else if (linuxvt) {
   2249      // Linux uses an idiosyncratic escape code to set the cursor shape and
   2250      // does not support DECSCUSR.
   2251      // See http://linuxgazette.net/137/anonymous.html for more info
   2252      terminfo_set_str(tui, kTerm_set_cursor_style,
   2253                       "\x1b[?"
   2254                       "%?"
   2255                       // The parameter passed to Ss is the DECSCUSR parameter, so the
   2256                       // terminal capability has to translate into the Linux idiosyncratic
   2257                       // parameter.
   2258                       //
   2259                       // linuxvt only supports block and underline. It is also only
   2260                       // possible to have a steady block (no steady underline)
   2261                       "%p1%{2}%<" "%t%{8}"       // blink block
   2262                       "%e%p1%{2}%=" "%t%{112}"   // steady block
   2263                       "%e%p1%{3}%=" "%t%{4}"     // blink underline (set to half block)
   2264                       "%e%p1%{4}%=" "%t%{4}"     // steady underline
   2265                       "%e%p1%{5}%=" "%t%{2}"     // blink bar (set to underline)
   2266                       "%e%p1%{6}%=" "%t%{2}"     // steady bar
   2267                       "%e%{0}"                   // anything else
   2268                       "%;" "%dc");
   2269      terminfo_set_str(tui, kTerm_reset_cursor_style, "\x1b[?c");
   2270    } else if (konsolev > 0 && konsolev < 180770) {
   2271      // Konsole before version 18.07.70: set up a nonce profile. This has
   2272      // side effects on temporary font resizing. #6798
   2273      terminfo_set_str(tui, kTerm_set_cursor_style,
   2274                       TMUX_WRAP(tmux,
   2275                                 "\x1b]50;CursorShape=%?"
   2276                                 "%p1%{3}%<" "%t%{0}"    // block
   2277                                 "%e%p1%{5}%<" "%t%{2}"  // underline
   2278                                 "%e%{1}"                // everything else is bar
   2279                                 "%;%d;BlinkingCursorEnabled=%?"
   2280                                 "%p1%{1}%<" "%t%{1}"  // Fortunately if we exclude zero as special,
   2281                                 "%e%p1%{1}%&"  // in all other cases we can treat bit #0 as a flag.
   2282                                 "%;%d\x07"));
   2283      terminfo_set_str(tui, kTerm_reset_cursor_style, "\x1b]50;\x07");
   2284    } else {
   2285      tui->ti.defs[kTerm_reset_cursor_style] = NULL;
   2286    }
   2287  }
   2288 
   2289  xfree(xterm_version);
   2290 }
   2291 
   2292 /// This adds stuff that is not in standard terminfo.
   2293 static void augment_terminfo(TUIData *tui, const char *term, int vte_version, int konsolev,
   2294                             const char *weztermv, bool iterm_env, bool nsterm)
   2295 {
   2296  char *xterm_version = os_getenv("XTERM_VERSION");
   2297  bool xterm = terminfo_is_term_family(term, "xterm")
   2298               // Treat Terminal.app as generic xterm-like, for now.
   2299               || nsterm;
   2300  bool hterm = terminfo_is_term_family(term, "hterm");
   2301  bool bsdvt = terminfo_is_bsd_console(term);
   2302  bool dtterm = terminfo_is_term_family(term, "dtterm");
   2303  bool rxvt = terminfo_is_term_family(term, "rxvt");
   2304  bool teraterm = terminfo_is_term_family(term, "teraterm");
   2305  bool putty = terminfo_is_term_family(term, "putty");
   2306  bool screen = terminfo_is_term_family(term, "screen");
   2307  bool tmux = terminfo_is_term_family(term, "tmux") || os_env_exists("TMUX", true);
   2308  bool st = terminfo_is_term_family(term, "st");
   2309  bool iterm = terminfo_is_term_family(term, "iterm")
   2310               || terminfo_is_term_family(term, "iterm2")
   2311               || terminfo_is_term_family(term, "iTerm.app")
   2312               || terminfo_is_term_family(term, "iTerm2.app");
   2313  bool alacritty = terminfo_is_term_family(term, "alacritty");
   2314  bool kitty = terminfo_is_term_family(term, "xterm-kitty");
   2315  // None of the following work over SSH; see :help TERM .
   2316  bool iterm_pretending_xterm = xterm && iterm_env;
   2317 
   2318  bool true_xterm = xterm && !!xterm_version && !bsdvt;
   2319 
   2320  // Only define this capability for terminal types that we know understand it.
   2321  if (dtterm         // originated this extension
   2322      || xterm       // per xterm ctlseqs doco
   2323      || konsolev    // per commentary in VT102Emulation.cpp
   2324      || teraterm    // per TeraTerm "Supported Control Functions" doco
   2325      || rxvt) {     // per command.C
   2326    tui->can_resize_screen = true;
   2327  }
   2328 
   2329  if (putty || xterm || hterm || rxvt) {
   2330    tui->terminfo_ext.reset_scroll_region = "\x1b[r";
   2331  }
   2332 
   2333  // It should be pretty safe to always enable this, as terminals will ignore
   2334  // unrecognised SGR numbers.
   2335  tui->terminfo_ext.enter_altfont_mode = "\x1b[11m";
   2336 
   2337  // Dickey ncurses terminfo does not include the setrgbf and setrgbb
   2338  // capabilities, proposed by Rüdiger Sonderfeld on 2013-10-15.  Adding
   2339  // them here when terminfo lacks them is an augmentation, not a fixup.
   2340  // https://github.com/termstandard/colors
   2341 
   2342  // At this time (2017-07-12) it seems like all terminals that support rgb
   2343  // color codes can use semicolons in the terminal code and be fine.
   2344  // However, this is not correct according to the spec. So to reward those
   2345  // terminals that also support colons, we output the code that way on these
   2346  // specific ones.
   2347 
   2348  // can use colons like ISO 8613-6:1994/ITU T.416:1993 says.
   2349  bool has_colon_rgb = !tmux && !screen
   2350                       && !vte_version  // VTE colon-support has a big memory leak. #7573
   2351                       && (iterm || iterm_pretending_xterm  // per VT100Terminal.m
   2352                           // per http://invisible-island.net/xterm/xterm.log.html#xterm_282
   2353                           || true_xterm);
   2354 
   2355  if (tui->ti.defs[kTerm_set_rgb_foreground] == NULL) {
   2356    if (has_colon_rgb) {
   2357      tui->ti.defs[kTerm_set_rgb_foreground] = "\x1b[38:2:%p1%d:%p2%d:%p3%dm";
   2358    } else {
   2359      tui->ti.defs[kTerm_set_rgb_foreground] = "\x1b[38;2;%p1%d;%p2%d;%p3%dm";
   2360    }
   2361  }
   2362  if (tui->ti.defs[kTerm_set_rgb_background] == NULL) {
   2363    if (has_colon_rgb) {
   2364      tui->ti.defs[kTerm_set_rgb_background] = "\x1b[48:2:%p1%d:%p2%d:%p3%dm";
   2365    } else {
   2366      tui->ti.defs[kTerm_set_rgb_background] = "\x1b[48;2;%p1%d;%p2%d;%p3%dm";
   2367    }
   2368  }
   2369 
   2370  if (tui->ti.defs[kTerm_set_cursor_color] == NULL) {
   2371    if (iterm || iterm_pretending_xterm) {
   2372      // FIXME: Bypassing tmux like this affects the cursor colour globally, in
   2373      // all panes, which is not particularly desirable.  A better approach
   2374      // would use a tmux control sequence and an extra if(screen) test.
   2375      tui->ti.defs[kTerm_set_cursor_color] = TMUX_WRAP(tmux, "\033]Pl%p1%06x\033\\");
   2376    } else if ((xterm || hterm || rxvt || tmux || alacritty || st)
   2377               && (vte_version == 0 || vte_version >= 3900)) {
   2378      // Supported in urxvt, newer VTE.
   2379      // Supported in st, but currently missing in ncurses definitions. #32217
   2380      tui->ti.defs[kTerm_set_cursor_color] = "\033]12;%p1%s\007";
   2381    }
   2382  }
   2383  if (tui->ti.defs[kTerm_set_cursor_color] != NULL) {
   2384    // Some terminals supporting cursor color changing specify their Cs
   2385    // capability to take a string parameter. Others take a numeric parameter.
   2386    // If and only if the format string contains `%s` we assume a string
   2387    // parameter. #20628
   2388    tui->set_cursor_color_as_str = strstr(tui->ti.defs[kTerm_set_cursor_color], "%s") != NULL;
   2389 
   2390    terminfo_set_if_empty(tui, kTerm_reset_cursor_color, "\x1b]112\x07");
   2391  }
   2392 
   2393  if (tui->ti.defs[kTerm_to_status_line] != NULL && tui->ti.defs[kTerm_from_status_line] != NULL) {
   2394    tui->can_set_title = true;
   2395  }
   2396 
   2397  // For urxvt send BOTH xterm and old urxvt sequences. #8695
   2398  tui->terminfo_ext.enable_focus_reporting =
   2399    rxvt
   2400    ? "\x1b[?1004h\x1b]777;focus;on\x7"
   2401    : "\x1b[?1004h";
   2402  tui->terminfo_ext.disable_focus_reporting =
   2403    rxvt ? "\x1b[?1004l\x1b]777;focus;off\x7" : "\x1b[?1004l";
   2404 
   2405  // Extended underline.
   2406  // terminfo will have Smulx for this (but no support for colors yet).
   2407  if (tui->ti.defs[kTerm_set_underline_style] == NULL) {
   2408    if (vte_version >= 5102 || konsolev >= 221170
   2409        || tui->ti.Su || (weztermv != NULL && strcmp(weztermv, "20210203-095643") > 0)) {
   2410      tui_enable_extended_underline(tui);
   2411    }
   2412  } else {
   2413    tui_enable_extended_underline(tui);
   2414  }
   2415 
   2416  if (kitty || (vte_version != 0 && vte_version < 5400)) {
   2417    // Never use modifyOtherKeys in kitty if kitty keyboard protocol query fails.
   2418    // Also don't emit the sequence to enable modifyOtherKeys in old VTE versions.
   2419    tui->input.key_encoding = kKeyEncodingLegacy;
   2420  } else {
   2421    // Fallback to Xterm's modifyOtherKeys if terminal does not support the
   2422    // Kitty keyboard protocol. We don't actually enable the key encoding here
   2423    // though: it won't be enabled until the terminal responds to our query for
   2424    // kitty keyboard support.
   2425    tui->input.key_encoding = kKeyEncodingXterm;
   2426  }
   2427 
   2428  xfree(xterm_version);
   2429 }
   2430 
   2431 static bool should_invisible(TUIData *tui)
   2432 {
   2433  return tui->busy || tui->want_invisible;
   2434 }
   2435 
   2436 /// Write the sequence to begin flushing output to `buf`.
   2437 /// If 'termsync' is set and the terminal supports synchronized output, begin synchronized update.
   2438 /// Otherwise, hide the cursor to avoid cursor jumping.
   2439 ///
   2440 /// @param buf  the buffer to write the sequence to
   2441 /// @param len  the length of `buf`
   2442 static size_t flush_buf_start(TUIData *tui, char *buf, size_t len)
   2443  FUNC_ATTR_NONNULL_ALL
   2444 {
   2445  if (tui->sync_output && tui->has_sync_mode) {
   2446    return xstrlcpy(buf, "\x1b[?2026h", len);
   2447  } else if (!tui->is_invisible) {
   2448    tui->is_invisible = true;
   2449 
   2450    // TODO(bfredl): zero-param terminfo strings should be pre-filtered so we can just
   2451    // return a cached string here
   2452    TPVAR null_params[9] = { 0 };
   2453    const char *str = tui->ti.defs[kTerm_cursor_invisible];
   2454    if (str != NULL) {
   2455      return terminfo_fmt(buf, buf + len, str, null_params);
   2456    }
   2457  }
   2458 
   2459  return 0;
   2460 }
   2461 
   2462 /// Write the sequence to end flushing output to `buf`.
   2463 /// If 'termsync' is set and the terminal supports synchronized output, end synchronized update.
   2464 /// Otherwise, make the cursor visible again.
   2465 ///
   2466 /// @param buf  the buffer to write the sequence to
   2467 /// @param len  the length of `buf`
   2468 static size_t flush_buf_end(TUIData *tui, char *buf, size_t len)
   2469  FUNC_ATTR_NONNULL_ALL
   2470 {
   2471  size_t offset = 0;
   2472  if (tui->sync_output && tui->has_sync_mode) {
   2473 #define SYNC_END "\x1b[?2026l"
   2474    memcpy(buf, SYNC_END, sizeof SYNC_END);
   2475    offset += sizeof SYNC_END - 1;
   2476  }
   2477 
   2478  const char *str = NULL;
   2479  if (tui->is_invisible && !should_invisible(tui)) {
   2480    str = tui->ti.defs[kTerm_cursor_normal];
   2481    tui->is_invisible = false;
   2482  } else if (!tui->is_invisible && should_invisible(tui)) {
   2483    str = tui->ti.defs[kTerm_cursor_invisible];
   2484    tui->is_invisible = true;
   2485  }
   2486  TPVAR null_params[9] = { 0 };
   2487  if (str != NULL) {
   2488    offset += terminfo_fmt(buf + offset, buf + len, str, null_params);
   2489  }
   2490 
   2491  return offset;
   2492 }
   2493 
   2494 /// Flushes the rendered buffer to the TTY.
   2495 ///
   2496 /// @see tui_flush
   2497 static void flush_buf(TUIData *tui)
   2498 {
   2499  uv_write_t req;
   2500  uv_buf_t bufs[3];
   2501  char pre[32];
   2502  char post[32];
   2503 
   2504  if (tui->bufpos <= 0 && tui->is_invisible == should_invisible(tui)) {
   2505    return;
   2506  }
   2507 
   2508  bufs[0].base = pre;
   2509  bufs[0].len = UV_BUF_LEN(flush_buf_start(tui, pre, sizeof(pre)));
   2510 
   2511  bufs[1].base = tui->buf_to_flush != NULL ? tui->buf_to_flush : tui->buf;
   2512  bufs[1].len = UV_BUF_LEN(tui->bufpos);
   2513 
   2514  bufs[2].base = post;
   2515  bufs[2].len = UV_BUF_LEN(flush_buf_end(tui, post, sizeof(post)));
   2516 
   2517  if (tui->screenshot) {
   2518    for (size_t i = 0; i < ARRAY_SIZE(bufs); i++) {
   2519      fwrite(bufs[i].base, bufs[i].len, 1, tui->screenshot);
   2520    }
   2521  } else {
   2522    int ret
   2523      = uv_write(&req, (uv_stream_t *)&tui->output_handle, bufs, ARRAY_SIZE(bufs), NULL);
   2524    if (ret) {
   2525      ELOG("uv_write failed: %s", uv_strerror(ret));
   2526    }
   2527    uv_run(&tui->write_loop, UV_RUN_DEFAULT);
   2528  }
   2529  tui->buf_to_flush = NULL;
   2530  tui->bufpos = 0;
   2531 }
   2532 
   2533 /// Try to get "kbs" code from stty because "the terminfo kbs entry is extremely
   2534 /// unreliable." (Vim, Bash, and tmux also do this.)
   2535 /// On Windows, use 0x7f as Backspace if VT input has been enabled by stream_init().
   2536 ///
   2537 /// @see tmux/tty-keys.c fe4e9470bb504357d073320f5d305b22663ee3fd
   2538 /// @see https://bugzilla.redhat.com/show_bug.cgi?id=142659
   2539 /// @see https://github.com/microsoft/terminal/issues/4949
   2540 static const char *tui_get_stty_erase(TermInput *input)
   2541 {
   2542  static char stty_erase[2] = { 0 };
   2543 #if defined(HAVE_TERMIOS_H)
   2544  struct termios t;
   2545  if (tcgetattr(input->in_fd, &t) != -1) {
   2546    stty_erase[0] = (char)t.c_cc[VERASE];
   2547    stty_erase[1] = NUL;
   2548    DLOG("stty/termios:erase=%s", stty_erase);
   2549  }
   2550 #elif defined(MSWIN)
   2551  DWORD dwMode;
   2552  if (((uv_handle_t *)&input->read_stream.s.uv)->type == UV_TTY
   2553      && GetConsoleMode(input->read_stream.s.uv.tty.handle, &dwMode)
   2554      && (dwMode & ENABLE_VIRTUAL_TERMINAL_INPUT)) {
   2555    stty_erase[0] = '\x7f';
   2556    stty_erase[1] = NUL;
   2557  }
   2558 #endif
   2559  return stty_erase;
   2560 }
   2561 
   2562 /// libtermkey hook to override terminfo entries.
   2563 /// @see TermInput.tk_ti_hook_fn
   2564 static const char *tui_tk_ti_getstr(const char *name, const char *value, void *data)
   2565 {
   2566  TermInput *input = data;
   2567  static const char *stty_erase = NULL;
   2568  if (stty_erase == NULL) {
   2569    stty_erase = tui_get_stty_erase(input);
   2570  }
   2571 
   2572  if (strequal(name, "key_backspace")) {
   2573    DLOG("libtermkey:kbs=%s", value);
   2574    if (stty_erase[0] != 0) {
   2575      return stty_erase;
   2576    }
   2577  } else if (strequal(name, "key_dc")) {
   2578    DLOG("libtermkey:kdch1=%s", value);
   2579    // Vim: "If <BS> and <DEL> are now the same, redefine <DEL>."
   2580    if (value != NULL && value != (char *)-1 && strequal(stty_erase, value)) {
   2581      return stty_erase[0] == DEL ? CTRL_H_STR : DEL_STR;
   2582    }
   2583  } else if (strequal(name, "key_mouse")) {
   2584    DLOG("libtermkey:kmous=%s", value);
   2585    // If key_mouse is found, libtermkey uses its terminfo driver (driver-ti.c)
   2586    // for mouse input, which by accident only supports X10 protocol.
   2587    // Force libtermkey to fallback to its CSI driver (driver-csi.c). #7948
   2588    return NULL;
   2589  }
   2590 
   2591  return value;
   2592 }