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 }