aboutsummaryrefslogtreecommitdiff
path: root/src/nvim
diff options
context:
space:
mode:
authordundargoc <gocdundar@gmail.com>2024-08-14 15:52:51 +0200
committerdundargoc <33953936+dundargoc@users.noreply.github.com>2024-09-05 14:28:12 +0200
commitf9108378b7a7e08b48685f0a3ff4f7a3a14b56d6 (patch)
treec347ffd91ee7e05a981f313aa4c15d8a2354fbf7 /src/nvim
parent975aeee537375a14c0e16916e1ef312aae90b85f (diff)
downloadrneovim-f9108378b7a7e08b48685f0a3ff4f7a3a14b56d6.tar.gz
rneovim-f9108378b7a7e08b48685f0a3ff4f7a3a14b56d6.tar.bz2
rneovim-f9108378b7a7e08b48685f0a3ff4f7a3a14b56d6.zip
refactor: adopt termkey and eliminate duplicate code
Termkey is abandoned and it's now our code, so there's no reason not to treat it as such. An alternative approach could be to have a proper repo that we maintain such as with unibilium, although with this approach we can make a few assumptions that will allow us to remove more code. Also eliminate duplicate code from both termkey and libvterm.
Diffstat (limited to 'src/nvim')
-rw-r--r--src/nvim/CMakeLists.txt55
-rw-r--r--src/nvim/os/win_defs.h3
-rw-r--r--src/nvim/tui/input.c17
-rw-r--r--src/nvim/tui/input.h2
-rw-r--r--src/nvim/tui/termkey/README1
-rw-r--r--src/nvim/tui/termkey/driver-csi.c902
-rw-r--r--src/nvim/tui/termkey/driver-csi.h7
-rw-r--r--src/nvim/tui/termkey/driver-ti.c593
-rw-r--r--src/nvim/tui/termkey/driver-ti.h7
-rw-r--r--src/nvim/tui/termkey/termkey-internal.h109
-rw-r--r--src/nvim/tui/termkey/termkey.c1315
-rw-r--r--src/nvim/tui/termkey/termkey.h10
-rw-r--r--src/nvim/tui/termkey/termkey_defs.h199
13 files changed, 3179 insertions, 41 deletions
diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt
index 1b229c1d87..ab08f01e33 100644
--- a/src/nvim/CMakeLists.txt
+++ b/src/nvim/CMakeLists.txt
@@ -54,8 +54,6 @@ if(ENABLE_WASMTIME)
target_compile_definitions(nvim_bin PRIVATE HAVE_WASMTIME)
endif()
-target_compile_definitions(main_lib INTERFACE HAVE_UNIBILIUM)
-
# The unit test lib requires LuaJIT; it will be skipped if LuaJIT is missing.
option(PREFER_LUA "Prefer Lua over LuaJIT in the nvim executable." OFF)
if(PREFER_LUA)
@@ -153,7 +151,7 @@ if(UNIX)
endif()
if(CMAKE_SYSTEM_NAME MATCHES "Windows")
- target_compile_definitions(main_lib INTERFACE _WIN32_WINNT=0x0602 MSWIN)
+ target_compile_definitions(main_lib INTERFACE _WIN32_WINNT=0x0602 MSWIN WIN32_LEAN_AND_MEAN)
target_link_libraries(main_lib INTERFACE netapi32)
elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin")
target_link_libraries(nvim_bin PRIVATE "-framework CoreServices")
@@ -366,8 +364,8 @@ file(MAKE_DIRECTORY ${TOUCHES_DIR} ${GENERATED_DIR} ${GENERATED_INCLUDES_DIR})
file(GLOB NVIM_SOURCES CONFIGURE_DEPENDS *.c)
file(GLOB NVIM_HEADERS CONFIGURE_DEPENDS *.h)
-file(GLOB EXTERNAL_SOURCES CONFIGURE_DEPENDS ../xdiff/*.c ../mpack/*.c ../cjson/*.c ../klib/*.c ../termkey/*.c ../vterm/*.c)
-file(GLOB EXTERNAL_HEADERS CONFIGURE_DEPENDS ../xdiff/*.h ../mpack/*.h ../cjson/*.h ../klib/*.h ../termkey/*.h ../vterm/*.h)
+file(GLOB EXTERNAL_SOURCES CONFIGURE_DEPENDS ../xdiff/*.c ../mpack/*.c ../cjson/*.c ../klib/*.c ../vterm/*.c)
+file(GLOB EXTERNAL_HEADERS CONFIGURE_DEPENDS ../xdiff/*.h ../mpack/*.h ../cjson/*.h ../klib/*.h ../vterm/*.h)
file(GLOB NLUA0_SOURCES CONFIGURE_DEPENDS ../mpack/*.c)
@@ -378,6 +376,15 @@ if(PREFER_LUA)
target_compile_definitions(main_lib INTERFACE NVIM_VENDOR_BIT)
endif()
+# Inlined external projects, we don't maintain it. #9306
+if(MSVC)
+ set_source_files_properties(
+ ${EXTERNAL_SOURCES} PROPERTIES COMPILE_OPTIONS "-wd4090;-wd4244;-wd4267")
+else()
+ set_source_files_properties(
+ ${EXTERNAL_SOURCES} PROPERTIES COMPILE_OPTIONS "-Wno-conversion;-Wno-missing-noreturn;-Wno-missing-format-attribute;-Wno-double-promotion;-Wno-strict-prototypes;-Wno-misleading-indentation;-Wno-sign-compare;-Wno-implicit-fallthrough;-Wno-missing-prototypes;-Wno-missing-field-initializers")
+endif()
+
list(APPEND NLUA0_SOURCES ${PROJECT_SOURCE_DIR}/src/nlua0.c)
foreach(subdir
@@ -386,6 +393,7 @@ foreach(subdir
api/private
msgpack_rpc
tui
+ tui/termkey
event
eval
lua
@@ -407,49 +415,36 @@ endforeach()
list(SORT NVIM_SOURCES)
list(SORT NVIM_HEADERS)
-list(APPEND LINT_NVIM_SOURCES ${NVIM_SOURCES} ${NVIM_HEADERS})
-
foreach(sfile ${NVIM_SOURCES})
get_filename_component(f ${sfile} NAME)
if(WIN32 AND ${f} MATCHES "^(pty_process_unix.c)$")
- list(APPEND to_remove ${sfile})
+ list(REMOVE_ITEM NVIM_SOURCES ${sfile})
endif()
if(NOT WIN32 AND ${f} MATCHES "^(pty_process_win.c)$")
- list(APPEND to_remove ${sfile})
+ list(REMOVE_ITEM NVIM_SOURCES ${sfile})
endif()
if(NOT WIN32 AND ${f} MATCHES "^(pty_conpty_win.c)$")
- list(APPEND to_remove ${sfile})
+ list(REMOVE_ITEM NVIM_SOURCES ${sfile})
endif()
if(NOT WIN32 AND ${f} MATCHES "^(os_win_console.c)$")
- list(APPEND to_remove ${sfile})
+ list(REMOVE_ITEM NVIM_SOURCES ${sfile})
endif()
endforeach()
-list(REMOVE_ITEM NVIM_SOURCES ${to_remove})
-
foreach(hfile ${NVIM_HEADERS})
get_filename_component(f ${hfile} NAME)
if(WIN32 AND ${f} MATCHES "^(unix_defs.h)$")
- list(APPEND to_remove_h ${hfile})
+ list(REMOVE_ITEM NVIM_HEADERS ${hfile})
endif()
if(WIN32 AND ${f} MATCHES "^(pty_process_unix.h)$")
- list(APPEND to_remove_h ${hfile})
+ list(REMOVE_ITEM NVIM_HEADERS ${hfile})
endif()
if(NOT WIN32 AND ${f} MATCHES "^(win_defs.h)$")
- list(APPEND to_remove_h ${hfile})
+ list(REMOVE_ITEM NVIM_HEADERS ${hfile})
endif()
endforeach()
-list(REMOVE_ITEM NVIM_HEADERS ${to_remove_h})
-
-# xdiff, mpack, lua-cjson, termkey: inlined external project, we don't maintain it. #9306
-if(MSVC)
- set_source_files_properties(
- ${EXTERNAL_SOURCES} PROPERTIES COMPILE_OPTIONS "-wd4090;-wd4244;-wd4267")
-else()
- set_source_files_properties(
- ${EXTERNAL_SOURCES} PROPERTIES COMPILE_OPTIONS "-Wno-conversion;-Wno-missing-noreturn;-Wno-missing-format-attribute;-Wno-double-promotion;-Wno-strict-prototypes;-Wno-misleading-indentation;-Wno-sign-compare;-Wno-implicit-fallthrough;-Wno-missing-prototypes;-Wno-missing-field-initializers")
-endif()
+list(APPEND LINT_NVIM_SOURCES ${NVIM_SOURCES} ${NVIM_HEADERS})
# Log level (NVIM_LOG_DEBUG in log.h)
if(CI_BUILD)
@@ -849,7 +844,7 @@ endif()
add_glob_target(
TARGET lintc-clang-tidy
COMMAND ${CLANG_TIDY_PRG}
- FILES ${NVIM_SOURCES} ${NVIM_HEADERS}
+ FILES ${LINT_NVIM_SOURCES}
FLAGS --quiet
EXCLUDE ${EXCLUDE_CLANG_TIDY})
@@ -862,7 +857,7 @@ endif()
add_glob_target(
TARGET clang-analyzer
COMMAND ${CLANG_TIDY_PRG}
- FILES ${NVIM_SOURCES} ${NVIM_HEADERS}
+ FILES ${LINT_NVIM_SOURCES}
FLAGS --quiet
--checks='
-*,
@@ -905,13 +900,13 @@ add_glob_target(
TARGET lintc-uncrustify
COMMAND ${UNCRUSTIFY_PRG}
FLAGS -c ${UNCRUSTIFY_CONFIG} -q --check
- FILES ${LINT_NVIM_SOURCES})
+ FILES ${NVIM_SOURCES} ${NVIM_HEADERS})
add_glob_target(
TARGET formatc
COMMAND ${UNCRUSTIFY_PRG}
FLAGS -c ${UNCRUSTIFY_CONFIG} --replace --no-backup
- FILES ${LINT_NVIM_SOURCES})
+ FILES ${NVIM_SOURCES} ${NVIM_HEADERS})
add_dependencies(lintc-uncrustify uncrustify_update_config)
add_dependencies(formatc uncrustify_update_config)
diff --git a/src/nvim/os/win_defs.h b/src/nvim/os/win_defs.h
index 024719806c..852059f78b 100644
--- a/src/nvim/os/win_defs.h
+++ b/src/nvim/os/win_defs.h
@@ -16,9 +16,6 @@
#include <sys/stat.h>
#include <windows.h>
-// vterm.h defines an `unsigned int small` in a struct, triggering error C2632
-#undef small
-
// Windows does not have S_IFLNK but libuv defines it
// and sets the flag for us when calling uv_fs_stat.
#include <uv.h>
diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c
index a6e27c9391..3eb8d4ba2e 100644
--- a/src/nvim/tui/input.c
+++ b/src/nvim/tui/input.c
@@ -7,24 +7,27 @@
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/event/loop.h"
+#include "nvim/event/rstream.h"
#include "nvim/event/stream.h"
#include "nvim/macros_defs.h"
#include "nvim/main.h"
#include "nvim/map_defs.h"
#include "nvim/memory.h"
+#include "nvim/msgpack_rpc/channel.h"
#include "nvim/option_vars.h"
#include "nvim/os/os.h"
#include "nvim/os/os_defs.h"
#include "nvim/strings.h"
#include "nvim/tui/input.h"
#include "nvim/tui/input_defs.h"
+#include "nvim/tui/termkey/driver-csi.h"
+#include "nvim/tui/termkey/termkey.h"
#include "nvim/tui/tui.h"
#include "nvim/ui_client.h"
+
#ifdef MSWIN
# include "nvim/os/os_win_console.h"
#endif
-#include "nvim/event/rstream.h"
-#include "nvim/msgpack_rpc/channel.h"
#define READ_STREAM_SIZE 0xfff
@@ -261,7 +264,7 @@ static size_t handle_more_modifiers(TermKeyKey *key, char *buf, size_t buflen)
static void handle_kitty_key_protocol(TermInput *input, TermKeyKey *key)
{
- const char *name = pmap_get(int)(&kitty_key_map, (int)key->code.codepoint);
+ const char *name = pmap_get(int)(&kitty_key_map, key->code.codepoint);
if (name) {
char buf[64];
size_t len = 0;
@@ -598,7 +601,7 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key)
// contain, so just allocate enough space for a large upper bound
TermKeyCsiParam params[16];
size_t nparams = 16;
- unsigned long cmd;
+ unsigned cmd;
if (termkey_interpret_csi(input->tk, key, params, &nparams, &cmd) != TERMKEY_RES_KEY) {
return;
}
@@ -641,7 +644,7 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key)
case 't':
if (nparams == 5) {
// We only care about the first 3 parameters, and we ignore subparameters
- long args[3];
+ int args[3];
for (size_t i = 0; i < ARRAY_SIZE(args); i++) {
if (termkey_interpret_csi_param(params[i], &args[i], NULL, NULL) != TERMKEY_RES_KEY) {
return;
@@ -650,8 +653,8 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key)
if (args[0] == 48) {
// In-band resize event (DEC private mode 2048)
- int height_chars = (int)args[1];
- int width_chars = (int)args[2];
+ int height_chars = args[1];
+ int width_chars = args[2];
tui_set_size(input->tui_data, width_chars, height_chars);
ui_client_set_size(width_chars, height_chars);
}
diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h
index 8d0c0c20e9..4c2baf908e 100644
--- a/src/nvim/tui/input.h
+++ b/src/nvim/tui/input.h
@@ -6,9 +6,9 @@
#include "nvim/event/defs.h"
#include "nvim/tui/input_defs.h" // IWYU pragma: keep
+#include "nvim/tui/termkey/termkey_defs.h"
#include "nvim/tui/tui_defs.h"
#include "nvim/types_defs.h"
-#include "termkey/termkey.h"
typedef enum {
kKeyEncodingLegacy, ///< Legacy key encoding
diff --git a/src/nvim/tui/termkey/README b/src/nvim/tui/termkey/README
new file mode 100644
index 0000000000..fd081025fc
--- /dev/null
+++ b/src/nvim/tui/termkey/README
@@ -0,0 +1 @@
+// Adapted from libtermkey: https://github.com/neovim/libtermkey
diff --git a/src/nvim/tui/termkey/driver-csi.c b/src/nvim/tui/termkey/driver-csi.c
new file mode 100644
index 0000000000..28c7eaccfd
--- /dev/null
+++ b/src/nvim/tui/termkey/driver-csi.c
@@ -0,0 +1,902 @@
+#include <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..09c6a35004
--- /dev/null
+++ b/src/nvim/tui/termkey/driver-ti.c
@@ -0,0 +1,593 @@
+#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) {
+ size_t written = (size_t)write(tk->fd, start_string, (unsigned)len);
+ if (written == (size_t)-1) {
+ return 0;
+ }
+ start_string += written;
+ len -= written;
+ }
+ return 1;
+}
+
+int stop_driver_ti(TermKey *tk, void *info)
+{
+ TermKeyTI *ti = info;
+ struct stat statbuf;
+ char *stop_string = ti->stop_string;
+ size_t len;
+
+ if (tk->fd == -1 || !stop_string) {
+ return 1;
+ }
+
+ // There's no point trying to write() to a pipe
+ if (fstat(tk->fd, &statbuf) == -1) {
+ return 0;
+ }
+
+#ifndef _WIN32
+ if (S_ISFIFO(statbuf.st_mode)) {
+ return 1;
+ }
+#endif
+
+ // The terminfo database will contain keys in application cursor key mode.
+ // We may need to enable that mode
+
+ // Can't call putp or tputs because they suck and don't give us fd control
+ len = strlen(stop_string);
+ while (len) {
+ size_t written = (size_t)write(tk->fd, stop_string, (unsigned)len);
+ if (written == (size_t)-1) {
+ return 0;
+ }
+ stop_string += written;
+ len -= written;
+ }
+ return 1;
+}
+
+void free_driver_ti(void *info)
+{
+ TermKeyTI *ti = info;
+
+ free_trie(ti->root);
+
+ if (ti->start_string) {
+ xfree(ti->start_string);
+ }
+
+ if (ti->stop_string) {
+ xfree(ti->stop_string);
+ }
+
+ if (ti->unibi) {
+ unibi_destroy(ti->unibi);
+ }
+
+ xfree(ti);
+}
+
+#define CHARAT(i) (tk->buffer[tk->buffstart + (i)])
+
+TermKeyResult peekkey_ti(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytep)
+{
+ TermKeyTI *ti = info;
+
+ if (tk->buffcount == 0) {
+ return tk->is_closed ? TERMKEY_RES_EOF : TERMKEY_RES_NONE;
+ }
+
+ struct trie_node *p = ti->root;
+
+ unsigned pos = 0;
+ while (pos < tk->buffcount) {
+ p = lookup_next(p, CHARAT(pos));
+ if (!p) {
+ break;
+ }
+
+ pos++;
+
+ if (p->type != TYPE_KEY) {
+ continue;
+ }
+
+ struct trie_node_key *nk = (struct trie_node_key *)p;
+ if (nk->key.type == TERMKEY_TYPE_MOUSE) {
+ tk->buffstart += pos;
+ tk->buffcount -= pos;
+
+ TermKeyResult mouse_result = (*tk->method.peekkey_mouse)(tk, key, nbytep);
+
+ tk->buffstart -= pos;
+ tk->buffcount += pos;
+
+ if (mouse_result == TERMKEY_RES_KEY) {
+ *nbytep += pos;
+ }
+
+ return mouse_result;
+ }
+
+ key->type = nk->key.type;
+ key->code.sym = nk->key.sym;
+ key->modifiers = nk->key.modifier_set;
+ *nbytep = pos;
+ return TERMKEY_RES_KEY;
+ }
+
+ // If p is not NULL then we hadn't walked off the end yet, so we have a
+ // partial match
+ if (p && !force) {
+ return TERMKEY_RES_AGAIN;
+ }
+
+ return TERMKEY_RES_NONE;
+}
+
+static int insert_seq(TermKeyTI *ti, const char *seq, struct trie_node *node)
+{
+ int pos = 0;
+ struct trie_node *p = ti->root;
+
+ // Unsigned because we'll be using it as an array subscript
+ unsigned char b;
+
+ while ((b = (unsigned char)seq[pos])) {
+ struct trie_node *next = lookup_next(p, b);
+ if (!next) {
+ break;
+ }
+ p = next;
+ pos++;
+ }
+
+ while ((b = (unsigned char)seq[pos])) {
+ struct trie_node *next;
+ if (seq[pos + 1]) {
+ // Intermediate node
+ next = new_node_arr(0, 0xff);
+ } else {
+ // Final key node
+ next = node;
+ }
+
+ if (!next) {
+ return 0;
+ }
+
+ switch (p->type) {
+ case TYPE_ARR: {
+ struct trie_node_arr *nar = (struct trie_node_arr *)p;
+ if (b < nar->min || b > nar->max) {
+ fprintf(stderr,
+ "ASSERT FAIL: Trie insert at 0x%02x is outside of extent bounds (0x%02x..0x%02x)\n",
+ b, nar->min, nar->max);
+ abort();
+ }
+ nar->arr[b - nar->min] = next;
+ p = next;
+ break;
+ }
+ case TYPE_KEY:
+ fprintf(stderr, "ASSERT FAIL: Tried to insert child node in TYPE_KEY\n");
+ abort();
+ }
+
+ pos++;
+ }
+
+ return 1;
+}
diff --git a/src/nvim/tui/termkey/driver-ti.h b/src/nvim/tui/termkey/driver-ti.h
new file mode 100644
index 0000000000..df9bd72d5b
--- /dev/null
+++ b/src/nvim/tui/termkey/driver-ti.h
@@ -0,0 +1,7 @@
+#pragma once
+
+#include "nvim/tui/termkey/termkey_defs.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "tui/termkey/driver-ti.h.generated.h"
+#endif
diff --git a/src/nvim/tui/termkey/termkey-internal.h b/src/nvim/tui/termkey/termkey-internal.h
new file mode 100644
index 0000000000..107591f950
--- /dev/null
+++ b/src/nvim/tui/termkey/termkey-internal.h
@@ -0,0 +1,109 @@
+#pragma once
+
+#include <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;