diff --git a/docs/keyboard-protocol.rst b/docs/keyboard-protocol.rst index e65426cdd..8929a1c82 100644 --- a/docs/keyboard-protocol.rst +++ b/docs/keyboard-protocol.rst @@ -44,7 +44,7 @@ sending an escape code to toggle the mode. The central escape code used to encode key events is:: - CSI unicode-key-code:alternate-key-codes ; modifiers:event-type u + CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u Spaces in the above definition are present for clarity and should be ignored. ``CSI`` is the bytes ``0x1b 0x5b``. All parameters are decimal numbers. Fields @@ -133,6 +133,21 @@ field is present. The ``repeat`` type is ``2`` and the ``release`` type is events are not supported for them, unless the application requests *key report mode*, see below. +.. _text_as_codepoints: + +Text as code points +~~~~~~~~~~~~~~~~~~~~~ + +The terminal can optionally send the text associated with key events as a +sequence of Unicode code points. This behavior is opt-in by the progressive +enhancement mechanism described below. Some examples:: + + shift+a -> CSI 97 ; 2 ; 65 u # The text 'A' is reported as 65 + option+a -> CSI 97 ; ; 229 u # The text 'å' is reported as 229 + +If multiple code points are present, they must be separated by colons. +If no known key is associated with the text the key number ``0`` must be used. + Non-Unicode keys ~~~~~~~~~~~~~~~~~~~~~~~ @@ -174,7 +189,8 @@ The value ``3`` means all set bits are reset, unset bits are left unchanged. "0b1 (1)", "Disambiguate escape codes" "0b10 (2)", "Report key event types" "0b100 (4)", "Report alternate keys" - "0b1000 (8)", "Report all keys as ``CSI u`` escape codes" + "0b1000 (8)", "Report all keys as escape codes" + "0b10000 (16)", "Report associated text" The program running in the terminal can query the terminal for the current values of the flags by sending:: @@ -229,7 +245,7 @@ much easier to integrate into the application event loop. Report event types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This type of progressive enhancement causes the terminal to report key repeat +This progressive enhancement (``0b10``) 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. @@ -238,9 +254,30 @@ 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. +This progressive enhancement (``0b100``) 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. + +Report all keys as escape codes +---------------------------------- + +Key events that generate text, such as plain key presses without modifiers, +result in just the text being sent, in the legacy protocol. There is no way to +be notified of key repeat/release events. These types of events are needed for +some applications, such as games (think of movement using the ``WASD`` keys). + +This progressive enhancement (``0b1000``) turns on key reporting even for key +events that generate next. When it is enabled, text will not be sent, instead +only key events are sent. If the text is needed as well, combine with the +Report associated text enhancement below. + + +Report associated text +------------------------ + +This progressive enhancement (``0b10000``) causes key events that generate text +to be reported as ``CSI u`` escape codes with the text embedded in the escape +code. See :ref:`text_as_codepoints` above for details on the mechanism. Legacy key event encoding -------------------------------- @@ -332,8 +369,10 @@ For legacy compatibility, the keys following algorithm: #. If the :kbd:`alt` key is pressed output the byte for ``ESC (0x1b)`` -#. If the :kbd:`ctrl` modifier is pressed mask the seventh bit ``(& 0b111111)`` in the key's ASCII code number and output that -#. Otherwise, if the :kbd:`shift` modifier is pressed, output the shifted key, for example, ``A`` for ``a`` and ``$`` for ``4``. +#. If the :kbd:`ctrl` modifier is pressed mask the seventh bit ``(0b1000000)`` + in the key's ASCII code number and output that +#. Otherwise, if the :kbd:`shift` modifier is pressed, output the shifted key, + for example, ``A`` for ``a`` and ``$`` for ``4``. #. Otherwise, output the key unmodified Additionally, :kbd:`ctrl+space` is output as the NULL byte ``(0x0)``. diff --git a/kitty/key_encoding.c b/kitty/key_encoding.c index 82d28a111..eace1d24f 100644 --- a/kitty/key_encoding.c +++ b/kitty/key_encoding.c @@ -18,15 +18,16 @@ typedef struct { char encoded[4]; } mods; KeyAction action; - bool cursor_key_mode, disambiguate, report_all_event_types, report_alternate_key; + bool cursor_key_mode, disambiguate, report_all_event_types, report_alternate_key, report_text, embed_text; const char *text; bool has_text; } KeyEvent; typedef struct { uint32_t key, shifted_key, alternate_key; - bool add_alternates, has_mods, add_actions; + bool add_alternates, has_mods, add_actions, add_text; char encoded_mods[4]; + const char *text; KeyAction action; } EncodingData; @@ -55,24 +56,42 @@ init_encoding_data(EncodingData *ans, const KeyEvent *ev) { if (ans->add_alternates) { if (ev->mods.shift) ans->shifted_key = ev->shifted_key; ans->alternate_key = ev->alternate_key; } ans->action = ev->action; ans->key = ev->key; + ans->add_text = ev->embed_text && ev->text && ev->text[0]; + ans->text = ev->text; memcpy(ans->encoded_mods, ev->mods.encoded, sizeof(ans->encoded_mods)); } static inline int serialize(const EncodingData *data, char *output, const char csi_trailer) { int pos = 0; + bool second_field_not_empty = data->has_mods || data->add_actions; + bool third_field_not_empty = data->add_text; #define P(fmt, ...) pos += snprintf(output + pos, KEY_BUFFER_SIZE - 2 - pos, fmt, __VA_ARGS__) P("\x1b%s", "["); - if (data->key != 1 || data->add_alternates || data->has_mods || data->add_actions) P("%u", data->key); + if (data->key != 1 || data->add_alternates || second_field_not_empty || third_field_not_empty) P("%u", data->key); if (data->add_alternates) { P("%s", ":"); if (data->shifted_key) P("%u", data->shifted_key); if (data->alternate_key) P(":%u", data->alternate_key); } - if (data->has_mods || data->add_actions) { - P(";%s", data->encoded_mods); + if (second_field_not_empty || third_field_not_empty) { + P("%s", ";"); + if (second_field_not_empty) P("%s", data->encoded_mods); if (data->add_actions) P(":%u", data->action + 1); } + if (third_field_not_empty) { + const char *p = data->text; + uint32_t codep, state = UTF8_ACCEPT; + bool first = true; + while(*p) { + if (decode_utf8(&state, &codep, *p) == UTF8_ACCEPT) { + if (first) { P(";%u", codep); first = false; } + else P(":%u", codep); + } + state = UTF8_ACCEPT; + p++; + } + } #undef P output[pos++] = csi_trailer; output[pos] = 0; @@ -287,11 +306,14 @@ encode_key(const KeyEvent *ev, char *output) { if (GLFW_FKEY_FIRST <= ev->key && ev->key <= GLFW_FKEY_LAST) return encode_function_key(ev, output); EncodingData ed = {0}; init_encoding_data(&ed, ev); - bool simple_encoding_ok = !ed.add_actions && !ed.add_alternates; + bool simple_encoding_ok = !ed.add_actions && !ed.add_alternates && !ed.add_text; if (simple_encoding_ok) { - if (!ed.has_mods) return encode_utf8(ev->key, output); - if (!ev->disambiguate) { + if (!ed.has_mods) { + if (ev->report_text) return serialize(&ed, output, 'u'); + return encode_utf8(ev->key, output); + } + if (!ev->disambiguate && !ev->report_text) { if (is_legacy_ascii_key(ev->key)) { int ret = encode_printable_ascii_key_legacy(ev, output); if (ret > 0) return ret; @@ -318,9 +340,12 @@ encode_glfw_key_event(const GLFWkeyevent *e, const bool cursor_key_mode, const u .cursor_key_mode = cursor_key_mode, .disambiguate = key_encoding_flags & 1, .report_all_event_types = key_encoding_flags & 2, - .report_alternate_key = key_encoding_flags & 4 + .report_alternate_key = key_encoding_flags & 4, + .report_text = key_encoding_flags & 8, + .embed_text = key_encoding_flags & 16 }; ev.has_text = e->text && !is_ascii_control_char(e->text[0]); + bool send_text_standalone = !ev.report_text; if (!ev.disambiguate && GLFW_FKEY_KP_0 <= ev.key && ev.key <= GLFW_FKEY_KP_DELETE) { ev.key = convert_kp_key_to_normal_key(ev.key); } @@ -329,7 +354,7 @@ encode_glfw_key_event(const GLFWkeyevent *e, const bool cursor_key_mode, const u case GLFW_REPEAT: ev.action = REPEAT; break; case GLFW_RELEASE: ev.action = RELEASE; break; } - if (ev.has_text && (ev.action == PRESS || ev.action == REPEAT)) return SEND_TEXT_TO_CHILD; + if (send_text_standalone && ev.has_text && (ev.action == PRESS || ev.action == REPEAT)) return SEND_TEXT_TO_CHILD; convert_glfw_mods(e->mods, &ev); return encode_key(&ev, output); } diff --git a/kitty/keys.h b/kitty/keys.h index 9a3a23158..19f3f3b3a 100644 --- a/kitty/keys.h +++ b/kitty/keys.h @@ -11,7 +11,7 @@ #include "glfw-wrapper.h" #include -#define KEY_BUFFER_SIZE 32 +#define KEY_BUFFER_SIZE 128 #define SEND_TEXT_TO_CHILD INT_MIN #define debug(...) if (OPT(debug_keyboard)) printf(__VA_ARGS__); diff --git a/kitty/screen.c b/kitty/screen.c index bf13b3e2a..97a707acf 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -840,7 +840,7 @@ screen_set_8bit_controls(Screen *self, bool 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; + if (self->key_encoding_flags[i] & 0x80) return self->key_encoding_flags[i] & 0x7f; } return 0; } @@ -858,7 +858,7 @@ screen_set_key_encoding_flags(Screen *self, uint32_t val, uint32_t how) { 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; + uint8_t q = val & 0x7f; 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; @@ -867,7 +867,7 @@ screen_set_key_encoding_flags(Screen *self, uint32_t val, uint32_t how) { void screen_push_key_encoding_flags(Screen *self, uint32_t val) { - uint8_t q = val & 0xf; + uint8_t q = val & 0x7f; const unsigned sz = arraysz(self->main_key_encoding_flags); unsigned current_idx = 0; for (unsigned i = arraysz(self->main_key_encoding_flags); i-- > 0; ) { diff --git a/kitty_tests/keys.py b/kitty_tests/keys.py index f2271c08e..f49fa329a 100644 --- a/kitty_tests/keys.py +++ b/kitty_tests/keys.py @@ -15,11 +15,11 @@ class TestKeys(BaseTest): shift, alt, ctrl, super = defines.GLFW_MOD_SHIFT, defines.GLFW_MOD_ALT, defines.GLFW_MOD_CONTROL, defines.GLFW_MOD_SUPER # noqa press, repeat, release = defines.GLFW_PRESS, defines.GLFW_REPEAT, defines.GLFW_RELEASE # noqa - def csi(mods=0, num=1, action=1, shifted_key=0, alternate_key=0, trailer='u'): + def csi(mods=0, num=1, action=1, shifted_key=0, alternate_key=0, text=None, trailer='u'): ans = '\033[' if isinstance(num, str): num = ord(num) - if num != 1 or mods or shifted_key or alternate_key: + if num != 1 or mods or shifted_key or alternate_key or text: ans += f'{num}' if shifted_key or alternate_key: if isinstance(shifted_key, str): @@ -29,7 +29,7 @@ class TestKeys(BaseTest): if isinstance(alternate_key, str): alternate_key = ord(alternate_key) ans += f':{alternate_key}' - if mods or action > 1: + if mods or action > 1 or text: m = 0 if mods & shift: m |= 1 @@ -39,9 +39,14 @@ class TestKeys(BaseTest): m |= 4 if mods & super: m |= 8 - ans += f';{m+1}' - if action > 1: - ans += f':{action}' + if action > 1 or m: + ans += f';{m+1}' + if action > 1: + ans += f':{action}' + elif text: + ans += ';' + if text: + ans += ';' + ':'.join(map(str, map(ord, text))) return ans + trailer def mods_test(key, plain=None, shift=None, ctrl=None, alt=None, calt=None, cshift=None, ashift=None, csi_num=None, trailer='u'): @@ -419,6 +424,19 @@ class TestKeys(BaseTest): ae(aq(ord('a'), alternate_key=ord('A')), csi(num='a', alternate_key='A')) ae(aq(ord('a'), mods=shift, shifted_key=ord('A'), alternate_key=ord('b')), csi(shift, 'a', shifted_key='A', alternate_key='b')) + # test report all keys + kq = partial(enc, key_encoding_flags=0b1000) + ae(kq(ord('a')), csi(num='a')) + ae(kq(ord('a'), action=defines.GLFW_REPEAT), csi(num='a')) + ae(kq(ord('a'), mods=ctrl), csi(ctrl, num='a')) + ae(kq(defines.GLFW_FKEY_UP), '\x1b[A') + + # test embed text + eq = partial(enc, key_encoding_flags=0b11000) + ae(eq(ord('a'), text='a'), csi(num='a', text='a')) + ae(eq(ord('a'), mods=shift, text='A'), csi(shift, num='a', text='A')) + ae(eq(ord('a'), mods=shift, text='AB'), csi(shift, num='a', text='AB')) + def test_encode_mouse_event(self): NORMAL_PROTOCOL, UTF8_PROTOCOL, SGR_PROTOCOL, URXVT_PROTOCOL = range(4) L, M, R = 1, 2, 3