From bc63ffcf39e8ad6c0925c0ad8503bfb3ed8497f3 Mon Sep 17 00:00:00 2001 From: luukvbaal Date: Sun, 26 May 2024 19:54:08 +0200 Subject: fix(tui): reset clear_region attributes during startup #28713 Problem: Fix added in #28676 worked accidentally(used variables were themselves uninitialized at this point during startup) and does not always work. Solution: Reset attributes when clearing regions during startup. --- src/nvim/tui/tui.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 2a9530defb..dc8c8def5b 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -115,6 +115,7 @@ struct TUIData { kvec_t(HlAttrs) attrs; int print_attr_id; bool default_attr; + bool set_default_colors; bool can_clear_attr; ModeShape showing_mode; Integer verbose; @@ -166,14 +167,6 @@ void tui_start(TUIData **tui_p, int *width, int *height, char **term, bool *rgb) tui->seen_error_exit = 0; tui->loop = &main_loop; tui->url = -1; - // Because setting the default colors is delayed until after startup to avoid - // flickering with the default colorscheme background, any flush that happens - // during startup in turn would result in clearing invalidated regions with - // uninitialized attrs(black). Instead initialize clear_attrs with current - // terminal background so that it is at least not perceived as flickering, even - // though it may be different from the colorscheme that is set during startup. - tui->clear_attrs.rgb_bg_color = normal_bg; - tui->clear_attrs.cterm_bg_color = (int16_t)cterm_normal_bg_color; kv_init(tui->invalid_regions); kv_init(tui->urlbuf); @@ -1016,7 +1009,16 @@ static void clear_region(TUIData *tui, int top, int bot, int left, int right, in { UGrid *grid = &tui->grid; - update_attrs(tui, attr_id); + // Setting the default colors is delayed until after startup to avoid flickering + // with the default colorscheme background. Consequently, any flush that happens + // during startup would result in clearing invalidated regions with zeroed + // clear_attrs, perceived as a black flicker. Reset attributes to clear with + // current terminal background instead(#28667, #28668). + if (tui->set_default_colors) { + update_attrs(tui, attr_id); + } else { + unibi_out(tui, unibi_exit_attribute_mode); + } // Background is set to the default color and the right edge matches the // screen end, try to use terminal codes for clearing the requested area. @@ -1419,6 +1421,7 @@ void tui_default_colors_set(TUIData *tui, Integer rgb_fg, Integer rgb_bg, Intege tui->clear_attrs.cterm_bg_color = (int16_t)cterm_bg; tui->print_attr_id = -1; + tui->set_default_colors = true; invalidate(tui, 0, tui->grid.height, 0, tui->grid.width); } -- cgit From c13c50b752dca322a5ec77dea6188c9e3694549b Mon Sep 17 00:00:00 2001 From: bfredl Date: Thu, 30 May 2024 12:59:02 +0200 Subject: refactor(io): separate types for read and write streams This is a structural refactor with no logical changes, yet. Done in preparation for simplifying rstream/rbuffer which will require more state inline in RStream. The initial idea was to have RStream and WStream as sub-types symetrically but that doesn't work, as sockets are both reading and writing. Also there is very little write-specific state to start with, so the benefit of a separate WStream struct is a lot smaller. Just document what fields in `Stream` are write specific. --- src/nvim/tui/input.c | 4 ++-- src/nvim/tui/input.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index f1594dfcb9..588fed2d90 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -167,7 +167,7 @@ void tinput_destroy(TermInput *input) map_destroy(int, &kitty_key_map); rbuffer_free(input->key_buffer); uv_close((uv_handle_t *)&input->timer_handle, NULL); - stream_close(&input->read_stream, NULL, NULL); + rstream_may_close(&input->read_stream); termkey_destroy(input->tk); } @@ -737,7 +737,7 @@ static void handle_raw_buffer(TermInput *input, bool force) } } -static void tinput_read_cb(Stream *stream, RBuffer *buf, size_t count_, void *data, bool eof) +static void tinput_read_cb(RStream *stream, RBuffer *buf, size_t count_, void *data, bool eof) { TermInput *input = data; diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h index bf6d0f2978..646fbdd16a 100644 --- a/src/nvim/tui/input.h +++ b/src/nvim/tui/input.h @@ -33,7 +33,7 @@ typedef struct { TermKey_Terminfo_Getstr_Hook *tk_ti_hook_fn; ///< libtermkey terminfo hook uv_timer_t timer_handle; Loop *loop; - Stream read_stream; + RStream read_stream; RBuffer *key_buffer; TUIData *tui_data; } TermInput; -- cgit From 0ba087df5e3c69e1f5a5e6551dc05d6793f5f64f Mon Sep 17 00:00:00 2001 From: bfredl Date: Fri, 31 May 2024 17:51:52 +0200 Subject: refactor(tui): use a linear buffer for buffered keys This buffer is completely emptied every time it is read from. Thus there is no point in using a ring buffer. --- src/nvim/tui/input.c | 25 ++++++++----------------- src/nvim/tui/input.h | 4 +++- 2 files changed, 11 insertions(+), 18 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 588fed2d90..5130678a81 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -28,7 +28,6 @@ #include "nvim/msgpack_rpc/channel.h" #define READ_STREAM_SIZE 0xfff -#define KEY_BUFFER_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 @@ -132,7 +131,6 @@ void tinput_init(TermInput *input, Loop *loop) input->key_encoding = kKeyEncodingLegacy; input->ttimeout = (bool)p_ttimeout; input->ttimeoutlen = p_ttm; - input->key_buffer = rbuffer_new(KEY_BUFFER_SIZE); 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); @@ -165,7 +163,6 @@ void tinput_init(TermInput *input, Loop *loop) void tinput_destroy(TermInput *input) { map_destroy(int, &kitty_key_map); - rbuffer_free(input->key_buffer); uv_close((uv_handle_t *)&input->timer_handle, NULL); rstream_may_close(&input->read_stream); termkey_destroy(input->tk); @@ -191,44 +188,38 @@ static void tinput_done_event(void **argv) /// 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 - const size_t len = rbuffer_size(input->key_buffer); - String keys = { .data = xmallocz(len), .size = len }; - rbuffer_read(input->key_buffer, keys.data, len); 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); - api_free_string(keys); if (input->paste == 1) { // Paste phase: "continue" input->paste = 2; } - rbuffer_reset(input->key_buffer); } else { // enqueue input - RBUFFER_UNTIL_EMPTY(input->key_buffer, buf, len) { - const String keys = { .data = buf, .size = len }; + 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); - rbuffer_consumed(input->key_buffer, len); - rbuffer_reset(input->key_buffer); } } + input->key_buffer_len = 0; } static void tinput_enqueue(TermInput *input, char *buf, size_t size) { - if (rbuffer_size(input->key_buffer) > - rbuffer_capacity(input->key_buffer) - 0xff) { - // don't ever let the buffer get too full or we risk putting incomplete keys - // into it + if (input->key_buffer_len > KEY_BUFFER_SIZE - 0xff) { + // don't ever let the buffer get too full or we risk putting incomplete keys into it tinput_flush(input); } - rbuffer_write(input->key_buffer, buf, size); + 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. diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h index 646fbdd16a..c594228c07 100644 --- a/src/nvim/tui/input.h +++ b/src/nvim/tui/input.h @@ -17,6 +17,7 @@ typedef enum { kKeyEncodingXterm, ///< Xterm's modifyOtherKeys encoding (XTMODKEYS) } KeyEncoding; +#define KEY_BUFFER_SIZE 0xfff typedef struct { int in_fd; // Phases: -1=all 0=disabled 1=first-chunk 2=continue 3=last-chunk @@ -34,8 +35,9 @@ typedef struct { uv_timer_t timer_handle; Loop *loop; RStream read_stream; - RBuffer *key_buffer; TUIData *tui_data; + char key_buffer[KEY_BUFFER_SIZE]; + size_t key_buffer_len; } TermInput; typedef enum { -- cgit From 200e7ad1578619e78c664bd0c6be024168433412 Mon Sep 17 00:00:00 2001 From: James Tirta Halim Date: Mon, 3 Jun 2024 11:10:30 +0700 Subject: fixup: apply the change on more files --- src/nvim/tui/terminfo.c | 2 +- src/nvim/tui/tui.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/terminfo.c b/src/nvim/tui/terminfo.c index 3cf9650428..657bd6dd10 100644 --- a/src/nvim/tui/terminfo.c +++ b/src/nvim/tui/terminfo.c @@ -35,7 +35,7 @@ bool terminfo_is_term_family(const char *term, const char *family) // The screen terminfo may have a terminal name like screen.xterm. By making // the dot(.) a valid separator, such terminal names will also be the // terminal family of the screen. - && ('\0' == term[flen] || '-' == term[flen] || '.' == term[flen]); + && (NUL == term[flen] || '-' == term[flen] || '.' == term[flen]); } bool terminfo_is_bsd_console(const char *term) diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index dc8c8def5b..650133e6a2 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -2508,7 +2508,7 @@ static const char *tui_get_stty_erase(int fd) struct termios t; if (tcgetattr(fd, &t) != -1) { stty_erase[0] = (char)t.c_cc[VERASE]; - stty_erase[1] = '\0'; + stty_erase[1] = NUL; DLOG("stty/termios:erase=%s", stty_erase); } #endif -- cgit From d7651b27d54a87c5783c0a579af11da9a16a39aa Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Wed, 5 Jun 2024 08:27:56 -0500 Subject: fix(tui): move $COLORTERM check to _defaults.lua (#29197) We currently check $COLORTERM in the TUI process to determine if the terminal supports 24 bit color (truecolor). If $COLORTERM is "truecolor" or "24bit" then we automatically assume that the terminal supports truecolor, but if $COLORTERM is set to any other value we still query the terminal. The `rgb` flag of the UI struct is a boolean which only indicates whether the UI supports truecolor, but does not have a 3rd state that we can use to represent "we don't know if the UI supports truecolor". We currently use `rgb=false` to represent this "we don't know" state, and we use XTGETTCAP and DECRQSS queries to determine at runtime if the terminal supports truecolor. However, if $COLORTERM is set to a value besides "truecolor" or "24bit" (e.g. "256" or "16) that is a clear indication that the terminal _does not_ support truecolor, so it is incorrect to treat `rgb=false` as "we don't know" in that case. Instead, in the TUI process we only check for the terminfo capabilities. This must be done in the TUI process because we do not have access to this information in the core Neovim process when `_defaults.lua` runs. If the TUI cannot determine truecolor support from terminfo alone, we set `rgb=false` to indicate "we don't know if the terminal supports truecolor yet, keep checking". When we get to `_defaults.lua`, we can then check $COLORTERM and only query the terminal if it is unset. This means that users can explicitly opt out of truecolor determination by setting `COLORTERM=256` (or similar) in their environment. --- src/nvim/tui/tui.c | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 650133e6a2..57696b1839 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -1855,20 +1855,12 @@ static int unibi_find_ext_bool(unibi_term *ut, const char *name) return -1; } -/// Determine if the terminal supports truecolor or not: +/// Determine if the terminal supports truecolor or not. /// -/// 1. If $COLORTERM is "24bit" or "truecolor", return true -/// 2. Else, check terminfo for Tc, RGB, setrgbf, or setrgbb capabilities. If -/// found, return true -/// 3. Else, return false +/// If terminfo contains Tc, RGB, or both setrgbf and setrgbb capabilities, return true. static bool term_has_truecolor(TUIData *tui, const char *colorterm) { - // Check $COLORTERM - if (strequal(colorterm, "truecolor") || strequal(colorterm, "24bit")) { - return true; - } - - // Check for Tc and RGB + // Check for Tc or RGB for (size_t i = 0; i < unibi_count_ext_bool(tui->ut); i++) { const char *n = unibi_get_ext_bool_name(tui->ut, i); if (n && (!strcmp(n, "Tc") || !strcmp(n, "RGB"))) { -- cgit From 78d21593a35cf89692224f1000a04d3c9fff8add Mon Sep 17 00:00:00 2001 From: bfredl Date: Fri, 31 May 2024 14:40:53 +0200 Subject: refactor(io): make rstream use a linear buffer If you like it you shouldn't put a ring on it. This is what _every_ consumer of RStream used anyway, either by calling rbuffer_reset, or rbuffer_consumed_compact (same as rbuffer_reset without needing a scratch buffer), or by consuming everything in each stream_read_cb call directly. --- src/nvim/tui/input.c | 146 +++++++++++++++++++++++++-------------------------- src/nvim/tui/input.h | 9 +--- 2 files changed, 74 insertions(+), 81 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 5130678a81..a5768cfc06 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -15,7 +15,6 @@ #include "nvim/option_vars.h" #include "nvim/os/os.h" #include "nvim/os/os_defs.h" -#include "nvim/rbuffer.h" #include "nvim/strings.h" #include "nvim/tui/input.h" #include "nvim/tui/input_defs.h" @@ -153,7 +152,7 @@ void tinput_init(TermInput *input, Loop *loop) termkey_set_canonflags(input->tk, curflags | TERMKEY_CANON_DELBS); // setup input handle - rstream_init_fd(loop, &input->read_stream, input->in_fd, READ_STREAM_SIZE); + 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); @@ -211,9 +210,9 @@ static void tinput_flush(TermInput *input) input->key_buffer_len = 0; } -static void tinput_enqueue(TermInput *input, char *buf, size_t size) +static void tinput_enqueue(TermInput *input, const char *buf, size_t size) { - if (input->key_buffer_len > KEY_BUFFER_SIZE - 0xff) { + 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); } @@ -463,8 +462,10 @@ 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 paster sequence. - if (rbuffer_size(input->read_stream.buffer)) { - handle_raw_buffer(input, true); + 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); @@ -478,39 +479,37 @@ static void tinput_timer_cb(uv_timer_t *handle) /// /// @param input the input stream /// @return true iff handle_focus_event consumed some input -static bool handle_focus_event(TermInput *input) +static size_t handle_focus_event(TermInput *input, const char *ptr, size_t size) { - if (rbuffer_size(input->read_stream.buffer) > 2 - && (!rbuffer_cmp(input->read_stream.buffer, "\x1b[I", 3) - || !rbuffer_cmp(input->read_stream.buffer, "\x1b[O", 3))) { - bool focus_gained = *rbuffer_get(input->read_stream.buffer, 2) == 'I'; - // Advance past the sequence - rbuffer_consumed(input->read_stream.buffer, 3); + 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 true; + return 3; // Advance past the sequence } - return false; + return 0; } #define START_PASTE "\x1b[200~" #define END_PASTE "\x1b[201~" -static HandleState handle_bracketed_paste(TermInput *input) +static size_t handle_bracketed_paste(TermInput *input, const char *ptr, size_t size, + bool *incomplete) { - size_t buf_size = rbuffer_size(input->read_stream.buffer); - if (buf_size > 5 - && (!rbuffer_cmp(input->read_stream.buffer, START_PASTE, 6) - || !rbuffer_cmp(input->read_stream.buffer, END_PASTE, 6))) { - bool enable = *rbuffer_get(input->read_stream.buffer, 4) == '0'; + if (size >= 6 + && (!memcmp(ptr, START_PASTE, 6) + || !memcmp(ptr, END_PASTE, 6))) { + bool enable = ptr[4] == '0'; if (input->paste && enable) { - return kNotApplicable; // Pasting "start paste" code literally. + return 0; // Pasting "start paste" code literally. } + // Advance past the sequence - rbuffer_consumed(input->read_stream.buffer, 6); if (!!input->paste == enable) { - return kComplete; // Spurious "disable paste" code. + return 6; // Spurious "disable paste" code. } if (enable) { @@ -525,15 +524,15 @@ static HandleState handle_bracketed_paste(TermInput *input) // Paste phase: "disabled". input->paste = 0; } - return kComplete; - } else if (buf_size < 6 - && (!rbuffer_cmp(input->read_stream.buffer, START_PASTE, buf_size) - || !rbuffer_cmp(input->read_stream.buffer, - END_PASTE, buf_size))) { + 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. - return kIncomplete; + *incomplete = true; + return 0; } - return kNotApplicable; + return 0; } /// Handle an OSC or DCS response sequence from the terminal. @@ -644,20 +643,31 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) } } -static void handle_raw_buffer(TermInput *input, bool force) +static size_t handle_raw_buffer(TermInput *input, bool force, const char *data, size_t size) { - HandleState is_paste = kNotApplicable; + const char *ptr = data; do { - if (!force - && (handle_focus_event(input) - || (is_paste = handle_bracketed_paste(input)) != kNotApplicable)) { - if (is_paste == kIncomplete) { + 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; + return (size_t)(ptr - data); + } else if (consumed) { + ptr += consumed; + size -= consumed; + continue; } - continue; } // @@ -666,55 +676,47 @@ static void handle_raw_buffer(TermInput *input, bool force) // calls (above) depend on this. // size_t count = 0; - RBUFFER_EACH(input->read_stream.buffer, c, i) { + for (size_t i = 0; i < size; i++) { count = i + 1; - if (c == '\x1b' && count > 1) { + if (ptr[i] == '\x1b' && count > 1) { count--; break; } } // Push bytes directly (paste). if (input->paste) { - RBUFFER_UNTIL_EMPTY(input->read_stream.buffer, ptr, len) { - size_t consumed = MIN(count, len); - assert(consumed <= input->read_stream.buffer->size); - tinput_enqueue(input, ptr, consumed); - rbuffer_consumed(input->read_stream.buffer, consumed); - if (!(count -= consumed)) { - break; - } - } + tinput_enqueue(input, ptr, count); + ptr += count; + size -= count; continue; } + // Push through libtermkey (translates to "" strings, etc.). - RBUFFER_UNTIL_EMPTY(input->read_stream.buffer, ptr, len) { - const size_t size = MIN(count, len); - if (size > termkey_get_buffer_remaining(input->tk)) { + { + 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 = size - termkey_get_buffer_remaining(input->tk); + 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, size); + 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 <= rbuffer_size(input->read_stream.buffer)); - rbuffer_consumed(input->read_stream.buffer, consumed); + assert(consumed <= to_use); + ptr += consumed; + size -= consumed; // Process the input buffer now for any keys tk_getkeys(input, false); - - if (!(count -= consumed)) { - break; - } } - } while (rbuffer_size(input->read_stream.buffer)); + } while (size); const size_t tk_size = termkey_get_buffer_size(input->tk); const size_t tk_remaining = termkey_get_buffer_remaining(input->tk); @@ -726,23 +728,25 @@ static void handle_raw_buffer(TermInput *input, bool force) abort(); } } + + return (size_t)(ptr - data); } -static void tinput_read_cb(RStream *stream, RBuffer *buf, size_t count_, void *data, bool eof) +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; + return consumed; } - handle_raw_buffer(input, false); - tinput_flush(input); - // An incomplete sequence was found. Leave it in the raw buffer and wait for // the next input. - if (rbuffer_size(input->read_stream.buffer)) { + 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 @@ -750,11 +754,7 @@ static void tinput_read_cb(RStream *stream, RBuffer *buf, size_t count_, void *d // 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; } - // Make sure the next input escape sequence fits into the ring buffer without - // wraparound, else it could be misinterpreted (because rbuffer_read_ptr() - // exposes the underlying buffer to callers unaware of the wraparound). - rbuffer_reset(input->read_stream.buffer); + return consumed; } diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h index c594228c07..8d0c0c20e9 100644 --- a/src/nvim/tui/input.h +++ b/src/nvim/tui/input.h @@ -5,7 +5,6 @@ #include #include "nvim/event/defs.h" -#include "nvim/rbuffer_defs.h" #include "nvim/tui/input_defs.h" // IWYU pragma: keep #include "nvim/tui/tui_defs.h" #include "nvim/types_defs.h" @@ -17,7 +16,7 @@ typedef enum { kKeyEncodingXterm, ///< Xterm's modifyOtherKeys encoding (XTMODKEYS) } KeyEncoding; -#define KEY_BUFFER_SIZE 0xfff +#define KEY_BUFFER_SIZE 0x1000 typedef struct { int in_fd; // Phases: -1=all 0=disabled 1=first-chunk 2=continue 3=last-chunk @@ -40,12 +39,6 @@ typedef struct { size_t key_buffer_len; } TermInput; -typedef enum { - kIncomplete = -1, - kNotApplicable = 0, - kComplete = 1, -} HandleState; - #ifdef INCLUDE_GENERATED_DECLARATIONS # include "tui/input.h.generated.h" #endif -- cgit From 25c59d08c4df9952c606bbc96b7b26dca429bb9c Mon Sep 17 00:00:00 2001 From: dundargoc <33953936+dundargoc@users.noreply.github.com> Date: Thu, 4 Jul 2024 23:20:45 +0200 Subject: docs: misc (#29410) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Härtl Co-authored-by: zeertzjq --- src/nvim/tui/tui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 57696b1839..67ada24cd7 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -1704,7 +1704,7 @@ void tui_guess_size(TUIData *tui) int width = 0; int height = 0; - // 1 - try from a system call(ioctl/TIOCGWINSZ on unix) + // 1 - try from a system call (ioctl/TIOCGWINSZ on unix) if (tui->out_isatty && !uv_tty_get_winsize(&tui->output_handle.tty, &width, &height)) { goto end; -- cgit From e41368f3bc1d08d900425608bd199f585d6fce59 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Fri, 19 Jul 2024 08:29:29 -0500 Subject: feat(tui): support in-band resize events (#29791) DEC mode 2048 is a newly proposed private mode for sending resize events in band to applications from the terminal emulator, instead of relying on SIGWINCH. Full text of the specification is here: https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83 --- src/nvim/tui/input.c | 9 +++++++++ src/nvim/tui/tui.c | 35 ++++++++++++++++++++++++++++++----- src/nvim/tui/tui_defs.h | 1 + 3 files changed, 40 insertions(+), 5 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index a5768cfc06..9f58607bf7 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -638,6 +638,15 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) break; } break; + case 't': + if (nargs == 5 && args[0] == 48) { + // In-band resize event (DEC private mode 2048) + int height_chars = (int)args[1]; + int width_chars = (int)args[2]; + tui_set_size(input->tui_data, width_chars, height_chars); + ui_client_set_size(width_chars, height_chars); + } + break; default: break; } diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 67ada24cd7..c7ec013b28 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -171,6 +171,7 @@ void tui_start(TUIData **tui_p, int *width, int *height, char **term, bool *rgb) kv_init(tui->invalid_regions); kv_init(tui->urlbuf); signal_watcher_init(tui->loop, &tui->winch_handle, tui); + signal_watcher_start(&tui->winch_handle, sigwinch_cb, SIGWINCH); // TODO(bfredl): zero hl is empty, send this explicitly? kv_push(tui->attrs, HLATTRS_INIT); @@ -205,6 +206,16 @@ static void tui_request_term_mode(TUIData *tui, TermMode mode) out(tui, buf, (size_t)len); } +/// Set (DECSET) or reset (DECRST) a terminal mode. +static void tui_set_term_mode(TUIData *tui, TermMode mode, bool set) + FUNC_ATTR_NONNULL_ALL +{ + char buf[12]; + int len = snprintf(buf, sizeof(buf), "\x1b[?%d%c", (int)mode, set ? 'h' : 'l'); + assert((len > 0) && (len < (int)sizeof(buf))); + out(tui, buf, (size_t)len); +} + /// Handle a mode report (DECRPM) from the terminal. void tui_handle_term_mode(TUIData *tui, TermMode mode, TermModeState state) FUNC_ATTR_NONNULL_ALL @@ -224,6 +235,11 @@ void tui_handle_term_mode(TUIData *tui, TermMode mode, TermModeState state) // Ref: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036 tui->unibi_ext.sync = (int)unibi_add_ext_str(tui->ut, "Sync", "\x1b[?2026%?%p1%{1}%-%tl%eh%;"); + break; + case kTermModeResizeEvents: + signal_watcher_stop(&tui->winch_handle); + tui_set_term_mode(tui, mode, true); + break; } } } @@ -417,6 +433,7 @@ static void terminfo_start(TUIData *tui) // Some terminals (such as Terminal.app) do not support DECRQM, so skip the query. if (!nsterm) { tui_request_term_mode(tui, kTermModeSynchronizedOutput); + tui_request_term_mode(tui, kTermModeResizeEvents); } // Don't use DECRQSS in screen or tmux, as they behave strangely when receiving it. @@ -475,6 +492,9 @@ static void terminfo_stop(TUIData *tui) // Reset the key encoding tui_reset_key_encoding(tui); + // Disable resize events + tui_set_term_mode(tui, kTermModeResizeEvents, false); + // May restore old title before exiting alternate screen. tui_set_title(tui, NULL_STRING); if (ui_client_exit_status == 0) { @@ -510,7 +530,6 @@ static void tui_terminal_start(TUIData *tui) tui->print_attr_id = -1; terminfo_start(tui); tui_guess_size(tui); - signal_watcher_start(&tui->winch_handle, sigwinch_cb, SIGWINCH); tinput_start(&tui->input); } @@ -539,7 +558,6 @@ static void tui_terminal_stop(TUIData *tui) return; } tinput_stop(&tui->input); - signal_watcher_stop(&tui->winch_handle); // Position the cursor on the last screen line, below all the text cursor_goto(tui, tui->height - 1, 0); terminfo_stop(tui); @@ -556,6 +574,7 @@ void tui_stop(TUIData *tui) stream_set_blocking(tui->input.in_fd, true); // normalize stream (#2598) tinput_destroy(&tui->input); tui->stopped = true; + signal_watcher_stop(&tui->winch_handle); signal_watcher_close(&tui->winch_handle, NULL); uv_close((uv_handle_t *)&tui->startup_delay_timer, NULL); } @@ -1697,6 +1716,14 @@ static void ensure_space_buf_size(TUIData *tui, size_t len) } } +void tui_set_size(TUIData *tui, int width, int height) + FUNC_ATTR_NONNULL_ALL +{ + tui->width = width; + tui->height = height; + ensure_space_buf_size(tui, (size_t)tui->width); +} + /// Tries to get the user's wanted dimensions (columns and rows) for the entire /// application (i.e., the host terminal). void tui_guess_size(TUIData *tui) @@ -1731,9 +1758,7 @@ void tui_guess_size(TUIData *tui) height = DFLT_ROWS; } - tui->width = width; - tui->height = height; - ensure_space_buf_size(tui, (size_t)tui->width); + tui_set_size(tui, width, height); // Redraw on SIGWINCH event if size didn't change. #23411 ui_client_set_size(width, height); diff --git a/src/nvim/tui/tui_defs.h b/src/nvim/tui/tui_defs.h index c5149d4829..46913e07a2 100644 --- a/src/nvim/tui/tui_defs.h +++ b/src/nvim/tui/tui_defs.h @@ -4,6 +4,7 @@ typedef struct TUIData TUIData; typedef enum { kTermModeSynchronizedOutput = 2026, + kTermModeResizeEvents = 2048, } TermMode; typedef enum { -- cgit From f93ecd2760f5859fd5eeec28c7c2196ece98e9a1 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Sun, 21 Jul 2024 21:47:37 -0500 Subject: feat(tui): parse CSI subparams in termkey (#29805) libtermkey does not know how to parse CSI subparameters (parameters separated by ':', ASCII 0x3A) and currently just ignores them. However, many important CSI sequences sent by the terminal make use of subparameters, most notably key events when using the kitty keyboard protocol [1]. Enabling subparameters is a prerequisite for expanding kitty keyboard protocol support in Neovim. Concretely, we do this by returning pointers into the internal termkey buffer for each CSI parameter rather than parsing them into integers directly. When a caller wants to actually use the parameter as an integer, they must call termkey_interpret_csi_param, which parses the full parameter string into an integer parameter and zero or more subparameters. The pointers into the internal buffer will become invalidated when new input arrives from the terminal so it is important that the individual params are used and parsed right away. All of our code (and libtermkey's code) does this, so this is fine for now, but is something to keep in mind moving forward. [1]: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ --- src/nvim/tui/input.c | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 9f58607bf7..a6e27c9391 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -596,10 +596,10 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) { // 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 - long args[16]; - size_t nargs = 16; + TermKeyCsiParam params[16]; + size_t nparams = 16; unsigned long cmd; - if (termkey_interpret_csi(input->tk, key, args, &nargs, &cmd) != TERMKEY_RES_KEY) { + if (termkey_interpret_csi(input->tk, key, params, &nparams, &cmd) != TERMKEY_RES_KEY) { return; } @@ -639,12 +639,22 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) } break; case 't': - if (nargs == 5 && args[0] == 48) { - // In-band resize event (DEC private mode 2048) - int height_chars = (int)args[1]; - int width_chars = (int)args[2]; - tui_set_size(input->tui_data, width_chars, height_chars); - ui_client_set_size(width_chars, height_chars); + if (nparams == 5) { + // We only care about the first 3 parameters, and we ignore subparameters + long 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 = (int)args[1]; + int width_chars = (int)args[2]; + tui_set_size(input->tui_data, width_chars, height_chars); + ui_client_set_size(width_chars, height_chars); + } } break; default: -- cgit From b02c83941493db79e4ab7ba23adb665d4528f791 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:04:09 -0500 Subject: fix(tui): set id parameter in OSC 8 sequences (#29840) The id parameter is used to communicate to the terminal that two URLs are the same. Without an id, the terminal must rely on heuristics to determine which cells belong together to make a single hyperlink. See the relevant section in the spec [1] for more details. [1]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#hover-underlining-and-the-id-parameter --- src/nvim/tui/tui.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index c7ec013b28..0adf0712c0 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -793,7 +793,12 @@ static void update_attrs(TUIData *tui, int attr_id) if (attrs.url >= 0) { const char *url = urls.keys[attrs.url]; kv_size(tui->urlbuf) = 0; - kv_printf(tui->urlbuf, "\x1b]8;;%s\x1b\\", url); + + // Add some fixed offset to the URL ID to deconflict with other + // applications which may set their own IDs + const uint64_t id = 0xE1EA0000U + (uint32_t)attrs.url; + + kv_printf(tui->urlbuf, "\x1b]8;id=%" PRIu64 ";%s\x1b\\", id, url); out(tui, tui->urlbuf.items, kv_size(tui->urlbuf)); } else { out(tui, S_LEN("\x1b]8;;\x1b\\")); -- cgit From f32557ca679cbb1d7de52ab54dc35585af9ab9d0 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:00:04 -0500 Subject: fix(tui): reset active attr ID when OSC 8 sequence is terminated (#29960) When the cursor is moved we terminate any active OSC 8 sequences to prevent the sequence from inadvertently spanning regions it is not meant to span. However, if we do not also reset the TUI's active attr id (print_attr_id) then the TUI does not "know" that it's current attribute set has changed. When cursor_goto is called to wrap a line, the TUI does not recompute the attributes so the OSC 8 sequence is not restarted again. When we terminate an OSC 8 sequence before moving the cursor, also reset the active attr id so that the attributes are recomputed for URLs. --- src/nvim/tui/tui.c | 1 + 1 file changed, 1 insertion(+) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 0adf0712c0..d8b79fb193 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -888,6 +888,7 @@ static void cursor_goto(TUIData *tui, int row, int col) if (tui->url >= 0) { out(tui, S_LEN("\x1b]8;;\x1b\\")); tui->url = -1; + tui->print_attr_id = -1; } if (0 == row && 0 == col) { -- cgit From cd05a72fec49bfaa3911c141ac605b67b6e2270a Mon Sep 17 00:00:00 2001 From: dundargoc <33953936+dundargoc@users.noreply.github.com> Date: Thu, 29 Aug 2024 00:11:32 +0200 Subject: docs: misc (#29719) Co-authored-by: Evgeni Chasnovski Co-authored-by: Lauri Heiskanen Co-authored-by: Piotr Doroszewski <5605596+Doroszewski@users.noreply.github.com> Co-authored-by: Tobiasz Laskowski Co-authored-by: ariel-lindemann <41641978+ariel-lindemann@users.noreply.github.com> Co-authored-by: glepnir Co-authored-by: zeertzjq --- src/nvim/tui/tui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index d8b79fb193..1866a4a592 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -1038,7 +1038,7 @@ static void clear_region(TUIData *tui, int top, int bot, int left, int right, in // with the default colorscheme background. Consequently, any flush that happens // during startup would result in clearing invalidated regions with zeroed // clear_attrs, perceived as a black flicker. Reset attributes to clear with - // current terminal background instead(#28667, #28668). + // current terminal background instead (#28667, #28668). if (tui->set_default_colors) { update_attrs(tui, attr_id); } else { -- cgit From cfdf68a7acde16597fbd896674af68c42361102c Mon Sep 17 00:00:00 2001 From: bfredl Date: Thu, 8 Aug 2024 10:42:08 +0200 Subject: feat(mbyte): support extended grapheme clusters including more emoji Use the grapheme break algorithm from utf8proc to support grapheme clusters from recent unicode versions. Handle variant selector VS16 turning some codepoints into double-width emoji. This means we need to use ptr2cells rather than char2cells when possible. --- src/nvim/tui/tui.c | 17 +++++++++++++++-- src/nvim/tui/tui_defs.h | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 1866a4a592..7e1068ed56 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -109,6 +109,7 @@ struct TUIData { bool set_cursor_color_as_str; bool cursor_color_changed; bool is_starting; + bool did_set_grapheme_cluster_mode; FILE *screenshot; cursorentry_T cursor_shapes[SHAPE_IDX_COUNT]; HlAttrs clear_attrs; @@ -220,6 +221,7 @@ static void tui_set_term_mode(TUIData *tui, TermMode mode, bool set) void tui_handle_term_mode(TUIData *tui, TermMode mode, TermModeState state) FUNC_ATTR_NONNULL_ALL { + bool is_set = false; switch (state) { case kTermModeNotRecognized: case kTermModePermanentlySet: @@ -228,6 +230,8 @@ void tui_handle_term_mode(TUIData *tui, TermMode mode, TermModeState state) // then there is nothing to do break; case kTermModeSet: + is_set = true; + FALLTHROUGH; case kTermModeReset: // The terminal supports changing the given mode switch (mode) { @@ -240,6 +244,12 @@ void tui_handle_term_mode(TUIData *tui, TermMode mode, TermModeState state) signal_watcher_stop(&tui->winch_handle); tui_set_term_mode(tui, mode, true); break; + case kTermModeGraphemeClusters: + if (!is_set) { + tui_set_term_mode(tui, mode, true); + tui->did_set_grapheme_cluster_mode = true; + } + break; } } } @@ -434,6 +444,7 @@ static void terminfo_start(TUIData *tui) if (!nsterm) { tui_request_term_mode(tui, kTermModeSynchronizedOutput); tui_request_term_mode(tui, kTermModeResizeEvents); + tui_request_term_mode(tui, kTermModeGraphemeClusters); } // Don't use DECRQSS in screen or tmux, as they behave strangely when receiving it. @@ -494,7 +505,9 @@ static void terminfo_stop(TUIData *tui) // Disable resize events tui_set_term_mode(tui, kTermModeResizeEvents, false); - + if (tui->did_set_grapheme_cluster_mode) { + tui_set_term_mode(tui, kTermModeGraphemeClusters, false); + } // May restore old title before exiting alternate screen. tui_set_title(tui, NULL_STRING); if (ui_client_exit_status == 0) { @@ -1010,7 +1023,7 @@ static void print_cell_at_pos(TUIData *tui, int row, int col, UCell *cell, bool char buf[MAX_SCHAR_SIZE]; schar_get(buf, cell->data); int c = utf_ptr2char(buf); - bool is_ambiwidth = utf_ambiguous_width(c); + bool is_ambiwidth = utf_ambiguous_width(buf); if (is_doublewidth && (is_ambiwidth || utf_char2cells(c) == 1)) { // If the server used setcellwidths() to treat a single-width char as double-width, // it needs to be treated like an ambiguous-width char. diff --git a/src/nvim/tui/tui_defs.h b/src/nvim/tui/tui_defs.h index 46913e07a2..bd99d6b0ad 100644 --- a/src/nvim/tui/tui_defs.h +++ b/src/nvim/tui/tui_defs.h @@ -4,6 +4,7 @@ typedef struct TUIData TUIData; typedef enum { kTermModeSynchronizedOutput = 2026, + kTermModeGraphemeClusters = 2027, kTermModeResizeEvents = 2048, } TermMode; -- cgit From f9108378b7a7e08b48685f0a3ff4f7a3a14b56d6 Mon Sep 17 00:00:00 2001 From: dundargoc Date: Wed, 14 Aug 2024 15:52:51 +0200 Subject: refactor: adopt termkey and eliminate duplicate code Termkey is abandoned and it's now our code, so there's no reason not to treat it as such. An alternative approach could be to have a proper repo that we maintain such as with unibilium, although with this approach we can make a few assumptions that will allow us to remove more code. Also eliminate duplicate code from both termkey and libvterm. --- src/nvim/tui/input.c | 17 +- src/nvim/tui/input.h | 2 +- src/nvim/tui/termkey/README | 1 + src/nvim/tui/termkey/driver-csi.c | 902 +++++++++++++++++++++ src/nvim/tui/termkey/driver-csi.h | 7 + src/nvim/tui/termkey/driver-ti.c | 593 ++++++++++++++ src/nvim/tui/termkey/driver-ti.h | 7 + src/nvim/tui/termkey/termkey-internal.h | 109 +++ src/nvim/tui/termkey/termkey.c | 1315 +++++++++++++++++++++++++++++++ src/nvim/tui/termkey/termkey.h | 10 + src/nvim/tui/termkey/termkey_defs.h | 199 +++++ 11 files changed, 3154 insertions(+), 8 deletions(-) create mode 100644 src/nvim/tui/termkey/README create mode 100644 src/nvim/tui/termkey/driver-csi.c create mode 100644 src/nvim/tui/termkey/driver-csi.h create mode 100644 src/nvim/tui/termkey/driver-ti.c create mode 100644 src/nvim/tui/termkey/driver-ti.h create mode 100644 src/nvim/tui/termkey/termkey-internal.h create mode 100644 src/nvim/tui/termkey/termkey.c create mode 100644 src/nvim/tui/termkey/termkey.h create mode 100644 src/nvim/tui/termkey/termkey_defs.h (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index a6e27c9391..3eb8d4ba2e 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -7,24 +7,27 @@ #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" #include "nvim/event/loop.h" +#include "nvim/event/rstream.h" #include "nvim/event/stream.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 -#include "nvim/event/rstream.h" -#include "nvim/msgpack_rpc/channel.h" #define READ_STREAM_SIZE 0xfff @@ -261,7 +264,7 @@ static size_t handle_more_modifiers(TermKeyKey *key, char *buf, size_t buflen) static void handle_kitty_key_protocol(TermInput *input, TermKeyKey *key) { - const char *name = pmap_get(int)(&kitty_key_map, (int)key->code.codepoint); + const char *name = pmap_get(int)(&kitty_key_map, key->code.codepoint); if (name) { char buf[64]; size_t len = 0; @@ -598,7 +601,7 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) // contain, so just allocate enough space for a large upper bound TermKeyCsiParam params[16]; size_t nparams = 16; - unsigned long cmd; + unsigned cmd; if (termkey_interpret_csi(input->tk, key, params, &nparams, &cmd) != TERMKEY_RES_KEY) { return; } @@ -641,7 +644,7 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) case 't': if (nparams == 5) { // We only care about the first 3 parameters, and we ignore subparameters - long args[3]; + 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; @@ -650,8 +653,8 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) if (args[0] == 48) { // In-band resize event (DEC private mode 2048) - int height_chars = (int)args[1]; - int width_chars = (int)args[2]; + 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); } diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h index 8d0c0c20e9..4c2baf908e 100644 --- a/src/nvim/tui/input.h +++ b/src/nvim/tui/input.h @@ -6,9 +6,9 @@ #include "nvim/event/defs.h" #include "nvim/tui/input_defs.h" // IWYU pragma: keep +#include "nvim/tui/termkey/termkey_defs.h" #include "nvim/tui/tui_defs.h" #include "nvim/types_defs.h" -#include "termkey/termkey.h" typedef enum { kKeyEncodingLegacy, ///< Legacy key encoding diff --git a/src/nvim/tui/termkey/README b/src/nvim/tui/termkey/README new file mode 100644 index 0000000000..fd081025fc --- /dev/null +++ b/src/nvim/tui/termkey/README @@ -0,0 +1 @@ +// Adapted from libtermkey: https://github.com/neovim/libtermkey diff --git a/src/nvim/tui/termkey/driver-csi.c b/src/nvim/tui/termkey/driver-csi.c new file mode 100644 index 0000000000..28c7eaccfd --- /dev/null +++ b/src/nvim/tui/termkey/driver-csi.c @@ -0,0 +1,902 @@ +#include +#include +#include + +#include "nvim/memory.h" +#include "nvim/tui/termkey/driver-csi.h" +#include "nvim/tui/termkey/termkey-internal.h" +#include "nvim/tui/termkey/termkey.h" +#include "nvim/tui/termkey/termkey_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/driver-csi.c.generated.h" +#endif + +// There are 64 codes 0x40 - 0x7F +static int keyinfo_initialised = 0; +static struct keyinfo ss3s[64]; +static char ss3_kpalts[64]; + +typedef TermKeyResult CsiHandler(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams); +static CsiHandler *csi_handlers[64]; + +// Handler for CSI/SS3 cmd keys + +static struct keyinfo csi_ss3s[64]; + +static TermKeyResult handle_csi_ss3_full(TermKey *tk, TermKeyKey *key, int cmd, + TermKeyCsiParam *params, int nparams) +{ + TermKeyResult result = TERMKEY_RES_KEY; + + if (nparams > 1 && params[1].param != NULL) { + int arg = 0; + result = termkey_interpret_csi_param(params[1], &arg, NULL, NULL); + if (result != TERMKEY_RES_KEY) { + return result; + } + + key->modifiers = arg - 1; + } else { + key->modifiers = 0; + } + + key->type = csi_ss3s[cmd - 0x40].type; + key->code.sym = csi_ss3s[cmd - 0x40].sym; + key->modifiers &= ~(csi_ss3s[cmd - 0x40].modifier_mask); + key->modifiers |= csi_ss3s[cmd - 0x40].modifier_set; + + if (key->code.sym == TERMKEY_SYM_UNKNOWN) { + result = TERMKEY_RES_NONE; + } + + return result; +} + +static void register_csi_ss3_full(TermKeyType type, TermKeySym sym, int modifier_set, + int modifier_mask, unsigned char cmd) +{ + if (cmd < 0x40 || cmd >= 0x80) { + return; + } + + csi_ss3s[cmd - 0x40].type = type; + csi_ss3s[cmd - 0x40].sym = sym; + csi_ss3s[cmd - 0x40].modifier_set = modifier_set; + csi_ss3s[cmd - 0x40].modifier_mask = modifier_mask; + + csi_handlers[cmd - 0x40] = &handle_csi_ss3_full; +} + +static void register_csi_ss3(TermKeyType type, TermKeySym sym, unsigned char cmd) +{ + register_csi_ss3_full(type, sym, 0, 0, cmd); +} + +/// Handler for SS3 keys with kpad alternate representations +static void register_ss3kpalt(TermKeyType type, TermKeySym sym, unsigned char cmd, char kpalt) +{ + if (cmd < 0x40 || cmd >= 0x80) { + return; + } + + ss3s[cmd - 0x40].type = type; + ss3s[cmd - 0x40].sym = sym; + ss3s[cmd - 0x40].modifier_set = 0; + ss3s[cmd - 0x40].modifier_mask = 0; + ss3_kpalts[cmd - 0x40] = kpalt; +} + +// Handler for CSI number ~ function keys + +#define NCSIFUNCS 35 // This value must be increased if more CSI function keys are added +static struct keyinfo csifuncs[NCSIFUNCS]; + +static TermKeyResult handle_csifunc(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams) +{ + if (nparams == 0) { + return TERMKEY_RES_NONE; + } + + TermKeyResult result = TERMKEY_RES_KEY; + int args[3]; + + if (nparams > 1 && params[1].param != NULL) { + result = termkey_interpret_csi_param(params[1], &args[1], NULL, NULL); + if (result != TERMKEY_RES_KEY) { + return result; + } + + key->modifiers = args[1] - 1; + } else { + key->modifiers = 0; + } + + key->type = TERMKEY_TYPE_KEYSYM; + + result = termkey_interpret_csi_param(params[0], &args[0], NULL, NULL); + if (result != TERMKEY_RES_KEY) { + return result; + } + + if (args[0] == 27 && nparams > 2 && params[2].param != NULL) { + result = termkey_interpret_csi_param(params[2], &args[2], NULL, NULL); + if (result != TERMKEY_RES_KEY) { + return result; + } + + int mod = key->modifiers; + (*tk->method.emit_codepoint)(tk, args[2], key); + key->modifiers |= mod; + } else if (args[0] >= 0 && args[0] < NCSIFUNCS) { + key->type = csifuncs[args[0]].type; + key->code.sym = csifuncs[args[0]].sym; + key->modifiers &= ~(csifuncs[args[0]].modifier_mask); + key->modifiers |= csifuncs[args[0]].modifier_set; + } else { + key->code.sym = TERMKEY_SYM_UNKNOWN; + } + + if (key->code.sym == TERMKEY_SYM_UNKNOWN) { +#ifdef DEBUG + fprintf(stderr, "CSI: Unknown function key %ld\n", arg[0]); +#endif + result = TERMKEY_RES_NONE; + } + + return result; +} + +static void register_csifunc(TermKeyType type, TermKeySym sym, int number) +{ + if (number >= NCSIFUNCS) { + return; + } + + csifuncs[number].type = type; + csifuncs[number].sym = sym; + csifuncs[number].modifier_set = 0; + csifuncs[number].modifier_mask = 0; + + csi_handlers['~' - 0x40] = &handle_csifunc; +} + +/// Handler for CSI u extended Unicode keys +static TermKeyResult handle_csi_u(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams) +{ + switch (cmd) { + case 'u': { + int args[2]; + if (nparams > 1 && params[1].param != NULL) { + int subparam = 0; + size_t nsubparams = 1; + if (termkey_interpret_csi_param(params[1], &args[1], &subparam, + &nsubparams) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } + + if (nsubparams > 0 && subparam != 1) { + // Not a press event. Ignore for now + return TERMKEY_RES_NONE; + } + + key->modifiers = args[1] - 1; + } else { + key->modifiers = 0; + } + + if (termkey_interpret_csi_param(params[0], &args[0], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } + + int mod = key->modifiers; + key->type = TERMKEY_TYPE_KEYSYM; + (*tk->method.emit_codepoint)(tk, args[0], key); + key->modifiers |= mod; + + return TERMKEY_RES_KEY; + } + default: + return TERMKEY_RES_NONE; + } +} + +/// Handler for CSI M / CSI m mouse events in SGR and rxvt encodings +/// Note: This does not handle X10 encoding +static TermKeyResult handle_csi_m(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams) +{ + int initial = cmd >> 8; + cmd &= 0xff; + + switch (cmd) { + case 'M': + case 'm': + break; + default: + return TERMKEY_RES_NONE; + } + + if (nparams < 3) { + return TERMKEY_RES_NONE; + } + + int args[3]; + for (size_t i = 0; i < 3; i++) { + if (termkey_interpret_csi_param(params[i], &args[i], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } + } + + if (!initial) { // rxvt protocol + key->type = TERMKEY_TYPE_MOUSE; + key->code.mouse[0] = (char)args[0]; + + key->modifiers = (key->code.mouse[0] & 0x1c) >> 2; + key->code.mouse[0] &= ~0x1c; + + termkey_key_set_linecol(key, args[1], args[2]); + + return TERMKEY_RES_KEY; + } + + if (initial == '<') { // SGR protocol + key->type = TERMKEY_TYPE_MOUSE; + key->code.mouse[0] = (char)args[0]; + + key->modifiers = (key->code.mouse[0] & 0x1c) >> 2; + key->code.mouse[0] &= ~0x1c; + + termkey_key_set_linecol(key, args[1], args[2]); + + if (cmd == 'm') { // release + key->code.mouse[3] |= 0x80; + } + + return TERMKEY_RES_KEY; + } + + return TERMKEY_RES_NONE; +} + +TermKeyResult termkey_interpret_mouse(TermKey *tk, const TermKeyKey *key, TermKeyMouseEvent *event, + int *button, int *line, int *col) +{ + if (key->type != TERMKEY_TYPE_MOUSE) { + return TERMKEY_RES_NONE; + } + + if (button) { + *button = 0; + } + + termkey_key_get_linecol(key, line, col); + + if (!event) { + return TERMKEY_RES_KEY; + } + + int btn = 0; + + int code = (unsigned char)key->code.mouse[0]; + + int drag = code & 0x20; + + code &= ~0x3c; + + switch (code) { + case 0: + case 1: + case 2: + *event = drag ? TERMKEY_MOUSE_DRAG : TERMKEY_MOUSE_PRESS; + btn = code + 1; + break; + + case 3: + *event = TERMKEY_MOUSE_RELEASE; + // no button hint + break; + + case 64: + case 65: + case 66: + case 67: + *event = drag ? TERMKEY_MOUSE_DRAG : TERMKEY_MOUSE_PRESS; + btn = code + 4 - 64; + break; + + default: + *event = TERMKEY_MOUSE_UNKNOWN; + } + + if (button) { + *button = btn; + } + + if (key->code.mouse[3] & 0x80) { + *event = TERMKEY_MOUSE_RELEASE; + } + + return TERMKEY_RES_KEY; +} + +/// Handler for CSI ? R position reports +/// A plain CSI R with no arguments is probably actually +static TermKeyResult handle_csi_R(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams) +{ + switch (cmd) { + case 'R'|'?' << 8: + if (nparams < 2) { + return TERMKEY_RES_NONE; + } + + int args[2]; + if (termkey_interpret_csi_param(params[0], &args[0], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } + + if (termkey_interpret_csi_param(params[1], &args[1], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } + + key->type = TERMKEY_TYPE_POSITION; + termkey_key_set_linecol(key, args[1], args[0]); + return TERMKEY_RES_KEY; + + default: + return handle_csi_ss3_full(tk, key, cmd, params, nparams); + } +} + +TermKeyResult termkey_interpret_position(TermKey *tk, const TermKeyKey *key, int *line, int *col) +{ + if (key->type != TERMKEY_TYPE_POSITION) { + return TERMKEY_RES_NONE; + } + + termkey_key_get_linecol(key, line, col); + + return TERMKEY_RES_KEY; +} + +/// Handler for CSI $y mode status reports +static TermKeyResult handle_csi_y(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams) +{ + switch (cmd) { + case 'y'|'$' << 16: + case 'y'|'$' << 16 | '?' << 8: + if (nparams < 2) { + return TERMKEY_RES_NONE; + } + + int args[2]; + if (termkey_interpret_csi_param(params[0], &args[0], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } + + if (termkey_interpret_csi_param(params[1], &args[1], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } + + key->type = TERMKEY_TYPE_MODEREPORT; + key->code.mouse[0] = (char)(cmd >> 8); + key->code.mouse[1] = (char)(args[0] >> 8); + key->code.mouse[2] = (char)(args[0] & 0xff); + key->code.mouse[3] = (char)args[1]; + return TERMKEY_RES_KEY; + + default: + return TERMKEY_RES_NONE; + } +} + +TermKeyResult termkey_interpret_modereport(TermKey *tk, const TermKeyKey *key, int *initial, + int *mode, int *value) +{ + if (key->type != TERMKEY_TYPE_MODEREPORT) { + return TERMKEY_RES_NONE; + } + + if (initial) { + *initial = (unsigned char)key->code.mouse[0]; + } + + if (mode) { + *mode = ((uint8_t)key->code.mouse[1] << 8) | (uint8_t)key->code.mouse[2]; + } + + if (value) { + *value = (unsigned char)key->code.mouse[3]; + } + + return TERMKEY_RES_KEY; +} + +#define CHARAT(i) (tk->buffer[tk->buffstart + (i)]) + +static TermKeyResult parse_csi(TermKey *tk, size_t introlen, size_t *csi_len, + TermKeyCsiParam params[], size_t *nargs, unsigned *commandp) +{ + size_t csi_end = introlen; + + while (csi_end < tk->buffcount) { + if (CHARAT(csi_end) >= 0x40 && CHARAT(csi_end) < 0x80) { + break; + } + csi_end++; + } + + if (csi_end >= tk->buffcount) { + return TERMKEY_RES_AGAIN; + } + + unsigned char cmd = CHARAT(csi_end); + *commandp = cmd; + + char present = 0; + int argi = 0; + + size_t p = introlen; + + // See if there is an initial byte + if (CHARAT(p) >= '<' && CHARAT(p) <= '?') { + *commandp |= (unsigned)(CHARAT(p) << 8); + p++; + } + + // Now attempt to parse out up number;number;... separated values + while (p < csi_end) { + unsigned char c = CHARAT(p); + + if (c >= '0' && c < ';') { + if (!present) { + params[argi].param = &CHARAT(p); + present = 1; + } + } else if (c == ';') { + if (!present) { + params[argi].param = NULL; + params[argi].length = 0; + } else { + params[argi].length = (size_t)(&CHARAT(p) - params[argi].param); + } + present = 0; + argi++; + + if (argi > 16) { + break; + } + } else if (c >= 0x20 && c <= 0x2f) { + *commandp |= (unsigned)(c << 16); + break; + } + + p++; + } + + if (present) { + params[argi].length = (size_t)(&CHARAT(p) - params[argi].param); + argi++; + } + + *nargs = (size_t)argi; + *csi_len = csi_end + 1; + + return TERMKEY_RES_KEY; +} + +TermKeyResult termkey_interpret_csi(TermKey *tk, const TermKeyKey *key, TermKeyCsiParam params[], + size_t *nparams, unsigned *cmd) +{ + size_t dummy; + + if (tk->hightide == 0) { + return TERMKEY_RES_NONE; + } + if (key->type != TERMKEY_TYPE_UNKNOWN_CSI) { + return TERMKEY_RES_NONE; + } + + return parse_csi(tk, 0, &dummy, params, nparams, cmd); +} + +TermKeyResult termkey_interpret_csi_param(TermKeyCsiParam param, int *paramp, int subparams[], + size_t *nsubparams) +{ + if (paramp == NULL) { + return TERMKEY_RES_ERROR; + } + + if (param.param == NULL) { + *paramp = -1; + if (nsubparams) { + *nsubparams = 0; + } + return TERMKEY_RES_KEY; + } + + int arg = 0; + size_t i = 0; + size_t capacity = nsubparams ? *nsubparams : 0; + size_t length = 0; + for (; i < param.length && length <= capacity; i++) { + unsigned char c = param.param[i]; + if (c == ':') { + if (length == 0) { + *paramp = arg; + } else { + subparams[length - 1] = arg; + } + + arg = 0; + length++; + continue; + } + + assert(c >= '0' && c <= '9'); + arg = (10 * arg) + (c - '0'); + } + + if (length == 0) { + *paramp = arg; + } else { + subparams[length - 1] = arg; + } + + if (nsubparams) { + *nsubparams = length; + } + + return TERMKEY_RES_KEY; +} + +static int register_keys(void) +{ + int i; + + for (i = 0; i < 64; i++) { + csi_ss3s[i].sym = TERMKEY_SYM_UNKNOWN; + ss3s[i].sym = TERMKEY_SYM_UNKNOWN; + ss3_kpalts[i] = 0; + } + + for (i = 0; i < NCSIFUNCS; i++) { + csifuncs[i].sym = TERMKEY_SYM_UNKNOWN; + } + + register_csi_ss3(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_UP, 'A'); + register_csi_ss3(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_DOWN, 'B'); + register_csi_ss3(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_RIGHT, 'C'); + register_csi_ss3(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_LEFT, 'D'); + register_csi_ss3(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BEGIN, 'E'); + register_csi_ss3(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_END, 'F'); + register_csi_ss3(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_HOME, 'H'); + register_csi_ss3(TERMKEY_TYPE_FUNCTION, 1, 'P'); + register_csi_ss3(TERMKEY_TYPE_FUNCTION, 2, 'Q'); + register_csi_ss3(TERMKEY_TYPE_FUNCTION, 3, 'R'); + register_csi_ss3(TERMKEY_TYPE_FUNCTION, 4, 'S'); + + register_csi_ss3_full(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_TAB, TERMKEY_KEYMOD_SHIFT, + TERMKEY_KEYMOD_SHIFT, 'Z'); + + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KPENTER, 'M', 0); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KPEQUALS, 'X', '='); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KPMULT, 'j', '*'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KPPLUS, 'k', '+'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KPCOMMA, 'l', ','); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KPMINUS, 'm', '-'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KPPERIOD, 'n', '.'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KPDIV, 'o', '/'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KP0, 'p', '0'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KP1, 'q', '1'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KP2, 'r', '2'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KP3, 's', '3'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KP4, 't', '4'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KP5, 'u', '5'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KP6, 'v', '6'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KP7, 'w', '7'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KP8, 'x', '8'); + register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KP9, 'y', '9'); + + register_csifunc(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_FIND, 1); + register_csifunc(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_INSERT, 2); + register_csifunc(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_DELETE, 3); + register_csifunc(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SELECT, 4); + register_csifunc(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEUP, 5); + register_csifunc(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEDOWN, 6); + register_csifunc(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_HOME, 7); + register_csifunc(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_END, 8); + + register_csifunc(TERMKEY_TYPE_FUNCTION, 1, 11); + register_csifunc(TERMKEY_TYPE_FUNCTION, 2, 12); + register_csifunc(TERMKEY_TYPE_FUNCTION, 3, 13); + register_csifunc(TERMKEY_TYPE_FUNCTION, 4, 14); + register_csifunc(TERMKEY_TYPE_FUNCTION, 5, 15); + register_csifunc(TERMKEY_TYPE_FUNCTION, 6, 17); + register_csifunc(TERMKEY_TYPE_FUNCTION, 7, 18); + register_csifunc(TERMKEY_TYPE_FUNCTION, 8, 19); + register_csifunc(TERMKEY_TYPE_FUNCTION, 9, 20); + register_csifunc(TERMKEY_TYPE_FUNCTION, 10, 21); + register_csifunc(TERMKEY_TYPE_FUNCTION, 11, 23); + register_csifunc(TERMKEY_TYPE_FUNCTION, 12, 24); + register_csifunc(TERMKEY_TYPE_FUNCTION, 13, 25); + register_csifunc(TERMKEY_TYPE_FUNCTION, 14, 26); + register_csifunc(TERMKEY_TYPE_FUNCTION, 15, 28); + register_csifunc(TERMKEY_TYPE_FUNCTION, 16, 29); + register_csifunc(TERMKEY_TYPE_FUNCTION, 17, 31); + register_csifunc(TERMKEY_TYPE_FUNCTION, 18, 32); + register_csifunc(TERMKEY_TYPE_FUNCTION, 19, 33); + register_csifunc(TERMKEY_TYPE_FUNCTION, 20, 34); + + csi_handlers['u' - 0x40] = &handle_csi_u; + + csi_handlers['M' - 0x40] = &handle_csi_m; + csi_handlers['m' - 0x40] = &handle_csi_m; + + csi_handlers['R' - 0x40] = &handle_csi_R; + + csi_handlers['y' - 0x40] = &handle_csi_y; + + keyinfo_initialised = 1; + return 1; +} + +void *new_driver_csi(TermKey *tk, const char *term) +{ + if (!keyinfo_initialised) { + if (!register_keys()) { + return NULL; + } + } + + TermKeyCsi *csi = xmalloc(sizeof *csi); + + csi->tk = tk; + csi->saved_string_id = 0; + csi->saved_string = NULL; + + return csi; +} + +void free_driver_csi(void *info) +{ + TermKeyCsi *csi = info; + + if (csi->saved_string) { + xfree(csi->saved_string); + } + + xfree(csi); +} + +static TermKeyResult peekkey_csi_csi(TermKey *tk, TermKeyCsi *csi, size_t introlen, TermKeyKey *key, + int force, size_t *nbytep) +{ + size_t csi_len; + size_t nparams = 16; + TermKeyCsiParam params[16]; + unsigned cmd; + + TermKeyResult ret = parse_csi(tk, introlen, &csi_len, params, &nparams, &cmd); + + if (ret == TERMKEY_RES_AGAIN) { + if (!force) { + return TERMKEY_RES_AGAIN; + } + + (*tk->method.emit_codepoint)(tk, '[', key); + key->modifiers |= TERMKEY_KEYMOD_ALT; + *nbytep = introlen; + return TERMKEY_RES_KEY; + } + + if (cmd == 'M' && nparams < 3) { // Mouse in X10 encoding consumes the next 3 bytes also + tk->buffstart += csi_len; + tk->buffcount -= csi_len; + + TermKeyResult mouse_result = (*tk->method.peekkey_mouse)(tk, key, nbytep); + + tk->buffstart -= csi_len; + tk->buffcount += csi_len; + + if (mouse_result == TERMKEY_RES_KEY) { + *nbytep += csi_len; + } + + return mouse_result; + } + + TermKeyResult result = TERMKEY_RES_NONE; + + // We know from the logic above that cmd must be >= 0x40 and < 0x80 + if (csi_handlers[(cmd & 0xff) - 0x40]) { + result = (*csi_handlers[(cmd & 0xff) - 0x40])(tk, key, (int)cmd, params, (int)nparams); + } + + if (result == TERMKEY_RES_NONE) { +#ifdef DEBUG + switch (args) { + case 0: + fprintf(stderr, "CSI: Unknown cmd=%c\n", (char)cmd); + break; + case 1: + fprintf(stderr, "CSI: Unknown arg1=%ld cmd=%c\n", arg[0], (char)cmd); + break; + case 2: + fprintf(stderr, "CSI: Unknown arg1=%ld arg2=%ld cmd=%c\n", arg[0], arg[1], (char)cmd); + break; + case 3: + fprintf(stderr, "CSI: Unknown arg1=%ld arg2=%ld arg3=%ld cmd=%c\n", arg[0], arg[1], arg[2], + (char)cmd); + break; + default: + fprintf(stderr, "CSI: Unknown arg1=%ld arg2=%ld arg3=%ld ... args=%d cmd=%c\n", arg[0], + arg[1], arg[2], args, (char)cmd); + break; + } +#endif + key->type = TERMKEY_TYPE_UNKNOWN_CSI; + key->code.number = (int)cmd; + key->modifiers = 0; + + tk->hightide = csi_len - introlen; + *nbytep = introlen; // Do not yet eat the data bytes + return TERMKEY_RES_KEY; + } + + *nbytep = csi_len; + return result; +} + +static TermKeyResult peekkey_ss3(TermKey *tk, TermKeyCsi *csi, size_t introlen, TermKeyKey *key, + int force, size_t *nbytep) +{ + if (tk->buffcount < introlen + 1) { + if (!force) { + return TERMKEY_RES_AGAIN; + } + + (*tk->method.emit_codepoint)(tk, 'O', key); + key->modifiers |= TERMKEY_KEYMOD_ALT; + *nbytep = tk->buffcount; + return TERMKEY_RES_KEY; + } + + unsigned char cmd = CHARAT(introlen); + + if (cmd < 0x40 || cmd >= 0x80) { + return TERMKEY_RES_NONE; + } + + key->type = csi_ss3s[cmd - 0x40].type; + key->code.sym = csi_ss3s[cmd - 0x40].sym; + key->modifiers = csi_ss3s[cmd - 0x40].modifier_set; + + if (key->code.sym == TERMKEY_SYM_UNKNOWN) { + if (tk->flags & TERMKEY_FLAG_CONVERTKP && ss3_kpalts[cmd - 0x40]) { + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = (unsigned char)ss3_kpalts[cmd - 0x40]; + key->modifiers = 0; + + key->utf8[0] = (char)key->code.codepoint; + key->utf8[1] = 0; + } else { + key->type = ss3s[cmd - 0x40].type; + key->code.sym = ss3s[cmd - 0x40].sym; + key->modifiers = ss3s[cmd - 0x40].modifier_set; + } + } + + if (key->code.sym == TERMKEY_SYM_UNKNOWN) { +#ifdef DEBUG + fprintf(stderr, "CSI: Unknown SS3 %c (0x%02x)\n", (char)cmd, cmd); +#endif + return TERMKEY_RES_NONE; + } + + *nbytep = introlen + 1; + + return TERMKEY_RES_KEY; +} + +static TermKeyResult peekkey_ctrlstring(TermKey *tk, TermKeyCsi *csi, size_t introlen, + TermKeyKey *key, int force, size_t *nbytep) +{ + size_t str_end = introlen; + + while (str_end < tk->buffcount) { + if (CHARAT(str_end) == 0x07) { // BEL + break; + } + if (CHARAT(str_end) == 0x9c) { // ST + break; + } + if (CHARAT(str_end) == 0x1b + && (str_end + 1) < tk->buffcount + && CHARAT(str_end + 1) == 0x5c) { // ESC-prefixed ST + break; + } + + str_end++; + } + + if (str_end >= tk->buffcount) { + return TERMKEY_RES_AGAIN; + } + +#ifdef DEBUG + fprintf(stderr, "Found a control string: %*s", + str_end - introlen, tk->buffer + tk->buffstart + introlen); +#endif + + *nbytep = str_end + 1; + if (CHARAT(str_end) == 0x1b) { + (*nbytep)++; + } + + if (csi->saved_string) { + xfree(csi->saved_string); + } + + size_t len = str_end - introlen; + + csi->saved_string_id++; + csi->saved_string = xmalloc(len + 1); + + strncpy(csi->saved_string, (char *)tk->buffer + tk->buffstart + introlen, len); // NOLINT(runtime/printf) + csi->saved_string[len] = 0; + + key->type = (CHARAT(introlen - 1) & 0x1f) == 0x10 + ? TERMKEY_TYPE_DCS : TERMKEY_TYPE_OSC; + key->code.number = csi->saved_string_id; + key->modifiers = 0; + + return TERMKEY_RES_KEY; +} + +TermKeyResult peekkey_csi(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytep) +{ + if (tk->buffcount == 0) { + return tk->is_closed ? TERMKEY_RES_EOF : TERMKEY_RES_NONE; + } + + TermKeyCsi *csi = info; + + switch (CHARAT(0)) { + case 0x1b: + if (tk->buffcount < 2) { + return TERMKEY_RES_NONE; + } + + switch (CHARAT(1)) { + case 0x4f: // ESC-prefixed SS3 + return peekkey_ss3(tk, csi, 2, key, force, nbytep); + + case 0x50: // ESC-prefixed DCS + case 0x5d: // ESC-prefixed OSC + return peekkey_ctrlstring(tk, csi, 2, key, force, nbytep); + + case 0x5b: // ESC-prefixed CSI + return peekkey_csi_csi(tk, csi, 2, key, force, nbytep); + } + + return TERMKEY_RES_NONE; + + case 0x8f: // SS3 + return peekkey_ss3(tk, csi, 1, key, force, nbytep); + + case 0x90: // DCS + case 0x9d: // OSC + return peekkey_ctrlstring(tk, csi, 1, key, force, nbytep); + + case 0x9b: // CSI + return peekkey_csi_csi(tk, csi, 1, key, force, nbytep); + } + + return TERMKEY_RES_NONE; +} diff --git a/src/nvim/tui/termkey/driver-csi.h b/src/nvim/tui/termkey/driver-csi.h new file mode 100644 index 0000000000..0abd8b5c2e --- /dev/null +++ b/src/nvim/tui/termkey/driver-csi.h @@ -0,0 +1,7 @@ +#pragma once + +#include "nvim/tui/termkey/termkey_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/driver-csi.h.generated.h" +#endif diff --git a/src/nvim/tui/termkey/driver-ti.c b/src/nvim/tui/termkey/driver-ti.c new file mode 100644 index 0000000000..09c6a35004 --- /dev/null +++ b/src/nvim/tui/termkey/driver-ti.c @@ -0,0 +1,593 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "nvim/memory.h" +#include "nvim/tui/termkey/driver-ti.h" +#include "nvim/tui/termkey/termkey-internal.h" +#include "nvim/tui/termkey/termkey.h" + +#ifndef _WIN32 +# include +#else +# include +#endif + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/driver-ti.c.generated.h" +#endif + +#define streq(a, b) (!strcmp(a, b)) + +#define MAX_FUNCNAME 9 + +static struct { + const char *funcname; + TermKeyType type; + TermKeySym sym; + int mods; +} funcs[] = { + // THIS LIST MUST REMAIN SORTED! + { "backspace", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BACKSPACE, 0 }, + { "begin", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BEGIN, 0 }, + { "beg", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BEGIN, 0 }, + { "btab", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_TAB, TERMKEY_KEYMOD_SHIFT }, + { "cancel", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_CANCEL, 0 }, + { "clear", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_CLEAR, 0 }, + { "close", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_CLOSE, 0 }, + { "command", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_COMMAND, 0 }, + { "copy", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_COPY, 0 }, + { "dc", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_DELETE, 0 }, + { "down", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_DOWN, 0 }, + { "end", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_END, 0 }, + { "enter", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_ENTER, 0 }, + { "exit", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_EXIT, 0 }, + { "find", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_FIND, 0 }, + { "help", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_HELP, 0 }, + { "home", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_HOME, 0 }, + { "ic", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_INSERT, 0 }, + { "left", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_LEFT, 0 }, + { "mark", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MARK, 0 }, + { "message", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MESSAGE, 0 }, + { "move", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MOVE, 0 }, + { "next", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEDOWN, 0 }, // Not quite, but it's the best we can do + { "npage", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEDOWN, 0 }, + { "open", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_OPEN, 0 }, + { "options", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_OPTIONS, 0 }, + { "ppage", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEUP, 0 }, + { "previous", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEUP, 0 }, // Not quite, but it's the best we can do + { "print", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PRINT, 0 }, + { "redo", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REDO, 0 }, + { "reference", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REFERENCE, 0 }, + { "refresh", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REFRESH, 0 }, + { "replace", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REPLACE, 0 }, + { "restart", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_RESTART, 0 }, + { "resume", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_RESUME, 0 }, + { "right", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_RIGHT, 0 }, + { "save", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SAVE, 0 }, + { "select", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SELECT, 0 }, + { "suspend", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SUSPEND, 0 }, + { "undo", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_UNDO, 0 }, + { "up", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_UP, 0 }, + { NULL, 0, 0, 0 }, +}; + +static enum unibi_string unibi_lookup_str(const char *name) +{ + for (enum unibi_string ret = unibi_string_begin_ + 1; ret < unibi_string_end_; ret++) { + if (streq(unibi_name_str(ret), name)) { + return ret; + } + } + + return (enum unibi_string)-1; +} + +static const char *unibi_get_str_by_name(const unibi_term *ut, const char *name) +{ + enum unibi_string idx = unibi_lookup_str(name); + if (idx == (enum unibi_string)-1) { + return NULL; + } + + return unibi_get_str(ut, idx); +} + +// To be efficient at lookups, we store the byte sequence => keyinfo mapping +// in a trie. This avoids a slow linear search through a flat list of +// sequences. Because it is likely most nodes will be very sparse, we optimise +// vector to store an extent map after the database is loaded. + +typedef enum { + TYPE_KEY, + TYPE_ARR, +} trie_nodetype; + +struct trie_node { + trie_nodetype type; +}; + +struct trie_node_key { + trie_nodetype type; + struct keyinfo key; +}; + +struct trie_node_arr { + trie_nodetype type; + unsigned char min, max; // INCLUSIVE endpoints of the extent range + struct trie_node *arr[]; // dynamic size at allocation time +}; + +static int insert_seq(TermKeyTI *ti, const char *seq, struct trie_node *node); + +static struct trie_node *new_node_key(TermKeyType type, TermKeySym sym, int modmask, int modset) +{ + struct trie_node_key *n = xmalloc(sizeof(*n)); + + n->type = TYPE_KEY; + + n->key.type = type; + n->key.sym = sym; + n->key.modifier_mask = modmask; + n->key.modifier_set = modset; + + return (struct trie_node *)n; +} + +static struct trie_node *new_node_arr(unsigned char min, unsigned char max) +{ + struct trie_node_arr *n = xmalloc(sizeof(*n) + (max - min + 1) * sizeof(n->arr[0])); + + n->type = TYPE_ARR; + n->min = min; n->max = max; + + int i; + for (i = min; i <= max; i++) { + n->arr[i - min] = NULL; + } + + return (struct trie_node *)n; +} + +static struct trie_node *lookup_next(struct trie_node *n, unsigned char b) +{ + switch (n->type) { + case TYPE_KEY: + fprintf(stderr, "ABORT: lookup_next within a TYPE_KEY node\n"); + abort(); + case TYPE_ARR: { + struct trie_node_arr *nar = (struct trie_node_arr *)n; + if (b < nar->min || b > nar->max) { + return NULL; + } + return nar->arr[b - nar->min]; + } + } + + return NULL; // Never reached but keeps compiler happy +} + +static void free_trie(struct trie_node *n) +{ + switch (n->type) { + case TYPE_KEY: + break; + case TYPE_ARR: { + struct trie_node_arr *nar = (struct trie_node_arr *)n; + int i; + for (i = nar->min; i <= nar->max; i++) { + if (nar->arr[i - nar->min]) { + free_trie(nar->arr[i - nar->min]); + } + } + break; + } + } + + xfree(n); +} + +static struct trie_node *compress_trie(struct trie_node *n) +{ + if (!n) { + return NULL; + } + + switch (n->type) { + case TYPE_KEY: + return n; + case TYPE_ARR: { + struct trie_node_arr *nar = (struct trie_node_arr *)n; + unsigned char min, max; + // Find the real bounds + for (min = 0; !nar->arr[min]; min++) { + if (min == 255 && !nar->arr[min]) { + xfree(nar); + return new_node_arr(1, 0); + } + } + + for (max = 0xff; !nar->arr[max]; max--) {} + + struct trie_node_arr *new = (struct trie_node_arr *)new_node_arr(min, max); + int i; + for (i = min; i <= max; i++) { + new->arr[i - min] = compress_trie(nar->arr[i]); + } + + xfree(nar); + return (struct trie_node *)new; + } + } + + return n; +} + +static bool try_load_terminfo_key(TermKeyTI *ti, const char *name, struct keyinfo *info) +{ + const char *value = NULL; + + if (ti->unibi) { + value = unibi_get_str_by_name(ti->unibi, name); + } + + if (ti->tk->ti_getstr_hook) { + value = (ti->tk->ti_getstr_hook)(name, value, ti->tk->ti_getstr_hook_data); + } + + if (!value || value == (char *)-1 || !value[0]) { + return false; + } + + struct trie_node *node = new_node_key(info->type, info->sym, info->modifier_mask, + info->modifier_set); + insert_seq(ti, value, node); + + return true; +} + +static int load_terminfo(TermKeyTI *ti) +{ + int i; + + unibi_term *unibi = ti->unibi; + + ti->root = new_node_arr(0, 0xff); + if (!ti->root) { + return 0; + } + + // First the regular key strings + for (i = 0; funcs[i].funcname; i++) { + char name[MAX_FUNCNAME + 5 + 1]; + + sprintf(name, "key_%s", funcs[i].funcname); // NOLINT(runtime/printf) + if (!try_load_terminfo_key(ti, name, &(struct keyinfo){ + .type = funcs[i].type, + .sym = funcs[i].sym, + .modifier_mask = funcs[i].mods, + .modifier_set = funcs[i].mods, + })) { + continue; + } + + // Maybe it has a shifted version + sprintf(name, "key_s%s", funcs[i].funcname); // NOLINT(runtime/printf) + try_load_terminfo_key(ti, name, &(struct keyinfo){ + .type = funcs[i].type, + .sym = funcs[i].sym, + .modifier_mask = funcs[i].mods | TERMKEY_KEYMOD_SHIFT, + .modifier_set = funcs[i].mods | TERMKEY_KEYMOD_SHIFT, + }); + } + + // Now the F keys + for (i = 1; i < 255; i++) { + char name[9]; + sprintf(name, "key_f%d", i); // NOLINT(runtime/printf) + if (!try_load_terminfo_key(ti, name, &(struct keyinfo){ + .type = TERMKEY_TYPE_FUNCTION, + .sym = i, + .modifier_mask = 0, + .modifier_set = 0, + })) { + break; + } + } + + // Finally mouse mode + { + const char *value = NULL; + + if (ti->unibi) { + value = unibi_get_str_by_name(ti->unibi, "key_mouse"); + } + + if (ti->tk->ti_getstr_hook) { + value = (ti->tk->ti_getstr_hook)("key_mouse", value, ti->tk->ti_getstr_hook_data); + } + + // Some terminfos (e.g. xterm-1006) claim a different key_mouse that won't + // give X10 encoding. We'll only accept this if it's exactly "\e[M" + if (value && streq(value, "\x1b[M")) { + struct trie_node *node = new_node_key(TERMKEY_TYPE_MOUSE, 0, 0, 0); + insert_seq(ti, value, node); + } + } + + // Take copies of these terminfo strings, in case we build multiple termkey + // instances for multiple different termtypes, and it's different by the + // time we want to use it + const char *keypad_xmit = unibi + ? unibi_get_str(unibi, unibi_keypad_xmit) + : NULL; + + if (keypad_xmit) { + ti->start_string = xstrdup(keypad_xmit); + } else { + ti->start_string = NULL; + } + + const char *keypad_local = unibi + ? unibi_get_str(unibi, unibi_keypad_local) + : NULL; + + if (keypad_local) { + ti->stop_string = xstrdup(keypad_local); + } else { + ti->stop_string = NULL; + } + + if (unibi) { + unibi_destroy(unibi); + } + + ti->unibi = NULL; + + ti->root = compress_trie(ti->root); + + return 1; +} + +void *new_driver_ti(TermKey *tk, const char *term) +{ + TermKeyTI *ti = xmalloc(sizeof *ti); + + ti->tk = tk; + ti->root = NULL; + ti->start_string = NULL; + ti->stop_string = NULL; + + ti->unibi = unibi_from_term(term); + int saved_errno = errno; + if (!ti->unibi && saved_errno != ENOENT) { + xfree(ti); + return NULL; + } + // ti->unibi may be NULL if errno == ENOENT. That means the terminal wasn't + // known. Lets keep going because if we get getstr hook that might invent + // new strings for us + + return ti; +} + +int start_driver_ti(TermKey *tk, void *info) +{ + TermKeyTI *ti = info; + struct stat statbuf; + char *start_string; + size_t len; + + if (!ti->root) { + load_terminfo(ti); + } + + start_string = ti->start_string; + + if (tk->fd == -1 || !start_string) { + return 1; + } + + // The terminfo database will contain keys in application cursor key mode. + // We may need to enable that mode + + // There's no point trying to write() to a pipe + if (fstat(tk->fd, &statbuf) == -1) { + return 0; + } + +#ifndef _WIN32 + if (S_ISFIFO(statbuf.st_mode)) { + return 1; + } +#endif + + // Can't call putp or tputs because they suck and don't give us fd control + len = strlen(start_string); + while (len) { + size_t written = (size_t)write(tk->fd, start_string, (unsigned)len); + if (written == (size_t)-1) { + return 0; + } + start_string += written; + len -= written; + } + return 1; +} + +int stop_driver_ti(TermKey *tk, void *info) +{ + TermKeyTI *ti = info; + struct stat statbuf; + char *stop_string = ti->stop_string; + size_t len; + + if (tk->fd == -1 || !stop_string) { + return 1; + } + + // There's no point trying to write() to a pipe + if (fstat(tk->fd, &statbuf) == -1) { + return 0; + } + +#ifndef _WIN32 + if (S_ISFIFO(statbuf.st_mode)) { + return 1; + } +#endif + + // The terminfo database will contain keys in application cursor key mode. + // We may need to enable that mode + + // Can't call putp or tputs because they suck and don't give us fd control + len = strlen(stop_string); + while (len) { + size_t written = (size_t)write(tk->fd, stop_string, (unsigned)len); + if (written == (size_t)-1) { + return 0; + } + stop_string += written; + len -= written; + } + return 1; +} + +void free_driver_ti(void *info) +{ + TermKeyTI *ti = info; + + free_trie(ti->root); + + if (ti->start_string) { + xfree(ti->start_string); + } + + if (ti->stop_string) { + xfree(ti->stop_string); + } + + if (ti->unibi) { + unibi_destroy(ti->unibi); + } + + xfree(ti); +} + +#define CHARAT(i) (tk->buffer[tk->buffstart + (i)]) + +TermKeyResult peekkey_ti(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytep) +{ + TermKeyTI *ti = info; + + if (tk->buffcount == 0) { + return tk->is_closed ? TERMKEY_RES_EOF : TERMKEY_RES_NONE; + } + + struct trie_node *p = ti->root; + + unsigned pos = 0; + while (pos < tk->buffcount) { + p = lookup_next(p, CHARAT(pos)); + if (!p) { + break; + } + + pos++; + + if (p->type != TYPE_KEY) { + continue; + } + + struct trie_node_key *nk = (struct trie_node_key *)p; + if (nk->key.type == TERMKEY_TYPE_MOUSE) { + tk->buffstart += pos; + tk->buffcount -= pos; + + TermKeyResult mouse_result = (*tk->method.peekkey_mouse)(tk, key, nbytep); + + tk->buffstart -= pos; + tk->buffcount += pos; + + if (mouse_result == TERMKEY_RES_KEY) { + *nbytep += pos; + } + + return mouse_result; + } + + key->type = nk->key.type; + key->code.sym = nk->key.sym; + key->modifiers = nk->key.modifier_set; + *nbytep = pos; + return TERMKEY_RES_KEY; + } + + // If p is not NULL then we hadn't walked off the end yet, so we have a + // partial match + if (p && !force) { + return TERMKEY_RES_AGAIN; + } + + return TERMKEY_RES_NONE; +} + +static int insert_seq(TermKeyTI *ti, const char *seq, struct trie_node *node) +{ + int pos = 0; + struct trie_node *p = ti->root; + + // Unsigned because we'll be using it as an array subscript + unsigned char b; + + while ((b = (unsigned char)seq[pos])) { + struct trie_node *next = lookup_next(p, b); + if (!next) { + break; + } + p = next; + pos++; + } + + while ((b = (unsigned char)seq[pos])) { + struct trie_node *next; + if (seq[pos + 1]) { + // Intermediate node + next = new_node_arr(0, 0xff); + } else { + // Final key node + next = node; + } + + if (!next) { + return 0; + } + + switch (p->type) { + case TYPE_ARR: { + struct trie_node_arr *nar = (struct trie_node_arr *)p; + if (b < nar->min || b > nar->max) { + fprintf(stderr, + "ASSERT FAIL: Trie insert at 0x%02x is outside of extent bounds (0x%02x..0x%02x)\n", + b, nar->min, nar->max); + abort(); + } + nar->arr[b - nar->min] = next; + p = next; + break; + } + case TYPE_KEY: + fprintf(stderr, "ASSERT FAIL: Tried to insert child node in TYPE_KEY\n"); + abort(); + } + + pos++; + } + + return 1; +} diff --git a/src/nvim/tui/termkey/driver-ti.h b/src/nvim/tui/termkey/driver-ti.h new file mode 100644 index 0000000000..df9bd72d5b --- /dev/null +++ b/src/nvim/tui/termkey/driver-ti.h @@ -0,0 +1,7 @@ +#pragma once + +#include "nvim/tui/termkey/termkey_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/driver-ti.h.generated.h" +#endif diff --git a/src/nvim/tui/termkey/termkey-internal.h b/src/nvim/tui/termkey/termkey-internal.h new file mode 100644 index 0000000000..107591f950 --- /dev/null +++ b/src/nvim/tui/termkey/termkey-internal.h @@ -0,0 +1,109 @@ +#pragma once + +#include + +#include "nvim/tui/termkey/termkey_defs.h" + +#define HAVE_TERMIOS +#ifdef _WIN32 +# undef HAVE_TERMIOS +#endif + +#ifdef HAVE_TERMIOS +# include +#endif + +#ifdef _MSC_VER +# include +typedef SSIZE_T ssize_t; +#endif + +struct TermKeyDriver { + const char *name; + void *(*new_driver)(TermKey *tk, const char *term); + void (*free_driver)(void *info); + int (*start_driver)(TermKey *tk, void *info); + int (*stop_driver)(TermKey *tk, void *info); + TermKeyResult (*peekkey)(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytes); +}; + +struct keyinfo { + TermKeyType type; + TermKeySym sym; + int modifier_mask; + int modifier_set; +}; + +struct TermKeyDriverNode; +struct TermKeyDriverNode { + struct TermKeyDriver *driver; + void *info; + struct TermKeyDriverNode *next; +}; + +struct TermKey { + int fd; + int flags; + int canonflags; + unsigned char *buffer; + size_t buffstart; // First offset in buffer + size_t buffcount; // NUMBER of entires valid in buffer + size_t buffsize; // Total malloc'ed size + size_t hightide; // Position beyond buffstart at which peekkey() should next start + // normally 0, but see also termkey_interpret_csi + +#ifdef HAVE_TERMIOS + struct termios restore_termios; + char restore_termios_valid; +#endif + + TermKey_Terminfo_Getstr_Hook *ti_getstr_hook; + void *ti_getstr_hook_data; + + int waittime; // msec + + char is_closed; + char is_started; + + int nkeynames; + const char **keynames; + + // There are 32 C0 codes + struct keyinfo c0[32]; + + struct TermKeyDriverNode *drivers; + + // Now some "protected" methods for the driver to call but which we don't + // want exported as real symbols in the library + struct { + void (*emit_codepoint)(TermKey *tk, int codepoint, TermKeyKey *key); + TermKeyResult (*peekkey_simple)(TermKey *tk, TermKeyKey *key, int force, size_t *nbytes); + TermKeyResult (*peekkey_mouse)(TermKey *tk, TermKeyKey *key, size_t *nbytes); + } method; +}; + +static inline void termkey_key_get_linecol(const TermKeyKey *key, int *line, int *col) +{ + if (col) { + *col = (unsigned char)key->code.mouse[1] | ((unsigned char)key->code.mouse[3] & 0x0f) << 8; + } + + if (line) { + *line = (unsigned char)key->code.mouse[2] | ((unsigned char)key->code.mouse[3] & 0x70) << 4; + } +} + +static inline void termkey_key_set_linecol(TermKeyKey *key, int line, int col) +{ + if (line > 0xfff) { + line = 0xfff; + } + + if (col > 0x7ff) { + col = 0x7ff; + } + + key->code.mouse[1] = (char)(line & 0x0ff); + key->code.mouse[2] = (char)(col & 0x0ff); + key->code.mouse[3] = (line & 0xf00) >> 8 | (col & 0x300) >> 4; +} diff --git a/src/nvim/tui/termkey/termkey.c b/src/nvim/tui/termkey/termkey.c new file mode 100644 index 0000000000..e6440118f3 --- /dev/null +++ b/src/nvim/tui/termkey/termkey.c @@ -0,0 +1,1315 @@ +#include +#include +#include +#include +#include + +#include "nvim/mbyte.h" +#include "nvim/memory.h" +#include "nvim/tui/termkey/driver-csi.h" +#include "nvim/tui/termkey/driver-ti.h" +#include "nvim/tui/termkey/termkey-internal.h" +#include "nvim/tui/termkey/termkey.h" +#include "nvim/tui/termkey/termkey_defs.h" + +#ifndef _WIN32 +# include +# include +# include +#else +# include +#endif + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/termkey.c.generated.h" +#endif + +#ifdef _MSC_VER +# define strcaseeq(a, b) (_stricmp(a, b) == 0) +#else +# define strcaseeq(a, b) (strcasecmp(a, b) == 0) +#endif + +struct TermKeyDriver termkey_driver_ti = { + .name = "terminfo", + + .new_driver = new_driver_ti, + .free_driver = free_driver_ti, + + .start_driver = start_driver_ti, + .stop_driver = stop_driver_ti, + + .peekkey = peekkey_ti, +}; + +struct TermKeyDriver termkey_driver_csi = { + .name = "CSI", + + .new_driver = new_driver_csi, + .free_driver = free_driver_csi, + + .peekkey = peekkey_csi, +}; + +static struct TermKeyDriver *drivers[] = { + &termkey_driver_ti, + &termkey_driver_csi, + NULL, +}; + +static struct { + TermKeySym sym; + const char *name; +} keynames[] = { + { TERMKEY_SYM_NONE, "NONE" }, + { TERMKEY_SYM_BACKSPACE, "Backspace" }, + { TERMKEY_SYM_TAB, "Tab" }, + { TERMKEY_SYM_ENTER, "Enter" }, + { TERMKEY_SYM_ESCAPE, "Escape" }, + { TERMKEY_SYM_SPACE, "Space" }, + { TERMKEY_SYM_DEL, "DEL" }, + { TERMKEY_SYM_UP, "Up" }, + { TERMKEY_SYM_DOWN, "Down" }, + { TERMKEY_SYM_LEFT, "Left" }, + { TERMKEY_SYM_RIGHT, "Right" }, + { TERMKEY_SYM_BEGIN, "Begin" }, + { TERMKEY_SYM_FIND, "Find" }, + { TERMKEY_SYM_INSERT, "Insert" }, + { TERMKEY_SYM_DELETE, "Delete" }, + { TERMKEY_SYM_SELECT, "Select" }, + { TERMKEY_SYM_PAGEUP, "PageUp" }, + { TERMKEY_SYM_PAGEDOWN, "PageDown" }, + { TERMKEY_SYM_HOME, "Home" }, + { TERMKEY_SYM_END, "End" }, + { TERMKEY_SYM_CANCEL, "Cancel" }, + { TERMKEY_SYM_CLEAR, "Clear" }, + { TERMKEY_SYM_CLOSE, "Close" }, + { TERMKEY_SYM_COMMAND, "Command" }, + { TERMKEY_SYM_COPY, "Copy" }, + { TERMKEY_SYM_EXIT, "Exit" }, + { TERMKEY_SYM_HELP, "Help" }, + { TERMKEY_SYM_MARK, "Mark" }, + { TERMKEY_SYM_MESSAGE, "Message" }, + { TERMKEY_SYM_MOVE, "Move" }, + { TERMKEY_SYM_OPEN, "Open" }, + { TERMKEY_SYM_OPTIONS, "Options" }, + { TERMKEY_SYM_PRINT, "Print" }, + { TERMKEY_SYM_REDO, "Redo" }, + { TERMKEY_SYM_REFERENCE, "Reference" }, + { TERMKEY_SYM_REFRESH, "Refresh" }, + { TERMKEY_SYM_REPLACE, "Replace" }, + { TERMKEY_SYM_RESTART, "Restart" }, + { TERMKEY_SYM_RESUME, "Resume" }, + { TERMKEY_SYM_SAVE, "Save" }, + { TERMKEY_SYM_SUSPEND, "Suspend" }, + { TERMKEY_SYM_UNDO, "Undo" }, + { TERMKEY_SYM_KP0, "KP0" }, + { TERMKEY_SYM_KP1, "KP1" }, + { TERMKEY_SYM_KP2, "KP2" }, + { TERMKEY_SYM_KP3, "KP3" }, + { TERMKEY_SYM_KP4, "KP4" }, + { TERMKEY_SYM_KP5, "KP5" }, + { TERMKEY_SYM_KP6, "KP6" }, + { TERMKEY_SYM_KP7, "KP7" }, + { TERMKEY_SYM_KP8, "KP8" }, + { TERMKEY_SYM_KP9, "KP9" }, + { TERMKEY_SYM_KPENTER, "KPEnter" }, + { TERMKEY_SYM_KPPLUS, "KPPlus" }, + { TERMKEY_SYM_KPMINUS, "KPMinus" }, + { TERMKEY_SYM_KPMULT, "KPMult" }, + { TERMKEY_SYM_KPDIV, "KPDiv" }, + { TERMKEY_SYM_KPCOMMA, "KPComma" }, + { TERMKEY_SYM_KPPERIOD, "KPPeriod" }, + { TERMKEY_SYM_KPEQUALS, "KPEquals" }, + { 0, NULL }, +}; + +// Mouse event names +static const char *evnames[] = { "Unknown", "Press", "Drag", "Release" }; + +#define CHARAT(i) (tk->buffer[tk->buffstart + (i)]) + +#ifdef DEBUG +// Some internal debugging functions + +static void print_buffer(TermKey *tk) +{ + int i; + for (i = 0; i < tk->buffcount && i < 20; i++) { + fprintf(stderr, "%02x ", CHARAT(i)); + } + if (tk->buffcount > 20) { + fprintf(stderr, "..."); + } +} + +static void print_key(TermKey *tk, TermKeyKey *key) +{ + switch (key->type) { + case TERMKEY_TYPE_UNICODE: + fprintf(stderr, "Unicode codepoint=U+%04lx utf8='%s'", key->code.codepoint, key->utf8); + break; + case TERMKEY_TYPE_FUNCTION: + fprintf(stderr, "Function F%d", key->code.number); + break; + case TERMKEY_TYPE_KEYSYM: + fprintf(stderr, "Keysym sym=%d(%s)", key->code.sym, termkey_get_keyname(tk, key->code.sym)); + break; + case TERMKEY_TYPE_MOUSE: { + TermKeyMouseEvent ev; + int button, line, col; + termkey_interpret_mouse(tk, key, &ev, &button, &line, &col); + fprintf(stderr, "Mouse ev=%d button=%d pos=(%d,%d)\n", ev, button, line, col); + } + break; + case TERMKEY_TYPE_POSITION: { + int line, col; + termkey_interpret_position(tk, key, &line, &col); + fprintf(stderr, "Position report pos=(%d,%d)\n", line, col); + } + break; + case TERMKEY_TYPE_MODEREPORT: { + int initial, mode, value; + termkey_interpret_modereport(tk, key, &initial, &mode, &value); + fprintf(stderr, "Mode report mode=%s %d val=%d\n", initial == '?' ? "DEC" : "ANSI", mode, + value); + } + break; + case TERMKEY_TYPE_DCS: + fprintf(stderr, "Device Control String"); + break; + case TERMKEY_TYPE_OSC: + fprintf(stderr, "Operating System Control"); + break; + case TERMKEY_TYPE_UNKNOWN_CSI: + fprintf(stderr, "unknown CSI\n"); + break; + } + + int m = key->modifiers; + fprintf(stderr, " mod=%s%s%s+%02x", + (m & TERMKEY_KEYMOD_CTRL ? "C" : ""), + (m & TERMKEY_KEYMOD_ALT ? "A" : ""), + (m & TERMKEY_KEYMOD_SHIFT ? "S" : ""), + m & ~(TERMKEY_KEYMOD_CTRL|TERMKEY_KEYMOD_ALT|TERMKEY_KEYMOD_SHIFT)); +} + +static const char *res2str(TermKeyResult res) +{ + static char errorbuffer[256]; + + switch (res) { + case TERMKEY_RES_KEY: + return "TERMKEY_RES_KEY"; + case TERMKEY_RES_EOF: + return "TERMKEY_RES_EOF"; + case TERMKEY_RES_AGAIN: + return "TERMKEY_RES_AGAIN"; + case TERMKEY_RES_NONE: + return "TERMKEY_RES_NONE"; + case TERMKEY_RES_ERROR: + snprintf(errorbuffer, sizeof errorbuffer, "TERMKEY_RES_ERROR(errno=%d)\n", errno); + return (const char *)errorbuffer; + } + + return "unknown"; +} +#endif + +TermKeyResult termkey_interpret_string(TermKey *tk, const TermKeyKey *key, const char **strp) +{ + struct TermKeyDriverNode *p; + for (p = tk->drivers; p; p = p->next) { + if (p->driver == &termkey_driver_csi) { + break; + } + } + + if (!p) { + return TERMKEY_RES_NONE; + } + + if (key->type != TERMKEY_TYPE_DCS + && key->type != TERMKEY_TYPE_OSC) { + return TERMKEY_RES_NONE; + } + + TermKeyCsi *csi = p->info; + + if (csi->saved_string_id != key->code.number) { + return TERMKEY_RES_NONE; + } + + *strp = csi->saved_string; + + return TERMKEY_RES_KEY; +} + +/// Similar to snprintf(str, size, "%s", src) except it turns CamelCase into +/// space separated values +static int snprint_cameltospaces(char *str, size_t size, const char *src) +{ + int prev_lower = 0; + size_t l = 0; + while (*src && l < size - 1) { + if (isupper(*src) && prev_lower) { + if (str) { + str[l++] = ' '; + } + if (l >= size - 1) { + break; + } + } + prev_lower = islower(*src); + str[l++] = (char)tolower(*src++); + } + str[l] = 0; + // For consistency with snprintf, return the number of bytes that would have + // been written, excluding '\0' + while (*src) { + if (isupper(*src) && prev_lower) { + l++; + } + prev_lower = islower(*src); + src++; l++; + } + return (int)l; +} + +/// Similar to strcmp(str, strcamel, n) except that: +/// it compares CamelCase in strcamel with space separated values in str; +/// it takes char**s and updates them +/// n counts bytes of strcamel, not str +static int strpncmp_camel(const char **strp, const char **strcamelp, size_t n) +{ + const char *str = *strp, *strcamel = *strcamelp; + int prev_lower = 0; + + for (; (*str || *strcamel) && n; n--) { + char b = (char)tolower(*strcamel); + if (isupper(*strcamel) && prev_lower) { + if (*str != ' ') { + break; + } + str++; + if (*str != b) { + break; + } + } else if (*str != b) { + break; + } + + prev_lower = islower(*strcamel); + + str++; + strcamel++; + } + + *strp = str; + *strcamelp = strcamel; + return *str - *strcamel; +} + +static TermKey *termkey_alloc(void) +{ + TermKey *tk = xmalloc(sizeof(TermKey)); + + // Default all the object fields but don't allocate anything + + tk->fd = -1; + tk->flags = 0; + tk->canonflags = 0; + + tk->buffer = NULL; + tk->buffstart = 0; + tk->buffcount = 0; + tk->buffsize = 256; // bytes + tk->hightide = 0; + +#ifdef HAVE_TERMIOS + tk->restore_termios_valid = 0; +#endif + + tk->ti_getstr_hook = NULL; + tk->ti_getstr_hook_data = NULL; + + tk->waittime = 50; // msec + + tk->is_closed = 0; + tk->is_started = 0; + + tk->nkeynames = 64; + tk->keynames = NULL; + + for (int i = 0; i < 32; i++) { + tk->c0[i].sym = TERMKEY_SYM_NONE; + } + + tk->drivers = NULL; + + tk->method.emit_codepoint = &emit_codepoint; + tk->method.peekkey_simple = &peekkey_simple; + tk->method.peekkey_mouse = &peekkey_mouse; + + return tk; +} + +static int termkey_init(TermKey *tk, const char *term) +{ + tk->buffer = xmalloc(tk->buffsize); + tk->keynames = xmalloc(sizeof(tk->keynames[0]) * (size_t)tk->nkeynames); + + int i; + for (i = 0; i < tk->nkeynames; i++) { + tk->keynames[i] = NULL; + } + + for (i = 0; keynames[i].name; i++) { + if (termkey_register_keyname(tk, keynames[i].sym, keynames[i].name) == -1) { + goto abort_free_keynames; + } + } + + register_c0(tk, TERMKEY_SYM_TAB, 0x09, NULL); + register_c0(tk, TERMKEY_SYM_ENTER, 0x0d, NULL); + register_c0(tk, TERMKEY_SYM_ESCAPE, 0x1b, NULL); + + struct TermKeyDriverNode *tail = NULL; + + for (i = 0; drivers[i]; i++) { + void *info = (*drivers[i]->new_driver)(tk, term); + if (!info) { + continue; + } + +#ifdef DEBUG + fprintf(stderr, "Loading the %s driver...\n", drivers[i]->name); +#endif + + struct TermKeyDriverNode *thisdrv = xmalloc(sizeof(*thisdrv)); + if (!thisdrv) { + goto abort_free_drivers; + } + + thisdrv->driver = drivers[i]; + thisdrv->info = info; + thisdrv->next = NULL; + + if (!tail) { + tk->drivers = thisdrv; + } else { + tail->next = thisdrv; + } + + tail = thisdrv; + +#ifdef DEBUG + fprintf(stderr, "Loaded %s driver\n", drivers[i]->name); +#endif + } + + if (!tk->drivers) { + errno = ENOENT; + goto abort_free_keynames; + } + + return 1; + +abort_free_drivers: + for (struct TermKeyDriverNode *p = tk->drivers; p;) { + (*p->driver->free_driver)(p->info); + struct TermKeyDriverNode *next = p->next; + xfree(p); + p = next; + } + +abort_free_keynames: + xfree(tk->keynames); + xfree(tk->buffer); + + return 0; +} + +TermKey *termkey_new_abstract(const char *term, int flags) +{ + TermKey *tk = termkey_alloc(); + if (!tk) { + return NULL; + } + + tk->fd = -1; + + termkey_set_flags(tk, flags); + + if (!termkey_init(tk, term)) { + xfree(tk); + return NULL; + } + + if (!(flags & TERMKEY_FLAG_NOSTART) && !termkey_start(tk)) { + goto abort; + } + + return tk; + +abort: + xfree(tk); + return NULL; +} + +void termkey_free(TermKey *tk) +{ + xfree(tk->buffer); tk->buffer = NULL; + xfree(tk->keynames); tk->keynames = NULL; + + struct TermKeyDriverNode *p; + for (p = tk->drivers; p;) { + (*p->driver->free_driver)(p->info); + struct TermKeyDriverNode *next = p->next; + xfree(p); + p = next; + } + + xfree(tk); +} + +void termkey_destroy(TermKey *tk) +{ + if (tk->is_started) { + termkey_stop(tk); + } + + termkey_free(tk); +} + +void termkey_hook_terminfo_getstr(TermKey *tk, TermKey_Terminfo_Getstr_Hook *hookfn, void *data) +{ + tk->ti_getstr_hook = hookfn; + tk->ti_getstr_hook_data = data; +} + +int termkey_start(TermKey *tk) +{ + if (tk->is_started) { + return 1; + } + +#ifdef HAVE_TERMIOS + if (tk->fd != -1 && !(tk->flags & TERMKEY_FLAG_NOTERMIOS)) { + struct termios termios; + if (tcgetattr(tk->fd, &termios) == 0) { + tk->restore_termios = termios; + tk->restore_termios_valid = 1; + + termios.c_iflag &= (tcflag_t) ~(IXON|INLCR|ICRNL); + termios.c_lflag &= (tcflag_t) ~(ICANON|ECHO +# ifdef IEXTEN + | IEXTEN +# endif + ); + termios.c_cc[VMIN] = 1; + termios.c_cc[VTIME] = 0; + + if (tk->flags & TERMKEY_FLAG_CTRLC) { + // want no signal keys at all, so just disable ISIG + termios.c_lflag &= (tcflag_t) ~ISIG; + } else { + // Disable Ctrl-\==VQUIT and Ctrl-D==VSUSP but leave Ctrl-C as SIGINT + termios.c_cc[VQUIT] = _POSIX_VDISABLE; + termios.c_cc[VSUSP] = _POSIX_VDISABLE; + // Some OSes have Ctrl-Y==VDSUSP +# ifdef VDSUSP + termios.c_cc[VDSUSP] = _POSIX_VDISABLE; +# endif + } + +# ifdef DEBUG + fprintf(stderr, "Setting termios(3) flags\n"); +# endif + tcsetattr(tk->fd, TCSANOW, &termios); + } + } +#endif + + struct TermKeyDriverNode *p; + for (p = tk->drivers; p; p = p->next) { + if (p->driver->start_driver) { + if (!(*p->driver->start_driver)(tk, p->info)) { + return 0; + } + } + } + +#ifdef DEBUG + fprintf(stderr, "Drivers started; termkey instance %p is ready\n", tk); +#endif + + tk->is_started = 1; + return 1; +} + +int termkey_stop(TermKey *tk) +{ + if (!tk->is_started) { + return 1; + } + + struct TermKeyDriverNode *p; + for (p = tk->drivers; p; p = p->next) { + if (p->driver->stop_driver) { + (*p->driver->stop_driver)(tk, p->info); + } + } + +#ifdef HAVE_TERMIOS + if (tk->restore_termios_valid) { + tcsetattr(tk->fd, TCSANOW, &tk->restore_termios); + } +#endif + + tk->is_started = 0; + + return 1; +} + +void termkey_set_flags(TermKey *tk, int newflags) +{ + tk->flags = newflags; + + if (tk->flags & TERMKEY_FLAG_SPACESYMBOL) { + tk->canonflags |= TERMKEY_CANON_SPACESYMBOL; + } else { + tk->canonflags &= ~TERMKEY_CANON_SPACESYMBOL; + } +} + +int termkey_get_canonflags(TermKey *tk) +{ + return tk->canonflags; +} + +void termkey_set_canonflags(TermKey *tk, int flags) +{ + tk->canonflags = flags; + + if (tk->canonflags & TERMKEY_CANON_SPACESYMBOL) { + tk->flags |= TERMKEY_FLAG_SPACESYMBOL; + } else { + tk->flags &= ~TERMKEY_FLAG_SPACESYMBOL; + } +} + +size_t termkey_get_buffer_size(TermKey *tk) +{ + return tk->buffsize; +} + +int termkey_set_buffer_size(TermKey *tk, size_t size) +{ + unsigned char *buffer = xrealloc(tk->buffer, size); + + tk->buffer = buffer; + tk->buffsize = size; + + return 1; +} + +size_t termkey_get_buffer_remaining(TermKey *tk) +{ + // Return the total number of free bytes in the buffer, because that's what + // is available to the user. + return tk->buffsize - tk->buffcount; +} + +static void eat_bytes(TermKey *tk, size_t count) +{ + if (count >= tk->buffcount) { + tk->buffstart = 0; + tk->buffcount = 0; + return; + } + + tk->buffstart += count; + tk->buffcount -= count; +} + +// TODO(dundargoc): we should be able to replace this with utf_char2bytes from mbyte.c +int fill_utf8(int codepoint, char *str) +{ + int nbytes = utf_char2len(codepoint); + + str[nbytes] = 0; + + // This is easier done backwards + int b = nbytes; + while (b > 1) { + b--; + str[b] = (char)0x80 | (codepoint & 0x3f); + codepoint >>= 6; + } + + switch (nbytes) { + case 1: + str[0] = (codepoint & 0x7f); break; + case 2: + str[0] = (char)0xc0 | (codepoint & 0x1f); break; + case 3: + str[0] = (char)0xe0 | (codepoint & 0x0f); break; + case 4: + str[0] = (char)0xf0 | (codepoint & 0x07); break; + case 5: + str[0] = (char)0xf8 | (codepoint & 0x03); break; + case 6: + str[0] = (char)0xfc | (codepoint & 0x01); break; + } + + return nbytes; +} + +#define UTF8_INVALID 0xFFFD +static TermKeyResult parse_utf8(const unsigned char *bytes, size_t len, int *cp, size_t *nbytep) +{ + unsigned nbytes; + + unsigned char b0 = bytes[0]; + + if (b0 < 0x80) { + // Single byte ASCII + *cp = b0; + *nbytep = 1; + return TERMKEY_RES_KEY; + } else if (b0 < 0xc0) { + // Starts with a continuation byte - that's not right + *cp = UTF8_INVALID; + *nbytep = 1; + return TERMKEY_RES_KEY; + } else if (b0 < 0xe0) { + nbytes = 2; + *cp = b0 & 0x1f; + } else if (b0 < 0xf0) { + nbytes = 3; + *cp = b0 & 0x0f; + } else if (b0 < 0xf8) { + nbytes = 4; + *cp = b0 & 0x07; + } else if (b0 < 0xfc) { + nbytes = 5; + *cp = b0 & 0x03; + } else if (b0 < 0xfe) { + nbytes = 6; + *cp = b0 & 0x01; + } else { + *cp = UTF8_INVALID; + *nbytep = 1; + return TERMKEY_RES_KEY; + } + + for (unsigned b = 1; b < nbytes; b++) { + unsigned char cb; + + if (b >= len) { + return TERMKEY_RES_AGAIN; + } + + cb = bytes[b]; + if (cb < 0x80 || cb >= 0xc0) { + *cp = UTF8_INVALID; + *nbytep = b; + return TERMKEY_RES_KEY; + } + + *cp <<= 6; + *cp |= cb & 0x3f; + } + + // Check for overlong sequences + if ((int)nbytes > utf_char2len(*cp)) { + *cp = UTF8_INVALID; + } + + // Check for UTF-16 surrogates or invalid *cps + if ((*cp >= 0xD800 && *cp <= 0xDFFF) + || *cp == 0xFFFE + || *cp == 0xFFFF) { + *cp = UTF8_INVALID; + } + + *nbytep = nbytes; + return TERMKEY_RES_KEY; +} + +static void emit_codepoint(TermKey *tk, int codepoint, TermKeyKey *key) +{ + if (codepoint == 0) { + // ASCII NUL = Ctrl-Space + key->type = TERMKEY_TYPE_KEYSYM; + key->code.sym = TERMKEY_SYM_SPACE; + key->modifiers = TERMKEY_KEYMOD_CTRL; + } else if (codepoint < 0x20) { + // C0 range + key->code.codepoint = 0; + key->modifiers = 0; + + if (!(tk->flags & TERMKEY_FLAG_NOINTERPRET) && tk->c0[codepoint].sym != TERMKEY_SYM_UNKNOWN) { + key->code.sym = tk->c0[codepoint].sym; + key->modifiers |= tk->c0[codepoint].modifier_set; + } + + if (!key->code.sym) { + key->type = TERMKEY_TYPE_UNICODE; + // Generically modified Unicode ought not report the SHIFT state, or else + // we get into complications trying to report Shift-; vs : and so on... + // In order to be able to represent Ctrl-Shift-A as CTRL modified + // unicode A, we need to call Ctrl-A simply 'a', lowercase + if (codepoint + 0x40 >= 'A' && codepoint + 0x40 <= 'Z') { + // it's a letter - use lowercase instead + key->code.codepoint = codepoint + 0x60; + } else { + key->code.codepoint = codepoint + 0x40; + } + key->modifiers = TERMKEY_KEYMOD_CTRL; + } else { + key->type = TERMKEY_TYPE_KEYSYM; + } + } else if (codepoint == 0x7f && !(tk->flags & TERMKEY_FLAG_NOINTERPRET)) { + // ASCII DEL + key->type = TERMKEY_TYPE_KEYSYM; + key->code.sym = TERMKEY_SYM_DEL; + key->modifiers = 0; + } else if (codepoint >= 0x20 && codepoint < 0x80) { + // ASCII lowbyte range + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = codepoint; + key->modifiers = 0; + } else if (codepoint >= 0x80 && codepoint < 0xa0) { + // UTF-8 never starts with a C1 byte. So we can be sure of these + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = codepoint - 0x40; + key->modifiers = TERMKEY_KEYMOD_CTRL|TERMKEY_KEYMOD_ALT; + } else { + // UTF-8 codepoint + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = codepoint; + key->modifiers = 0; + } + + termkey_canonicalise(tk, key); + + if (key->type == TERMKEY_TYPE_UNICODE) { + fill_utf8(key->code.codepoint, key->utf8); + } +} + +void termkey_canonicalise(TermKey *tk, TermKeyKey *key) +{ + int flags = tk->canonflags; + + if (flags & TERMKEY_CANON_SPACESYMBOL) { + if (key->type == TERMKEY_TYPE_UNICODE && key->code.codepoint == 0x20) { + key->type = TERMKEY_TYPE_KEYSYM; + key->code.sym = TERMKEY_SYM_SPACE; + } + } else { + if (key->type == TERMKEY_TYPE_KEYSYM && key->code.sym == TERMKEY_SYM_SPACE) { + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = 0x20; + fill_utf8(key->code.codepoint, key->utf8); + } + } + + if (flags & TERMKEY_CANON_DELBS) { + if (key->type == TERMKEY_TYPE_KEYSYM && key->code.sym == TERMKEY_SYM_DEL) { + key->code.sym = TERMKEY_SYM_BACKSPACE; + } + } +} + +static TermKeyResult peekkey(TermKey *tk, TermKeyKey *key, int force, size_t *nbytep) +{ + int again = 0; + + if (!tk->is_started) { + errno = EINVAL; + return TERMKEY_RES_ERROR; + } + +#ifdef DEBUG + fprintf(stderr, "getkey(force=%d): buffer ", force); + print_buffer(tk); + fprintf(stderr, "\n"); +#endif + + if (tk->hightide) { + tk->buffstart += tk->hightide; + tk->buffcount -= tk->hightide; + tk->hightide = 0; + } + + TermKeyResult ret; + struct TermKeyDriverNode *p; + for (p = tk->drivers; p; p = p->next) { + ret = (p->driver->peekkey)(tk, p->info, key, force, nbytep); + +#ifdef DEBUG + fprintf(stderr, "Driver %s yields %s\n", p->driver->name, res2str(ret)); +#endif + + switch (ret) { + case TERMKEY_RES_KEY: +#ifdef DEBUG + print_key(tk, key); fprintf(stderr, "\n"); +#endif + // Slide the data down to stop it running away + { + size_t halfsize = tk->buffsize / 2; + + if (tk->buffstart > halfsize) { + memcpy(tk->buffer, tk->buffer + halfsize, halfsize); + tk->buffstart -= halfsize; + } + } + FALLTHROUGH; + case TERMKEY_RES_EOF: + case TERMKEY_RES_ERROR: + return ret; + + case TERMKEY_RES_AGAIN: + if (!force) { + again = 1; + } + FALLTHROUGH; + case TERMKEY_RES_NONE: + break; + } + } + + if (again) { + return TERMKEY_RES_AGAIN; + } + + ret = peekkey_simple(tk, key, force, nbytep); + +#ifdef DEBUG + fprintf(stderr, "getkey_simple(force=%d) yields %s\n", force, res2str(ret)); + if (ret == TERMKEY_RES_KEY) { + print_key(tk, key); fprintf(stderr, "\n"); + } +#endif + + return ret; +} + +static TermKeyResult peekkey_simple(TermKey *tk, TermKeyKey *key, int force, size_t *nbytep) +{ + if (tk->buffcount == 0) { + return tk->is_closed ? TERMKEY_RES_EOF : TERMKEY_RES_NONE; + } + + unsigned char b0 = CHARAT(0); + + if (b0 == 0x1b) { + // Escape-prefixed value? Might therefore be Alt+key + if (tk->buffcount == 1) { + // This might be an press, or it may want to be part of a longer + // sequence + if (!force) { + return TERMKEY_RES_AGAIN; + } + + (*tk->method.emit_codepoint)(tk, b0, key); + *nbytep = 1; + return TERMKEY_RES_KEY; + } + + // Try another key there + tk->buffstart++; + tk->buffcount--; + + // Run the full driver + TermKeyResult metakey_result = peekkey(tk, key, force, nbytep); + + tk->buffstart--; + tk->buffcount++; + + switch (metakey_result) { + case TERMKEY_RES_KEY: + key->modifiers |= TERMKEY_KEYMOD_ALT; + (*nbytep)++; + break; + + case TERMKEY_RES_NONE: + case TERMKEY_RES_EOF: + case TERMKEY_RES_AGAIN: + case TERMKEY_RES_ERROR: + break; + } + + return metakey_result; + } else if (b0 < 0xa0) { + // Single byte C0, G0 or C1 - C1 is never UTF-8 initial byte + (*tk->method.emit_codepoint)(tk, b0, key); + *nbytep = 1; + return TERMKEY_RES_KEY; + } else if (tk->flags & TERMKEY_FLAG_UTF8) { + // Some UTF-8 + int codepoint; + TermKeyResult res = parse_utf8(tk->buffer + tk->buffstart, tk->buffcount, &codepoint, nbytep); + + if (res == TERMKEY_RES_AGAIN && force) { + // There weren't enough bytes for a complete UTF-8 sequence but caller + // demands an answer. About the best thing we can do here is eat as many + // bytes as we have, and emit a UTF8_INVALID. If the remaining bytes + // arrive later, they'll be invalid too. + codepoint = UTF8_INVALID; + *nbytep = tk->buffcount; + res = TERMKEY_RES_KEY; + } + + key->type = TERMKEY_TYPE_UNICODE; + key->modifiers = 0; + (*tk->method.emit_codepoint)(tk, codepoint, key); + return res; + } else { + // Non UTF-8 case - just report the raw byte + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = b0; + key->modifiers = 0; + + key->utf8[0] = (char)key->code.codepoint; + key->utf8[1] = 0; + + *nbytep = 1; + + return TERMKEY_RES_KEY; + } +} + +static TermKeyResult peekkey_mouse(TermKey *tk, TermKeyKey *key, size_t *nbytep) +{ + if (tk->buffcount < 3) { + return TERMKEY_RES_AGAIN; + } + + key->type = TERMKEY_TYPE_MOUSE; + key->code.mouse[0] = (char)CHARAT(0) - 0x20; + key->code.mouse[1] = (char)CHARAT(1) - 0x20; + key->code.mouse[2] = (char)CHARAT(2) - 0x20; + key->code.mouse[3] = 0; + + key->modifiers = (key->code.mouse[0] & 0x1c) >> 2; + key->code.mouse[0] &= ~0x1c; + + *nbytep = 3; + return TERMKEY_RES_KEY; +} + +TermKeyResult termkey_getkey(TermKey *tk, TermKeyKey *key) +{ + size_t nbytes = 0; + TermKeyResult ret = peekkey(tk, key, 0, &nbytes); + + if (ret == TERMKEY_RES_KEY) { + eat_bytes(tk, nbytes); + } + + if (ret == TERMKEY_RES_AGAIN) { + // Call peekkey() again in force mode to obtain whatever it can + (void)peekkey(tk, key, 1, &nbytes); + } + // Don't eat it yet though + + return ret; +} + +TermKeyResult termkey_getkey_force(TermKey *tk, TermKeyKey *key) +{ + size_t nbytes = 0; + TermKeyResult ret = peekkey(tk, key, 1, &nbytes); + + if (ret == TERMKEY_RES_KEY) { + eat_bytes(tk, nbytes); + } + + return ret; +} + +size_t termkey_push_bytes(TermKey *tk, const char *bytes, size_t len) +{ + if (tk->buffstart) { + memmove(tk->buffer, tk->buffer + tk->buffstart, tk->buffcount); + tk->buffstart = 0; + } + + // Not expecting it ever to be greater but doesn't hurt to handle that + if (tk->buffcount >= tk->buffsize) { + errno = ENOMEM; + return (size_t)-1; + } + + if (len > tk->buffsize - tk->buffcount) { + len = tk->buffsize - tk->buffcount; + } + + // memcpy(), not strncpy() in case of null bytes in input + memcpy(tk->buffer + tk->buffcount, bytes, len); + tk->buffcount += len; + + return len; +} + +TermKeySym termkey_register_keyname(TermKey *tk, TermKeySym sym, const char *name) +{ + if (!sym) { + sym = tk->nkeynames; + } + + if (sym >= tk->nkeynames) { + const char **new_keynames = xrealloc(tk->keynames, sizeof(new_keynames[0]) * ((size_t)sym + 1)); + + tk->keynames = new_keynames; + + // Fill in the hole + for (int i = tk->nkeynames; i < sym; i++) { + tk->keynames[i] = NULL; + } + + tk->nkeynames = sym + 1; + } + + tk->keynames[sym] = name; + + return sym; +} + +const char *termkey_get_keyname(TermKey *tk, TermKeySym sym) +{ + if (sym == TERMKEY_SYM_UNKNOWN) { + return "UNKNOWN"; + } + + if (sym < tk->nkeynames) { + return tk->keynames[sym]; + } + + return "UNKNOWN"; +} + +static const char *termkey_lookup_keyname_format(TermKey *tk, const char *str, TermKeySym *sym, + TermKeyFormat format) +{ + // We store an array, so we can't do better than a linear search. Doesn't + // matter because user won't be calling this too often + + for (*sym = 0; *sym < tk->nkeynames; (*sym)++) { + const char *thiskey = tk->keynames[*sym]; + if (!thiskey) { + continue; + } + size_t len = strlen(thiskey); + if (format & TERMKEY_FORMAT_LOWERSPACE) { + const char *thisstr = str; + if (strpncmp_camel(&thisstr, &thiskey, len) == 0) { + return thisstr; + } + } else { + if (strncmp(str, thiskey, len) == 0) { + return (char *)str + len; + } + } + } + + return NULL; +} + +const char *termkey_lookup_keyname(TermKey *tk, const char *str, TermKeySym *sym) +{ + return termkey_lookup_keyname_format(tk, str, sym, 0); +} + +static TermKeySym register_c0(TermKey *tk, TermKeySym sym, unsigned char ctrl, const char *name) +{ + return register_c0_full(tk, sym, 0, 0, ctrl, name); +} + +static TermKeySym register_c0_full(TermKey *tk, TermKeySym sym, int modifier_set, int modifier_mask, + unsigned char ctrl, const char *name) +{ + if (ctrl >= 0x20) { + errno = EINVAL; + return -1; + } + + if (name) { + sym = termkey_register_keyname(tk, sym, name); + } + + tk->c0[ctrl].sym = sym; + tk->c0[ctrl].modifier_set = modifier_set; + tk->c0[ctrl].modifier_mask = modifier_mask; + + return sym; +} + +static struct modnames { + const char *shift, *alt, *ctrl; +} +modnames[] = { + { "S", "A", "C" }, // 0 + { "Shift", "Alt", "Ctrl" }, // LONGMOD + { "S", "M", "C" }, // ALTISMETA + { "Shift", "Meta", "Ctrl" }, // ALTISMETA+LONGMOD + { "s", "a", "c" }, // LOWERMOD + { "shift", "alt", "ctrl" }, // LOWERMOD+LONGMOD + { "s", "m", "c" }, // LOWERMOD+ALTISMETA + { "shift", "meta", "ctrl" }, // LOWERMOD+ALTISMETA+LONGMOD +}; + +size_t termkey_strfkey(TermKey *tk, char *buffer, size_t len, TermKeyKey *key, TermKeyFormat format) +{ + size_t pos = 0; + size_t l = 0; + + struct modnames *mods = &modnames[!!(format & TERMKEY_FORMAT_LONGMOD) + + !!(format & TERMKEY_FORMAT_ALTISMETA) * 2 + + !!(format & TERMKEY_FORMAT_LOWERMOD) * 4]; + + int wrapbracket = (format & TERMKEY_FORMAT_WRAPBRACKET) + && (key->type != TERMKEY_TYPE_UNICODE || key->modifiers != 0); + + char sep = (format & TERMKEY_FORMAT_SPACEMOD) ? ' ' : '-'; + + if (format & TERMKEY_FORMAT_CARETCTRL + && key->type == TERMKEY_TYPE_UNICODE + && key->modifiers == TERMKEY_KEYMOD_CTRL) { + long codepoint = key->code.codepoint; + + // Handle some of the special cases first + if (codepoint >= 'a' && codepoint <= 'z') { + l = (size_t)snprintf(buffer + pos, len - pos, wrapbracket ? "<^%c>" : "^%c", + (char)codepoint - 0x20); + if (l <= 0) { + return pos; + } + pos += l; + return pos; + } else if ((codepoint >= '@' && codepoint < 'A') + || (codepoint > 'Z' && codepoint <= '_')) { + l = (size_t)snprintf(buffer + pos, len - pos, wrapbracket ? "<^%c>" : "^%c", (char)codepoint); + if (l <= 0) { + return pos; + } + pos += l; + return pos; + } + } + + if (wrapbracket) { + l = (size_t)snprintf(buffer + pos, len - pos, "<"); + if (l <= 0) { + return pos; + } + pos += l; + } + + if (key->modifiers & TERMKEY_KEYMOD_ALT) { + l = (size_t)snprintf(buffer + pos, len - pos, "%s%c", mods->alt, sep); + if (l <= 0) { + return pos; + } + pos += l; + } + + if (key->modifiers & TERMKEY_KEYMOD_CTRL) { + l = (size_t)snprintf(buffer + pos, len - pos, "%s%c", mods->ctrl, sep); + if (l <= 0) { + return pos; + } + pos += l; + } + + if (key->modifiers & TERMKEY_KEYMOD_SHIFT) { + l = (size_t)snprintf(buffer + pos, len - pos, "%s%c", mods->shift, sep); + if (l <= 0) { + return pos; + } + pos += l; + } + + switch (key->type) { + case TERMKEY_TYPE_UNICODE: + if (!key->utf8[0]) { // In case of user-supplied key structures + fill_utf8(key->code.codepoint, key->utf8); + } + l = (size_t)snprintf(buffer + pos, len - pos, "%s", key->utf8); + break; + case TERMKEY_TYPE_KEYSYM: { + const char *name = termkey_get_keyname(tk, key->code.sym); + if (format & TERMKEY_FORMAT_LOWERSPACE) { + l = (size_t)snprint_cameltospaces(buffer + pos, len - pos, name); + } else { + l = (size_t)snprintf(buffer + pos, len - pos, "%s", name); + } + } + break; + case TERMKEY_TYPE_FUNCTION: + l = (size_t)snprintf(buffer + pos, len - pos, "%c%d", + (format & TERMKEY_FORMAT_LOWERSPACE ? 'f' : 'F'), key->code.number); + break; + case TERMKEY_TYPE_MOUSE: { + TermKeyMouseEvent ev; + int button; + int line, col; + termkey_interpret_mouse(tk, key, &ev, &button, &line, &col); + + l = (size_t)snprintf(buffer + pos, len - pos, "Mouse%s(%d)", + evnames[ev], button); + + if (format & TERMKEY_FORMAT_MOUSE_POS) { + if (l <= 0) { + return pos; + } + pos += l; + + l = (size_t)snprintf(buffer + pos, len - pos, " @ (%u,%u)", col, line); + } + } + break; + case TERMKEY_TYPE_POSITION: + l = (size_t)snprintf(buffer + pos, len - pos, "Position"); + break; + case TERMKEY_TYPE_MODEREPORT: { + int initial, mode, value; + termkey_interpret_modereport(tk, key, &initial, &mode, &value); + if (initial) { + l = (size_t)snprintf(buffer + pos, len - pos, "Mode(%c%d=%d)", initial, mode, value); + } else { + l = (size_t)snprintf(buffer + pos, len - pos, "Mode(%d=%d)", mode, value); + } + } + break; + case TERMKEY_TYPE_DCS: + l = (size_t)snprintf(buffer + pos, len - pos, "DCS"); + break; + case TERMKEY_TYPE_OSC: + l = (size_t)snprintf(buffer + pos, len - pos, "OSC"); + break; + case TERMKEY_TYPE_UNKNOWN_CSI: + l = (size_t)snprintf(buffer + pos, len - pos, "CSI %c", key->code.number & 0xff); + break; + } + + if (l <= 0) { + return pos; + } + pos += l; + + if (wrapbracket) { + l = (size_t)snprintf(buffer + pos, len - pos, ">"); + if (l <= 0) { + return pos; + } + pos += l; + } + + return pos; +} diff --git a/src/nvim/tui/termkey/termkey.h b/src/nvim/tui/termkey/termkey.h new file mode 100644 index 0000000000..21ed141346 --- /dev/null +++ b/src/nvim/tui/termkey/termkey.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +#include "nvim/tui/termkey/termkey_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/termkey.h.generated.h" +#endif diff --git a/src/nvim/tui/termkey/termkey_defs.h b/src/nvim/tui/termkey/termkey_defs.h new file mode 100644 index 0000000000..7c218ba7c2 --- /dev/null +++ b/src/nvim/tui/termkey/termkey_defs.h @@ -0,0 +1,199 @@ +#pragma once + +#include +#include +#include +#include + +#include "nvim/event/defs.h" +#include "nvim/tui/tui_defs.h" +#include "nvim/types_defs.h" + +typedef struct TermKey TermKey; + +typedef struct { + TermKey *tk; + int saved_string_id; + char *saved_string; +} TermKeyCsi; + +typedef enum { + TERMKEY_RES_NONE, + TERMKEY_RES_KEY, + TERMKEY_RES_EOF, + TERMKEY_RES_AGAIN, + TERMKEY_RES_ERROR, +} TermKeyResult; + +typedef enum { + TERMKEY_SYM_UNKNOWN = -1, + TERMKEY_SYM_NONE = 0, + + // Special names in C0 + TERMKEY_SYM_BACKSPACE, + TERMKEY_SYM_TAB, + TERMKEY_SYM_ENTER, + TERMKEY_SYM_ESCAPE, + + // Special names in G0 + TERMKEY_SYM_SPACE, + TERMKEY_SYM_DEL, + + // Special keys + TERMKEY_SYM_UP, + TERMKEY_SYM_DOWN, + TERMKEY_SYM_LEFT, + TERMKEY_SYM_RIGHT, + TERMKEY_SYM_BEGIN, + TERMKEY_SYM_FIND, + TERMKEY_SYM_INSERT, + TERMKEY_SYM_DELETE, + TERMKEY_SYM_SELECT, + TERMKEY_SYM_PAGEUP, + TERMKEY_SYM_PAGEDOWN, + TERMKEY_SYM_HOME, + TERMKEY_SYM_END, + + // Special keys from terminfo + TERMKEY_SYM_CANCEL, + TERMKEY_SYM_CLEAR, + TERMKEY_SYM_CLOSE, + TERMKEY_SYM_COMMAND, + TERMKEY_SYM_COPY, + TERMKEY_SYM_EXIT, + TERMKEY_SYM_HELP, + TERMKEY_SYM_MARK, + TERMKEY_SYM_MESSAGE, + TERMKEY_SYM_MOVE, + TERMKEY_SYM_OPEN, + TERMKEY_SYM_OPTIONS, + TERMKEY_SYM_PRINT, + TERMKEY_SYM_REDO, + TERMKEY_SYM_REFERENCE, + TERMKEY_SYM_REFRESH, + TERMKEY_SYM_REPLACE, + TERMKEY_SYM_RESTART, + TERMKEY_SYM_RESUME, + TERMKEY_SYM_SAVE, + TERMKEY_SYM_SUSPEND, + TERMKEY_SYM_UNDO, + + // Numeric keypad special keys + TERMKEY_SYM_KP0, + TERMKEY_SYM_KP1, + TERMKEY_SYM_KP2, + TERMKEY_SYM_KP3, + TERMKEY_SYM_KP4, + TERMKEY_SYM_KP5, + TERMKEY_SYM_KP6, + TERMKEY_SYM_KP7, + TERMKEY_SYM_KP8, + TERMKEY_SYM_KP9, + TERMKEY_SYM_KPENTER, + TERMKEY_SYM_KPPLUS, + TERMKEY_SYM_KPMINUS, + TERMKEY_SYM_KPMULT, + TERMKEY_SYM_KPDIV, + TERMKEY_SYM_KPCOMMA, + TERMKEY_SYM_KPPERIOD, + TERMKEY_SYM_KPEQUALS, + + // et cetera ad nauseum + TERMKEY_N_SYMS, +} TermKeySym; + +typedef enum { + TERMKEY_TYPE_UNICODE, + TERMKEY_TYPE_FUNCTION, + TERMKEY_TYPE_KEYSYM, + TERMKEY_TYPE_MOUSE, + TERMKEY_TYPE_POSITION, + TERMKEY_TYPE_MODEREPORT, + TERMKEY_TYPE_DCS, + TERMKEY_TYPE_OSC, + // add other recognised types here + + TERMKEY_TYPE_UNKNOWN_CSI = -1, +} TermKeyType; + +typedef enum { + TERMKEY_MOUSE_UNKNOWN, + TERMKEY_MOUSE_PRESS, + TERMKEY_MOUSE_DRAG, + TERMKEY_MOUSE_RELEASE, +} TermKeyMouseEvent; + +enum { + TERMKEY_KEYMOD_SHIFT = 1 << 0, + TERMKEY_KEYMOD_ALT = 1 << 1, + TERMKEY_KEYMOD_CTRL = 1 << 2, +}; + +typedef struct { + const unsigned char *param; + size_t length; +} TermKeyCsiParam; + +enum { + TERMKEY_FLAG_NOINTERPRET = 1 << 0, // Do not interpret C0//DEL codes if possible + TERMKEY_FLAG_CONVERTKP = 1 << 1, // Convert KP codes to regular keypresses + TERMKEY_FLAG_RAW = 1 << 2, // Input is raw bytes, not UTF-8 + TERMKEY_FLAG_UTF8 = 1 << 3, // Input is definitely UTF-8 + TERMKEY_FLAG_NOTERMIOS = 1 << 4, // Do not make initial termios calls on construction + TERMKEY_FLAG_SPACESYMBOL = 1 << 5, // Sets TERMKEY_CANON_SPACESYMBOL + TERMKEY_FLAG_CTRLC = 1 << 6, // Allow Ctrl-C to be read as normal, disabling SIGINT + TERMKEY_FLAG_EINTR = 1 << 7, // Return ERROR on signal (EINTR) rather than retry + TERMKEY_FLAG_NOSTART = 1 << 8, // Do not call termkey_start() in constructor +}; + +enum { + TERMKEY_CANON_SPACESYMBOL = 1 << 0, // Space is symbolic rather than Unicode + TERMKEY_CANON_DELBS = 1 << 1, // Del is converted to Backspace +}; + +typedef struct { + TermKeyType type; + union { + int codepoint; // TERMKEY_TYPE_UNICODE + int number; // TERMKEY_TYPE_FUNCTION + TermKeySym sym; // TERMKEY_TYPE_KEYSYM + char mouse[4]; // TERMKEY_TYPE_MOUSE + // opaque. see termkey_interpret_mouse + } code; + + int modifiers; + + // Any Unicode character can be UTF-8 encoded in no more than 6 bytes, plus + // terminating NUL + char utf8[7]; +} TermKeyKey; + +// Mostly-undocumented hooks for doing evil evil things +typedef const char *TermKey_Terminfo_Getstr_Hook(const char *name, const char *value, void *data); + +typedef enum { + TERMKEY_FORMAT_LONGMOD = 1 << 0, // Shift-... instead of S-... + TERMKEY_FORMAT_CARETCTRL = 1 << 1, // ^X instead of C-X + TERMKEY_FORMAT_ALTISMETA = 1 << 2, // Meta- or M- instead of Alt- or A- + TERMKEY_FORMAT_WRAPBRACKET = 1 << 3, // Wrap special keys in brackets like + TERMKEY_FORMAT_SPACEMOD = 1 << 4, // M Foo instead of M-Foo + TERMKEY_FORMAT_LOWERMOD = 1 << 5, // meta or m instead of Meta or M + TERMKEY_FORMAT_LOWERSPACE = 1 << 6, // page down instead of PageDown + + TERMKEY_FORMAT_MOUSE_POS = 1 << 8, // Include mouse position if relevant; @ col,line +} TermKeyFormat; + +// Some useful combinations + +#define TERMKEY_FORMAT_VIM (TermKeyFormat)(TERMKEY_FORMAT_ALTISMETA|TERMKEY_FORMAT_WRAPBRACKET) + +typedef struct { + TermKey *tk; + + unibi_term *unibi; // only valid until first 'start' call + + struct trie_node *root; + + char *start_string; + char *stop_string; +} TermKeyTI; -- cgit From ff85e54939b0aca34a779a2b6381d09db1858b29 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 18 Sep 2024 04:14:06 -0700 Subject: feat(tui): builtin UI (TUI) sets client info #30397 Problem: The default builtin UI client does not declare its client info. This reduces discoverability and makes it difficult for plugins to identify the UI. Solution: - Call nvim_set_client_info after attaching, as recommended by `:help dev-ui`. - Also set the "pid" field. - Also change `ui_active()` to return a count. Not directly relevant to this commit, but will be useful later. --- src/nvim/tui/input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 3eb8d4ba2e..98dd7b4b45 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -464,7 +464,7 @@ 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 paster sequence. + // 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); -- cgit From 0fe4362e216e659e5236cf49beba0e10cce0579d Mon Sep 17 00:00:00 2001 From: Devon Gardner Date: Thu, 19 Sep 2024 08:33:40 +0000 Subject: fix(coverity/509227/509228): tui driver_ti underflow #30341 Problem: write() can return -1 but is cast to unsigned type causing coverity to detect possible overflowed integer Solution: Perform check to ensure all negative values are captured rather than just -1 before casting to unsigned type --- src/nvim/tui/termkey/driver-ti.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/termkey/driver-ti.c b/src/nvim/tui/termkey/driver-ti.c index 09c6a35004..745ee9902f 100644 --- a/src/nvim/tui/termkey/driver-ti.c +++ b/src/nvim/tui/termkey/driver-ti.c @@ -410,10 +410,11 @@ int start_driver_ti(TermKey *tk, void *info) // Can't call putp or tputs because they suck and don't give us fd control len = strlen(start_string); while (len) { - size_t written = (size_t)write(tk->fd, start_string, (unsigned)len); - if (written == (size_t)-1) { + ssize_t result = write(tk->fd, start_string, (unsigned)len); + if (result < 0) { return 0; } + size_t written = (size_t)result; start_string += written; len -= written; } @@ -448,10 +449,11 @@ int stop_driver_ti(TermKey *tk, void *info) // Can't call putp or tputs because they suck and don't give us fd control len = strlen(stop_string); while (len) { - size_t written = (size_t)write(tk->fd, stop_string, (unsigned)len); - if (written == (size_t)-1) { + ssize_t result = write(tk->fd, stop_string, (unsigned)len); + if (result < 0) { return 0; } + size_t written = (size_t)result; stop_string += written; len -= written; } -- cgit From 737f58e23230ea14f1648ac1fc7f442ea0f8563c Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Fri, 20 Sep 2024 07:34:50 +0200 Subject: refactor(api)!: rename Dictionary => Dict In the api_info() output: :new|put =map(filter(api_info().functions, '!has_key(v:val,''deprecated_since'')'), 'v:val') ... {'return_type': 'ArrayOf(Integer, 2)', 'name': 'nvim_win_get_position', 'method': v:true, 'parameters': [['Window', 'window']], 'since': 1} The `ArrayOf(Integer, 2)` return type didn't break clients when we added it, which is evidence that clients don't use the `return_type` field, thus renaming Dictionary => Dict in api_info() is not (in practice) a breaking change. --- src/nvim/tui/tui.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/nvim/tui') diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 7e1068ed56..fa50a8252d 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -1199,7 +1199,7 @@ static CursorShape tui_cursor_decode_shape(const char *shape_str) return shape; } -static cursorentry_T decode_cursor_entry(Dictionary args) +static cursorentry_T decode_cursor_entry(Dict args) { cursorentry_T r = shape_table[0]; @@ -1231,8 +1231,8 @@ void tui_mode_info_set(TUIData *tui, bool guicursor_enabled, Array args) // cursor style entries as defined by `shape_table`. for (size_t i = 0; i < args.size; i++) { - assert(args.items[i].type == kObjectTypeDictionary); - cursorentry_T r = decode_cursor_entry(args.items[i].data.dictionary); + assert(args.items[i].type == kObjectTypeDict); + cursorentry_T r = decode_cursor_entry(args.items[i].data.dict); tui->cursor_shapes[i] = r; } @@ -1539,7 +1539,7 @@ static void show_verbose_terminfo(TUIData *tui) ADD_C(args, BOOLEAN_OBJ(true)); // history MAXSIZE_TEMP_DICT(opts, 1); PUT_C(opts, "verbose", BOOLEAN_OBJ(true)); - ADD_C(args, DICTIONARY_OBJ(opts)); + ADD_C(args, DICT_OBJ(opts)); rpc_send_event(ui_client_channel_id, "nvim_echo", args); xfree(str.data); } -- cgit