Files
neovim/src/nvim/tui/input.c
Gregory Anders f93ecd2760 feat(tui): parse CSI subparams in termkey (#29805)
libtermkey does not know how to parse CSI subparameters (parameters
separated by ':', ASCII 0x3A) and currently just ignores them. However,
many important CSI sequences sent by the terminal make use of
subparameters, most notably key events when using the kitty keyboard
protocol [1]. Enabling subparameters is a prerequisite for expanding
kitty keyboard protocol support in Neovim.

Concretely, we do this by returning pointers into the internal termkey
buffer for each CSI parameter rather than parsing them into integers
directly. When a caller wants to actually use the parameter as an
integer, they must call termkey_interpret_csi_param, which parses the
full parameter string into an integer parameter and zero or more
subparameters.

The pointers into the internal buffer will become invalidated when new
input arrives from the terminal so it is important that the individual
params are used and parsed right away. All of our code (and libtermkey's
code) does this, so this is fine for now, but is something to keep in
mind moving forward.

[1]: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
2024-07-21 21:47:37 -05:00

780 lines
25 KiB
C

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