diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2024-11-19 22:57:13 +0000 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2024-11-19 22:57:13 +0000 |
| commit | 9be89f131f87608f224f0ee06d199fcd09d32176 (patch) | |
| tree | 11022dcfa9e08cb4ac5581b16734196128688d48 /src/nvim/tui | |
| parent | ff7ed8f586589d620a806c3758fac4a47a8e7e15 (diff) | |
| parent | 88085c2e80a7e3ac29aabb6b5420377eed99b8b6 (diff) | |
| download | rneovim-9be89f131f87608f224f0ee06d199fcd09d32176.tar.gz rneovim-9be89f131f87608f224f0ee06d199fcd09d32176.tar.bz2 rneovim-9be89f131f87608f224f0ee06d199fcd09d32176.zip | |
Merge remote-tracking branch 'upstream/master' into mix_20240309
Diffstat (limited to 'src/nvim/tui')
| -rw-r--r-- | src/nvim/tui/input.c | 209 | ||||
| -rw-r--r-- | src/nvim/tui/input.h | 15 | ||||
| -rw-r--r-- | src/nvim/tui/terminfo.c | 2 | ||||
| -rw-r--r-- | src/nvim/tui/termkey/README | 1 | ||||
| -rw-r--r-- | src/nvim/tui/termkey/driver-csi.c | 902 | ||||
| -rw-r--r-- | src/nvim/tui/termkey/driver-csi.h | 7 | ||||
| -rw-r--r-- | src/nvim/tui/termkey/driver-ti.c | 595 | ||||
| -rw-r--r-- | src/nvim/tui/termkey/driver-ti.h | 7 | ||||
| -rw-r--r-- | src/nvim/tui/termkey/termkey-internal.h | 109 | ||||
| -rw-r--r-- | src/nvim/tui/termkey/termkey.c | 1315 | ||||
| -rw-r--r-- | src/nvim/tui/termkey/termkey.h | 10 | ||||
| -rw-r--r-- | src/nvim/tui/termkey/termkey_defs.h | 199 | ||||
| -rw-r--r-- | src/nvim/tui/tui.c | 105 | ||||
| -rw-r--r-- | src/nvim/tui/tui_defs.h | 2 |
14 files changed, 3336 insertions, 142 deletions
diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index f1594dfcb9..98dd7b4b45 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -7,28 +7,29 @@ #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/rbuffer.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 -#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 +133,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); @@ -155,7 +155,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); @@ -165,9 +165,8 @@ 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); - stream_close(&input->read_stream, NULL, NULL); + rstream_may_close(&input->read_stream); termkey_destroy(input->tk); } @@ -191,44 +190,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) +static void tinput_enqueue(TermInput *input, const 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 - size) { + // 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. @@ -271,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; @@ -471,9 +464,11 @@ 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); + // processing an incomplete bracketed paste sequence. + size_t size = rstream_available(&input->read_stream); + if (size) { + size_t consumed = handle_raw_buffer(input, true, input->read_stream.read_pos, size); + rstream_consume(&input->read_stream, consumed); } tk_getkeys(input, true); tinput_flush(input); @@ -487,39 +482,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) { @@ -534,15 +527,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. @@ -606,10 +599,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; - unsigned long cmd; - if (termkey_interpret_csi(input->tk, key, args, &nargs, &cmd) != TERMKEY_RES_KEY) { + TermKeyCsiParam params[16]; + size_t nparams = 16; + unsigned cmd; + if (termkey_interpret_csi(input->tk, key, params, &nparams, &cmd) != TERMKEY_RES_KEY) { return; } @@ -648,25 +641,55 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) break; } break; + case 't': + if (nparams == 5) { + // We only care about the first 3 parameters, and we ignore subparameters + int args[3]; + for (size_t i = 0; i < ARRAY_SIZE(args); i++) { + if (termkey_interpret_csi_param(params[i], &args[i], NULL, NULL) != TERMKEY_RES_KEY) { + return; + } + } + + if (args[0] == 48) { + // In-band resize event (DEC private mode 2048) + int height_chars = args[1]; + int width_chars = args[2]; + tui_set_size(input->tui_data, width_chars, height_chars); + ui_client_set_size(width_chars, height_chars); + } + } + break; default: break; } } -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; } // @@ -675,55 +698,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 "<keycode>" 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); @@ -735,23 +750,25 @@ static void handle_raw_buffer(TermInput *input, bool force) abort(); } } + + return (size_t)(ptr - data); } -static void tinput_read_cb(Stream *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 @@ -759,11 +776,7 @@ static void tinput_read_cb(Stream *stream, RBuffer *buf, size_t count_, void *da // 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 bf6d0f2978..4c2baf908e 100644 --- a/src/nvim/tui/input.h +++ b/src/nvim/tui/input.h @@ -5,11 +5,10 @@ #include <uv.h> #include "nvim/event/defs.h" -#include "nvim/rbuffer_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 @@ -17,6 +16,7 @@ typedef enum { kKeyEncodingXterm, ///< Xterm's modifyOtherKeys encoding (XTMODKEYS) } KeyEncoding; +#define KEY_BUFFER_SIZE 0x1000 typedef struct { int in_fd; // Phases: -1=all 0=disabled 1=first-chunk 2=continue 3=last-chunk @@ -33,17 +33,12 @@ typedef struct { TermKey_Terminfo_Getstr_Hook *tk_ti_hook_fn; ///< libtermkey terminfo hook uv_timer_t timer_handle; Loop *loop; - Stream read_stream; - RBuffer *key_buffer; + RStream read_stream; TUIData *tui_data; + char key_buffer[KEY_BUFFER_SIZE]; + 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 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/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 <assert.h> +#include <stdio.h> +#include <string.h> + +#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 <F3> +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..745ee9902f --- /dev/null +++ b/src/nvim/tui/termkey/driver-ti.c @@ -0,0 +1,595 @@ +#include <ctype.h> +#include <errno.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unibilium.h> + +#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 <unistd.h> +#else +# include <io.h> +#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<digit> 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) { + 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; + } + 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) { + 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; + } + 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 <stdint.h> + +#include "nvim/tui/termkey/termkey_defs.h" + +#define HAVE_TERMIOS +#ifdef _WIN32 +# undef HAVE_TERMIOS +#endif + +#ifdef HAVE_TERMIOS +# include <termios.h> +#endif + +#ifdef _MSC_VER +# include <BaseTsd.h> +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 <ctype.h> +#include <errno.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> + +#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 <poll.h> +# include <strings.h> +# include <unistd.h> +#else +# include <io.h> +#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 <Esc> 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 <stdint.h> +#include <stdlib.h> + +#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 <stddef.h> +#include <stdint.h> +#include <unibilium.h> +#include <uv.h> + +#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 <Escape> + 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; diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 2a9530defb..fa50a8252d 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -109,12 +109,14 @@ 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; 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,18 +168,11 @@ 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); 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); @@ -212,10 +207,21 @@ 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 { + bool is_set = false; switch (state) { case kTermModeNotRecognized: case kTermModePermanentlySet: @@ -224,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) { @@ -231,6 +239,17 @@ 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; + case kTermModeGraphemeClusters: + if (!is_set) { + tui_set_term_mode(tui, mode, true); + tui->did_set_grapheme_cluster_mode = true; + } + break; } } } @@ -424,6 +443,8 @@ 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); + tui_request_term_mode(tui, kTermModeGraphemeClusters); } // Don't use DECRQSS in screen or tmux, as they behave strangely when receiving it. @@ -482,6 +503,11 @@ 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); + 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) { @@ -517,7 +543,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); } @@ -546,7 +571,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); @@ -563,6 +587,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); } @@ -781,7 +806,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\\")); @@ -871,6 +901,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) { @@ -992,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. @@ -1016,7 +1047,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. @@ -1159,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]; @@ -1191,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; } @@ -1419,6 +1459,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); } @@ -1498,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); } @@ -1694,6 +1735,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) @@ -1701,7 +1750,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; @@ -1728,9 +1777,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); @@ -1852,20 +1899,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"))) { @@ -2505,7 +2544,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 diff --git a/src/nvim/tui/tui_defs.h b/src/nvim/tui/tui_defs.h index c5149d4829..bd99d6b0ad 100644 --- a/src/nvim/tui/tui_defs.h +++ b/src/nvim/tui/tui_defs.h @@ -4,6 +4,8 @@ typedef struct TUIData TUIData; typedef enum { kTermModeSynchronizedOutput = 2026, + kTermModeGraphemeClusters = 2027, + kTermModeResizeEvents = 2048, } TermMode; typedef enum { |