ui_client.c (11594B)
1 /// Nvim's own UI client, which attaches to a child or remote Nvim server. 2 3 #include <assert.h> 4 #include <stdbool.h> 5 #include <stdint.h> 6 #include <stdlib.h> 7 8 #include "nvim/api/keysets_defs.h" 9 #include "nvim/api/private/defs.h" 10 #include "nvim/api/private/helpers.h" 11 #include "nvim/channel.h" 12 #include "nvim/channel_defs.h" 13 #include "nvim/eval/typval_defs.h" 14 #include "nvim/event/multiqueue.h" 15 #include "nvim/event/socket.h" 16 #include "nvim/globals.h" 17 #include "nvim/highlight.h" 18 #include "nvim/highlight_defs.h" 19 #include "nvim/log.h" 20 #include "nvim/main.h" 21 #include "nvim/memory.h" 22 #include "nvim/memory_defs.h" 23 #include "nvim/msgpack_rpc/channel.h" 24 #include "nvim/msgpack_rpc/channel_defs.h" 25 #include "nvim/os/os.h" 26 #include "nvim/profile.h" 27 #include "nvim/tui/tui.h" 28 #include "nvim/tui/tui_defs.h" 29 #include "nvim/ui.h" 30 #include "nvim/ui_client.h" 31 #include "nvim/ui_defs.h" 32 33 #ifdef MSWIN 34 # include "nvim/os/os_win_console.h" 35 #endif 36 37 static TUIData *tui = NULL; 38 static int tui_width = 0; 39 static int tui_height = 0; 40 static char *tui_term = ""; 41 static bool tui_rgb = false; 42 static bool ui_client_is_remote = false; 43 44 // uncrustify:off 45 #include "ui_client.c.generated.h" 46 #include "ui_events_client.generated.h" 47 // uncrustify:on 48 49 uint64_t ui_client_start_server(const char *exepath, size_t argc, char **argv) 50 { 51 char **args = xmalloc((2 + argc) * sizeof(char *)); 52 int args_idx = 0; 53 args[args_idx++] = xstrdup(argv[0]); 54 args[args_idx++] = xstrdup("--embed"); 55 for (size_t i = 1; i < argc; i++) { 56 args[args_idx++] = xstrdup(argv[i]); 57 } 58 args[args_idx++] = NULL; 59 60 CallbackReader on_err = CALLBACK_READER_INIT; 61 on_err.fwd_err = true; 62 63 bool detach = true; 64 varnumber_T exit_status; 65 Channel *channel = channel_job_start(args, exepath, 66 CALLBACK_READER_INIT, on_err, CALLBACK_NONE, 67 false, true, true, detach, kChannelStdinPipe, 68 NULL, 0, 0, NULL, &exit_status); 69 if (!channel) { 70 return 0; 71 } 72 73 // If stdin is not a pty, it is forwarded to the client. 74 // Replace stdin in the TUI process with the tty fd. 75 if (!stdin_isatty) { 76 close(0); 77 #ifdef MSWIN 78 os_open_conin_fd(); 79 #else 80 dup(stderr_isatty ? STDERR_FILENO : STDOUT_FILENO); 81 #endif 82 } 83 84 return channel->id; 85 } 86 87 /// Attaches this client to the UI channel, and sets its client info. 88 void ui_client_attach(int width, int height, char *term, bool rgb) 89 { 90 // 91 // nvim_ui_attach 92 // 93 MAXSIZE_TEMP_ARRAY(args, 3); 94 ADD_C(args, INTEGER_OBJ(width)); 95 ADD_C(args, INTEGER_OBJ(height)); 96 MAXSIZE_TEMP_DICT(opts, 9); 97 PUT_C(opts, "rgb", BOOLEAN_OBJ(rgb)); 98 PUT_C(opts, "ext_linegrid", BOOLEAN_OBJ(true)); 99 PUT_C(opts, "ext_termcolors", BOOLEAN_OBJ(true)); 100 if (term) { 101 PUT_C(opts, "term_name", CSTR_AS_OBJ(term)); 102 } 103 PUT_C(opts, "term_colors", INTEGER_OBJ(t_colors)); 104 PUT_C(opts, "stdin_tty", BOOLEAN_OBJ(stdin_isatty)); 105 PUT_C(opts, "stdout_tty", BOOLEAN_OBJ(stdout_isatty)); 106 if (ui_client_forward_stdin) { 107 PUT_C(opts, "stdin_fd", INTEGER_OBJ(UI_CLIENT_STDIN_FD)); 108 ui_client_forward_stdin = false; // stdin shouldn't be forwarded again #22292 109 } 110 ADD_C(args, DICT_OBJ(opts)); 111 112 rpc_send_event(ui_client_channel_id, "nvim_ui_attach", args); 113 ui_client_attached = true; 114 115 TIME_MSG("nvim_ui_attach"); 116 117 // 118 // nvim_set_client_info 119 // 120 MAXSIZE_TEMP_ARRAY(args2, 5); 121 ADD_C(args2, CSTR_AS_OBJ("nvim-tui")); // name 122 Object m = api_metadata(); 123 Dict version = { 0 }; 124 assert(m.data.dict.size > 0); 125 for (size_t i = 0; i < m.data.dict.size; i++) { 126 if (strequal(m.data.dict.items[i].key.data, "version")) { 127 version = m.data.dict.items[i].value.data.dict; 128 break; 129 } else if (i + 1 == m.data.dict.size) { 130 abort(); 131 } 132 } 133 ADD_C(args2, DICT_OBJ(version)); // version 134 ADD_C(args2, CSTR_AS_OBJ("ui")); // type 135 // We don't send api_metadata.functions as the "methods" because: 136 // 1. it consumes memory. 137 // 2. it is unlikely to be useful, since the peer can just call `nvim_get_api`. 138 // 3. nvim_set_client_info expects a dict instead of an array. 139 ADD_C(args2, ARRAY_OBJ((Array)ARRAY_DICT_INIT)); // methods 140 MAXSIZE_TEMP_DICT(info, 9); // attributes 141 PUT_C(info, "website", CSTR_AS_OBJ("https://neovim.io")); 142 PUT_C(info, "license", CSTR_AS_OBJ("Apache 2")); 143 PUT_C(info, "pid", INTEGER_OBJ(os_get_pid())); 144 ADD_C(args2, DICT_OBJ(info)); // attributes 145 rpc_send_event(ui_client_channel_id, "nvim_set_client_info", args2); 146 147 TIME_MSG("nvim_set_client_info"); 148 } 149 150 void ui_client_detach(void) 151 { 152 rpc_send_event(ui_client_channel_id, "nvim_ui_detach", (Array)ARRAY_DICT_INIT); 153 ui_client_attached = false; 154 } 155 156 void ui_client_run(bool remote_ui) 157 FUNC_ATTR_NORETURN 158 { 159 ui_client_is_remote = remote_ui; 160 tui_start(&tui, &tui_width, &tui_height, &tui_term, &tui_rgb); 161 ui_client_attach(tui_width, tui_height, tui_term, tui_rgb); 162 163 // TODO(justinmk): this is for log_spec. Can remove this after nvim_log #7062 is merged. 164 if (os_env_exists("__NVIM_TEST_LOG", true)) { 165 ELOG("test log message"); 166 } 167 168 time_finish(); 169 170 // os_exit() will be invoked when the client channel detaches 171 while (true) { 172 LOOP_PROCESS_EVENTS(&main_loop, resize_events, -1); 173 } 174 } 175 176 void ui_client_stop(void) 177 { 178 ui_client_attached = false; 179 if (!tui_is_stopped(tui)) { 180 tui_stop(tui); 181 } 182 } 183 184 void ui_client_set_size(int width, int height) 185 { 186 // The currently known size will be sent when attaching 187 if (ui_client_attached) { 188 MAXSIZE_TEMP_ARRAY(args, 2); 189 ADD_C(args, INTEGER_OBJ((int)width)); 190 ADD_C(args, INTEGER_OBJ((int)height)); 191 rpc_send_event(ui_client_channel_id, "nvim_ui_try_resize", args); 192 } 193 tui_width = width; 194 tui_height = height; 195 } 196 197 UIClientHandler ui_client_get_redraw_handler(const char *name, size_t name_len, Error *error) 198 { 199 int hash = ui_client_handler_hash(name, name_len); 200 if (hash < 0) { 201 return (UIClientHandler){ NULL, NULL }; 202 } 203 return event_handlers[hash]; 204 } 205 206 /// Placeholder for _sync_ requests with 'redraw' method name 207 /// 208 /// async 'redraw' events, which are expected when nvim acts as a ui client. 209 /// get handled in msgpack_rpc/unpacker.c and directly dispatched to handlers 210 /// of specific ui events, like ui_client_event_grid_resize and so on. 211 Object handle_ui_client_redraw(uint64_t channel_id, Array args, Arena *arena, Error *error) 212 { 213 api_set_error(error, kErrorTypeValidation, "'redraw' cannot be sent as a request"); 214 return NIL; 215 } 216 217 static HlAttrs ui_client_dict2hlattrs(Dict d, bool rgb) 218 { 219 Error err = ERROR_INIT; 220 Dict(highlight) dict = KEYDICT_INIT; 221 if (!api_dict_to_keydict(&dict, DictHash(highlight), d, &err)) { 222 // TODO(bfredl): log "err" 223 return HLATTRS_INIT; 224 } 225 226 HlAttrs attrs = dict2hlattrs(&dict, rgb, NULL, &err); 227 228 if (HAS_KEY(&dict, highlight, url)) { 229 attrs.url = tui_add_url(tui, dict.url.data); 230 } 231 232 return attrs; 233 } 234 235 void ui_client_event_grid_resize(Array args) 236 { 237 if (args.size < 3 238 || args.items[0].type != kObjectTypeInteger 239 || args.items[1].type != kObjectTypeInteger 240 || args.items[2].type != kObjectTypeInteger) { 241 ELOG("Error handling ui event 'grid_resize'"); 242 return; 243 } 244 245 Integer grid = args.items[0].data.integer; 246 Integer width = args.items[1].data.integer; 247 Integer height = args.items[2].data.integer; 248 tui_grid_resize(tui, grid, width, height); 249 250 if (grid_line_buf_size < (size_t)width) { 251 xfree(grid_line_buf_char); 252 xfree(grid_line_buf_attr); 253 grid_line_buf_size = (size_t)width; 254 grid_line_buf_char = xmalloc(grid_line_buf_size * sizeof(schar_T)); 255 grid_line_buf_attr = xmalloc(grid_line_buf_size * sizeof(sattr_T)); 256 } 257 } 258 259 void ui_client_event_grid_line(Array args) 260 FUNC_ATTR_NORETURN 261 { 262 abort(); // unreachable 263 } 264 265 void ui_client_event_raw_line(GridLineEvent *g) 266 { 267 int grid = g->args[0]; 268 int row = g->args[1]; 269 int startcol = g->args[2]; 270 Integer endcol = startcol + g->coloff; 271 Integer clearcol = endcol + g->clear_width; 272 LineFlags lineflags = g->wrap ? kLineFlagWrap : 0; 273 274 tui_raw_line(tui, grid, row, startcol, endcol, clearcol, g->cur_attr, lineflags, 275 (const schar_T *)grid_line_buf_char, grid_line_buf_attr); 276 } 277 278 void ui_client_event_connect(Array args) 279 { 280 if (args.size < 1 || args.items[0].type != kObjectTypeString) { 281 ELOG("Error handling UI event 'connect'"); 282 return; 283 } 284 285 char *server_addr = args.items[0].data.string.data; 286 multiqueue_put(main_loop.fast_events, channel_connect_event, server_addr); 287 // Set a dummy channel ID to prevent client exit when server detaches. 288 ui_client_channel_id = UINT64_MAX; 289 } 290 291 static void channel_connect_event(void **argv) 292 { 293 char *server_addr = argv[0]; 294 295 const char *err = ""; 296 bool is_tcp = socket_address_tcp_host_end(server_addr) != NULL; 297 CallbackReader on_data = CALLBACK_READER_INIT; 298 uint64_t chan = channel_connect(is_tcp, server_addr, true, on_data, 50, &err); 299 300 if (!strequal(err, "")) { 301 ELOG("Cannot connect to server %s: %s", server_addr, err); 302 ui_client_exit_status = 1; 303 os_exit(1); 304 } 305 306 ui_client_channel_id = chan; 307 ui_client_is_remote = true; 308 ui_client_attach(tui_width, tui_height, tui_term, tui_rgb); 309 310 ILOG("Connected to server %s on channel %" PRId64, server_addr, chan); 311 } 312 313 /// When a "restart" UI event is received, its arguments are saved here when 314 /// waiting for the server to exit. 315 static Array restart_args = ARRAY_DICT_INIT; 316 static bool restart_pending = false; 317 318 void ui_client_event_restart(Array args) 319 { 320 // NB: don't send nvim_ui_detach to server, as it may have already exited. 321 // ui_client_detach(); 322 323 // Save the arguments for ui_client_may_restart_server() later. 324 api_free_array(restart_args); 325 restart_args = copy_array(args, NULL); 326 restart_pending = true; 327 } 328 329 /// Called when the current server has exited. 330 void ui_client_may_restart_server(void) 331 { 332 if (!restart_pending) { 333 return; 334 } 335 restart_pending = false; 336 337 size_t argc; 338 char **argv = NULL; 339 if (restart_args.size < 2 340 || restart_args.items[0].type != kObjectTypeString 341 || restart_args.items[1].type != kObjectTypeArray 342 || (argc = restart_args.items[1].data.array.size) < 1) { 343 ELOG("Error handling ui event 'restart'"); 344 goto cleanup; 345 } 346 347 // 1. Get executable path and command-line arguments. 348 const char *exepath = restart_args.items[0].data.string.data; 349 argv = xcalloc(argc + 1, sizeof(char *)); 350 for (size_t i = 0; i < argc; i++) { 351 if (restart_args.items[1].data.array.items[i].type == kObjectTypeString) { 352 argv[i] = restart_args.items[1].data.array.items[i].data.string.data; 353 } 354 if (argv[i] == NULL) { 355 argv[i] = ""; 356 } 357 } 358 359 // 2. Start a new `nvim --embed` server. 360 uint64_t rv = ui_client_start_server(exepath, argc, argv); 361 if (!rv) { 362 ELOG("failed to start nvim server"); 363 goto cleanup; 364 } 365 366 // 3. Client-side server re-attach. 367 ui_client_channel_id = rv; 368 ui_client_is_remote = false; 369 ui_client_attach(tui_width, tui_height, tui_term, tui_rgb); 370 371 ILOG("restarted server id=%" PRId64, rv); 372 cleanup: 373 xfree(argv); 374 api_free_array(restart_args); 375 restart_args = (Array)ARRAY_DICT_INIT; 376 } 377 378 void ui_client_event_error_exit(Array args) 379 { 380 if (args.size < 1 381 || args.items[0].type != kObjectTypeInteger) { 382 ELOG("Error handling ui event 'error_exit'"); 383 return; 384 } 385 ui_client_error_exit = (int)args.items[0].data.integer; 386 } 387 388 #ifdef EXITFREE 389 void ui_client_free_all_mem(void) 390 { 391 tui_free_all_mem(tui); 392 xfree(grid_line_buf_char); 393 xfree(grid_line_buf_attr); 394 } 395 #endif