1// This is an open source non-commercial project. Dear PVS-Studio, please check
2// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3
4
5#include "nvim/tui/input.h"
6#include "nvim/vim.h"
7#include "nvim/api/vim.h"
8#include "nvim/api/private/helpers.h"
9#include "nvim/ascii.h"
10#include "nvim/charset.h"
11#include "nvim/main.h"
12#include "nvim/aucmd.h"
13#include "nvim/ex_docmd.h"
14#include "nvim/option.h"
15#include "nvim/os/os.h"
16#include "nvim/os/input.h"
17#include "nvim/event/rstream.h"
18
19#define KEY_BUFFER_SIZE 0xfff
20
21#ifdef INCLUDE_GENERATED_DECLARATIONS
22# include "tui/input.c.generated.h"
23#endif
24
25void tinput_init(TermInput *input, Loop *loop)
26{
27 input->loop = loop;
28 input->paste = 0;
29 input->in_fd = 0;
30 input->key_buffer = rbuffer_new(KEY_BUFFER_SIZE);
31 uv_mutex_init(&input->key_buffer_mutex);
32 uv_cond_init(&input->key_buffer_cond);
33
34 // If stdin is not a pty, switch to stderr. For cases like:
35 // echo q | nvim -es
36 // ls *.md | xargs nvim
37#ifdef WIN32
38 if (!os_isatty(0)) {
39 const HANDLE conin_handle = CreateFile("CONIN$",
40 GENERIC_READ | GENERIC_WRITE,
41 FILE_SHARE_READ | FILE_SHARE_WRITE,
42 (LPSECURITY_ATTRIBUTES)NULL,
43 OPEN_EXISTING, 0, (HANDLE)NULL);
44 input->in_fd = _open_osfhandle(conin_handle, _O_RDONLY);
45 assert(input->in_fd != -1);
46 }
47#else
48 if (!os_isatty(0) && os_isatty(2)) {
49 input->in_fd = 2;
50 }
51#endif
52 input_global_fd_init(input->in_fd);
53
54 const char *term = os_getenv("TERM");
55 if (!term) {
56 term = ""; // termkey_new_abstract assumes non-null (#2745)
57 }
58
59#if TERMKEY_VERSION_MAJOR > 0 || TERMKEY_VERSION_MINOR > 18
60 input->tk = termkey_new_abstract(term,
61 TERMKEY_FLAG_UTF8 | TERMKEY_FLAG_NOSTART);
62 termkey_hook_terminfo_getstr(input->tk, input->tk_ti_hook_fn, NULL);
63 termkey_start(input->tk);
64#else
65 input->tk = termkey_new_abstract(term, TERMKEY_FLAG_UTF8);
66#endif
67
68 int curflags = termkey_get_canonflags(input->tk);
69 termkey_set_canonflags(input->tk, curflags | TERMKEY_CANON_DELBS);
70
71 // setup input handle
72 rstream_init_fd(loop, &input->read_stream, input->in_fd, 0xfff);
73 // initialize a timer handle for handling ESC with libtermkey
74 time_watcher_init(loop, &input->timer_handle, input);
75}
76
77void tinput_destroy(TermInput *input)
78{
79 rbuffer_free(input->key_buffer);
80 uv_mutex_destroy(&input->key_buffer_mutex);
81 uv_cond_destroy(&input->key_buffer_cond);
82 time_watcher_close(&input->timer_handle, NULL);
83 stream_close(&input->read_stream, NULL, NULL);
84 termkey_destroy(input->tk);
85}
86
87void tinput_start(TermInput *input)
88{
89 rstream_start(&input->read_stream, tinput_read_cb, input);
90}
91
92void tinput_stop(TermInput *input)
93{
94 rstream_stop(&input->read_stream);
95 time_watcher_stop(&input->timer_handle);
96}
97
98static void tinput_done_event(void **argv)
99{
100 input_done();
101}
102
103static void tinput_wait_enqueue(void **argv)
104{
105 TermInput *input = argv[0];
106 RBUFFER_UNTIL_EMPTY(input->key_buffer, buf, len) {
107 const String keys = { .data = buf, .size = len };
108 if (input->paste) {
109 String copy = copy_string(keys);
110 multiqueue_put(main_loop.events, tinput_paste_event, 3,
111 copy.data, copy.size, (intptr_t)input->paste);
112 if (input->paste == 1) {
113 // Paste phase: "continue"
114 input->paste = 2;
115 }
116 rbuffer_consumed(input->key_buffer, len);
117 rbuffer_reset(input->key_buffer);
118 } else {
119 const size_t consumed = input_enqueue(keys);
120 if (consumed) {
121 rbuffer_consumed(input->key_buffer, consumed);
122 }
123 rbuffer_reset(input->key_buffer);
124 if (consumed < len) {
125 break;
126 }
127 }
128 }
129 uv_mutex_lock(&input->key_buffer_mutex);
130 input->waiting = false;
131 uv_cond_signal(&input->key_buffer_cond);
132 uv_mutex_unlock(&input->key_buffer_mutex);
133}
134
135static void tinput_paste_event(void **argv)
136{
137 String keys = { .data = argv[0], .size = (size_t)argv[1] };
138 intptr_t phase = (intptr_t)argv[2];
139
140 Error err = ERROR_INIT;
141 nvim_paste(keys, true, phase, &err);
142 if (ERROR_SET(&err)) {
143 emsgf("paste: %s", err.msg);
144 api_clear_error(&err);
145 }
146
147 api_free_string(keys);
148}
149
150static void tinput_flush(TermInput *input, bool wait_until_empty)
151{
152 size_t drain_boundary = wait_until_empty ? 0 : 0xff;
153 do {
154 uv_mutex_lock(&input->key_buffer_mutex);
155 loop_schedule_fast(&main_loop, event_create(tinput_wait_enqueue, 1, input));
156 input->waiting = true;
157 while (input->waiting) {
158 uv_cond_wait(&input->key_buffer_cond, &input->key_buffer_mutex);
159 }
160 uv_mutex_unlock(&input->key_buffer_mutex);
161 } while (rbuffer_size(input->key_buffer) > drain_boundary);
162}
163
164static void tinput_enqueue(TermInput *input, char *buf, size_t size)
165{
166 if (rbuffer_size(input->key_buffer) >
167 rbuffer_capacity(input->key_buffer) - 0xff) {
168 // don't ever let the buffer get too full or we risk putting incomplete keys
169 // into it
170 tinput_flush(input, false);
171 }
172 rbuffer_write(input->key_buffer, buf, size);
173}
174
175static void forward_simple_utf8(TermInput *input, TermKeyKey *key)
176{
177 size_t len = 0;
178 char buf[64];
179 char *ptr = key->utf8;
180
181 while (*ptr) {
182 if (*ptr == '<') {
183 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "<lt>");
184 } else {
185 buf[len++] = *ptr;
186 }
187 ptr++;
188 }
189
190 tinput_enqueue(input, buf, len);
191}
192
193static void forward_modified_utf8(TermInput *input, TermKeyKey *key)
194{
195 size_t len;
196 char buf[64];
197
198 if (key->type == TERMKEY_TYPE_KEYSYM
199 && key->code.sym == TERMKEY_SYM_ESCAPE) {
200 len = (size_t)snprintf(buf, sizeof(buf), "<Esc>");
201 } else if (key->type == TERMKEY_TYPE_KEYSYM
202 && key->code.sym == TERMKEY_SYM_SUSPEND) {
203 len = (size_t)snprintf(buf, sizeof(buf), "<C-Z>");
204 } else {
205 len = termkey_strfkey(input->tk, buf, sizeof(buf), key, TERMKEY_FORMAT_VIM);
206 }
207
208 tinput_enqueue(input, buf, len);
209}
210
211static void forward_mouse_event(TermInput *input, TermKeyKey *key)
212{
213 char buf[64];
214 size_t len = 0;
215 int button, row, col;
216 static int last_pressed_button = 0;
217 TermKeyMouseEvent ev;
218 termkey_interpret_mouse(input->tk, key, &ev, &button, &row, &col);
219
220 if ((ev == TERMKEY_MOUSE_RELEASE || ev == TERMKEY_MOUSE_DRAG)
221 && button == 0) {
222 // Some terminals (like urxvt) don't report which button was released.
223 // libtermkey reports button 0 in this case.
224 // For drag and release, we can reasonably infer the button to be the last
225 // pressed one.
226 button = last_pressed_button;
227 }
228
229 if (button == 0 || (ev != TERMKEY_MOUSE_PRESS && ev != TERMKEY_MOUSE_DRAG
230 && ev != TERMKEY_MOUSE_RELEASE)) {
231 return;
232 }
233
234 row--; col--; // Termkey uses 1-based coordinates
235 buf[len++] = '<';
236
237 if (key->modifiers & TERMKEY_KEYMOD_SHIFT) {
238 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "S-");
239 }
240
241 if (key->modifiers & TERMKEY_KEYMOD_CTRL) {
242 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "C-");
243 }
244
245 if (key->modifiers & TERMKEY_KEYMOD_ALT) {
246 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "A-");
247 }
248
249 if (button == 1) {
250 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Left");
251 } else if (button == 2) {
252 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Middle");
253 } else if (button == 3) {
254 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Right");
255 }
256
257 switch (ev) {
258 case TERMKEY_MOUSE_PRESS:
259 if (button == 4) {
260 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelUp");
261 } else if (button == 5) {
262 len += (size_t)snprintf(buf + len, sizeof(buf) - len,
263 "ScrollWheelDown");
264 } else {
265 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Mouse");
266 last_pressed_button = button;
267 }
268 break;
269 case TERMKEY_MOUSE_DRAG:
270 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Drag");
271 break;
272 case TERMKEY_MOUSE_RELEASE:
273 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Release");
274 break;
275 case TERMKEY_MOUSE_UNKNOWN:
276 assert(false);
277 }
278
279 len += (size_t)snprintf(buf + len, sizeof(buf) - len, "><%d,%d>", col, row);
280 tinput_enqueue(input, buf, len);
281}
282
283static TermKeyResult tk_getkey(TermKey *tk, TermKeyKey *key, bool force)
284{
285 return force ? termkey_getkey_force(tk, key) : termkey_getkey(tk, key);
286}
287
288static void tinput_timer_cb(TimeWatcher *watcher, void *data);
289
290static int get_key_code_timeout(void)
291{
292 Integer ms = -1;
293 // Check 'ttimeout' to determine if we should send ESC after 'ttimeoutlen'.
294 Error err = ERROR_INIT;
295 if (nvim_get_option(cstr_as_string("ttimeout"), &err).data.boolean) {
296 Object rv = nvim_get_option(cstr_as_string("ttimeoutlen"), &err);
297 if (!ERROR_SET(&err)) {
298 ms = rv.data.integer;
299 }
300 }
301 api_clear_error(&err);
302 return (int)ms;
303}
304
305static void tk_getkeys(TermInput *input, bool force)
306{
307 TermKeyKey key;
308 TermKeyResult result;
309
310 while ((result = tk_getkey(input->tk, &key, force)) == TERMKEY_RES_KEY) {
311 if (key.type == TERMKEY_TYPE_UNICODE && !key.modifiers) {
312 forward_simple_utf8(input, &key);
313 } else if (key.type == TERMKEY_TYPE_UNICODE
314 || key.type == TERMKEY_TYPE_FUNCTION
315 || key.type == TERMKEY_TYPE_KEYSYM) {
316 forward_modified_utf8(input, &key);
317 } else if (key.type == TERMKEY_TYPE_MOUSE) {
318 forward_mouse_event(input, &key);
319 }
320 }
321
322 if (result != TERMKEY_RES_AGAIN) {
323 return;
324 }
325 // else: Partial keypress event was found in the buffer, but it does not
326 // yet contain all the bytes required. `key` structure indicates what
327 // termkey_getkey_force() would return.
328
329 int ms = get_key_code_timeout();
330
331 if (ms > 0) {
332 // Stop the current timer if already running
333 time_watcher_stop(&input->timer_handle);
334 time_watcher_start(&input->timer_handle, tinput_timer_cb, (uint32_t)ms, 0);
335 } else {
336 tk_getkeys(input, true);
337 }
338}
339
340static void tinput_timer_cb(TimeWatcher *watcher, void *data)
341{
342 tk_getkeys(data, true);
343 tinput_flush(data, true);
344}
345
346/// Handle focus events.
347///
348/// If the upcoming sequence of bytes in the input stream matches the termcode
349/// for "focus gained" or "focus lost", consume that sequence and schedule an
350/// event on the main loop.
351///
352/// @param input the input stream
353/// @return true iff handle_focus_event consumed some input
354static bool handle_focus_event(TermInput *input)
355{
356 if (rbuffer_size(input->read_stream.buffer) > 2
357 && (!rbuffer_cmp(input->read_stream.buffer, "\x1b[I", 3)
358 || !rbuffer_cmp(input->read_stream.buffer, "\x1b[O", 3))) {
359 bool focus_gained = *rbuffer_get(input->read_stream.buffer, 2) == 'I';
360 // Advance past the sequence
361 rbuffer_consumed(input->read_stream.buffer, 3);
362 aucmd_schedule_focusgained(focus_gained);
363 return true;
364 }
365 return false;
366}
367
368static bool handle_bracketed_paste(TermInput *input)
369{
370 if (rbuffer_size(input->read_stream.buffer) > 5
371 && (!rbuffer_cmp(input->read_stream.buffer, "\x1b[200~", 6)
372 || !rbuffer_cmp(input->read_stream.buffer, "\x1b[201~", 6))) {
373 bool enable = *rbuffer_get(input->read_stream.buffer, 4) == '0';
374 if (input->paste && enable) {
375 return false; // Pasting "start paste" code literally.
376 }
377 // Advance past the sequence
378 rbuffer_consumed(input->read_stream.buffer, 6);
379 if (!!input->paste == enable) {
380 return true; // Spurious "disable paste" code.
381 }
382
383 if (enable) {
384 // Flush before starting paste.
385 tinput_flush(input, true);
386 // Paste phase: "first-chunk".
387 input->paste = 1;
388 } else if (input->paste) {
389 // Paste phase: "last-chunk".
390 input->paste = input->paste == 2 ? 3 : -1;
391 tinput_flush(input, true);
392 // Paste phase: "disabled".
393 input->paste = 0;
394 }
395 return true;
396 }
397 return false;
398}
399
400// ESC NUL => <Esc>
401static bool handle_forced_escape(TermInput *input)
402{
403 if (rbuffer_size(input->read_stream.buffer) > 1
404 && !rbuffer_cmp(input->read_stream.buffer, "\x1b\x00", 2)) {
405 // skip the ESC and NUL and push one <esc> to the input buffer
406 size_t rcnt;
407 termkey_push_bytes(input->tk, rbuffer_read_ptr(input->read_stream.buffer,
408 &rcnt), 1);
409 rbuffer_consumed(input->read_stream.buffer, 2);
410 tk_getkeys(input, true);
411 return true;
412 }
413 return false;
414}
415
416static void set_bg_deferred(void **argv)
417{
418 char *bgvalue = argv[0];
419 if (!option_was_set("bg") && !strequal((char *)p_bg, bgvalue)) {
420 // Value differs, apply it.
421 if (starting) {
422 // Wait until after startup, so OptionSet is triggered.
423 do_cmdline_cmd((bgvalue[0] == 'l')
424 ? "autocmd VimEnter * ++once ++nested set bg=light"
425 : "autocmd VimEnter * ++once ++nested set bg=dark");
426 } else {
427 set_option_value("bg", 0L, bgvalue, 0);
428 reset_option_was_set("bg");
429 }
430 }
431}
432
433// During startup, tui.c requests the background color (see `ext.get_bg`).
434//
435// Here in input.c, we watch for the terminal response `\e]11;COLOR\a`. If
436// COLOR matches `rgb:RRRR/GGGG/BBBB/AAAA` where R, G, B, and A are hex digits,
437// then compute the luminance[1] of the RGB color and classify it as light/dark
438// accordingly. Note that the color components may have anywhere from one to
439// four hex digits, and require scaling accordingly as values out of 4, 8, 12,
440// or 16 bits. Also note the A(lpha) component is optional, and is parsed but
441// ignored in the calculations.
442//
443// [1] https://en.wikipedia.org/wiki/Luma_%28video%29
444static bool handle_background_color(TermInput *input)
445{
446 size_t count = 0;
447 size_t component = 0;
448 size_t header_size = 0;
449 size_t num_components = 0;
450 uint16_t rgb[] = { 0, 0, 0 };
451 uint16_t rgb_max[] = { 0, 0, 0 };
452 bool eat_backslash = false;
453 bool done = false;
454 bool bad = false;
455 if (rbuffer_size(input->read_stream.buffer) >= 9
456 && !rbuffer_cmp(input->read_stream.buffer, "\x1b]11;rgb:", 9)) {
457 header_size = 9;
458 num_components = 3;
459 } else if (rbuffer_size(input->read_stream.buffer) >= 10
460 && !rbuffer_cmp(input->read_stream.buffer, "\x1b]11;rgba:", 10)) {
461 header_size = 10;
462 num_components = 4;
463 } else {
464 return false;
465 }
466 rbuffer_consumed(input->read_stream.buffer, header_size);
467 RBUFFER_EACH(input->read_stream.buffer, c, i) {
468 count = i + 1;
469 if (eat_backslash) {
470 done = true;
471 break;
472 } else if (c == '\x07') {
473 done = true;
474 break;
475 } else if (c == '\x1b') {
476 eat_backslash = true;
477 } else if (bad) {
478 // ignore
479 } else if ((c == '/') && (++component < num_components)) {
480 // work done in condition
481 } else if (ascii_isxdigit(c)) {
482 if (component < 3 && rgb_max[component] != 0xffff) {
483 rgb_max[component] = (uint16_t)((rgb_max[component] << 4) | 0xf);
484 rgb[component] = (uint16_t)((rgb[component] << 4) | hex2nr(c));
485 }
486 } else {
487 bad = true;
488 }
489 }
490 rbuffer_consumed(input->read_stream.buffer, count);
491 if (done && !bad && rgb_max[0] && rgb_max[1] && rgb_max[2]) {
492 double r = (double)rgb[0] / (double)rgb_max[0];
493 double g = (double)rgb[1] / (double)rgb_max[1];
494 double b = (double)rgb[2] / (double)rgb_max[2];
495 double luminance = (0.299 * r) + (0.587 * g) + (0.114 * b); // CCIR 601
496 char *bgvalue = luminance < 0.5 ? "dark" : "light";
497 DLOG("bg response: %s", bgvalue);
498 loop_schedule_deferred(&main_loop,
499 event_create(set_bg_deferred, 1, bgvalue));
500 } else {
501 DLOG("failed to parse bg response");
502 return false;
503 }
504 return true;
505}
506
507static void tinput_read_cb(Stream *stream, RBuffer *buf, size_t count_,
508 void *data, bool eof)
509{
510 TermInput *input = data;
511
512 if (eof) {
513 loop_schedule_fast(&main_loop, event_create(tinput_done_event, 0));
514 return;
515 }
516
517 do {
518 if (handle_focus_event(input)
519 || handle_bracketed_paste(input)
520 || handle_forced_escape(input)
521 || handle_background_color(input)) {
522 continue;
523 }
524
525 //
526 // Find the next ESC and push everything up to it (excluding), so it will
527 // be the first thing encountered on the next iteration. The `handle_*`
528 // calls (above) depend on this.
529 //
530 size_t count = 0;
531 RBUFFER_EACH(input->read_stream.buffer, c, i) {
532 count = i + 1;
533 if (c == '\x1b' && count > 1) {
534 count--;
535 break;
536 }
537 }
538 // Push bytes directly (paste).
539 if (input->paste) {
540 RBUFFER_UNTIL_EMPTY(input->read_stream.buffer, ptr, len) {
541 size_t consumed = MIN(count, len);
542 assert(consumed <= input->read_stream.buffer->size);
543 tinput_enqueue(input, ptr, consumed);
544 rbuffer_consumed(input->read_stream.buffer, consumed);
545 if (!(count -= consumed)) {
546 break;
547 }
548 }
549 continue;
550 }
551 // Push through libtermkey (translates to "<keycode>" strings, etc.).
552 RBUFFER_UNTIL_EMPTY(input->read_stream.buffer, ptr, len) {
553 size_t consumed = termkey_push_bytes(input->tk, ptr, MIN(count, len));
554 // termkey_push_bytes can return (size_t)-1, so it is possible that
555 // `consumed > input->read_stream.buffer->size`, but since tk_getkeys is
556 // called soon, it shouldn't happen.
557 assert(consumed <= input->read_stream.buffer->size);
558 rbuffer_consumed(input->read_stream.buffer, consumed);
559 // Process the keys now: there is no guarantee `count` will
560 // fit into libtermkey's input buffer.
561 tk_getkeys(input, false);
562 if (!(count -= consumed)) {
563 break;
564 }
565 }
566 } while (rbuffer_size(input->read_stream.buffer));
567 tinput_flush(input, true);
568 // Make sure the next input escape sequence fits into the ring buffer without
569 // wraparound, else it could be misinterpreted (because rbuffer_read_ptr()
570 // exposes the underlying buffer to callers unaware of the wraparound).
571 rbuffer_reset(input->read_stream.buffer);
572}
573