From a30ea2b7f85d4cd1bee5c2dce3a639c8550408ee Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 11 Jan 2021 17:21:07 +0530 Subject: [PATCH] Implement progressive enhancement of key event reporting --- docs/keyboard-protocol.rst | 94 ++++++++++++++++++++++++++++++++++++-- kitty/key_encoding.c | 19 ++++---- kitty/keys.c | 9 ++-- kitty/parser.c | 20 +++++++- kitty/screen.c | 54 +++++++++++++++++++++- kitty/screen.h | 8 +++- kitty_tests/keys.py | 73 +---------------------------- kitty_tests/screen.py | 43 +++++++++++++++++ 8 files changed, 227 insertions(+), 93 deletions(-) diff --git a/docs/keyboard-protocol.rst b/docs/keyboard-protocol.rst index db4accb75..44c61b2a7 100644 --- a/docs/keyboard-protocol.rst +++ b/docs/keyboard-protocol.rst @@ -21,11 +21,11 @@ advanced usages. The protocol is based on initial work in `fixterms `_, however, it corrects various issues in that proposal, namely: - * No way to disambiguate Esc keypresses, other than using 8-bit controls + * No way to disambiguate :kbd:`Esc` keypresses, other than using 8-bit controls which are undesirable for other reasons * Incorrectly encoding shifted keys when shift modifier is used - * No way to not have :kbd:`Alt+letter` key presses generate escape codes that - conflict with other escape codes + * No way to have non-conflicting escape codes for :kbd:`alt+letter, + ctrl+letter, ctrl+alt+letter` key presses * No way to specify both shifted and unshifted keys for robust shortcut matching (think matching :kbd:`ctrl+shift+equal` and :kbd:`ctrl+plus`) * No way to specify alternate layout key. This is useful for keyboard layouts @@ -62,6 +62,8 @@ are separated by the semi-colon and sub-fields by the colon. Only the escape code is terminated by the ``u`` character (the byte ``0x75``). +.. _key_codes: + Key codes ~~~~~~~~~~~~~~ @@ -113,6 +115,8 @@ and so on. If the modifier field is not present in the escape code, its default value is ``1`` which means no modifiers. +.. _event_types: + Event types ~~~~~~~~~~~~~~~~ @@ -130,8 +134,8 @@ has value ``1`` and is the default if no event type sub field is present. The .. note:: Key events that result in text are reported as plain UTF-8 text, so - events are not supported for them, unless the application requests key - report mode, see below. + events are not supported for them, unless the application requests *key + report mode*, see below. Non-Unicode keys @@ -145,11 +149,90 @@ names to code points for these keys is in the :ref:`Functional key definition table below `. +Progressive enhancement +-------------------------- + +While, in theory, every key event could be completely represented by this +protocol and all would be hunk-dory, in reality there is a vast universe of +existing terminal programs that expect legacy control codes for key events and +that are not likely to ever be updated. To support these, in default mode, +the terminal will emit legacy escape codes for compatibility. If a terminal +program wants more robust key handling, it can request it from the terminal, +via the mechanism described here. Each enhancement is described in detail +below. The escape code for requesting enhancements is:: + + CSI = flags ; mode u + +Here ``flags`` is a decimal encoded integer to specify a set of bit-flags. The +meanings of the flags are given below. The second, ``mode`` parameter is +optional (defaulting to ``1``) and specifies how the flags are applied. +The value ``1`` means all set bits are set and all unset bits are reset. +The value ``2`` means all set bits are set, unset bits are left unchanged. +The value ``3`` means all set bits are reset, unset bits are left unchanged. + +.. csv-table:: The progressive enhancement flags + :header: "Bit", "Meaning" + + "0b1 (1)", "Disambiguate escape codes" + "0b10 (2)", "Report key event types" + "0b100 (4)", "Report alternate keys" + "0b1000 (8)", "Report all keys as CSIu escape codes" + +The program running in the terminal can query the terminal for the +current values of the flags by sending:: + + CSI ? u + +The terminal will reply with:: + + CSI ? flags u + +The program can also push/pop the current flags onto a stack in the +terminal with:: + + CSI > flags u # for push, if flags ommitted default to zero + CSI < number u # to pop number entries, defaulting to 1 if unspecified + +Terminals should limit the size of the stack as appropriate, to prevent +Denial-of-Service attacks. Terminals must maintain separate stacks for the main +and alternate screens. If a pop request is received that empties the stack, +all flags are reset. If a push request is received and the stack is full, the +oldest entry from the stack must be evicted. + +Disambiguate escape codes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This type of progressive enhancement fixes the problem of some legacy key +press encodings overlapping with other control codes. For instance, pressing +the :kbd:`Esc` key generates the byte ``0x1b`` which also is used to indicate +the start of an escape code. Similarly pressing the key :kbd:`alt+[` will +generate the bytes used for CSI control codes. Turning on this flag will cause +the terminal to report the :kbd:`Esc, alt+letter, ctrl+letter, ctrl+alt+letter` +keys using CSIu sequences instead of legacy ones. Here letter is any printable +ASCII letter (from 32 (i.e. space) to 126 (i.e. ~)). + +Report event types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This type of progressive enhancement causes the terminal to report key repeat +and key release events. Normally only key press events are reported and key +repeat events are treated as key press events. See :ref:`event_types` for +details on how these are reported. + + +Report alternate keys +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This type of progressive enhancement causes the terminal to report alternate +key values in addition to the main value, to aid in shortcut matching. See +:ref:`key_codes` for details on how these are reported. + .. _functional: Functional key definitions ---------------------------- +.. {{{ .. start functional key table (auto generated by gen-key-constants.py do not edit) .. csv-table:: Functional key codes @@ -261,3 +344,4 @@ Functional key definitions "MUTE_VOLUME", "E067" .. end functional key table +.. }}} diff --git a/kitty/key_encoding.c b/kitty/key_encoding.c index 7ea805b9e..0d0924f36 100644 --- a/kitty/key_encoding.c +++ b/kitty/key_encoding.c @@ -160,8 +160,10 @@ encode_function_key(const KeyEvent *ev, char *output) { static int encode_printable_ascii_key_legacy(const KeyEvent *ev, char *output) { - char shifted_key = 0; + if (!ev->mods.value) return snprintf(output, KEY_BUFFER_SIZE, "%c", (char)ev->key); + if (ev->disambiguate) return 0; + char shifted_key = 0; if ('a' <= ev->key && ev->key <= 'z') shifted_key = ev->key + ('A' - 'a'); switch(ev->key) { #define S(which, val) case which: shifted_key = val; break; @@ -169,14 +171,13 @@ encode_printable_ascii_key_legacy(const KeyEvent *ev, char *output) { S('`', '~') S('-', '_') S('=', '+') S('[', '{') S(']', '}') S('\\', '|') S(';', ':') S('\'', '"') S(',', '<') S('.', '>') S('/', '?') #undef S } - - if (!ev->mods.value) return snprintf(output, KEY_BUFFER_SIZE, "%c", (char)ev->key); - if (!ev->disambiguate) { - if ((ev->mods.value == ALT || ev->mods.value == (SHIFT | ALT))) - return snprintf(output, KEY_BUFFER_SIZE, "\x1b%c", (shifted_key && ev->mods.shift) ? shifted_key : (char)ev->key); - } - if (ev->mods.value == CTRL && (ev->key != 'i' && ev->key != 'm' && ev->key != '[' && ev->key != '@')) - return snprintf(output, KEY_BUFFER_SIZE, "%c", ev->key & 0x7f); + shifted_key = (shifted_key && ev->mods.shift) ? shifted_key : (char)ev->key; + if ((ev->mods.value == ALT || ev->mods.value == (SHIFT | ALT))) + return snprintf(output, KEY_BUFFER_SIZE, "\x1b%c", shifted_key); + if (ev->mods.value == CTRL) + return snprintf(output, KEY_BUFFER_SIZE, "%c", ev->key & 0x1f); + if (ev->mods.value == (CTRL | ALT)) + return snprintf(output, KEY_BUFFER_SIZE, "\x1b%c", ev->key & 0x1f); return 0; } diff --git a/kitty/keys.c b/kitty/keys.c index f47c67144..065815511 100644 --- a/kitty/keys.c +++ b/kitty/keys.c @@ -111,7 +111,7 @@ on_key_input(GLFWkeyevent *ev) { screen_history_scroll(screen, SCROLL_FULL, false); // scroll back to bottom } char encoded_key[KEY_BUFFER_SIZE] = {0}; - int size = encode_glfw_key_event(ev, screen->modes.mDECCKM, screen->key_encoding_flags, encoded_key); + int size = encode_glfw_key_event(ev, screen->modes.mDECCKM, screen_current_key_encoding_flags(screen), encoded_key); if (size == SEND_TEXT_TO_CHILD) { schedule_write_to_child(w->id, 1, text, strlen(text)); debug("sent text to child\n"); @@ -130,19 +130,20 @@ fake_scroll(Window *w, int amount, bool upwards) { GLFWkeyevent ev = {.key = key }; char encoded_key[KEY_BUFFER_SIZE] = {0}; Screen *screen = w->render_data.screen; + uint8_t flags = screen_current_key_encoding_flags(screen); while (amount-- > 0) { ev.action = GLFW_PRESS; - int size = encode_glfw_key_event(&ev, screen->modes.mDECCKM, screen->key_encoding_flags, encoded_key); + int size = encode_glfw_key_event(&ev, screen->modes.mDECCKM, flags, encoded_key); if (size > 0) schedule_write_to_child(w->id, 1, encoded_key, size); ev.action = GLFW_RELEASE; - size = encode_glfw_key_event(&ev, screen->modes.mDECCKM, screen->key_encoding_flags, encoded_key); + size = encode_glfw_key_event(&ev, screen->modes.mDECCKM, flags, encoded_key); if (size > 0) schedule_write_to_child(w->id, 1, encoded_key, size); } } #define PYWRAP1(name) static PyObject* py##name(PyObject UNUSED *self, PyObject *args) #define PA(fmt, ...) if(!PyArg_ParseTuple(args, fmt, __VA_ARGS__)) return NULL; -#define M(name, arg_type) {#name, (PyCFunction)py##name, arg_type, NULL} +#define M(name, arg_type) {#name, (PyCFunction)(void (*) (void))(py##name), arg_type, NULL} PYWRAP1(key_for_native_key_name) { const char *name; diff --git a/kitty/parser.c b/kitty/parser.c index c8476f250..91785af11 100644 --- a/kitty/parser.c +++ b/kitty/parser.c @@ -683,7 +683,7 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) { unsigned int num = screen->parser_buf_pos, start, i, num_params=0, p1, p2; static unsigned int params[MAX_PARAMS] = {0}; bool private; - if (buf[0] == '>' || buf[0] == '?' || buf[0] == '!' || buf[0] == '=' || buf[0] == '-') { + if (buf[0] == '>' || buf[0] == '<' || buf[0] == '?' || buf[0] == '!' || buf[0] == '=' || buf[0] == '-') { start_modifier = (char)screen->parser_buf[0]; buf++; num--; } @@ -835,6 +835,23 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) { screen_restore_cursor(screen); break; } + if (!end_modifier && start_modifier == '?') { + REPORT_COMMAND(screen_report_key_encoding_flags); + screen_report_key_encoding_flags(screen); + break; + } + if (!end_modifier && start_modifier == '=') { + CALL_CSI_HANDLER2(screen_set_key_encoding_flags, 0, 1); + break; + } + if (!end_modifier && start_modifier == '>') { + CALL_CSI_HANDLER1(screen_push_key_encoding_flags, 0); + break; + } + if (!end_modifier && start_modifier == '<') { + CALL_CSI_HANDLER1(screen_pop_key_encoding_flags, 1); + break; + } REPORT_ERROR("Unknown CSI u sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params); break; case 'r': @@ -1090,6 +1107,7 @@ accumulate_csi(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_callback) break; case '?': case '>': + case '<': case '!': case '=': case '-': diff --git a/kitty/screen.c b/kitty/screen.c index e2c59b414..2ba485cea 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -135,6 +135,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { self->tabstops = self->main_tabstops; init_tabstops(self->main_tabstops, self->columns); init_tabstops(self->alt_tabstops, self->columns); + self->key_encoding_flags = self->main_key_encoding_flags; if (!init_overlay_line(self, self->columns)) { Py_CLEAR(self); return NULL; } self->hyperlink_pool = alloc_hyperlink_pool(); if (!self->hyperlink_pool) { Py_CLEAR(self); return PyErr_NoMemory(); } @@ -150,7 +151,8 @@ void screen_reset(Screen *self) { if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self, true, true); if (self->overlay_line.is_active) deactivate_overlay_line(self); - self->key_encoding_flags = 0; + memset(self->main_key_encoding_flags, 0, sizeof(self->main_key_encoding_flags)); + memset(self->alt_key_encoding_flags, 0, sizeof(self->alt_key_encoding_flags)); self->last_graphic_char = 0; self->main_savepoint.is_valid = false; self->alt_savepoint.is_valid = false; @@ -718,12 +720,14 @@ screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_scree if (save_cursor) screen_save_cursor(self); self->linebuf = self->alt_linebuf; self->tabstops = self->alt_tabstops; + self->key_encoding_flags = self->alt_key_encoding_flags; self->grman = self->alt_grman; screen_cursor_position(self, 1, 1); cursor_reset(self->cursor); } else { self->linebuf = self->main_linebuf; self->tabstops = self->main_tabstops; + self->key_encoding_flags = self->main_key_encoding_flags; if (save_cursor) screen_restore_cursor(self); self->grman = self->main_grman; } @@ -833,6 +837,54 @@ screen_set_8bit_controls(Screen *self, bool yes) { self->modes.eight_bit_controls = yes; } +uint8_t +screen_current_key_encoding_flags(Screen *self) { + for (unsigned i = arraysz(self->main_key_encoding_flags); i-- > 0; ) { + if (self->key_encoding_flags[i] & 0x80) return self->key_encoding_flags[i] & 0xf; + } + return 0; +} + +void +screen_report_key_encoding_flags(Screen *self) { + char buf[16] = {0}; + snprintf(buf, sizeof(buf), "?%uu", screen_current_key_encoding_flags(self)); + write_escape_code_to_child(self, CSI, buf); +} + +void +screen_set_key_encoding_flags(Screen *self, uint32_t val, uint32_t how) { + unsigned idx = 0; + for (unsigned i = arraysz(self->main_key_encoding_flags); i-- > 0; ) { + if (self->key_encoding_flags[i] & 0x80) { idx = i; break; } + } + uint8_t q = val & 0xf; + if (how == 1) self->key_encoding_flags[idx] = q; + else if (how == 2) self->key_encoding_flags[idx] |= q; + else if (how == 3) self->key_encoding_flags[idx] &= ~q; + self->key_encoding_flags[idx] |= 0x80; +} + +void +screen_push_key_encoding_flags(Screen *self, uint32_t val) { + uint8_t q = val & 0xf; + const unsigned sz = arraysz(self->main_key_encoding_flags); + unsigned current_idx = 0; + for (unsigned i = arraysz(self->main_key_encoding_flags); i-- > 0; ) { + if (self->key_encoding_flags[i] & 0x80) { current_idx = i; break; } + } + if (current_idx == sz - 1) memmove(self->key_encoding_flags, self->key_encoding_flags + 1, (sz - 1) * sizeof(self->main_key_encoding_flags[0])); + else self->key_encoding_flags[current_idx++] |= 0x80; + self->key_encoding_flags[current_idx] = 0x80 | q; +} + +void +screen_pop_key_encoding_flags(Screen *self, uint32_t num) { + for (unsigned i = arraysz(self->main_key_encoding_flags); num && i-- > 0; ) { + if (self->key_encoding_flags[i] & 0x80) { num--; self->key_encoding_flags[i] = 0; } + } +} + // }}} // Cursor {{{ diff --git a/kitty/screen.h b/kitty/screen.h index 8dbb65fbb..f1c06486c 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -129,7 +129,7 @@ typedef struct { HYPERLINK_POOL_HANDLE hyperlink_pool; ANSIBuf as_ansi_buf; char_type last_graphic_char; - unsigned key_encoding_flags; + uint8_t main_key_encoding_flags[8], alt_key_encoding_flags[8], *key_encoding_flags; } Screen; @@ -223,9 +223,15 @@ void screen_rescale_images(Screen *self); void screen_report_size(Screen *, unsigned int which); void screen_manipulate_title_stack(Screen *, unsigned int op, unsigned int which); void screen_draw_overlay_text(Screen *self, const char *utf8_text); +void screen_set_key_encoding_flags(Screen *self, uint32_t val, uint32_t how); +void screen_push_key_encoding_flags(Screen *self, uint32_t val); +void screen_pop_key_encoding_flags(Screen *self, uint32_t num); +uint8_t screen_current_key_encoding_flags(Screen *self); +void screen_report_key_encoding_flags(Screen *self); #define DECLARE_CH_SCREEN_HANDLER(name) void screen_##name(Screen *screen); DECLARE_CH_SCREEN_HANDLER(bell) DECLARE_CH_SCREEN_HANDLER(backspace) DECLARE_CH_SCREEN_HANDLER(tab) DECLARE_CH_SCREEN_HANDLER(linefeed) DECLARE_CH_SCREEN_HANDLER(carriage_return) +#undef DECLARE_CH_SCREEN_HANDLER diff --git a/kitty_tests/keys.py b/kitty_tests/keys.py index a4eacdeda..c0c3d24cc 100644 --- a/kitty_tests/keys.py +++ b/kitty_tests/keys.py @@ -2,82 +2,11 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal -from functools import partial - import kitty.fast_data_types as defines -from kitty.keys import ( - interpret_key_event, modify_complex_key, modify_key_bytes, smkx_key_map -) - from . import BaseTest -class DummyWindow: - - def __init__(self): - self.screen = self - self.extended_keyboard = False - self.cursor_key_mode = True - - -class TestParser(BaseTest): - - def test_modify_complex_key(self): - self.ae(modify_complex_key('kcuu1', 4), b'\033[1;4A') - self.ae(modify_complex_key('kcuu1', 3), b'\033[1;3A') - self.ae(modify_complex_key('kf5', 3), b'\033[15;3~') - self.assertRaises(ValueError, modify_complex_key, 'kri', 3) - - def test_interpret_key_event(self): - # test rmkx/smkx - w = DummyWindow() - - def k(expected, key, mods=0): - actual = interpret_key_event( - getattr(defines, 'GLFW_KEY_' + key), - 0, - mods, - w, - defines.GLFW_PRESS, - ) - self.ae(b'\033' + expected.encode('ascii'), actual) - - for ckm, mch in {True: 'O', False: '['}.items(): - w.cursor_key_mode = ckm - for name, ch in { - 'UP': 'A', - 'DOWN': 'B', - 'RIGHT': 'C', - 'LEFT': 'D', - 'HOME': 'H', - 'END': 'F', - }.items(): - k(mch + ch, name) - w.cursor_key_mode = True - - # test remaining special keys - for key, num in zip('INSERT DELETE PAGE_UP PAGE_DOWN'.split(), '2356'): - k('[' + num + '~', key) - for key, num in zip('1234', 'PQRS'): - k('O' + num, 'F' + key) - for key, num in zip(range(5, 13), (15, 17, 18, 19, 20, 21, 23, 24)): - k('[' + str(num) + '~', 'F{}'.format(key)) - - # test modifiers - SPECIAL_KEYS = 'UP DOWN RIGHT LEFT HOME END INSERT DELETE PAGE_UP PAGE_DOWN ' - for i in range(1, 13): - SPECIAL_KEYS += 'F{} '.format(i) - SPECIAL_KEYS = SPECIAL_KEYS.strip().split() - for mods, num in zip(('CONTROL', 'ALT', 'SHIFT+ALT'), '534'): - fmods = 0 - num = int(num) - for m in mods.split('+'): - fmods |= getattr(defines, 'GLFW_MOD_' + m) - km = partial(k, mods=fmods) - for key in SPECIAL_KEYS: - keycode = getattr(defines, 'GLFW_KEY_' + key) - base_key = smkx_key_map[keycode] - km(modify_key_bytes(base_key, num).decode('ascii')[1:], key) +class TestKeys(BaseTest): def test_encode_mouse_event(self): NORMAL_PROTOCOL, UTF8_PROTOCOL, SGR_PROTOCOL, URXVT_PROTOCOL = range(4) diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index d9f3385db..0ac19d0b3 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -728,6 +728,49 @@ class TestScreen(BaseTest): self.ae(str(s.linebuf), '0\n5\n6\n7\n\n') self.ae(str(s.historybuf), '') + def test_key_encoding_flags_stack(self): + s = self.create_screen() + c = s.callbacks + + def w(code, p1='', p2=''): + p = f'{p1}' + if p2: + p += f';{p2}' + return parse_bytes(s, f'\033[{code}{p}u'.encode('ascii')) + + def ac(flags): + parse_bytes(s, '\033[?u'.encode('ascii')) + self.ae(c.wtcbuf, f'\033[?{flags}u'.encode('ascii')) + c.clear() + + ac(0) + w('=', 0b1001) + ac(0b1001) + w('=', 0b0011, 2) + ac(0b1011) + w('=', 0b0110, 3) + ac(0b1001) + s.reset() + ac(0) + + w('>', 0b0011) + ac(0b0011) + w('=', 0b1111) + ac(0b1111) + w('>', 0b10) + ac(0b10) + w('<') + ac(0b1111) + for i in range(10): + w('<') + ac(0) + s.reset() + + for i in range(1, 16): + w('>', i) + ac(15) + w('<'), ac(14), w('<'), ac(13) + def test_color_stack(self): s = self.create_screen() c = s.callbacks