Implement reporting of all keys as escape codes with text

This commit is contained in:
Kovid Goyal 2021-01-13 20:10:56 +05:30
parent 819bd5cd70
commit 47a901385f
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 110 additions and 28 deletions

View File

@ -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)``.

View File

@ -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);
}

View File

@ -11,7 +11,7 @@
#include "glfw-wrapper.h"
#include <limits.h>
#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__);

View File

@ -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; ) {

View File

@ -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