#include #include #include #include #include "klib/kvec.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" #include "nvim/event/loop.h" #include "nvim/event/rstream.h" #include "nvim/macros_defs.h" #include "nvim/main.h" #include "nvim/map_defs.h" #include "nvim/memory.h" #include "nvim/msgpack_rpc/channel.h" #include "nvim/option_vars.h" #include "nvim/os/os.h" #include "nvim/os/os_defs.h" #include "nvim/strings.h" #include "nvim/tui/input.h" #include "nvim/tui/input_defs.h" #include "nvim/tui/termkey/driver-csi.h" #include "nvim/tui/termkey/termkey.h" #include "nvim/tui/tui.h" #include "nvim/ui_client.h" #ifdef MSWIN # include "nvim/os/os_win_console.h" #endif #define READ_STREAM_SIZE 0xfff /// Size of libtermkey's internal input buffer. The buffer may grow larger than /// this when processing very long escape sequences, but will shrink back to /// this size afterward #define INPUT_BUFFER_SIZE 256 static const struct kitty_key_map_entry { int key; const char *name; } kitty_key_map_entry[] = { { KITTY_KEY_ESCAPE, "Esc" }, { KITTY_KEY_ENTER, "CR" }, { KITTY_KEY_TAB, "Tab" }, { KITTY_KEY_BACKSPACE, "BS" }, { KITTY_KEY_INSERT, "Insert" }, { KITTY_KEY_DELETE, "Del" }, { KITTY_KEY_LEFT, "Left" }, { KITTY_KEY_RIGHT, "Right" }, { KITTY_KEY_UP, "Up" }, { KITTY_KEY_DOWN, "Down" }, { KITTY_KEY_PAGE_UP, "PageUp" }, { KITTY_KEY_PAGE_DOWN, "PageDown" }, { KITTY_KEY_HOME, "Home" }, { KITTY_KEY_END, "End" }, { KITTY_KEY_F1, "F1" }, { KITTY_KEY_F2, "F2" }, { KITTY_KEY_F3, "F3" }, { KITTY_KEY_F4, "F4" }, { KITTY_KEY_F5, "F5" }, { KITTY_KEY_F6, "F6" }, { KITTY_KEY_F7, "F7" }, { KITTY_KEY_F8, "F8" }, { KITTY_KEY_F9, "F9" }, { KITTY_KEY_F10, "F10" }, { KITTY_KEY_F11, "F11" }, { KITTY_KEY_F12, "F12" }, { KITTY_KEY_F13, "F13" }, { KITTY_KEY_F14, "F14" }, { KITTY_KEY_F15, "F15" }, { KITTY_KEY_F16, "F16" }, { KITTY_KEY_F17, "F17" }, { KITTY_KEY_F18, "F18" }, { KITTY_KEY_F19, "F19" }, { KITTY_KEY_F20, "F20" }, { KITTY_KEY_F21, "F21" }, { KITTY_KEY_F22, "F22" }, { KITTY_KEY_F23, "F23" }, { KITTY_KEY_F24, "F24" }, { KITTY_KEY_F25, "F25" }, { KITTY_KEY_F26, "F26" }, { KITTY_KEY_F27, "F27" }, { KITTY_KEY_F28, "F28" }, { KITTY_KEY_F29, "F29" }, { KITTY_KEY_F30, "F30" }, { KITTY_KEY_F31, "F31" }, { KITTY_KEY_F32, "F32" }, { KITTY_KEY_F33, "F33" }, { KITTY_KEY_F34, "F34" }, { KITTY_KEY_F35, "F35" }, { KITTY_KEY_KP_0, "k0" }, { KITTY_KEY_KP_1, "k1" }, { KITTY_KEY_KP_2, "k2" }, { KITTY_KEY_KP_3, "k3" }, { KITTY_KEY_KP_4, "k4" }, { KITTY_KEY_KP_5, "k5" }, { KITTY_KEY_KP_6, "k6" }, { KITTY_KEY_KP_7, "k7" }, { KITTY_KEY_KP_8, "k8" }, { KITTY_KEY_KP_9, "k9" }, { KITTY_KEY_KP_DECIMAL, "kPoint" }, { KITTY_KEY_KP_DIVIDE, "kDivide" }, { KITTY_KEY_KP_MULTIPLY, "kMultiply" }, { KITTY_KEY_KP_SUBTRACT, "kMinus" }, { KITTY_KEY_KP_ADD, "kPlus" }, { KITTY_KEY_KP_ENTER, "kEnter" }, { KITTY_KEY_KP_EQUAL, "kEqual" }, { KITTY_KEY_KP_LEFT, "kLeft" }, { KITTY_KEY_KP_RIGHT, "kRight" }, { KITTY_KEY_KP_UP, "kUp" }, { KITTY_KEY_KP_DOWN, "kDown" }, { KITTY_KEY_KP_PAGE_UP, "kPageUp" }, { KITTY_KEY_KP_PAGE_DOWN, "kPageDown" }, { KITTY_KEY_KP_HOME, "kHome" }, { KITTY_KEY_KP_END, "kEnd" }, { KITTY_KEY_KP_INSERT, "kInsert" }, { KITTY_KEY_KP_DELETE, "kDel" }, { KITTY_KEY_KP_BEGIN, "kOrigin" }, }; static PMap(int) kitty_key_map = MAP_INIT; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "tui/input.c.generated.h" #endif void tinput_init(TermInput *input, Loop *loop) { input->loop = loop; input->paste = 0; input->in_fd = STDIN_FILENO; input->key_encoding = kKeyEncodingLegacy; input->ttimeout = (bool)p_ttimeout; input->ttimeoutlen = p_ttm; for (size_t i = 0; i < ARRAY_SIZE(kitty_key_map_entry); i++) { pmap_put(int)(&kitty_key_map, kitty_key_map_entry[i].key, (ptr_t)kitty_key_map_entry[i].name); } input->in_fd = STDIN_FILENO; const char *term = os_getenv("TERM"); if (!term) { term = ""; // termkey_new_abstract assumes non-null (#2745) } input->tk = termkey_new_abstract(term, TERMKEY_FLAG_UTF8 | TERMKEY_FLAG_NOSTART); termkey_set_buffer_size(input->tk, INPUT_BUFFER_SIZE); termkey_hook_terminfo_getstr(input->tk, input->tk_ti_hook_fn, input); termkey_start(input->tk); int curflags = termkey_get_canonflags(input->tk); termkey_set_canonflags(input->tk, curflags | TERMKEY_CANON_DELBS); // setup input handle rstream_init_fd(loop, &input->read_stream, input->in_fd); // initialize a timer handle for handling ESC with libtermkey uv_timer_init(&loop->uv, &input->timer_handle); input->timer_handle.data = input; uv_timer_init(&loop->uv, &input->bg_query_timer); input->bg_query_timer.data = input; } void tinput_destroy(TermInput *input) { map_destroy(int, &kitty_key_map); uv_close((uv_handle_t *)&input->timer_handle, NULL); uv_close((uv_handle_t *)&input->bg_query_timer, NULL); rstream_may_close(&input->read_stream); termkey_destroy(input->tk); } void tinput_start(TermInput *input) { rstream_start(&input->read_stream, tinput_read_cb, input); } void tinput_stop(TermInput *input) { rstream_stop(&input->read_stream); uv_timer_stop(&input->timer_handle); uv_timer_stop(&input->bg_query_timer); } static void tinput_done_event(void **argv) FUNC_ATTR_NORETURN { os_exit(1); } /// Send all pending input in key buffer to Nvim server. static void tinput_flush(TermInput *input) { String keys = { .data = input->key_buffer, .size = input->key_buffer_len }; if (input->paste) { // produce exactly one paste event MAXSIZE_TEMP_ARRAY(args, 3); ADD_C(args, STRING_OBJ(keys)); // 'data' ADD_C(args, BOOLEAN_OBJ(true)); // 'crlf' ADD_C(args, INTEGER_OBJ(input->paste)); // 'phase' rpc_send_event(ui_client_channel_id, "nvim_paste", args); if (input->paste == 1) { // Paste phase: "continue" input->paste = 2; } } else { // enqueue input if (input->key_buffer_len > 0) { MAXSIZE_TEMP_ARRAY(args, 1); ADD_C(args, STRING_OBJ(keys)); // NOTE: This is non-blocking and won't check partially processed input, // but should be fine as all big sends are handled with nvim_paste, not nvim_input rpc_send_event(ui_client_channel_id, "nvim_input", args); } } input->key_buffer_len = 0; } static void tinput_enqueue(TermInput *input, const char *buf, size_t size) { if (input->key_buffer_len > KEY_BUFFER_SIZE - size) { // don't ever let the buffer get too full or we risk putting incomplete keys into it tinput_flush(input); } size_t to_copy = MIN(size, KEY_BUFFER_SIZE - input->key_buffer_len); memcpy(input->key_buffer + input->key_buffer_len, buf, to_copy); input->key_buffer_len += to_copy; } /// Handle TERMKEY_KEYMOD_* modifiers, i.e. Shift, Alt and Ctrl. /// /// @return The number of bytes written into "buf", excluding the final NUL. static size_t handle_termkey_modifiers(TermKeyKey *key, char *buf, size_t buflen) FUNC_ATTR_WARN_UNUSED_RESULT { size_t len = 0; if (key->modifiers & TERMKEY_KEYMOD_SHIFT) { // Shift len += (size_t)snprintf(buf + len, buflen - len, "S-"); } if (key->modifiers & TERMKEY_KEYMOD_ALT) { // Alt len += (size_t)snprintf(buf + len, buflen - len, "A-"); } if (key->modifiers & TERMKEY_KEYMOD_CTRL) { // Ctrl len += (size_t)snprintf(buf + len, buflen - len, "C-"); } assert(len < buflen); return len; } /// Handle modifiers not handled by libtermkey. /// Currently only Super ("D-") and Meta ("T-") are supported in Nvim. /// /// @return The number of bytes written into "buf", excluding the final NUL. static size_t handle_more_modifiers(TermKeyKey *key, char *buf, size_t buflen) FUNC_ATTR_WARN_UNUSED_RESULT { size_t len = 0; if (key->modifiers & 8) { // Super len += (size_t)snprintf(buf + len, buflen - len, "D-"); } if (key->modifiers & 32) { // Meta len += (size_t)snprintf(buf + len, buflen - len, "T-"); } assert(len < buflen); return len; } static void handle_kitty_key_protocol(TermInput *input, TermKeyKey *key) { const char *name = pmap_get(int)(&kitty_key_map, key->code.codepoint); if (name) { char buf[64]; size_t len = 0; buf[len++] = '<'; len += handle_termkey_modifiers(key, buf + len, sizeof(buf) - len); len += handle_more_modifiers(key, buf + len, sizeof(buf) - len); len += (size_t)snprintf(buf + len, sizeof(buf) - len, "%s>", name); assert(len < sizeof(buf)); tinput_enqueue(input, buf, len); } } static void forward_simple_utf8(TermInput *input, TermKeyKey *key) { size_t len = 0; char buf[64]; char *ptr = key->utf8; if (key->code.codepoint >= 0xE000 && key->code.codepoint <= 0xF8FF && map_has(int, &kitty_key_map, (int)key->code.codepoint)) { handle_kitty_key_protocol(input, key); return; } while (*ptr) { if (*ptr == '<') { len += (size_t)snprintf(buf + len, sizeof(buf) - len, ""); } else { buf[len++] = *ptr; } assert(len < sizeof(buf)); ptr++; } tinput_enqueue(input, buf, len); } static void forward_modified_utf8(TermInput *input, TermKeyKey *key) { size_t len; char buf[64]; if (key->type == TERMKEY_TYPE_KEYSYM && key->code.sym == TERMKEY_SYM_SUSPEND) { len = (size_t)snprintf(buf, sizeof(buf), ""); } else if (key->type != TERMKEY_TYPE_UNICODE) { len = termkey_strfkey(input->tk, buf, sizeof(buf), key, TERMKEY_FORMAT_VIM); } else { assert(key->modifiers); if (key->code.codepoint >= 0xE000 && key->code.codepoint <= 0xF8FF && map_has(int, &kitty_key_map, (int)key->code.codepoint)) { handle_kitty_key_protocol(input, key); return; } // Termkey doesn't include the S- modifier for ASCII characters (e.g., // ctrl-shift-l is instead of . Vim, on the other hand, // treats and the same, requiring the S- modifier. len = termkey_strfkey(input->tk, buf, sizeof(buf), key, TERMKEY_FORMAT_VIM); if ((key->modifiers & TERMKEY_KEYMOD_CTRL) && !(key->modifiers & TERMKEY_KEYMOD_SHIFT) && ASCII_ISUPPER(key->code.codepoint)) { assert(len + 2 < sizeof(buf)); // Make room for the S- memmove(buf + 3, buf + 1, len - 1); buf[1] = 'S'; buf[2] = '-'; len += 2; } } char more_buf[25]; size_t more_len = handle_more_modifiers(key, more_buf, sizeof(more_buf)); if (more_len > 0) { assert(len + more_len < sizeof(buf)); memmove(buf + 1 + more_len, buf + 1, len - 1); memcpy(buf + 1, more_buf, more_len); len += more_len; } assert(len < sizeof(buf)); tinput_enqueue(input, buf, len); } static void forward_mouse_event(TermInput *input, TermKeyKey *key) { char buf[64]; size_t len = 0; int button, row, col; static int last_pressed_button = 0; TermKeyMouseEvent ev; termkey_interpret_mouse(input->tk, key, &ev, &button, &row, &col); if ((ev == TERMKEY_MOUSE_RELEASE || ev == TERMKEY_MOUSE_DRAG) && button == 0) { // Some terminals (like urxvt) don't report which button was released. // libtermkey reports button 0 in this case. // For drag and release, we can reasonably infer the button to be the last // pressed one. button = last_pressed_button; } if ((button == 0 && ev != TERMKEY_MOUSE_RELEASE) || (ev != TERMKEY_MOUSE_PRESS && ev != TERMKEY_MOUSE_DRAG && ev != TERMKEY_MOUSE_RELEASE)) { return; } row--; col--; // Termkey uses 1-based coordinates buf[len++] = '<'; len += handle_termkey_modifiers(key, buf + len, sizeof(buf) - len); // Doesn't actually work because there are only 3 bits (0x1c) for modifiers. // len += handle_more_modifiers(key, buf + len, sizeof(buf) - len); if (button == 1) { len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Left"); } else if (button == 2) { len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Middle"); } else if (button == 3) { len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Right"); } else if (button == 8) { len += (size_t)snprintf(buf + len, sizeof(buf) - len, "X1"); } else if (button == 9) { len += (size_t)snprintf(buf + len, sizeof(buf) - len, "X2"); } switch (ev) { case TERMKEY_MOUSE_PRESS: if (button == 4) { len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelUp"); } else if (button == 5) { len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelDown"); } else if (button == 6) { len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelLeft"); } else if (button == 7) { len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelRight"); } else { len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Mouse"); last_pressed_button = button; } break; case TERMKEY_MOUSE_DRAG: len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Drag"); break; case TERMKEY_MOUSE_RELEASE: len += (size_t)snprintf(buf + len, sizeof(buf) - len, button ? "Release" : "MouseMove"); last_pressed_button = 0; break; case TERMKEY_MOUSE_UNKNOWN: abort(); } len += (size_t)snprintf(buf + len, sizeof(buf) - len, "><%d,%d>", col, row); assert(len < sizeof(buf)); tinput_enqueue(input, buf, len); } static TermKeyResult tk_getkey(TermKey *tk, TermKeyKey *key, bool force) { return force ? termkey_getkey_force(tk, key) : termkey_getkey(tk, key); } static void tk_getkeys(TermInput *input, bool force) { TermKeyKey key; TermKeyResult result; while ((result = tk_getkey(input->tk, &key, force)) == TERMKEY_RES_KEY) { // Only press and repeat events are handled for now switch (key.event) { case TERMKEY_EVENT_PRESS: case TERMKEY_EVENT_REPEAT: break; default: continue; } if (key.type == TERMKEY_TYPE_UNICODE && !key.modifiers) { forward_simple_utf8(input, &key); } else if (key.type == TERMKEY_TYPE_UNICODE || key.type == TERMKEY_TYPE_FUNCTION || key.type == TERMKEY_TYPE_KEYSYM) { forward_modified_utf8(input, &key); } else if (key.type == TERMKEY_TYPE_MOUSE) { forward_mouse_event(input, &key); } else if (key.type == TERMKEY_TYPE_MODEREPORT) { handle_modereport(input, &key); } else if (key.type == TERMKEY_TYPE_UNKNOWN_CSI) { handle_unknown_csi(input, &key); } else if (key.type == TERMKEY_TYPE_OSC || key.type == TERMKEY_TYPE_DCS) { handle_term_response(input, &key); } } if (result != TERMKEY_RES_AGAIN) { return; } // else: Partial keypress event was found in the buffer, but it does not // yet contain all the bytes required. `key` structure indicates what // termkey_getkey_force() would return. if (input->ttimeout && input->ttimeoutlen >= 0) { // Stop the current timer if already running uv_timer_stop(&input->timer_handle); uv_timer_start(&input->timer_handle, tinput_timer_cb, (uint64_t)input->ttimeoutlen, 0); } else { tk_getkeys(input, true); } } static void tinput_timer_cb(uv_timer_t *handle) { TermInput *input = handle->data; // If the raw buffer is not empty, process the raw buffer first because it is // processing an incomplete bracketed paste sequence. size_t size = rstream_available(&input->read_stream); if (size) { size_t consumed = handle_raw_buffer(input, true, input->read_stream.read_pos, size); rstream_consume(&input->read_stream, consumed); } tk_getkeys(input, true); tinput_flush(input); } static void bg_query_timer_cb(uv_timer_t *handle) FUNC_ATTR_NONNULL_ALL { TermInput *input = handle->data; tui_query_bg_color(input->tui_data); } /// Handle focus events. /// /// If the upcoming sequence of bytes in the input stream matches the termcode /// for "focus gained" or "focus lost", consume that sequence and send an event /// to Nvim server. /// /// @param input the input stream /// @return true iff handle_focus_event consumed some input static size_t handle_focus_event(TermInput *input, const char *ptr, size_t size) { if (size >= 3 && (!memcmp(ptr, "\x1b[I", 3) || !memcmp(ptr, "\x1b[O", 3))) { bool focus_gained = ptr[2] == 'I'; MAXSIZE_TEMP_ARRAY(args, 1); ADD_C(args, BOOLEAN_OBJ(focus_gained)); rpc_send_event(ui_client_channel_id, "nvim_ui_set_focus", args); return 3; // Advance past the sequence } return 0; } #define START_PASTE "\x1b[200~" #define END_PASTE "\x1b[201~" static size_t handle_bracketed_paste(TermInput *input, const char *ptr, size_t size, bool *incomplete) { if (size >= 6 && (!memcmp(ptr, START_PASTE, 6) || !memcmp(ptr, END_PASTE, 6))) { bool enable = ptr[4] == '0'; if (input->paste && enable) { return 0; // Pasting "start paste" code literally. } // Advance past the sequence if (!!input->paste == enable) { return 6; // Spurious "disable paste" code. } if (enable) { // Flush before starting paste. tinput_flush(input); // Paste phase: "first-chunk". input->paste = 1; } else if (input->paste) { // Paste phase: "last-chunk". input->paste = input->paste == 2 ? 3 : -1; tinput_flush(input); // Paste phase: "disabled". input->paste = 0; } return 6; } else if (size < 6 && (!memcmp(ptr, START_PASTE, size) || !memcmp(ptr, END_PASTE, size))) { // Wait for further input, as the sequence may be split. *incomplete = true; return 0; } return 0; } /// Handle an OSC or DCS response sequence from the terminal. static void handle_term_response(TermInput *input, const TermKeyKey *key) FUNC_ATTR_NONNULL_ALL { const char *str = NULL; if (termkey_interpret_string(input->tk, key, &str) == TERMKEY_RES_KEY) { assert(str != NULL); // Handle DECRQSS SGR response for the query from tui_query_extended_underline(). // Some terminals include "0" in the attribute list unconditionally; others don't. if (key->type == TERMKEY_TYPE_DCS && (strnequal(str, S_LEN("1$r4:3m")) || strnequal(str, S_LEN("1$r0;4:3m")))) { tui_enable_extended_underline(input->tui_data); } // Send an event to nvim core. This will update the v:termresponse variable // and fire the TermResponse event MAXSIZE_TEMP_ARRAY(args, 2); ADD_C(args, STATIC_CSTR_AS_OBJ("termresponse")); // libtermkey strips the OSC/DCS bytes from the response. We add it back in // so that downstream consumers of v:termresponse can differentiate between // the two. StringBuilder response = KV_INITIAL_VALUE; switch (key->type) { case TERMKEY_TYPE_OSC: kv_printf(response, "\x1b]%s", str); break; case TERMKEY_TYPE_DCS: kv_printf(response, "\x1bP%s", str); break; default: // Key type already checked for OSC/DCS in termkey_interpret_string UNREACHABLE; } ADD_C(args, STRING_OBJ(cbuf_as_string(response.items, response.size))); rpc_send_event(ui_client_channel_id, "nvim_ui_term_event", args); kv_destroy(response); } } /// Handle a mode report (DECRPM) sequence from the terminal. static void handle_modereport(TermInput *input, const TermKeyKey *key) FUNC_ATTR_NONNULL_ALL { int initial; int mode; int value; if (termkey_interpret_modereport(input->tk, key, &initial, &mode, &value) == TERMKEY_RES_KEY) { (void)initial; // Unused tui_handle_term_mode(input->tui_data, (TermMode)mode, (TermModeState)value); } } /// Handle a CSI sequence from the terminal that is unrecognized by libtermkey. static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) FUNC_ATTR_NONNULL_ALL { // There is no specified limit on the number of parameters a CSI sequence can // contain, so just allocate enough space for a large upper bound TermKeyCsiParam params[16]; size_t nparams = 16; unsigned cmd; if (termkey_interpret_csi(input->tk, key, params, &nparams, &cmd) != TERMKEY_RES_KEY) { return; } uint8_t intermediate = (cmd >> 16) & 0xFF; uint8_t initial = (cmd >> 8) & 0xFF; uint8_t command = cmd & 0xFF; // Currently unused (void)intermediate; switch (command) { case 'u': switch (initial) { case '?': // Kitty keyboard protocol query response. if (input->waiting_for_kkp_response) { input->waiting_for_kkp_response = false; input->key_encoding = kKeyEncodingKitty; tui_set_key_encoding(input->tui_data); } break; } break; case 'c': switch (initial) { case '?': // Primary Device Attributes response if (input->waiting_for_kkp_response) { input->waiting_for_kkp_response = false; // Enable the fallback key encoding (if any) tui_set_key_encoding(input->tui_data); } break; } break; case 't': if (nparams == 5) { // We only care about the first 3 parameters, and we ignore subparameters int args[3]; for (size_t i = 0; i < ARRAY_SIZE(args); i++) { if (termkey_interpret_csi_param(params[i], &args[i], NULL, NULL) != TERMKEY_RES_KEY) { return; } } if (args[0] == 48) { // In-band resize event (DEC private mode 2048) int height_chars = args[1]; int width_chars = args[2]; tui_set_size(input->tui_data, width_chars, height_chars); ui_client_set_size(width_chars, height_chars); } } break; case 'n': // Device Status Report (DSR) if (nparams == 2) { int args[2]; for (size_t i = 0; i < ARRAY_SIZE(args); i++) { if (termkey_interpret_csi_param(params[i], &args[i], NULL, NULL) != TERMKEY_RES_KEY) { return; } } if (args[0] == 997) { // Theme update notification // https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md // The second argument tells us whether the OS theme is set to light // mode or dark mode, but all we care about is the background color of // the terminal emulator. We query for that with OSC 11 and the response // is handled by the autocommand created in _defaults.lua. The terminal // may send us multiple notifications all at once so we use a timer to // coalesce the queries. if (uv_timer_get_due_in(&input->bg_query_timer) > 0) { return; } uv_timer_start(&input->bg_query_timer, bg_query_timer_cb, 100, 0); } } break; default: break; } } static size_t handle_raw_buffer(TermInput *input, bool force, const char *data, size_t size) { const char *ptr = data; do { if (!force) { size_t consumed = handle_focus_event(input, ptr, size); if (consumed) { ptr += consumed; size -= consumed; continue; } bool incomplete = false; consumed = handle_bracketed_paste(input, ptr, size, &incomplete); if (incomplete) { assert(consumed == 0); // Wait for the next input, leaving it in the raw buffer due to an // incomplete sequence. return (size_t)(ptr - data); } else if (consumed) { ptr += consumed; size -= consumed; continue; } } // // Find the next ESC and push everything up to it (excluding), so it will // be the first thing encountered on the next iteration. The `handle_*` // calls (above) depend on this. // size_t count = 0; for (size_t i = 0; i < size; i++) { count = i + 1; if (ptr[i] == '\x1b' && count > 1) { count--; break; } } // Push bytes directly (paste). if (input->paste) { tinput_enqueue(input, ptr, count); ptr += count; size -= count; continue; } // Push through libtermkey (translates to "" strings, etc.). { const size_t to_use = MIN(count, size); if (to_use > termkey_get_buffer_remaining(input->tk)) { // We are processing a very long escape sequence. Increase termkey's // internal buffer size. We don't handle out of memory situations so // abort if it fails const size_t delta = to_use - termkey_get_buffer_remaining(input->tk); const size_t bufsize = termkey_get_buffer_size(input->tk); if (!termkey_set_buffer_size(input->tk, MAX(bufsize + delta, bufsize * 2))) { abort(); } } size_t consumed = termkey_push_bytes(input->tk, ptr, to_use); // We resize termkey's buffer when it runs out of space, so this should // never happen assert(consumed <= to_use); ptr += consumed; size -= consumed; // Process the input buffer now for any keys tk_getkeys(input, false); } } while (size); const size_t tk_size = termkey_get_buffer_size(input->tk); const size_t tk_remaining = termkey_get_buffer_remaining(input->tk); const size_t tk_count = tk_size - tk_remaining; if (tk_count < INPUT_BUFFER_SIZE && tk_size > INPUT_BUFFER_SIZE) { // If the termkey buffer was resized to handle a large input sequence then // shrink it back down to its original size. if (!termkey_set_buffer_size(input->tk, INPUT_BUFFER_SIZE)) { abort(); } } return (size_t)(ptr - data); } static size_t tinput_read_cb(RStream *stream, const char *buf, size_t count_, void *data, bool eof) { TermInput *input = data; size_t consumed = handle_raw_buffer(input, false, buf, count_); tinput_flush(input); if (eof) { loop_schedule_fast(&main_loop, event_create(tinput_done_event, NULL)); return consumed; } // An incomplete sequence was found. Leave it in the raw buffer and wait for // the next input. if (consumed < count_) { // If 'ttimeout' is not set, start the timer with a timeout of 0 to process // the next input. int64_t ms = input->ttimeout ? (input->ttimeoutlen >= 0 ? input->ttimeoutlen : 0) : 0; // Stop the current timer if already running uv_timer_stop(&input->timer_handle); uv_timer_start(&input->timer_handle, tinput_timer_cb, (uint32_t)ms, 0); } return consumed; }